From 4af4a863be67044069efe5315db677aeaecd0bdc Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Fri, 22 Aug 2025 11:09:00 -0400 Subject: [PATCH] [PM-25025] Additional defense against top-layer content (#16101) * additional defense against top-layer content * fix error and update tests --- ...tofill-inline-menu-content.service.spec.ts | 31 ++++++++++--- .../autofill-inline-menu-content.service.ts | 46 ++++++++++++------- 2 files changed, 54 insertions(+), 23 deletions(-) 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 62f9dbec824..2c9484c3a8b 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 @@ -41,6 +41,10 @@ describe("AutofillInlineMenuContentService", () => { autofillInlineMenuContentService as any, "sendExtensionMessage", ); + jest.spyOn(autofillInlineMenuContentService as any, "getPageIsOpaque"); + jest + .spyOn(autofillInlineMenuContentService as any, "getPageTopLayerInUse") + .mockResolvedValue(false); }); afterEach(() => { @@ -386,30 +390,45 @@ describe("AutofillInlineMenuContentService", () => { expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); }); + it("closes the inline menu if the page has content in the top layer", async () => { + document.querySelector("html").style.opacity = "1"; + document.body.style.opacity = "1"; + + jest + .spyOn(autofillInlineMenuContentService as any, "getPageTopLayerInUse") + .mockResolvedValue(true); + + await autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]); + + expect(autofillInlineMenuContentService["getPageIsOpaque"]).toHaveReturnedWith(true); + expect(autofillInlineMenuContentService["closeInlineMenu"]).toHaveBeenCalled(); + }); + it("closes the inline menu if the page body is not sufficiently opaque", async () => { document.querySelector("html").style.opacity = "0.9"; document.body.style.opacity = "0"; - autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]); + await autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]); - expect(autofillInlineMenuContentService["pageIsOpaque"]).toBe(false); + expect(autofillInlineMenuContentService["getPageIsOpaque"]).toHaveReturnedWith(false); expect(autofillInlineMenuContentService["closeInlineMenu"]).toHaveBeenCalled(); }); it("closes the inline menu if the page html is not sufficiently opaque", async () => { document.querySelector("html").style.opacity = "0.3"; document.body.style.opacity = "0.7"; - autofillInlineMenuContentService["handlePageMutations"]([mockHTMLMutationRecord]); + await autofillInlineMenuContentService["handlePageMutations"]([mockHTMLMutationRecord]); - expect(autofillInlineMenuContentService["pageIsOpaque"]).toBe(false); + expect(autofillInlineMenuContentService["getPageIsOpaque"]).toHaveReturnedWith(false); expect(autofillInlineMenuContentService["closeInlineMenu"]).toHaveBeenCalled(); }); it("does not close the inline menu if the page html and body is sufficiently opaque", async () => { document.querySelector("html").style.opacity = "0.9"; document.body.style.opacity = "1"; - autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]); + await autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]); + await waitForIdleCallback(); - expect(autofillInlineMenuContentService["pageIsOpaque"]).toBe(true); + expect(autofillInlineMenuContentService["getPageIsOpaque"]).toHaveReturnedWith(true); expect(autofillInlineMenuContentService["closeInlineMenu"]).not.toHaveBeenCalled(); }); 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 de401bf7e28..c531215af88 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 @@ -33,7 +33,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte private listElement?: HTMLElement; private htmlMutationObserver: MutationObserver; private bodyMutationObserver: MutationObserver; - private pageIsOpaque = true; private inlineMenuElementsMutationObserver: MutationObserver; private containerElementMutationObserver: MutationObserver; private mutationObserverIterations = 0; @@ -52,7 +51,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte }; constructor() { - this.checkPageOpacity(); this.setupMutationObserver(); } @@ -405,20 +403,35 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte }); }; - private checkPageOpacity = () => { - this.pageIsOpaque = this.getPageIsOpaque(); + private checkPageRisks = async () => { + const pageIsOpaque = await this.getPageIsOpaque(); + const pageTopLayerInUse = await this.getPageTopLayerInUse(); - if (!this.pageIsOpaque) { + const risksFound = !pageIsOpaque || pageTopLayerInUse; + + if (risksFound) { this.closeInlineMenu(); } + + return risksFound; + }; + + /* + * Checks for known risks at the page level + */ + private handlePageMutations = async (mutations: MutationRecord[]) => { + if (mutations.some(({ type }) => type === "attributes")) { + await this.checkPageRisks(); + } }; - private handlePageMutations = (mutations: MutationRecord[]) => { - for (const mutation of mutations) { - if (mutation.type === "attributes") { - this.checkPageOpacity(); - } - } + /** + * Checks if the page top layer has content (will obscure/overlap the inline menu) + */ + private getPageTopLayerInUse = () => { + const pageHasOpenPopover = !!globalThis.document.querySelector(":popover-open"); + + return pageHasOpenPopover; }; /** @@ -427,7 +440,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * of parents below the body. Assumes the target element will be a direct child of the page * `body` (enforced elsewhere). */ - private getPageIsOpaque() { + private getPageIsOpaque = () => { // These are computed style values, so we don't need to worry about non-float values // for `opacity`, here const htmlOpacity = globalThis.window.getComputedStyle( @@ -441,17 +454,16 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte const opacityThreshold = 0.6; return parseFloat(htmlOpacity) > opacityThreshold && parseFloat(bodyOpacity) > opacityThreshold; - } + }; /** * Processes the mutation of the element that contains the inline menu. Will trigger when an * idle moment in the execution of the main thread is detected. */ private processContainerElementMutation = async (containerElement: HTMLElement) => { - // If the computed opacity of the body and parent is not sufficiently opaque, tear - // down and prevent building the inline menu experience. - this.checkPageOpacity(); - if (!this.pageIsOpaque) { + // If the page contains risks, tear down and prevent building the inline menu experience. + const pageRisksFound = await this.checkPageRisks(); + if (pageRisksFound) { return; }