1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-04-18 20:46:00 +02:00

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

This commit is contained in:
Cesar Gonzalez 2024-06-07 13:37:09 -05:00
parent 025327e141
commit 794808529b
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
3 changed files with 458 additions and 340 deletions

View File

@ -34,6 +34,6 @@ export interface AutofillOverlayContentService {
autofillFieldElement: ElementWithOpId<FormFieldElement>,
autofillFieldData: AutofillField,
): Promise<void>;
blurMostRecentlyFocusedField(isRemovingInlineMenu?: boolean): void;
blurMostRecentlyFocusedField(isClosingInlineMenu?: boolean): void;
destroy(): void;
}

View File

@ -749,131 +749,6 @@ describe("AutofillOverlayContentService", () => {
});
});
describe("openAutofillInlineMenu", () => {
let autofillFieldElement: ElementWithOpId<FormFieldElement>;
beforeEach(() => {
document.body.innerHTML = `
<form id="validFormId">
<input type="text" id="username-field" placeholder="username" />
<input type="password" id="password-field" placeholder="password" />
</form>
`;
autofillFieldElement = document.getElementById(
"username-field",
) as ElementWithOpId<FormFieldElement>;
autofillFieldElement.opid = "op-1";
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
});
it("skips opening the overlay if a field has not been recently focused", () => {
autofillOverlayContentService["mostRecentlyFocusedField"] = undefined;
autofillOverlayContentService["openAutofillInlineMenu"]();
expect(sendExtensionMessageSpy).not.toHaveBeenCalled();
});
it("focuses the most recent overlay field if the field is not focused", () => {
jest.spyOn(autofillFieldElement, "getRootNode").mockReturnValue(document);
Object.defineProperty(document, "activeElement", {
value: document.createElement("div"),
writable: true,
});
const focusMostRecentOverlayFieldSpy = jest.spyOn(
autofillOverlayContentService as any,
"focusMostRecentlyFocusedField",
);
autofillOverlayContentService["openAutofillInlineMenu"]({ isFocusingFieldElement: true });
expect(focusMostRecentOverlayFieldSpy).toHaveBeenCalled();
});
it("skips focusing the most recent overlay field if the field is already focused", () => {
jest.spyOn(autofillFieldElement, "getRootNode").mockReturnValue(document);
Object.defineProperty(document, "activeElement", {
value: autofillFieldElement,
writable: true,
});
const focusMostRecentOverlayFieldSpy = jest.spyOn(
autofillOverlayContentService as any,
"focusMostRecentlyFocusedField",
);
autofillOverlayContentService["openAutofillInlineMenu"]({ isFocusingFieldElement: true });
expect(focusMostRecentOverlayFieldSpy).not.toHaveBeenCalled();
});
it("stores the user's auth status", () => {
autofillOverlayContentService["authStatus"] = undefined;
autofillOverlayContentService["openAutofillInlineMenu"]({
authStatus: AuthenticationStatus.Unlocked,
});
expect(autofillOverlayContentService["authStatus"]).toEqual(AuthenticationStatus.Unlocked);
});
it("opens both autofill overlay elements", () => {
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
autofillOverlayContentService["openAutofillInlineMenu"]();
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
overlayElement: AutofillOverlayElement.Button,
});
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
overlayElement: AutofillOverlayElement.List,
});
});
it("opens the autofill overlay button only if overlay visibility is set for onButtonClick", () => {
autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnButtonClick;
autofillOverlayContentService["openAutofillInlineMenu"]({
isOpeningFullAutofillInlineMenu: false,
});
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
overlayElement: AutofillOverlayElement.Button,
});
expect(sendExtensionMessageSpy).not.toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
overlayElement: AutofillOverlayElement.List,
});
});
it("overrides the onButtonClick visibility setting to open both overlay elements", () => {
autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnButtonClick;
autofillOverlayContentService["openAutofillInlineMenu"]({
isOpeningFullAutofillInlineMenu: true,
});
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
overlayElement: AutofillOverlayElement.Button,
});
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
overlayElement: AutofillOverlayElement.List,
});
});
it("sends an extension message requesting an re-collection of page details if they need to update", () => {
jest.spyOn(autofillOverlayContentService as any, "sendExtensionMessage");
autofillOverlayContentService.pageDetailsUpdateRequired = true;
autofillOverlayContentService["openAutofillInlineMenu"]();
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("bgCollectPageDetails", {
sender: "autofillOverlayContentService",
});
});
});
describe("focusMostRecentlyFocusedField", () => {
it("focuses the most recently focused overlay field", () => {
const mostRecentlyFocusedField = document.createElement(
@ -888,199 +763,6 @@ describe("AutofillOverlayContentService", () => {
});
});
describe("blurMostRecentlyFocusedField", () => {
it("removes focus from the most recently focused overlay field", () => {
const mostRecentlyFocusedField = document.createElement(
"input",
) as ElementWithOpId<HTMLInputElement>;
autofillOverlayContentService["mostRecentlyFocusedField"] = mostRecentlyFocusedField;
jest.spyOn(mostRecentlyFocusedField, "blur");
autofillOverlayContentService["blurMostRecentlyFocusedField"]();
expect(mostRecentlyFocusedField.blur).toHaveBeenCalled();
});
});
describe("addNewVaultItem", () => {
it("skips sending the message if the overlay list is not visible", async () => {
jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
.mockResolvedValue(false);
await autofillOverlayContentService.addNewVaultItem();
expect(sendExtensionMessageSpy).not.toHaveBeenCalled();
});
it("sends a message that facilitates adding a new vault item with empty fields", async () => {
jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
.mockResolvedValue(true);
await autofillOverlayContentService.addNewVaultItem();
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
login: {
username: "",
password: "",
uri: "http://localhost/",
hostname: "localhost",
},
});
});
it("sends a message that facilitates adding a new vault item with data from user filled fields", async () => {
document.body.innerHTML = `
<form id="validFormId">
<input type="text" id="username-field" placeholder="username" />
<input type="password" id="password-field" placeholder="password" />
</form>
`;
const usernameField = document.getElementById(
"username-field",
) as ElementWithOpId<HTMLInputElement>;
const passwordField = document.getElementById(
"password-field",
) as ElementWithOpId<HTMLInputElement>;
usernameField.value = "test-username";
passwordField.value = "test-password";
jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
.mockResolvedValue(true);
autofillOverlayContentService["userFilledFields"] = {
username: usernameField,
password: passwordField,
};
await autofillOverlayContentService.addNewVaultItem();
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
login: {
username: "test-username",
password: "test-password",
uri: "http://localhost/",
hostname: "localhost",
},
});
});
});
describe("redirectInlineMenuFocusOut", () => {
let autofillFieldElement: ElementWithOpId<FormFieldElement>;
let autofillFieldFocusSpy: jest.SpyInstance;
let findTabsSpy: jest.SpyInstance;
let previousFocusableElement: HTMLElement;
let nextFocusableElement: HTMLElement;
let isInlineMenuListVisibleSpy: jest.SpyInstance;
beforeEach(() => {
document.body.innerHTML = `
<div class="previous-focusable-element" tabindex="0"></div>
<form id="validFormId">
<input type="text" id="username-field" placeholder="username" />
<input type="password" id="password-field" placeholder="password" />
</form>
<div class="next-focusable-element" tabindex="0"></div>
`;
autofillFieldElement = document.getElementById(
"username-field",
) as ElementWithOpId<FormFieldElement>;
autofillFieldElement.opid = "op-1";
previousFocusableElement = document.querySelector(
".previous-focusable-element",
) as HTMLElement;
nextFocusableElement = document.querySelector(".next-focusable-element") as HTMLElement;
autofillFieldFocusSpy = jest.spyOn(autofillFieldElement, "focus");
findTabsSpy = jest.spyOn(autofillOverlayContentService as any, "findTabs");
isInlineMenuListVisibleSpy = jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
.mockResolvedValue(true);
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
autofillOverlayContentService["focusableElements"] = [
previousFocusableElement,
autofillFieldElement,
nextFocusableElement,
];
});
it("skips focusing an element if the overlay is not visible", async () => {
isInlineMenuListVisibleSpy.mockResolvedValue(false);
await autofillOverlayContentService["redirectInlineMenuFocusOut"](
RedirectFocusDirection.Next,
);
expect(findTabsSpy).not.toHaveBeenCalled();
});
it("skips focusing an element if no recently focused field exists", async () => {
autofillOverlayContentService["mostRecentlyFocusedField"] = undefined;
await autofillOverlayContentService["redirectInlineMenuFocusOut"](
RedirectFocusDirection.Next,
);
expect(findTabsSpy).not.toHaveBeenCalled();
});
it("focuses the most recently focused field if the focus direction is `Current`", async () => {
await autofillOverlayContentService["redirectInlineMenuFocusOut"](
RedirectFocusDirection.Current,
);
expect(findTabsSpy).not.toHaveBeenCalled();
expect(autofillFieldFocusSpy).toHaveBeenCalled();
});
it("removes the overlay if the focus direction is `Current`", async () => {
jest.useFakeTimers();
await autofillOverlayContentService["redirectInlineMenuFocusOut"](
RedirectFocusDirection.Current,
);
jest.advanceTimersByTime(150);
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu");
});
it("finds all focusable tabs if the focusable elements array is not populated", async () => {
autofillOverlayContentService["focusableElements"] = [];
findTabsSpy.mockReturnValue([
previousFocusableElement,
autofillFieldElement,
nextFocusableElement,
]);
await autofillOverlayContentService["redirectInlineMenuFocusOut"](
RedirectFocusDirection.Next,
);
expect(findTabsSpy).toHaveBeenCalledWith(globalThis.document.body, { getShadowRoot: true });
});
it("focuses the previous focusable element if the focus direction is `Previous`", async () => {
jest.spyOn(previousFocusableElement, "focus");
await autofillOverlayContentService["redirectInlineMenuFocusOut"](
RedirectFocusDirection.Previous,
);
expect(autofillFieldFocusSpy).not.toHaveBeenCalled();
expect(previousFocusableElement.focus).toHaveBeenCalled();
});
it("focuses the next focusable element if the focus direction is `Next`", async () => {
jest.spyOn(nextFocusableElement, "focus");
await autofillOverlayContentService["redirectInlineMenuFocusOut"](
RedirectFocusDirection.Next,
);
expect(autofillFieldFocusSpy).not.toHaveBeenCalled();
expect(nextFocusableElement.focus).toHaveBeenCalled();
});
});
describe("handleOverlayRepositionEvent", () => {
let checkShouldRepositionInlineMenuSpy: jest.SpyInstance;
@ -1319,12 +1001,120 @@ describe("AutofillOverlayContentService", () => {
describe("extension onMessage handlers", () => {
describe("openAutofillInlineMenu message handler", () => {
it("sends a message to the background to trigger an update in the inline menu's position", async () => {
autofillOverlayContentService["mostRecentlyFocusedField"] =
mock<ElementWithOpId<FormFieldElement>>();
let autofillFieldElement: ElementWithOpId<FormFieldElement>;
beforeEach(() => {
document.body.innerHTML = `
<form id="validFormId">
<input type="text" id="username-field" placeholder="username" />
<input type="password" id="password-field" placeholder="password" />
</form>
`;
autofillFieldElement = document.getElementById(
"username-field",
) as ElementWithOpId<FormFieldElement>;
autofillFieldElement.opid = "op-1";
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
});
it("skips opening the overlay if a field has not been recently focused", () => {
autofillOverlayContentService["mostRecentlyFocusedField"] = undefined;
sendMockExtensionMessage({ command: "openAutofillInlineMenu" });
expect(sendExtensionMessageSpy).not.toHaveBeenCalled();
});
it("focuses the most recent overlay field if the field is not focused", () => {
jest.spyOn(autofillFieldElement, "getRootNode").mockReturnValue(document);
Object.defineProperty(document, "activeElement", {
value: document.createElement("div"),
writable: true,
});
const focusMostRecentOverlayFieldSpy = jest.spyOn(
autofillOverlayContentService as any,
"focusMostRecentlyFocusedField",
);
sendMockExtensionMessage({
command: "openAutofillInlineMenu",
isFocusingFieldElement: true,
});
expect(focusMostRecentOverlayFieldSpy).toHaveBeenCalled();
});
it("skips focusing the most recent overlay field if the field is already focused", () => {
jest.spyOn(autofillFieldElement, "getRootNode").mockReturnValue(document);
Object.defineProperty(document, "activeElement", {
value: autofillFieldElement,
writable: true,
});
const focusMostRecentOverlayFieldSpy = jest.spyOn(
autofillOverlayContentService as any,
"focusMostRecentlyFocusedField",
);
sendMockExtensionMessage({
command: "openAutofillInlineMenu",
isFocusingFieldElement: true,
});
expect(focusMostRecentOverlayFieldSpy).not.toHaveBeenCalled();
});
it("stores the user's auth status", () => {
autofillOverlayContentService["authStatus"] = undefined;
sendMockExtensionMessage({
command: "openAutofillInlineMenu",
authStatus: AuthenticationStatus.Unlocked,
});
expect(autofillOverlayContentService["authStatus"]).toEqual(AuthenticationStatus.Unlocked);
});
it("opens both autofill overlay elements", () => {
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
sendMockExtensionMessage({ command: "openAutofillInlineMenu" });
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
overlayElement: AutofillOverlayElement.Button,
});
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
overlayElement: AutofillOverlayElement.List,
});
});
it("opens the autofill overlay button only if overlay visibility is set for onButtonClick", () => {
autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnButtonClick;
sendMockExtensionMessage({
command: "openAutofillInlineMenu",
isOpeningFullAutofillInlineMenu: false,
});
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
overlayElement: AutofillOverlayElement.Button,
});
expect(sendExtensionMessageSpy).not.toHaveBeenCalledWith(
"updateAutofillInlineMenuPosition",
{
overlayElement: AutofillOverlayElement.List,
},
);
});
it("overrides the onButtonClick visibility setting to open both overlay elements", () => {
autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnButtonClick;
sendMockExtensionMessage({
command: "openAutofillInlineMenu",
isOpeningFullAutofillInlineMenu: true,
});
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
@ -1334,17 +1124,38 @@ describe("AutofillOverlayContentService", () => {
overlayElement: AutofillOverlayElement.List,
});
});
it("sends an extension message requesting an re-collection of page details if they need to update", () => {
jest.spyOn(autofillOverlayContentService as any, "sendExtensionMessage");
autofillOverlayContentService.pageDetailsUpdateRequired = true;
autofillOverlayContentService["openAutofillInlineMenu"]();
sendMockExtensionMessage({ command: "openAutofillInlineMenu" });
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("bgCollectPageDetails", {
sender: "autofillOverlayContentService",
});
});
});
describe("addNewVaultItemFromOverlay message handler", () => {
it("sends an extension message with the cipher login details to add to the user's vault", async () => {
it("skips sending the message if the overlay list is not visible", async () => {
jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
.mockResolvedValue(false);
sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" });
await flushPromises();
expect(sendExtensionMessageSpy).not.toHaveBeenCalled();
});
it("sends a message that facilitates adding a new vault item with empty fields", async () => {
jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
.mockResolvedValue(true);
sendMockExtensionMessage({
command: "addNewVaultItemFromOverlay",
});
sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" });
await flushPromises();
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
@ -1356,6 +1167,219 @@ describe("AutofillOverlayContentService", () => {
},
});
});
it("sends a message that facilitates adding a new vault item with data from user filled fields", async () => {
document.body.innerHTML = `
<form id="validFormId">
<input type="text" id="username-field" placeholder="username" />
<input type="password" id="password-field" placeholder="password" />
</form>
`;
const usernameField = document.getElementById(
"username-field",
) as ElementWithOpId<HTMLInputElement>;
const passwordField = document.getElementById(
"password-field",
) as ElementWithOpId<HTMLInputElement>;
usernameField.value = "test-username";
passwordField.value = "test-password";
jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
.mockResolvedValue(true);
autofillOverlayContentService["userFilledFields"] = {
username: usernameField,
password: passwordField,
};
sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" });
await flushPromises();
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
login: {
username: "test-username",
password: "test-password",
uri: "http://localhost/",
hostname: "localhost",
},
});
});
});
describe("unsetMostRecentlyFocusedField message handler", () => {
it("will reset the mostRecentlyFocusedField value to a null value", () => {
autofillOverlayContentService["mostRecentlyFocusedField"] =
mock<ElementWithOpId<FormFieldElement>>();
sendMockExtensionMessage({
command: "unsetMostRecentlyFocusedField",
});
expect(autofillOverlayContentService["mostRecentlyFocusedField"]).toBeNull();
});
});
describe("messages that trigger a blur of the most recently focused field", () => {
const messages = [
"blurMostRecentlyFocusedField",
"bgUnlockPopoutOpened",
"bgVaultItemRepromptPopoutOpened",
];
messages.forEach((message, index) => {
const isClosingInlineMenu = index >= 1;
it(`will blur the most recently focused field${isClosingInlineMenu ? " and close the inline menu" : ""} when a ${message} message is received`, () => {
autofillOverlayContentService["mostRecentlyFocusedField"] =
mock<ElementWithOpId<FormFieldElement>>();
sendMockExtensionMessage({ command: message });
expect(autofillOverlayContentService["mostRecentlyFocusedField"].blur).toHaveBeenCalled();
if (isClosingInlineMenu) {
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu");
}
});
});
});
describe("redirectAutofillInlineMenuFocusOut message handler", () => {
let autofillFieldElement: ElementWithOpId<FormFieldElement>;
let autofillFieldFocusSpy: jest.SpyInstance;
let findTabsSpy: jest.SpyInstance;
let previousFocusableElement: HTMLElement;
let nextFocusableElement: HTMLElement;
let isInlineMenuListVisibleSpy: jest.SpyInstance;
beforeEach(() => {
document.body.innerHTML = `
<div class="previous-focusable-element" tabindex="0"></div>
<form id="validFormId">
<input type="text" id="username-field" placeholder="username" />
<input type="password" id="password-field" placeholder="password" />
</form>
<div class="next-focusable-element" tabindex="0"></div>
`;
autofillFieldElement = document.getElementById(
"username-field",
) as ElementWithOpId<FormFieldElement>;
autofillFieldElement.opid = "op-1";
previousFocusableElement = document.querySelector(
".previous-focusable-element",
) as HTMLElement;
nextFocusableElement = document.querySelector(".next-focusable-element") as HTMLElement;
autofillFieldFocusSpy = jest.spyOn(autofillFieldElement, "focus");
findTabsSpy = jest.spyOn(autofillOverlayContentService as any, "findTabs");
isInlineMenuListVisibleSpy = jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
.mockResolvedValue(true);
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
autofillOverlayContentService["focusableElements"] = [
previousFocusableElement,
autofillFieldElement,
nextFocusableElement,
];
});
it("skips focusing an element if the overlay is not visible", async () => {
isInlineMenuListVisibleSpy.mockResolvedValue(false);
sendMockExtensionMessage({
command: "redirectAutofillInlineMenuFocusOut",
data: { direction: RedirectFocusDirection.Next },
});
expect(findTabsSpy).not.toHaveBeenCalled();
});
it("skips focusing an element if no recently focused field exists", async () => {
autofillOverlayContentService["mostRecentlyFocusedField"] = undefined;
sendMockExtensionMessage({
command: "redirectAutofillInlineMenuFocusOut",
data: { direction: RedirectFocusDirection.Next },
});
expect(findTabsSpy).not.toHaveBeenCalled();
});
it("focuses the most recently focused field if the focus direction is `Current`", async () => {
sendMockExtensionMessage({
command: "redirectAutofillInlineMenuFocusOut",
data: { direction: RedirectFocusDirection.Current },
});
await flushPromises();
expect(findTabsSpy).not.toHaveBeenCalled();
expect(autofillFieldFocusSpy).toHaveBeenCalled();
});
it("removes the overlay if the focus direction is `Current`", async () => {
jest.useFakeTimers();
sendMockExtensionMessage({
command: "redirectAutofillInlineMenuFocusOut",
data: { direction: RedirectFocusDirection.Current },
});
await flushPromises();
jest.advanceTimersByTime(150);
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu");
});
it("finds all focusable tabs if the focusable elements array is not populated", async () => {
autofillOverlayContentService["focusableElements"] = [];
findTabsSpy.mockReturnValue([
previousFocusableElement,
autofillFieldElement,
nextFocusableElement,
]);
sendMockExtensionMessage({
command: "redirectAutofillInlineMenuFocusOut",
data: { direction: RedirectFocusDirection.Next },
});
await flushPromises();
expect(findTabsSpy).toHaveBeenCalledWith(globalThis.document.body, { getShadowRoot: true });
});
it("focuses the previous focusable element if the focus direction is `Previous`", async () => {
jest.spyOn(previousFocusableElement, "focus");
sendMockExtensionMessage({
command: "redirectAutofillInlineMenuFocusOut",
data: { direction: RedirectFocusDirection.Previous },
});
await flushPromises();
expect(autofillFieldFocusSpy).not.toHaveBeenCalled();
expect(previousFocusableElement.focus).toHaveBeenCalled();
});
it("focuses the next focusable element if the focus direction is `Next`", async () => {
jest.spyOn(nextFocusableElement, "focus");
sendMockExtensionMessage({
command: "redirectAutofillInlineMenuFocusOut",
data: { direction: RedirectFocusDirection.Next },
});
await flushPromises();
expect(autofillFieldFocusSpy).not.toHaveBeenCalled();
expect(nextFocusableElement.focus).toHaveBeenCalled();
});
});
describe("updateAutofillInlineMenuVisibility message handler", () => {
it("updates the inlineMenuVisibility property", () => {
sendMockExtensionMessage({
command: "updateAutofillInlineMenuVisibility",
data: { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick },
});
expect(autofillOverlayContentService["inlineMenuVisibility"]).toEqual(
AutofillOverlayVisibility.OnButtonClick,
);
});
});
});
});

