diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index fcfcdf8aaa..f240a1cba3 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -1,14 +1,15 @@ import { mock, MockProxy, mockReset } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; +import { BehaviorSubject } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { AutofillSettingsServiceAbstraction as AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DefaultDomainSettingsService, DomainSettingsService, } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { EnvironmentService, Region, @@ -27,60 +28,74 @@ import { import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; +import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service"; import { AutofillService } from "../services/abstractions/autofill.service"; import { createChromeTabMock, createAutofillPageDetailsMock } from "../spec/autofill-mocks"; -import { sendMockExtensionMessage } from "../spec/testing-utils"; +import { flushPromises, sendMockExtensionMessage } from "../spec/testing-utils"; +import { + PageDetailsForTab, + SubFrameOffsetData, + SubFrameOffsetsForTab, +} from "./abstractions/overlay.background"; import { OverlayBackground } from "./overlay.background"; describe("OverlayBackground", () => { const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; let fakeStateProvider: FakeStateProvider; + let showFaviconsMock$: BehaviorSubject; let domainSettingsService: DomainSettingsService; let logService: MockProxy; let cipherService: MockProxy; let autofillService: MockProxy; + let activeAccountStatusMock$: BehaviorSubject; let authService: MockProxy; + let environmentMock$: BehaviorSubject; let environmentService: MockProxy; - let stateService: MockProxy; + let inlineMenuVisibilityMock$: BehaviorSubject; let autofillSettingsService: MockProxy; let i18nService: MockProxy; let platformUtilsService: MockProxy; + let selectedThemeMock$: BehaviorSubject; let themeStateService: MockProxy; let overlayBackground: OverlayBackground; + let portKeyForTabSpy: Record; + let pageDetailsForTabSpy: PageDetailsForTab; + let subFrameOffsetsSpy: SubFrameOffsetsForTab; + let getFrameDetailsSpy: jest.SpyInstance; + let tabsSendMessageSpy: jest.SpyInstance; beforeEach(() => { accountService = mockAccountServiceWith(mockUserId); fakeStateProvider = new FakeStateProvider(accountService); + showFaviconsMock$ = new BehaviorSubject(true); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); - domainSettingsService.showFavicons$ = of(true); + domainSettingsService.showFavicons$ = showFaviconsMock$; logService = mock(); cipherService = mock(); autofillService = mock(); - authService = mock({ - activeAccountStatus$: new BehaviorSubject(AuthenticationStatus.Unlocked), - }); - environmentService = mock({ - environment$: new BehaviorSubject( - new CloudEnvironment({ - key: Region.US, - domain: "bitwarden.com", - urls: { icons: "https://icons.bitwarden.com/" }, - }), - ), - }); - stateService = mock(); - autofillSettingsService = mock({ - inlineMenuVisibility$: of(AutofillOverlayVisibility.OnFieldFocus), - }); + activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); + authService = mock(); + authService.activeAccountStatus$ = activeAccountStatusMock$; + environmentMock$ = new BehaviorSubject( + new CloudEnvironment({ + key: Region.US, + domain: "bitwarden.com", + urls: { icons: "https://icons.bitwarden.com/" }, + }), + ); + environmentService = mock(); + environmentService.environment$ = environmentMock$; + inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus); + autofillSettingsService = mock(); + autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$; i18nService = mock(); platformUtilsService = mock(); - themeStateService = mock({ - selectedTheme$: of(ThemeType.Light), - }); + selectedThemeMock$ = new BehaviorSubject(ThemeType.Light); + themeStateService = mock(); + themeStateService.selectedTheme$ = selectedThemeMock$; overlayBackground = new OverlayBackground( logService, cipherService, @@ -88,12 +103,16 @@ describe("OverlayBackground", () => { authService, environmentService, domainSettingsService, - stateService, autofillSettingsService, i18nService, platformUtilsService, themeStateService, ); + portKeyForTabSpy = overlayBackground["portKeyForTab"]; + pageDetailsForTabSpy = overlayBackground["pageDetailsForTab"]; + subFrameOffsetsSpy = overlayBackground["subFrameOffsetsForTab"]; + getFrameDetailsSpy = jest.spyOn(BrowserApi, "getFrameDetails"); + tabsSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage"); void overlayBackground.init(); }); @@ -103,21 +122,119 @@ describe("OverlayBackground", () => { mockReset(cipherService); }); - // TODO: describe init - - describe("removePageDetails", () => { - it("removes the page details for a specific tab from the pageDetailsForTab object", () => { - const tabId = 1; - const frameId = 2; + describe("storing pageDetails", () => { + const tabId = 1; + beforeEach(() => { sendMockExtensionMessage( { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, - mock({ tab: createChromeTabMock({ id: tabId }), frameId }), + mock({ tab: createChromeTabMock({ id: tabId }), frameId: 0 }), + ); + }); + + it("generates a random 12 character string used to validate port messages from the tab", () => { + expect(portKeyForTabSpy[tabId]).toHaveLength(12); + }); + + it("stores the page details for the tab", () => { + expect(pageDetailsForTabSpy[tabId]).toBeDefined(); + }); + + describe("building sub frame offsets", () => { + let getFrameCounter: number = 2; + + beforeEach(() => { + getFrameDetailsSpy.mockImplementation((_details: chrome.webNavigation.GetFrameDetails) => { + getFrameCounter--; + return mock({ + parentFrameId: getFrameCounter, + }); + }); + tabsSendMessageSpy.mockResolvedValue(mock()); + }); + + afterEach(() => { + getFrameCounter = 2; + }); + + it("builds the offset values for a sub frame within the tab", () => { + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab: createChromeTabMock({ id: tabId }), + frameId: 1, + }), + ); + + expect(subFrameOffsetsSpy[tabId]).toBeDefined(); + expect(pageDetailsForTabSpy[tabId].size).toBe(2); + }); + + it("skips building offset values for a previously calculated sub frame", async () => { + getFrameCounter = 0; + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab: createChromeTabMock({ id: tabId }), + frameId: 1, + }), + ); + await flushPromises(); + + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab: createChromeTabMock({ id: tabId }), + frameId: 1, + }), + ); + await flushPromises(); + + expect(getFrameDetailsSpy).toHaveBeenCalledTimes(1); + expect(subFrameOffsetsSpy[tabId]).toStrictEqual( + new Map([[1, { left: 0, top: 0, url: "url" }]]), + ); + }); + + it("will attempt to build the sub frame offsets by posting window messages if a set of offsets is not returned", async () => { + const tab = createChromeTabMock({ id: tabId }); + const frameId = 1; + tabsSendMessageSpy.mockResolvedValueOnce(null); + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab, + frameId, + }), + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { + command: "getSubFrameOffsetsFromWindowMessage", + subFrameId: frameId, + }, + { frameId }, + ); + expect(subFrameOffsetsSpy[tabId]).toStrictEqual(new Map([[frameId, null]])); + }); + }); + }); + + describe("removing pageDetails", () => { + it("removes the page details, sub frame details, and port key for a specific tab from the pageDetailsForTab object", () => { + const tabId = 1; + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ tab: createChromeTabMock({ id: tabId }), frameId: 1 }), ); overlayBackground.removePageDetails(tabId); - expect(overlayBackground["pageDetailsForTab"][tabId]).toBeUndefined(); + expect(pageDetailsForTabSpy[tabId]).toBeUndefined(); + expect(subFrameOffsetsSpy[tabId]).toBeUndefined(); + expect(portKeyForTabSpy[tabId]).toBeUndefined(); }); }); }); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 419f6faa61..c8fd2007ea 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -10,7 +10,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -128,7 +127,6 @@ export class OverlayBackground implements OverlayBackgroundInterface { private authService: AuthService, private environmentService: EnvironmentService, private domainSettingsService: DomainSettingsService, - private stateService: StateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, @@ -261,6 +259,13 @@ export class OverlayBackground implements OverlayBackgroundInterface { pageDetailsMap.set(sender.frameId, pageDetails); } + /** + * Handles sub frame offset calculations for the given tab and frame id. + * Is used in setting the position of the inline menu list and button. + * + * @param message - The message received from the `updateSubFrameData` command + * @param sender - The sender of the message + */ private updateSubFrameData( message: OverlayBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 4d4acd2128..28afd4efd9 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1049,7 +1049,6 @@ export default class MainBackground { this.authService, this.environmentService, this.domainSettingsService, - this.stateService, this.autofillSettingsService, this.i18nService, this.platformUtilsService,