1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 00:33:44 +00:00

[PM-5189] Working through jest tests for the AutofillOverlayContentService

This commit is contained in:
Cesar Gonzalez
2024-06-07 15:14:08 -05:00
parent c5169c96ee
commit 17fa4f57f9
2 changed files with 144 additions and 35 deletions

View File

@@ -1,14 +1,14 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; 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 AutofillInit from "../content/autofill-init";
import { AutofillOverlayElement, RedirectFocusDirection } from "../enums/autofill-overlay.enum"; import { AutofillOverlayElement, RedirectFocusDirection } from "../enums/autofill-overlay.enum";
import AutofillField from "../models/autofill-field"; import AutofillField from "../models/autofill-field";
import { createAutofillFieldMock } from "../spec/autofill-mocks"; import { createAutofillFieldMock } from "../spec/autofill-mocks";
import { flushPromises, sendMockExtensionMessage } from "../spec/testing-utils"; import { flushPromises, sendMockExtensionMessage } from "../spec/testing-utils";
import { ElementWithOpId, FormFieldElement } from "../types"; import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
import { AutoFillConstants } from "./autofill-constants"; import { AutoFillConstants } from "./autofill-constants";
import { AutofillOverlayContentService } from "./autofill-overlay-content.service"; import { AutofillOverlayContentService } from "./autofill-overlay-content.service";
@@ -44,6 +44,10 @@ describe("AutofillOverlayContentService", () => {
value: 1080, value: 1080,
writable: true, writable: true,
}); });
Object.defineProperty(window, "top", {
value: window,
writable: true,
});
}); });
afterEach(() => { afterEach(() => {
@@ -327,7 +331,7 @@ describe("AutofillOverlayContentService", () => {
jest.spyOn(globalThis.customElements, "define").mockImplementation(); 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" })); autofillFieldElement.dispatchEvent(new KeyboardEvent("keyup", { code: "Escape" }));
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", { 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"); jest.spyOn(autofillOverlayContentService as any, "openAutofillInlineMenu");
(autofillFieldElement as HTMLInputElement).value = ""; (autofillFieldElement as HTMLInputElement).value = "";
@@ -523,7 +527,7 @@ describe("AutofillOverlayContentService", () => {
expect(autofillOverlayContentService["openAutofillInlineMenu"]).toHaveBeenCalled(); 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, "isUserAuthed").mockReturnValue(true);
jest.spyOn(autofillOverlayContentService as any, "openAutofillInlineMenu"); jest.spyOn(autofillOverlayContentService as any, "openAutofillInlineMenu");
(autofillFieldElement as HTMLInputElement).value = ""; (autofillFieldElement as HTMLInputElement).value = "";
@@ -538,7 +542,7 @@ describe("AutofillOverlayContentService", () => {
expect(autofillOverlayContentService["openAutofillInlineMenu"]).toHaveBeenCalled(); 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, "isUserAuthed").mockReturnValue(false);
jest jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuCiphersPopulated") .spyOn(autofillOverlayContentService as any, "isInlineMenuCiphersPopulated")
@@ -558,18 +562,10 @@ describe("AutofillOverlayContentService", () => {
}); });
describe("form field click event listener", () => { describe("form field click event listener", () => {
let isInlineMenuButtonVisibleSpy: jest.SpyInstance;
beforeEach(async () => { beforeEach(async () => {
jest jest
.spyOn(autofillOverlayContentService as any, "triggerFormFieldFocusedAction") .spyOn(autofillOverlayContentService as any, "triggerFormFieldFocusedAction")
.mockImplementation(); .mockImplementation();
isInlineMenuButtonVisibleSpy = jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuButtonVisible")
.mockResolvedValue(false);
jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
.mockResolvedValue(false);
await autofillOverlayContentService.setupAutofillInlineMenuListenerOnField( await autofillOverlayContentService.setupAutofillInlineMenuListenerOnField(
autofillFieldElement, autofillFieldElement,
autofillFieldData, autofillFieldData,
@@ -584,7 +580,8 @@ describe("AutofillOverlayContentService", () => {
}); });
it("skips triggering the field focused handler if the overlay list is visible", () => { 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")); autofillFieldElement.dispatchEvent(new Event("click"));
@@ -594,7 +591,8 @@ describe("AutofillOverlayContentService", () => {
}); });
it("skips triggering the field focused handler if the overlay button is visible", () => { 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")); 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 = ""; (autofillFieldElement as HTMLInputElement).value = "";
autofillOverlayContentService["inlineMenuVisibility"] = autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnFieldFocus; AutofillOverlayVisibility.OnFieldFocus;
@@ -701,7 +699,7 @@ describe("AutofillOverlayContentService", () => {
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("openAutofillInlineMenu"); 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 = ""; (autofillFieldElement as HTMLInputElement).value = "";
autofillOverlayContentService["inlineMenuVisibility"] = autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnFieldFocus; AutofillOverlayVisibility.OnFieldFocus;
@@ -859,10 +857,9 @@ describe("AutofillOverlayContentService", () => {
<input type="password" id="password-field" placeholder="password" /> <input type="password" id="password-field" placeholder="password" />
</form> </form>
`; `;
const usernameField = document.getElementById( autofillOverlayContentService["mostRecentlyFocusedField"] = document.getElementById(
"username-field", "username-field",
) as ElementWithOpId<HTMLInputElement>; ) as ElementWithOpId<HTMLInputElement>;
autofillOverlayContentService["mostRecentlyFocusedField"] = usernameField;
autofillOverlayContentService["setOverlayRepositionEventListeners"](); autofillOverlayContentService["setOverlayRepositionEventListeners"]();
checkShouldRepositionInlineMenuSpy = jest checkShouldRepositionInlineMenuSpy = jest
.spyOn(autofillOverlayContentService as any, "checkShouldRepositionInlineMenu") .spyOn(autofillOverlayContentService as any, "checkShouldRepositionInlineMenu")
@@ -942,7 +939,9 @@ describe("AutofillOverlayContentService", () => {
globalThis.dispatchEvent(new Event(EVENTS.SCROLL)); globalThis.dispatchEvent(new Event(EVENTS.SCROLL));
await flushPromises(); await flushPromises();
jest.advanceTimersByTime(800); jest.advanceTimersByTime(750);
await flushPromises();
jest.advanceTimersByTime(50);
await flushPromises(); await flushPromises();
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", { expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
@@ -954,7 +953,36 @@ describe("AutofillOverlayContentService", () => {
expect(clearUserInteractionEventTimeoutSpy).toHaveBeenCalled(); 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.useFakeTimers();
jest jest
.spyOn(autofillOverlayContentService as any, "updateMostRecentlyFocusedField") .spyOn(autofillOverlayContentService as any, "updateMostRecentlyFocusedField")
@@ -1165,7 +1193,7 @@ describe("AutofillOverlayContentService", () => {
expect(autofillOverlayContentService["authStatus"]).toEqual(AuthenticationStatus.Unlocked); expect(autofillOverlayContentService["authStatus"]).toEqual(AuthenticationStatus.Unlocked);
}); });
it("opens both autofill overlay elements", () => { it("opens both autofill inline menu elements", () => {
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement; autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
sendMockExtensionMessage({ command: "openAutofillInlineMenu" }); 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"] = autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnButtonClick; 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 = `<iframe id="subframe" src="https://example.com/"></iframe>`;
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<MessageEvent>();
// @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 = `<iframe id="subframe" src="https://example.com/"></iframe>`;
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<MessageEvent>();
// @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", () => { describe("checkMostRecentlyFocusedFieldHasValue", () => {

View File

@@ -205,7 +205,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
if (direction === RedirectFocusDirection.Current) { if (direction === RedirectFocusDirection.Current) {
this.focusMostRecentlyFocusedField(); this.focusMostRecentlyFocusedField();
setTimeout(() => void this.sendExtensionMessage("closeAutofillInlineMenu"), 100); globalThis.setTimeout(() => void this.sendExtensionMessage("closeAutofillInlineMenu"), 100);
return; return;
} }
@@ -343,7 +343,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
if (this.mostRecentlyFocusedField && !(await this.isInlineMenuListVisible())) { if (this.mostRecentlyFocusedField && !(await this.isInlineMenuListVisible())) {
await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField); await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField);
this.openAutofillInlineMenu({ isOpeningFullAutofillInlineMenu: true }); this.openAutofillInlineMenu({ isOpeningFullAutofillInlineMenu: true });
setTimeout(() => this.sendExtensionMessage("focusAutofillInlineMenuList"), 125); globalThis.setTimeout(() => this.sendExtensionMessage("focusAutofillInlineMenuList"), 125);
return; return;
} }
@@ -808,7 +808,10 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
this.rebuildSubFrameOffsets(); this.rebuildSubFrameOffsets();
this.toggleAutofillInlineMenuHidden(true); this.toggleAutofillInlineMenuHidden(true);
this.clearUserInteractionEventTimeout(); 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() { private rebuildSubFrameOffsets() {
this.clearRecalculateSubFrameOffsetsTimeout(); this.clearRecalculateSubFrameOffsetsTimeout();
this.recalculateSubFrameOffsetsTimeout = setTimeout( this.recalculateSubFrameOffsetsTimeout = globalThis.setTimeout(
() => void this.sendExtensionMessage("rebuildSubFrameOffsets"), () => void this.sendExtensionMessage("rebuildSubFrameOffsets"),
150, 150,
); );
@@ -837,7 +840,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField); await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField);
this.updateAutofillInlineMenuElementsPosition(); this.updateAutofillInlineMenuElementsPosition();
setTimeout(async () => { globalThis.setTimeout(async () => {
this.toggleAutofillInlineMenuHidden(false, true); this.toggleAutofillInlineMenuHidden(false, true);
if ( if (
await this.hideAutofillInlineMenuListOnFilledField( await this.hideAutofillInlineMenuListOnFilledField(
@@ -931,7 +934,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* autofill overlay if the document is not visible. * autofill overlay if the document is not visible.
*/ */
private handleVisibilityChangeEvent = () => { private handleVisibilityChangeEvent = () => {
if (!this.mostRecentlyFocusedField || document.visibilityState === "visible") { if (!this.mostRecentlyFocusedField || globalThis.document.visibilityState === "visible") {
return; return;
} }
@@ -968,7 +971,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
const subFrameUrlWithoutTrailingSlash = subFrameUrl?.replace(/\/$/, ""); const subFrameUrlWithoutTrailingSlash = subFrameUrl?.replace(/\/$/, "");
let iframeElement: HTMLIFrameElement | null = null; let iframeElement: HTMLIFrameElement | null = null;
const iframeElements = document.querySelectorAll( const iframeElements = globalThis.document.querySelectorAll(
`iframe[src="${subFrameUrl}"], iframe[src="${subFrameUrlWithoutTrailingSlash}"]`, `iframe[src="${subFrameUrl}"], iframe[src="${subFrameUrlWithoutTrailingSlash}"]`,
) as NodeListOf<HTMLIFrameElement>; ) as NodeListOf<HTMLIFrameElement>;
if (iframeElements.length === 1) { if (iframeElements.length === 1) {
@@ -1052,7 +1055,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
private calculateSubFramePositioning = async (event: MessageEvent) => { private calculateSubFramePositioning = async (event: MessageEvent) => {
const subFrameData = event.data.subFrameData; const subFrameData = event.data.subFrameData;
let subFrameOffsets: SubFrameOffsetData; let subFrameOffsets: SubFrameOffsetData;
const iframes = document.querySelectorAll("iframe"); const iframes = globalThis.document.querySelectorAll("iframe");
for (let i = 0; i < iframes.length; i++) { for (let i = 0; i < iframes.length; i++) {
if (iframes[i].contentWindow === event.source) { if (iframes[i].contentWindow === event.source) {
const iframeElement = iframes[i]; const iframeElement = iframes[i];
@@ -1061,11 +1064,14 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
subFrameData.url, subFrameData.url,
subFrameData.frameId, subFrameData.frameId,
); );
const parentFrameId = await this.sendExtensionMessage("getCurrentTabFrameId");
subFrameData.top += subFrameOffsets.top; subFrameData.top += subFrameOffsets.top;
subFrameData.left += subFrameOffsets.left; subFrameData.left += subFrameOffsets.left;
subFrameData.parentFrameIds.push(parentFrameId);
const parentFrameId = await this.sendExtensionMessage("getCurrentTabFrameId");
if (typeof parentFrameId !== "undefined") {
subFrameData.parentFrameIds.push(parentFrameId);
}
break; break;
} }
@@ -1076,7 +1082,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return; return;
} }
void sendExtensionMessage("updateSubFrameData", { void this.sendExtensionMessage("updateSubFrameData", {
subFrameData, subFrameData,
}); });
}; };