diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts
index 0e675a1e39d..0a0bcbba670 100644
--- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts
+++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts
@@ -1,14 +1,14 @@
import { mock } from "jest-mock-extended";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-import { EVENTS, AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
+import { AutofillOverlayVisibility, EVENTS } from "@bitwarden/common/autofill/constants";
import AutofillInit from "../content/autofill-init";
import { AutofillOverlayElement, RedirectFocusDirection } from "../enums/autofill-overlay.enum";
import AutofillField from "../models/autofill-field";
import { createAutofillFieldMock } from "../spec/autofill-mocks";
import { flushPromises, sendMockExtensionMessage } from "../spec/testing-utils";
-import { ElementWithOpId, FormFieldElement } from "../types";
+import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
import { AutoFillConstants } from "./autofill-constants";
import { AutofillOverlayContentService } from "./autofill-overlay-content.service";
@@ -44,6 +44,10 @@ describe("AutofillOverlayContentService", () => {
value: 1080,
writable: true,
});
+ Object.defineProperty(window, "top", {
+ value: window,
+ writable: true,
+ });
});
afterEach(() => {
@@ -327,7 +331,7 @@ describe("AutofillOverlayContentService", () => {
jest.spyOn(globalThis.customElements, "define").mockImplementation();
});
- it("closes the autofill overlay when the `Escape` key is pressed", () => {
+ it("closes the autofill inline menu when the `Escape` key is pressed", () => {
autofillFieldElement.dispatchEvent(new KeyboardEvent("keyup", { code: "Escape" }));
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
@@ -509,7 +513,7 @@ describe("AutofillOverlayContentService", () => {
});
});
- it("opens the autofill overlay if the form field is empty", async () => {
+ it("opens the autofill inline menu if the form field is empty", async () => {
jest.spyOn(autofillOverlayContentService as any, "openAutofillInlineMenu");
(autofillFieldElement as HTMLInputElement).value = "";
@@ -523,7 +527,7 @@ describe("AutofillOverlayContentService", () => {
expect(autofillOverlayContentService["openAutofillInlineMenu"]).toHaveBeenCalled();
});
- it("opens the autofill overlay if the form field is empty and the user is authed", async () => {
+ it("opens the autofill inline menu if the form field is empty and the user is authed", async () => {
jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(true);
jest.spyOn(autofillOverlayContentService as any, "openAutofillInlineMenu");
(autofillFieldElement as HTMLInputElement).value = "";
@@ -538,7 +542,7 @@ describe("AutofillOverlayContentService", () => {
expect(autofillOverlayContentService["openAutofillInlineMenu"]).toHaveBeenCalled();
});
- it("opens the autofill overlay if the form field is empty and the overlay ciphers are not populated", async () => {
+ it("opens the autofill inline menu if the form field is empty and the overlay ciphers are not populated", async () => {
jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(false);
jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuCiphersPopulated")
@@ -558,18 +562,10 @@ describe("AutofillOverlayContentService", () => {
});
describe("form field click event listener", () => {
- let isInlineMenuButtonVisibleSpy: jest.SpyInstance;
-
beforeEach(async () => {
jest
.spyOn(autofillOverlayContentService as any, "triggerFormFieldFocusedAction")
.mockImplementation();
- isInlineMenuButtonVisibleSpy = jest
- .spyOn(autofillOverlayContentService as any, "isInlineMenuButtonVisible")
- .mockResolvedValue(false);
- jest
- .spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
- .mockResolvedValue(false);
await autofillOverlayContentService.setupAutofillInlineMenuListenerOnField(
autofillFieldElement,
autofillFieldData,
@@ -584,7 +580,8 @@ describe("AutofillOverlayContentService", () => {
});
it("skips triggering the field focused handler if the overlay list is visible", () => {
- isInlineMenuButtonVisibleSpy.mockResolvedValue(true);
+ // Mock resolved value from `isInlineMenuButtonVisible` message
+ sendExtensionMessageSpy.mockResolvedValueOnce(true);
autofillFieldElement.dispatchEvent(new Event("click"));
@@ -594,7 +591,8 @@ describe("AutofillOverlayContentService", () => {
});
it("skips triggering the field focused handler if the overlay button is visible", () => {
- isInlineMenuButtonVisibleSpy.mockResolvedValue(true);
+ // Mock resolved value from `isInlineMenuButtonVisible` message
+ sendExtensionMessageSpy.mockResolvedValueOnce(true);
autofillFieldElement.dispatchEvent(new Event("click"));
@@ -686,7 +684,7 @@ describe("AutofillOverlayContentService", () => {
});
});
- it("opens the autofill overlay if the form element has no value", async () => {
+ it("opens the autofill inline menu if the form element has no value", async () => {
(autofillFieldElement as HTMLInputElement).value = "";
autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnFieldFocus;
@@ -701,7 +699,7 @@ describe("AutofillOverlayContentService", () => {
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("openAutofillInlineMenu");
});
- it("opens the autofill overlay if the overlay ciphers are not populated and the user is authed", async () => {
+ it("opens the autofill inline menu if the overlay ciphers are not populated and the user is authed", async () => {
(autofillFieldElement as HTMLInputElement).value = "";
autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnFieldFocus;
@@ -859,10 +857,9 @@ describe("AutofillOverlayContentService", () => {
`;
- const usernameField = document.getElementById(
+ autofillOverlayContentService["mostRecentlyFocusedField"] = document.getElementById(
"username-field",
) as ElementWithOpId;
- autofillOverlayContentService["mostRecentlyFocusedField"] = usernameField;
autofillOverlayContentService["setOverlayRepositionEventListeners"]();
checkShouldRepositionInlineMenuSpy = jest
.spyOn(autofillOverlayContentService as any, "checkShouldRepositionInlineMenu")
@@ -942,7 +939,9 @@ describe("AutofillOverlayContentService", () => {
globalThis.dispatchEvent(new Event(EVENTS.SCROLL));
await flushPromises();
- jest.advanceTimersByTime(800);
+ jest.advanceTimersByTime(750);
+ await flushPromises();
+ jest.advanceTimersByTime(50);
await flushPromises();
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
@@ -954,7 +953,36 @@ describe("AutofillOverlayContentService", () => {
expect(clearUserInteractionEventTimeoutSpy).toHaveBeenCalled();
});
- it("removes the autofill overlay if the focused field is outside of the viewport", async () => {
+ it("removes the inline menu list if the focused field has a value", async () => {
+ jest.useFakeTimers();
+ jest
+ .spyOn(autofillOverlayContentService as any, "updateMostRecentlyFocusedField")
+ .mockImplementation(() => {
+ autofillOverlayContentService["focusedFieldData"] = {
+ focusedFieldRects: {
+ top: 100,
+ },
+ focusedFieldStyles: {},
+ };
+ });
+ (
+ autofillOverlayContentService["mostRecentlyFocusedField"] as FillableFormFieldElement
+ ).value = "test";
+
+ globalThis.dispatchEvent(new Event(EVENTS.SCROLL));
+ await flushPromises();
+ jest.advanceTimersByTime(750);
+ await flushPromises();
+ jest.advanceTimersByTime(50);
+ await flushPromises();
+
+ expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
+ overlayElement: AutofillOverlayElement.List,
+ forceCloseAutofillInlineMenu: true,
+ });
+ });
+
+ it("removes the autofill inline menu if the focused field is outside of the viewport", async () => {
jest.useFakeTimers();
jest
.spyOn(autofillOverlayContentService as any, "updateMostRecentlyFocusedField")
@@ -1165,7 +1193,7 @@ describe("AutofillOverlayContentService", () => {
expect(autofillOverlayContentService["authStatus"]).toEqual(AuthenticationStatus.Unlocked);
});
- it("opens both autofill overlay elements", () => {
+ it("opens both autofill inline menu elements", () => {
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
sendMockExtensionMessage({ command: "openAutofillInlineMenu" });
@@ -1178,7 +1206,7 @@ describe("AutofillOverlayContentService", () => {
});
});
- it("opens the autofill overlay button only if overlay visibility is set for onButtonClick", () => {
+ it("opens the autofill inline menu button only if overlay visibility is set for onButtonClick", () => {
autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnButtonClick;
@@ -1557,6 +1585,81 @@ describe("AutofillOverlayContentService", () => {
"*",
);
});
+
+ describe("calculateSubFramePositioning", () => {
+ beforeEach(() => {
+ autofillOverlayContentService.init();
+ jest.spyOn(globalThis.parent, "postMessage");
+ document.body.innerHTML = ``;
+ });
+
+ it("calculates the sub frame offset for the current frame and sends those values to the parent if not in the top frame", async () => {
+ Object.defineProperty(window, "top", {
+ value: null,
+ writable: true,
+ });
+ document.body.innerHTML = ``;
+ const iframe = document.querySelector("iframe") as HTMLIFrameElement;
+ const subFrameData = {
+ url: "https://example.com/",
+ frameId: 10,
+ left: 0,
+ top: 0,
+ parentFrameIds: [1, 2, 3],
+ };
+ const event = mock();
+ // @ts-expect-error - Need to mock the source to be the iframe content window
+ event.source = iframe.contentWindow;
+ event.data.subFrameData = subFrameData;
+ sendExtensionMessageSpy.mockResolvedValue(4);
+
+ await autofillOverlayContentService["calculateSubFramePositioning"](event);
+ await flushPromises();
+
+ expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
+ {
+ command: "calculateSubFramePositioning",
+ subFrameData: {
+ frameId: 10,
+ left: 2,
+ parentFrameIds: [1, 2, 3, 4],
+ top: 2,
+ url: "https://example.com/",
+ },
+ },
+ "*",
+ );
+ });
+
+ it("posts the calculated sub frame data to the background", async () => {
+ document.body.innerHTML = ``;
+ const iframe = document.querySelector("iframe") as HTMLIFrameElement;
+ const subFrameData = {
+ url: "https://example.com/",
+ frameId: 10,
+ left: 0,
+ top: 0,
+ parentFrameIds: [1, 2, 3],
+ };
+ const event = mock();
+ // @ts-expect-error - Need to mock the source to be the iframe content window
+ event.source = iframe.contentWindow;
+ event.data.subFrameData = subFrameData;
+
+ await autofillOverlayContentService["calculateSubFramePositioning"](event);
+ await flushPromises();
+
+ expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateSubFrameData", {
+ subFrameData: {
+ frameId: 10,
+ left: 2,
+ top: 2,
+ url: "https://example.com/",
+ parentFrameIds: [1, 2, 3],
+ },
+ });
+ });
+ });
});
describe("checkMostRecentlyFocusedFieldHasValue", () => {
diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
index 2732847c67f..7719458b82f 100644
--- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
+++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
@@ -205,7 +205,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
if (direction === RedirectFocusDirection.Current) {
this.focusMostRecentlyFocusedField();
- setTimeout(() => void this.sendExtensionMessage("closeAutofillInlineMenu"), 100);
+ globalThis.setTimeout(() => void this.sendExtensionMessage("closeAutofillInlineMenu"), 100);
return;
}
@@ -343,7 +343,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
if (this.mostRecentlyFocusedField && !(await this.isInlineMenuListVisible())) {
await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField);
this.openAutofillInlineMenu({ isOpeningFullAutofillInlineMenu: true });
- setTimeout(() => this.sendExtensionMessage("focusAutofillInlineMenuList"), 125);
+ globalThis.setTimeout(() => this.sendExtensionMessage("focusAutofillInlineMenuList"), 125);
return;
}
@@ -808,7 +808,10 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
this.rebuildSubFrameOffsets();
this.toggleAutofillInlineMenuHidden(true);
this.clearUserInteractionEventTimeout();
- this.userInteractionEventTimeout = setTimeout(this.triggerOverlayRepositionUpdates, 750);
+ this.userInteractionEventTimeout = globalThis.setTimeout(
+ this.triggerOverlayRepositionUpdates,
+ 750,
+ );
};
/**
@@ -816,7 +819,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
*/
private rebuildSubFrameOffsets() {
this.clearRecalculateSubFrameOffsetsTimeout();
- this.recalculateSubFrameOffsetsTimeout = setTimeout(
+ this.recalculateSubFrameOffsetsTimeout = globalThis.setTimeout(
() => void this.sendExtensionMessage("rebuildSubFrameOffsets"),
150,
);
@@ -837,7 +840,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField);
this.updateAutofillInlineMenuElementsPosition();
- setTimeout(async () => {
+ globalThis.setTimeout(async () => {
this.toggleAutofillInlineMenuHidden(false, true);
if (
await this.hideAutofillInlineMenuListOnFilledField(
@@ -931,7 +934,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* autofill overlay if the document is not visible.
*/
private handleVisibilityChangeEvent = () => {
- if (!this.mostRecentlyFocusedField || document.visibilityState === "visible") {
+ if (!this.mostRecentlyFocusedField || globalThis.document.visibilityState === "visible") {
return;
}
@@ -968,7 +971,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
const subFrameUrlWithoutTrailingSlash = subFrameUrl?.replace(/\/$/, "");
let iframeElement: HTMLIFrameElement | null = null;
- const iframeElements = document.querySelectorAll(
+ const iframeElements = globalThis.document.querySelectorAll(
`iframe[src="${subFrameUrl}"], iframe[src="${subFrameUrlWithoutTrailingSlash}"]`,
) as NodeListOf;
if (iframeElements.length === 1) {
@@ -1052,7 +1055,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
private calculateSubFramePositioning = async (event: MessageEvent) => {
const subFrameData = event.data.subFrameData;
let subFrameOffsets: SubFrameOffsetData;
- const iframes = document.querySelectorAll("iframe");
+ const iframes = globalThis.document.querySelectorAll("iframe");
for (let i = 0; i < iframes.length; i++) {
if (iframes[i].contentWindow === event.source) {
const iframeElement = iframes[i];
@@ -1061,11 +1064,14 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
subFrameData.url,
subFrameData.frameId,
);
- const parentFrameId = await this.sendExtensionMessage("getCurrentTabFrameId");
subFrameData.top += subFrameOffsets.top;
subFrameData.left += subFrameOffsets.left;
- subFrameData.parentFrameIds.push(parentFrameId);
+
+ const parentFrameId = await this.sendExtensionMessage("getCurrentTabFrameId");
+ if (typeof parentFrameId !== "undefined") {
+ subFrameData.parentFrameIds.push(parentFrameId);
+ }
break;
}
@@ -1076,7 +1082,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return;
}
- void sendExtensionMessage("updateSubFrameData", {
+ void this.sendExtensionMessage("updateSubFrameData", {
subFrameData,
});
};