diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index 65289c02da9..10f7583793d 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -30,6 +30,13 @@ export type WebsiteIconData = { icon: string; }; +export type FocusedFieldData = { + focusedFieldStyles: Partial; + focusedFieldRects: Partial; + tabId?: number; + frameId?: number; +}; + export type OverlayAddNewItemMessage = { login?: { uri?: string; @@ -39,11 +46,9 @@ export type OverlayAddNewItemMessage = { }; }; -export type FocusedFieldData = { - focusedFieldStyles: Partial; - focusedFieldRects: Partial; - tabId?: number; - frameId?: number; +export type CloseInlineMenuMessage = { + forceCloseAutofillInlineMenu?: boolean; + overlayElement?: string; }; export type OverlayBackgroundExtensionMessage = { @@ -52,8 +57,6 @@ export type OverlayBackgroundExtensionMessage = { tab?: chrome.tabs.Tab; sender?: string; details?: AutofillPageDetails; - overlayElement?: string; - forceCloseAutofillInlineMenu?: boolean; isAutofillInlineMenuHidden?: boolean; setTransparentInlineMenu?: boolean; isFieldCurrentlyFocused?: boolean; @@ -62,7 +65,8 @@ export type OverlayBackgroundExtensionMessage = { focusedFieldData?: FocusedFieldData; styles?: Partial; data?: LockedVaultPendingNotificationsData; -} & OverlayAddNewItemMessage; +} & OverlayAddNewItemMessage & + CloseInlineMenuMessage; export type OverlayPortMessage = { [key: string]: any; @@ -116,7 +120,9 @@ export type OverlayBackgroundExtensionMessageHandlers = { rebuildSubFrameOffsets: ({ sender }: BackgroundSenderParam) => void; collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; unlockCompleted: ({ message }: BackgroundMessageParam) => void; + addedCipher: () => void; addEditCipherSubmitted: () => void; + editedCipher: () => void; deletedCipher: () => void; }; diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index e1da0a1c4ad..41afd4a8de0 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -742,7 +742,7 @@ describe("OverlayBackground", () => { }); }); - describe("openAutofillInlineMenu", () => { + describe("openAutofillInlineMenu message handler", () => { let sender: chrome.runtime.MessageSender; beforeEach(() => { @@ -787,6 +787,161 @@ describe("OverlayBackground", () => { ); }); }); + + describe("closeAutofillInlineMenu", () => { + let sender: chrome.runtime.MessageSender; + + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFilling", + isFieldCurrentlyFilling: false, + }); + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: false, + }); + }); + + it("sends a message to close the inline menu without checking field focus state if forcing the closure", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + await flushPromises(); + + sendMockExtensionMessage( + { + command: "closeAutofillInlineMenu", + forceCloseAutofillInlineMenu: true, + overlayElement: AutofillOverlayElement.Button, + }, + sender, + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "closeInlineMenu", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + }); + + it("skips sending a message to close the inline menu if a form field is currently focused", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + await flushPromises(); + + sendMockExtensionMessage( + { + command: "closeAutofillInlineMenu", + forceCloseAutofillInlineMenu: false, + overlayElement: AutofillOverlayElement.Button, + }, + sender, + ); + await flushPromises(); + + expect(tabsSendMessageSpy).not.toHaveBeenCalled(); + }); + + it("sends a message to close the inline menu list only if the field is currently filling", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFilling", + isFieldCurrentlyFilling: true, + }); + await flushPromises(); + + sendMockExtensionMessage({ command: "closeAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "closeInlineMenu", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "closeInlineMenu", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + }); + + it("sends a message to close the inline menu if the form field is not focused and not filling", async () => { + sendMockExtensionMessage({ command: "closeAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "closeInlineMenu", + overlayElement: undefined, + }, + { frameId: 0 }, + ); + }); + }); + + describe("checkAutofillInlineMenuFocused message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("will check if the inline menu list is focused if the list port is open", () => { + sendMockExtensionMessage({ command: "checkAutofillInlineMenuFocused" }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + }); + + it("will check if the overlay button is focused if the list port is not open", () => { + overlayBackground["inlineMenuListPort"] = undefined; + + sendMockExtensionMessage({ command: "checkAutofillInlineMenuFocused" }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + }); + }); + + describe("extension messages that trigger an update of the inline menu ciphers", () => { + const extensionMessages = [ + "addedCipher", + "addEditCipherSubmitted", + "editedCipher", + "deletedCipher", + ]; + + beforeEach(() => { + jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation(); + }); + + extensionMessages.forEach((message) => { + it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => { + sendMockExtensionMessage({ command: message }); + expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled(); + }); + }); + }); }); describe("inline menu button message handlers", () => {}); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 8a519ad5a17..032d7628fef 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -43,6 +43,7 @@ import { PageDetailsForTab, SubFrameOffsetData, SubFrameOffsetsForTab, + CloseInlineMenuMessage, } from "./abstractions/overlay.background"; export class OverlayBackground implements OverlayBackgroundInterface { @@ -91,7 +92,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { rebuildSubFrameOffsets: ({ sender }) => this.rebuildSubFrameOffsets(sender), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), unlockCompleted: ({ message }) => this.unlockCompleted(message), + addedCipher: () => this.updateOverlayCiphers(), addEditCipherSubmitted: () => this.updateOverlayCiphers(), + editedCipher: () => this.updateOverlayCiphers(), deletedCipher: () => this.updateOverlayCiphers(), }; private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = { @@ -470,10 +473,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { */ private closeInlineMenu( sender: chrome.runtime.MessageSender, - { - forceCloseAutofillInlineMenu, - overlayElement, - }: { forceCloseAutofillInlineMenu?: boolean; overlayElement?: string } = {}, + { forceCloseAutofillInlineMenu, overlayElement }: CloseInlineMenuMessage = {}, ) { if (forceCloseAutofillInlineMenu) { void BrowserApi.tabSendMessage( @@ -507,6 +507,13 @@ export class OverlayBackground implements OverlayBackgroundInterface { ); } + /** + * Sends a message to the sender tab to trigger a delayed closure of the inline menu. + * This is used to ensure that we capture click events on the inline menu in the case + * that some on page programmatic method attempts to force focus redirection. + * + * @param sender - The sender of the port message + */ private triggerDelayedInlineMenuClosure(sender: chrome.runtime.MessageSender) { if (this.isFieldCurrentlyFocused) { return; @@ -589,6 +596,10 @@ export class OverlayBackground implements OverlayBackgroundInterface { }); } + /** + * Handles updating the opacity of both the inline menu button and list. + * This is used to simultaneously fade in the inline menu elements. + */ private setInlineMenuFadeInTimeout() { if (this.inlineMenuFadeInTimeout) { globalThis.clearTimeout(this.inlineMenuFadeInTimeout); @@ -598,7 +609,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.inlineMenuFadeInTimeout = globalThis.setTimeout(() => { this.inlineMenuButtonPort?.postMessage(message); this.inlineMenuListPort?.postMessage(message); - }, 75); + }, 50); } /** @@ -977,6 +988,11 @@ export class OverlayBackground implements OverlayBackgroundInterface { return this.isFieldCurrentlyFilling; } + /** + * Sends a message to the top level frame of the sender to check if the inline menu button is visible. + * + * @param sender - The sender of the message + */ private async checkIsAutofillInlineMenuButtonVisible(sender: chrome.runtime.MessageSender) { return await BrowserApi.tabSendMessage( sender.tab, @@ -985,6 +1001,11 @@ export class OverlayBackground implements OverlayBackgroundInterface { ); } + /** + * Sends a message to the top level frame of the sender to check if the inline menu list is visible. + * + * @param sender - The sender of the message + */ private async checkIsAutofillInlineMenuListVisible(sender: chrome.runtime.MessageSender) { return await BrowserApi.tabSendMessage( sender.tab, @@ -993,16 +1014,34 @@ export class OverlayBackground implements OverlayBackgroundInterface { ); } + /** + * Responds to the content script's request to check if the inline menu ciphers are populated. + * This will return true only if the sender is the focused field's tab and the inline menu + * ciphers are populated. + * + * @param sender - The sender of the message + */ private checkIsInlineMenuCiphersPopulated(sender: chrome.runtime.MessageSender) { return sender.tab.id === this.focusedFieldData.tabId && this.inlineMenuCiphers.size > 0; } + /** + * Triggers an update in the meta "color-scheme" value within the inline menu button. + * This is done to ensure that the button element has a transparent background, which + * is accomplished by setting the "color-scheme" meta value of the button iframe to + * the same value as the page's meta "color-scheme" value. + */ private updateInlineMenuButtonColorScheme() { this.inlineMenuButtonPort?.postMessage({ command: "updateAutofillInlineMenuColorScheme", }); } + /** + * Triggers an update in the inline menu list's height. + * + * @param message - Contains the dimensions of the inline menu list + */ private updateInlineMenuListHeight(message: OverlayBackgroundExtensionMessage) { this.inlineMenuListPort?.postMessage({ command: "updateInlineMenuIframePosition", @@ -1160,6 +1199,12 @@ export class OverlayBackground implements OverlayBackgroundInterface { handler({ message, port }); }; + /** + * Ensures that the inline menu list and button port + * references are reset when they are disconnected. + * + * @param port - The port that was disconnected + */ private handlePortOnDisconnect = (port: chrome.runtime.Port) => { if (port.name === AutofillOverlayPort.List) { this.inlineMenuListPort = null;