View File

@ -157,14 +157,17 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
/**
* Removes focus from the most recently focused field element.
*/
blurMostRecentlyFocusedField(isRemovingInlineMenu: boolean = false) {
blurMostRecentlyFocusedField(isClosingInlineMenu: boolean = false) {
this.mostRecentlyFocusedField?.blur();
if (isRemovingInlineMenu) {
void sendExtensionMessage("closeAutofillInlineMenu");
if (isClosingInlineMenu) {
void this.sendExtensionMessage("closeAutofillInlineMenu");
}
}
/**
* Sets the most recently focused field within the current frame to a `null` value.
*/
unsetMostRecentlyFocusedField() {
this.mostRecentlyFocusedField = null;
}
@ -694,6 +697,14 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return !isLoginCipherField;
}
/**
* Validates whether a field is considered to be "hidden" based on the field's attributes.
* If the field is hidden, a fallback listener will be set up to ensure that the
* field will have the inline menu set up on it when it becomes visible.
*
* @param formFieldElement - The form field element that triggered the focus event.
* @param autofillFieldData - Autofill field data captured from the form field element.
*/
private isHiddenField(
formFieldElement: ElementWithOpId<FormFieldElement>,
autofillFieldData: AutofillField,
@ -708,6 +719,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return true;
}
/**
* Sets up a fallback listener that will facilitate setting up the
* inline menu on the field when it becomes visible and focused.
*
* @param formFieldElement - The form field element that triggered the focus event.
* @param autofillFieldData - Autofill field data captured from the form field element.
*/
private setupHiddenFieldFallbackListener(
formFieldElement: ElementWithOpId<FormFieldElement>,
autofillFieldData: AutofillField,
@ -716,11 +734,23 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
formFieldElement.addEventListener(EVENTS.FOCUS, this.handleHiddenFieldFocusEvent);
}
/**
* Removes the fallback listener that facilitates setting up the inline
* menu on the field when it becomes visible and focused.
*
* @param formFieldElement - The form field element that triggered the focus event.
*/
private removeHiddenFieldFallbackListener(formFieldElement: ElementWithOpId<FormFieldElement>) {
formFieldElement.removeEventListener(EVENTS.FOCUS, this.handleHiddenFieldFocusEvent);
this.hiddenFormFieldElements.delete(formFieldElement);
}
/**
* Handles the focus event on a hidden field. When
* triggered, the inline menu is set up on the field.
*
* @param event - The focus event.
*/
private handleHiddenFieldFocusEvent = (event: FocusEvent) => {
const formFieldElement = event.target as ElementWithOpId<FormFieldElement>;
const autofillFieldData = this.hiddenFormFieldElements.get(formFieldElement);
@ -766,8 +796,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
globalThis.removeEventListener(EVENTS.RESIZE, this.handleOverlayRepositionEvent);
}
private overlayRepositionTimeout: number | NodeJS.Timeout;
/**
* Handles the resize or scroll events that enact
* repositioning of existing overlay elements.
@ -783,6 +811,9 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
this.userInteractionEventTimeout = setTimeout(this.triggerOverlayRepositionUpdates, 750);
};
/**
* Triggers a rebuild of a sub frame's offsets within the tab.
*/
private rebuildSubFrameOffsets() {
this.clearRecalculateSubFrameOffsetsTimeout();
this.recalculateSubFrameOffsetsTimeout = setTimeout(
@ -840,12 +871,19 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
}
/**
* Clears the timeout that facilitates recalculating the sub frame offsets.
*/
private clearRecalculateSubFrameOffsetsTimeout() {
if (this.recalculateSubFrameOffsetsTimeout) {
clearTimeout(this.recalculateSubFrameOffsetsTimeout);
}
}
/**
* Checks if the focused field is present within the bounds of the viewport.
* If not present, the inline menu will be closed.
*/
private isFocusedFieldWithinViewportBounds() {
const focusedFieldRectsTop = this.focusedFieldData?.focusedFieldRects?.top;
return (
@ -855,6 +893,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
);
}
/**
* Returns a value that indicates if we should hide the inline menu list due to a filled field.
*
* @param formFieldElement - The form field element that triggered the focus event.
*/
private async hideAutofillInlineMenuListOnFilledField(
formFieldElement?: FillableFormFieldElement,
): Promise<boolean> {
@ -864,6 +907,9 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
);
}
/**
* Indicates whether the most recently focused field has a value.
*/
private mostRecentlyFocusedFieldHasValue() {
return Boolean((this.mostRecentlyFocusedField as FillableFormFieldElement)?.value);
}
@ -889,7 +935,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return;
}
this.mostRecentlyFocusedField = null;
this.unsetMostRecentlyFocusedField();
void this.sendExtensionMessage("closeAutofillInlineMenu", {
forceCloseAutofillInlineMenu: true,
});
@ -909,6 +955,12 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return documentRoot?.activeElement;
}
/**
* Queries all iframe elements within the document and returns the
* sub frame offsets for each iframe element.
*
* @param message - The message object from the extension.
*/
private async getSubFrameOffsets(
message: AutofillExtensionMessage,
): Promise<SubFrameOffsetData | null> {
@ -930,6 +982,14 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return this.calculateSubFrameOffsets(iframeElement, subFrameUrl);
}
/**
* Calculates the bounding rect for the queried frame and returns the
* offset data for the sub frame.
*
* @param iframeElement - The iframe element to calculate the sub frame offsets for.
* @param subFrameUrl - The URL of the sub frame.
* @param frameId - The frame ID of the sub frame.
*/
private calculateSubFrameOffsets(
iframeElement: HTMLIFrameElement,
subFrameUrl?: string,
@ -950,6 +1010,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
};
}
/**
* Posts a message to the parent frame to calculate the sub frame offset of the current frame.
*
* @param message - The message object from the extension.
*/
private getSubFrameOffsetsFromWindowMessage(message: any) {
globalThis.parent.postMessage(
{
@ -966,14 +1031,24 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
);
}
/**
* Handles window messages that are sent to the current frame. Will trigger a
* calculation of the sub frame offsets through the parent frame.
*
* @param event - The message event.
*/
private handleWindowMessageEvent = (event: MessageEvent) => {
if (event.data?.command !== "calculateSubFramePositioning") {
return;
if (event.data?.command === "calculateSubFramePositioning") {
void this.calculateSubFramePositioning(event);
}
void this.calculateSubFramePositioning(event);
};
/**
* Calculates the sub frame positioning for the current frame
* through all parent frames until the top frame is reached.
*
* @param event - The message event.
*/
private calculateSubFramePositioning = async (event: MessageEvent) => {
const subFrameData = event.data.subFrameData;
let subFrameOffsets: SubFrameOffsetData;
@ -1006,30 +1081,49 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
});
};
/**
* Updates the local reference to the inline menu visibility setting.
*
* @param data - The data object from the extension message.
*/
private updateAutofillInlineMenuVisibility({ data }: AutofillExtensionMessage) {
if (isNaN(data?.inlineMenuVisibility)) {
return;
if (!isNaN(data?.inlineMenuVisibility)) {
this.inlineMenuVisibility = data.inlineMenuVisibility;
}
this.inlineMenuVisibility = data.inlineMenuVisibility;
}
/**
* Checks if a field is currently filling within an frame in the tab.
*/
private async isFieldCurrentlyFilling() {
return (await this.sendExtensionMessage("checkIsFieldCurrentlyFilling")) === true;
}
/**
* Checks if the inline menu button is visible at the top frame.
*/
private async isInlineMenuButtonVisible() {
return (await this.sendExtensionMessage("checkIsAutofillInlineMenuButtonVisible")) === true;
}
/**
* Checks if the inline menu list if visible at the top frame.
*/
private async isInlineMenuListVisible() {
return (await this.sendExtensionMessage("checkIsAutofillInlineMenuListVisible")) === true;
}
/**
* Checks if the current tab contains ciphers that can be used to populate the inline menu.
*/
private async isInlineMenuCiphersPopulated() {
return (await this.sendExtensionMessage("checkIsInlineMenuCiphersPopulated")) === true;
}
/**
* Triggers a validation to ensure that the inline menu is repositioned only when the
* current frame contains the focused field at any given depth level.
*/
private async checkShouldRepositionInlineMenu() {
return (await this.sendExtensionMessage("checkShouldRepositionInlineMenu")) === true;
}