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:
parent
025327e141
commit
794808529b
@ -34,6 +34,6 @@ export interface AutofillOverlayContentService {
|
||||
autofillFieldElement: ElementWithOpId<FormFieldElement>,
|
||||
autofillFieldData: AutofillField,
|
||||
): Promise<void>;
|
||||
blurMostRecentlyFocusedField(isRemovingInlineMenu?: boolean): void;
|
||||
blurMostRecentlyFocusedField(isClosingInlineMenu?: boolean): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user