diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index 159ba99651..f86cb9227b 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -65,6 +65,7 @@ export type OverlayBackgroundExtensionMessage = { details?: AutofillPageDetails; isFieldCurrentlyFocused?: boolean; isFieldCurrentlyFilling?: boolean; + isInlineMenuElementVisible?: boolean; subFrameData?: SubFrameOffsetData; focusedFieldData?: FocusedFieldData; styles?: Partial; @@ -119,9 +120,12 @@ export type OverlayBackgroundExtensionMessageHandlers = { message, sender, }: BackgroundOnMessageHandlerParams) => Promise; - toggleAutofillInlineMenuHidden: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - checkIsAutofillInlineMenuButtonVisible: ({ sender }: BackgroundSenderParam) => void; - checkIsAutofillInlineMenuListVisible: ({ sender }: BackgroundSenderParam) => void; + updateAutofillInlineMenuElementIsVisibleStatus: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => void; + checkIsAutofillInlineMenuButtonVisible: () => void; + checkIsAutofillInlineMenuListVisible: () => void; getCurrentTabFrameId: ({ sender }: BackgroundSenderParam) => number; updateSubFrameData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; triggerSubFrameFocusInRebuild: ({ sender }: BackgroundSenderParam) => void; diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 25f15e63c5..bbbdd8a03a 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -519,9 +519,10 @@ describe("OverlayBackground", () => { await flushPromises(); } - beforeEach(() => { + beforeEach(async () => { sender = mock({ tab, frameId: middleFrameId }); jest.useFakeTimers(); + await initOverlayElementPorts(); }); it("skips updating the position of either inline menu element if a field is not currently focused", async () => { @@ -559,11 +560,10 @@ describe("OverlayBackground", () => { sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); await flushUpdateInlineMenuPromises(); - expect(tabsSendMessageSpy).toHaveBeenCalledWith( - sender.tab, - { command: "toggleAutofillInlineMenuHidden", isInlineMenuHidden: true }, - { frameId: 0 }, - ); + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "toggleAutofillInlineMenuHidden", + styles: { display: "none" }, + }); expect(tabsSendMessageSpy).toHaveBeenCalledWith( sender.tab, { @@ -1312,73 +1312,35 @@ describe("OverlayBackground", () => { }); }); - describe("toggleAutofillInlineMenuHidden message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("returns early if the sender tab is not equal to the focused field tab", async () => { + describe("checkIsAutofillInlineMenuButtonVisible message handler", () => { + it("returns true when the inline menu button is visible", async () => { + overlayBackground["isInlineMenuButtonVisible"] = true; const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - sendMockExtensionMessage({ command: "toggleAutofillInlineMenuHidden" }, sender); - - expect(tabsSendMessageSpy).not.toHaveBeenCalled(); - }); - - it("posts a message to the overlay button and list which hides the menu", async () => { - const message = { - command: "toggleAutofillInlineMenuHidden", - isInlineMenuHidden: true, - setTransparentInlineMenu: false, - }; - - sendMockExtensionMessage(message); + sendMockExtensionMessage( + { command: "checkIsAutofillInlineMenuButtonVisible" }, + sender, + sendResponse, + ); await flushPromises(); - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "toggleAutofillInlineMenuHidden", - styles: { - display: "none", - opacity: "1", - }, - }); - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "toggleAutofillInlineMenuHidden", - styles: { - display: "none", - opacity: "1", - }, - }); - }); - }); - - describe("checkIsAutofillInlineMenuButtonVisible", () => { - it("sends a message to the top frame of the tab to identify if the inline menu button is visible", () => { - const sender = mock({ tab: { id: 1 } }); - - sendMockExtensionMessage({ command: "checkIsAutofillInlineMenuButtonVisible" }, sender); - - expect(tabsSendMessageSpy).toHaveBeenCalledWith( - sender.tab, - { command: "checkIsAutofillInlineMenuButtonVisible" }, - { frameId: 0 }, - ); + expect(sendResponse).toHaveBeenCalledWith(true); }); }); describe("checkIsAutofillInlineMenuListVisible message handler", () => { - it("sends a message to the top frame of the tab to identify if the inline menu list is visible", () => { + it("returns true when the inline menu list is visible", async () => { + overlayBackground["isInlineMenuListVisible"] = true; const sender = mock({ tab: { id: 1 } }); - sendMockExtensionMessage({ command: "checkIsAutofillInlineMenuListVisible" }, sender); - - expect(tabsSendMessageSpy).toHaveBeenCalledWith( - sender.tab, + sendMockExtensionMessage( { command: "checkIsAutofillInlineMenuListVisible" }, - { frameId: 0 }, + sender, + sendResponse, ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); }); }); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 0a3e924c9b..25aa65e682 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -74,6 +74,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { private focusedFieldData: FocusedFieldData; private isFieldCurrentlyFocused: boolean = false; private isFieldCurrentlyFilling: boolean = false; + private isInlineMenuButtonVisible: boolean = false; + private isInlineMenuListVisible: boolean = false; private iconsServerUrl: string; private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { autofillOverlayElementClosed: ({ message, sender }) => @@ -94,11 +96,10 @@ export class OverlayBackground implements OverlayBackgroundInterface { focusAutofillInlineMenuList: () => this.focusInlineMenuList(), updateAutofillInlineMenuPosition: ({ message, sender }) => this.updateInlineMenuPosition(message, sender), - toggleAutofillInlineMenuHidden: ({ message, sender }) => - this.toggleInlineMenuHidden(message, sender), - checkIsAutofillInlineMenuButtonVisible: ({ sender }) => - this.checkIsInlineMenuButtonVisible(sender), - checkIsAutofillInlineMenuListVisible: ({ sender }) => this.checkIsInlineMenuListVisible(sender), + updateAutofillInlineMenuElementIsVisibleStatus: ({ message, sender }) => + this.updateInlineMenuElementIsVisibleStatus(message, sender), + checkIsAutofillInlineMenuButtonVisible: () => this.checkIsInlineMenuButtonVisible(), + checkIsAutofillInlineMenuListVisible: () => this.checkIsInlineMenuListVisible(), getCurrentTabFrameId: ({ sender }) => this.getSenderFrameId(sender), updateSubFrameData: ({ message, sender }) => this.updateSubFrameData(message, sender), triggerSubFrameFocusInRebuild: ({ sender }) => this.triggerSubFrameFocusInRebuild(sender), @@ -453,7 +454,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { return; } - if (!(await this.checkIsInlineMenuButtonVisible(sender))) { + if (!this.checkIsInlineMenuButtonVisible()) { void this.toggleInlineMenuHidden( { isInlineMenuHidden: false, setTransparentInlineMenu: true }, sender, @@ -519,7 +520,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * if it is open, otherwise it will check the inline menu button. */ private checkInlineMenuFocused(sender: chrome.runtime.MessageSender) { - if (sender.tab.id !== this.focusedFieldData?.tabId) { + if (!this.senderTabHasFocusedField(sender)) { return; } @@ -561,6 +562,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { const sendOptions = { frameId: 0 }; if (forceCloseInlineMenu) { void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions); + this.isInlineMenuButtonVisible = false; + this.isInlineMenuListVisible = false; return; } @@ -574,9 +577,18 @@ export class OverlayBackground implements OverlayBackgroundInterface { { command, overlayElement: AutofillOverlayElement.List }, sendOptions, ); + this.isInlineMenuListVisible = false; return; } + if (overlayElement === AutofillOverlayElement.Button) { + this.isInlineMenuButtonVisible = false; + } + + if (overlayElement === AutofillOverlayElement.List) { + this.isInlineMenuListVisible = false; + } + void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions); } @@ -619,21 +631,24 @@ export class OverlayBackground implements OverlayBackgroundInterface { { overlayElement }: OverlayBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - if (sender.tab.id !== this.focusedFieldData?.tabId) { + if (!this.senderTabHasFocusedField(sender)) { this.expiredPorts.forEach((port) => port.disconnect()); this.expiredPorts = []; + return; } if (overlayElement === AutofillOverlayElement.Button) { this.inlineMenuButtonPort?.disconnect(); this.inlineMenuButtonPort = null; + this.isInlineMenuButtonVisible = false; return; } this.inlineMenuListPort?.disconnect(); this.inlineMenuListPort = null; + this.isInlineMenuListVisible = false; } /** @@ -647,7 +662,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { { overlayElement }: { overlayElement?: string }, sender: chrome.runtime.MessageSender, ) { - if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) { + if (!overlayElement || !this.senderTabHasFocusedField(sender)) { return; } @@ -686,6 +701,32 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.startInlineMenuFadeIn(); } + /** + * Triggers an update of the inline menu's visibility after the top level frame + * appends the element to the DOM. + * + * @param message - The message received from the content script + * @param sender - The sender of the port message + */ + private updateInlineMenuElementIsVisibleStatus( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + if (!this.senderTabHasFocusedField(sender)) { + return; + } + + const { overlayElement, isInlineMenuElementVisible } = message; + if (overlayElement === AutofillOverlayElement.Button) { + this.isInlineMenuButtonVisible = isInlineMenuElementVisible; + return; + } + + if (overlayElement === AutofillOverlayElement.List) { + this.isInlineMenuListVisible = isInlineMenuElementVisible; + } + } + /** * Handles updating the opacity of both the inline menu button and list. * This is used to simultaneously fade in the inline menu elements. @@ -797,7 +838,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { { isInlineMenuHidden, setTransparentInlineMenu }: ToggleInlineMenuHiddenMessage, sender: chrome.runtime.MessageSender, ) { - if (sender.tab.id !== this.focusedFieldData?.tabId) { + if (!this.senderTabHasFocusedField(sender)) { return; } @@ -810,15 +851,16 @@ export class OverlayBackground implements OverlayBackgroundInterface { styles = { ...styles, opacity }; } - await BrowserApi.tabSendMessage( - sender.tab, - { command: "toggleAutofillInlineMenuHidden", isInlineMenuHidden }, - { frameId: 0 }, - ); - const portMessage = { command: "toggleAutofillInlineMenuHidden", styles }; - this.inlineMenuButtonPort?.postMessage(portMessage); - this.inlineMenuListPort?.postMessage(portMessage); + if (this.inlineMenuButtonPort) { + this.isInlineMenuButtonVisible = !isInlineMenuHidden; + this.inlineMenuButtonPort.postMessage(portMessage); + } + + if (this.inlineMenuListPort) { + this.isInlineMenuListVisible = !isInlineMenuHidden; + this.inlineMenuListPort.postMessage(portMessage); + } if (setTransparentInlineMenu) { this.startInlineMenuFadeIn(); @@ -1010,7 +1052,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param sender - The sender of the port message */ private getNewVaultItemDetails({ sender }: chrome.runtime.Port) { - if (sender.tab.id !== this.focusedFieldData.tabId) { + if (!this.senderTabHasFocusedField(sender)) { return; } @@ -1095,33 +1137,17 @@ export class OverlayBackground implements OverlayBackgroundInterface { } /** - * 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 + * Returns the visibility status of the inline menu button. */ - private async checkIsInlineMenuButtonVisible( - sender: chrome.runtime.MessageSender, - ): Promise { - return await BrowserApi.tabSendMessage( - sender.tab, - { command: "checkIsAutofillInlineMenuButtonVisible" }, - { frameId: 0 }, - ); + private checkIsInlineMenuButtonVisible(): boolean { + return this.isInlineMenuButtonVisible; } /** - * 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 + * Returns the visibility status of the inline menu list. */ - private async checkIsInlineMenuListVisible( - sender: chrome.runtime.MessageSender, - ): Promise { - return await BrowserApi.tabSendMessage( - sender.tab, - { command: "checkIsAutofillInlineMenuListVisible" }, - { frameId: 0 }, - ); + private checkIsInlineMenuListVisible(): boolean { + return this.isInlineMenuListVisible; } /** @@ -1132,7 +1158,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param sender - The sender of the message */ private checkIsInlineMenuCiphersPopulated(sender: chrome.runtime.MessageSender) { - return sender.tab.id === this.focusedFieldData.tabId && this.inlineMenuCiphers.size > 0; + return this.senderTabHasFocusedField(sender) && this.inlineMenuCiphers.size > 0; } /** @@ -1166,7 +1192,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param sender - The sender of the message */ private checkShouldRepositionInlineMenu(sender: chrome.runtime.MessageSender): boolean { - if (!this.focusedFieldData || sender.tab.id !== this.focusedFieldData.tabId) { + if (!this.focusedFieldData || !this.senderTabHasFocusedField(sender)) { return false; } @@ -1186,6 +1212,15 @@ export class OverlayBackground implements OverlayBackgroundInterface { return false; } + /** + * Identifies if the sender tab is the same as the focused field's tab. + * + * @param sender - The sender of the message + */ + private senderTabHasFocusedField(sender: chrome.runtime.MessageSender) { + return sender.tab.id === this.focusedFieldData?.tabId; + } + /** * Triggers when a scroll or resize event occurs within a tab. Will reposition the inline menu * if the focused field is within the viewport. @@ -1448,10 +1483,12 @@ export class OverlayBackground implements OverlayBackgroundInterface { private handlePortOnDisconnect = (port: chrome.runtime.Port) => { if (port.name === AutofillOverlayPort.List) { this.inlineMenuListPort = null; + this.isInlineMenuListVisible = false; } if (port.name === AutofillOverlayPort.Button) { this.inlineMenuButtonPort = null; + this.isInlineMenuButtonVisible = false; } }; } diff --git a/apps/browser/src/autofill/content/abstractions/autofill-init.ts b/apps/browser/src/autofill/content/abstractions/autofill-init.ts index 1590a8dbb4..8b00b4ecc9 100644 --- a/apps/browser/src/autofill/content/abstractions/autofill-init.ts +++ b/apps/browser/src/autofill/content/abstractions/autofill-init.ts @@ -1,5 +1,6 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum"; import AutofillScript from "../../models/autofill-script"; export type AutofillExtensionMessage = { @@ -13,7 +14,7 @@ export type AutofillExtensionMessage = { pageDetailsUrl?: string; ciphers?: any; isInlineMenuHidden?: boolean; - overlayElement?: string; + overlayElement?: AutofillOverlayElementType; isFocusingFieldElement?: boolean; authStatus?: AuthenticationStatus; isOpeningFullInlineMenu?: boolean; diff --git a/apps/browser/src/autofill/enums/autofill-overlay.enum.ts b/apps/browser/src/autofill/enums/autofill-overlay.enum.ts index 8dc99e9c40..53f325d520 100644 --- a/apps/browser/src/autofill/enums/autofill-overlay.enum.ts +++ b/apps/browser/src/autofill/enums/autofill-overlay.enum.ts @@ -3,6 +3,9 @@ export const AutofillOverlayElement = { List: "autofill-inline-menu-list", } as const; +export type AutofillOverlayElementType = + (typeof AutofillOverlayElement)[keyof typeof AutofillOverlayElement]; + export const AutofillOverlayPort = { Button: "autofill-inline-menu-button-port", ButtonMessageConnector: "autofill-inline-menu-button-message-connector", diff --git a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-content.service.ts b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-content.service.ts index 21a4879c47..dc5a756250 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-content.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-content.service.ts @@ -4,9 +4,6 @@ export type InlineMenuExtensionMessageHandlers = { [key: string]: CallableFunction; closeAutofillInlineMenu: ({ message }: AutofillExtensionMessageParam) => void; appendAutofillInlineMenuToDom: ({ message }: AutofillExtensionMessageParam) => Promise; - toggleAutofillInlineMenuHidden: ({ message }: AutofillExtensionMessageParam) => void; - checkIsAutofillInlineMenuButtonVisible: () => boolean; - checkIsAutofillInlineMenuListVisible: () => boolean; }; export interface AutofillInlineMenuContentService { diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts index 147ffc612a..c5e1637fd4 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts @@ -1,5 +1,3 @@ -import { mock } from "jest-mock-extended"; - import AutofillInit from "../../../content/autofill-init"; import { AutofillOverlayElement } from "../../../enums/autofill-overlay.enum"; import { createMutationRecordMock } from "../../../spec/autofill-mocks"; @@ -13,7 +11,6 @@ describe("AutofillInlineMenuContentService", () => { let autofillInit: AutofillInit; let sendExtensionMessageSpy: jest.SpyInstance; let observeBodyMutationsSpy: jest.SpyInstance; - const sendResponseSpy = jest.fn(); beforeEach(() => { globalThis.document.body.innerHTML = ""; @@ -141,76 +138,6 @@ describe("AutofillInlineMenuContentService", () => { }); }); }); - - describe("toggleAutofillInlineMenuHidden message handler", () => { - it("sets the inline elements as hidden if the elements do not exist", () => { - sendMockExtensionMessage({ - command: "toggleAutofillInlineMenuHidden", - isInlineMenuHidden: false, - }); - - expect(autofillInlineMenuContentService["isButtonVisible"]).toBe(false); - expect(autofillInlineMenuContentService["isListVisible"]).toBe(false); - }); - - it("sets the inline elements as visible", () => { - autofillInlineMenuContentService["buttonElement"] = document.createElement("div"); - autofillInlineMenuContentService["listElement"] = document.createElement("div"); - - sendMockExtensionMessage({ - command: "toggleAutofillInlineMenuHidden", - isInlineMenuHidden: false, - }); - - expect(autofillInlineMenuContentService["isButtonVisible"]).toBe(true); - expect(autofillInlineMenuContentService["isListVisible"]).toBe(true); - }); - - it("sets the inline elements as hidden", () => { - autofillInlineMenuContentService["buttonElement"] = document.createElement("div"); - autofillInlineMenuContentService["listElement"] = document.createElement("div"); - autofillInlineMenuContentService["isButtonVisible"] = true; - autofillInlineMenuContentService["isListVisible"] = true; - - sendMockExtensionMessage({ - command: "toggleAutofillInlineMenuHidden", - isInlineMenuHidden: true, - }); - - expect(autofillInlineMenuContentService["isButtonVisible"]).toBe(false); - expect(autofillInlineMenuContentService["isListVisible"]).toBe(false); - }); - }); - - describe("checkIsAutofillInlineMenuButtonVisible message handler", () => { - it("returns true if the inline menu button is visible", async () => { - autofillInlineMenuContentService["isButtonVisible"] = true; - - sendMockExtensionMessage( - { command: "checkIsAutofillInlineMenuButtonVisible" }, - mock(), - sendResponseSpy, - ); - await flushPromises(); - - expect(sendResponseSpy).toHaveBeenCalledWith(true); - }); - }); - - describe("checkIsAutofillInlineMenuListVisible message handler", () => { - it("returns true if the inline menu list is visible", async () => { - autofillInlineMenuContentService["isListVisible"] = true; - - sendMockExtensionMessage( - { command: "checkIsAutofillInlineMenuListVisible" }, - mock(), - sendResponseSpy, - ); - await flushPromises(); - - expect(sendResponseSpy).toHaveBeenCalledWith(true); - }); - }); }); describe("handleInlineMenuElementMutationObserverUpdate", () => { @@ -295,6 +222,7 @@ describe("AutofillInlineMenuContentService", () => { describe("handleBodyElementMutationObserverUpdate", () => { let buttonElement: HTMLElement; let listElement: HTMLElement; + let isInlineMenuListVisibleSpy: jest.SpyInstance; beforeEach(() => { document.body.innerHTML = ` @@ -305,7 +233,9 @@ describe("AutofillInlineMenuContentService", () => { listElement = document.querySelector(".overlay-list") as HTMLElement; autofillInlineMenuContentService["buttonElement"] = buttonElement; autofillInlineMenuContentService["listElement"] = listElement; - autofillInlineMenuContentService["isListVisible"] = true; + isInlineMenuListVisibleSpy = jest + .spyOn(autofillInlineMenuContentService as any, "isInlineMenuListVisible") + .mockResolvedValue(true); jest.spyOn(globalThis.document.body, "insertBefore"); jest .spyOn( @@ -315,16 +245,16 @@ describe("AutofillInlineMenuContentService", () => { .mockReturnValue(false); }); - it("skips handling the mutation if the overlay elements are not present in the DOM", () => { + it("skips handling the mutation if the overlay elements are not present in the DOM", async () => { autofillInlineMenuContentService["buttonElement"] = undefined; autofillInlineMenuContentService["listElement"] = undefined; - autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); }); - it("skips handling the mutation if excessive mutations are being triggered", () => { + it("skips handling the mutation if excessive mutations are being triggered", async () => { jest .spyOn( autofillInlineMenuContentService as any, @@ -332,31 +262,31 @@ describe("AutofillInlineMenuContentService", () => { ) .mockReturnValue(true); - autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); }); - it("skips re-arranging the DOM elements if the last child of the body is the overlay list and the second to last child of the body is the overlay button", () => { - autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + it("skips re-arranging the DOM elements if the last child of the body is the overlay list and the second to last child of the body is the overlay button", async () => { + await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); }); - it("skips re-arranging the DOM elements if the last child is the overlay button and the overlay list is not visible", () => { + it("skips re-arranging the DOM elements if the last child is the overlay button and the overlay list is not visible", async () => { listElement.remove(); - autofillInlineMenuContentService["isListVisible"] = false; + isInlineMenuListVisibleSpy.mockResolvedValue(false); - autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); }); - it("positions the overlay button before the overlay list if an element has inserted itself after the button element", () => { + it("positions the overlay button before the overlay list if an element has inserted itself after the button element", async () => { const injectedElement = document.createElement("div"); document.body.insertBefore(injectedElement, listElement); - autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith( buttonElement, @@ -364,10 +294,10 @@ describe("AutofillInlineMenuContentService", () => { ); }); - it("positions the overlay button before the overlay list if the elements have inserted in incorrect order", () => { + it("positions the overlay button before the overlay list if the elements have inserted in incorrect order", async () => { document.body.appendChild(buttonElement); - autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith( buttonElement, @@ -375,11 +305,11 @@ describe("AutofillInlineMenuContentService", () => { ); }); - it("positions the last child before the overlay button if it is not the overlay list", () => { + it("positions the last child before the overlay button if it is not the overlay list", async () => { const injectedElement = document.createElement("div"); document.body.appendChild(injectedElement); - autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith( injectedElement, diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts index a9e3b541d3..b446b94567 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts @@ -1,5 +1,8 @@ import { AutofillExtensionMessage } from "../../../content/abstractions/autofill-init"; -import { AutofillOverlayElement } from "../../../enums/autofill-overlay.enum"; +import { + AutofillOverlayElement, + AutofillOverlayElementType, +} from "../../../enums/autofill-overlay.enum"; import { sendExtensionMessage, generateRandomCustomElementName, @@ -21,8 +24,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1; private buttonElement: HTMLElement; private listElement: HTMLElement; - private isButtonVisible = false; - private isListVisible = false; private inlineMenuElementsMutationObserver: MutationObserver; private bodyElementMutationObserver: MutationObserver; private mutationObserverIterations = 0; @@ -36,9 +37,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte private readonly extensionMessageHandlers: InlineMenuExtensionMessageHandlers = { closeAutofillInlineMenu: ({ message }) => this.closeInlineMenu(message), appendAutofillInlineMenuToDom: ({ message }) => this.appendInlineMenuElements(message), - toggleAutofillInlineMenuHidden: ({ message }) => this.toggleInlineMenuHidden(message), - checkIsAutofillInlineMenuButtonVisible: () => this.isInlineMenuButtonVisible(), - checkIsAutofillInlineMenuListVisible: () => this.isInlineMenuListVisible(), }; constructor() { @@ -62,28 +60,23 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } /** - * Identifies if the inline menu button is currently visible. + * Checks if the inline menu button is visible at the top frame. */ - private isInlineMenuButtonVisible() { - return this.isButtonVisible; + private async isInlineMenuButtonVisible() { + return ( + !!this.buttonElement && + (await this.sendExtensionMessage("checkIsAutofillInlineMenuButtonVisible")) === true + ); } /** - * Identifies if the inline menu list is currently visible. + * Checks if the inline menu list if visible at the top frame. */ - private isInlineMenuListVisible() { - return this.isListVisible; - } - - /** - * Sends a message that facilitates hiding the inline menu elements. - * - * @param message - The message that contains the visibility state of the inline menu elements. - */ - private toggleInlineMenuHidden(message: AutofillExtensionMessage) { - const { isInlineMenuHidden } = message; - this.isButtonVisible = !!this.buttonElement && !isInlineMenuHidden; - this.isListVisible = !!this.listElement && !isInlineMenuHidden; + private async isInlineMenuListVisible() { + return ( + !!this.listElement && + (await this.sendExtensionMessage("checkIsAutofillInlineMenuListVisible")) === true + ); } /** @@ -114,7 +107,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte private closeInlineMenuButton() { if (this.buttonElement) { this.buttonElement.remove(); - this.isButtonVisible = false; void this.sendExtensionMessage("autofillOverlayElementClosed", { overlayElement: AutofillOverlayElement.Button, }); @@ -127,7 +119,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte private closeInlineMenuList() { if (this.listElement) { this.listElement.remove(); - this.isListVisible = false; void this.sendExtensionMessage("autofillOverlayElementClosed", { overlayElement: AutofillOverlayElement.List, }); @@ -154,9 +145,9 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte this.updateCustomElementDefaultStyles(this.buttonElement); } - if (!this.isButtonVisible) { + if (!(await this.isInlineMenuButtonVisible())) { this.appendInlineMenuElementToBody(this.buttonElement); - this.isButtonVisible = true; + this.updateInlineMenuElementIsVisibleStatus(AutofillOverlayElement.Button, true); } } @@ -169,12 +160,28 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte this.updateCustomElementDefaultStyles(this.listElement); } - if (!this.isListVisible) { + if (!(await this.isInlineMenuListVisible())) { this.appendInlineMenuElementToBody(this.listElement); - this.isListVisible = true; + this.updateInlineMenuElementIsVisibleStatus(AutofillOverlayElement.List, true); } } + /** + * Updates the visibility status of the inline menu element within the background script. + * + * @param overlayElement - The inline menu element to update the visibility status for. + * @param isInlineMenuElementVisible - The visibility status to update the inline menu element to. + */ + private updateInlineMenuElementIsVisibleStatus( + overlayElement: AutofillOverlayElementType, + isInlineMenuElementVisible: boolean, + ) { + void this.sendExtensionMessage("updateAutofillInlineMenuElementIsVisibleStatus", { + overlayElement, + isInlineMenuElementVisible, + }); + } + /** * Appends the inline menu element to the body element. This method will also * observe the body element to ensure that the inline menu element is not @@ -360,7 +367,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * ensure that the inline menu elements are always present at the bottom of the * body element. */ - private handleBodyElementMutationObserverUpdate = () => { + private handleBodyElementMutationObserverUpdate = async () => { if ( (!this.buttonElement && !this.listElement) || this.isTriggeringExcessiveMutationObserverIterations() @@ -377,14 +384,14 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte if ( !lastChild || (lastChildIsInlineMenuList && secondToLastChildIsInlineMenuButton) || - (lastChildIsInlineMenuButton && !this.isListVisible) + (lastChildIsInlineMenuButton && !(await this.isInlineMenuListVisible())) ) { return; } if ( (lastChildIsInlineMenuList && !secondToLastChildIsInlineMenuButton) || - (lastChildIsInlineMenuButton && this.isListVisible) + (lastChildIsInlineMenuButton && (await this.isInlineMenuListVisible())) ) { globalThis.document.body.insertBefore(this.buttonElement, this.listElement); return;