591 lines
21 KiB
TypeScript
591 lines
21 KiB
TypeScript
import { mock } from "jest-mock-extended";
|
|
|
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
|
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
|
|
|
import AutofillPageDetails from "../models/autofill-page-details";
|
|
import AutofillScript from "../models/autofill-script";
|
|
import AutofillOverlayContentService from "../services/autofill-overlay-content.service";
|
|
import { flushPromises, sendExtensionRuntimeMessage } from "../spec/testing-utils";
|
|
import { RedirectFocusDirection } from "../utils/autofill-overlay.enum";
|
|
|
|
import { AutofillExtensionMessage } from "./abstractions/autofill-init";
|
|
import AutofillInit from "./autofill-init";
|
|
|
|
describe("AutofillInit", () => {
|
|
let autofillInit: AutofillInit;
|
|
const autofillOverlayContentService = mock<AutofillOverlayContentService>();
|
|
const originalDocumentReadyState = document.readyState;
|
|
|
|
beforeEach(() => {
|
|
chrome.runtime.connect = jest.fn().mockReturnValue({
|
|
onDisconnect: {
|
|
addListener: jest.fn(),
|
|
},
|
|
});
|
|
autofillInit = new AutofillInit(autofillOverlayContentService);
|
|
window.IntersectionObserver = jest.fn(() => mock<IntersectionObserver>());
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.resetModules();
|
|
jest.clearAllMocks();
|
|
Object.defineProperty(document, "readyState", {
|
|
value: originalDocumentReadyState,
|
|
writable: true,
|
|
});
|
|
});
|
|
|
|
describe("init", () => {
|
|
it("sets up the extension message listeners", () => {
|
|
jest.spyOn(autofillInit as any, "setupExtensionMessageListeners");
|
|
|
|
autofillInit.init();
|
|
|
|
expect(autofillInit["setupExtensionMessageListeners"]).toHaveBeenCalled();
|
|
});
|
|
|
|
it("triggers a collection of page details if the document is in a `complete` ready state", () => {
|
|
jest.useFakeTimers();
|
|
Object.defineProperty(document, "readyState", { value: "complete", writable: true });
|
|
|
|
autofillInit.init();
|
|
jest.advanceTimersByTime(250);
|
|
|
|
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith(
|
|
{
|
|
command: "bgCollectPageDetails",
|
|
sender: "autofillInit",
|
|
},
|
|
expect.any(Function),
|
|
);
|
|
});
|
|
|
|
it("registers a window load listener to collect the page details if the document is not in a `complete` ready state", () => {
|
|
jest.spyOn(window, "addEventListener");
|
|
Object.defineProperty(document, "readyState", { value: "loading", writable: true });
|
|
|
|
autofillInit.init();
|
|
|
|
expect(window.addEventListener).toHaveBeenCalledWith("load", expect.any(Function));
|
|
});
|
|
});
|
|
|
|
describe("setupExtensionMessageListeners", () => {
|
|
it("sets up a chrome runtime on message listener", () => {
|
|
jest.spyOn(chrome.runtime.onMessage, "addListener");
|
|
|
|
autofillInit["setupExtensionMessageListeners"]();
|
|
|
|
expect(chrome.runtime.onMessage.addListener).toHaveBeenCalledWith(
|
|
autofillInit["handleExtensionMessage"],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("handleExtensionMessage", () => {
|
|
let message: AutofillExtensionMessage;
|
|
let sender: chrome.runtime.MessageSender;
|
|
const sendResponse = jest.fn();
|
|
|
|
beforeEach(() => {
|
|
message = {
|
|
command: "collectPageDetails",
|
|
tab: mock<chrome.tabs.Tab>(),
|
|
sender: "sender",
|
|
};
|
|
sender = mock<chrome.runtime.MessageSender>();
|
|
});
|
|
|
|
it("returns a undefined value if a extension message handler is not found with the given message command", () => {
|
|
message.command = "unknownCommand";
|
|
|
|
const response = autofillInit["handleExtensionMessage"](message, sender, sendResponse);
|
|
|
|
expect(response).toBe(undefined);
|
|
});
|
|
|
|
it("returns a undefined value if the message handler does not return a response", async () => {
|
|
const response1 = await autofillInit["handleExtensionMessage"](message, sender, sendResponse);
|
|
await flushPromises();
|
|
|
|
expect(response1).not.toBe(false);
|
|
|
|
message.command = "removeAutofillOverlay";
|
|
message.fillScript = mock<AutofillScript>();
|
|
|
|
const response2 = autofillInit["handleExtensionMessage"](message, sender, sendResponse);
|
|
await flushPromises();
|
|
|
|
expect(response2).toBe(undefined);
|
|
});
|
|
|
|
it("returns a true value and calls sendResponse if the message handler returns a response", async () => {
|
|
message.command = "collectPageDetailsImmediately";
|
|
const pageDetails: AutofillPageDetails = {
|
|
title: "title",
|
|
url: "http://example.com",
|
|
documentUrl: "documentUrl",
|
|
forms: {},
|
|
fields: [],
|
|
collectedTimestamp: 0,
|
|
};
|
|
jest
|
|
.spyOn(autofillInit["collectAutofillContentService"], "getPageDetails")
|
|
.mockResolvedValue(pageDetails);
|
|
|
|
const response = await autofillInit["handleExtensionMessage"](message, sender, sendResponse);
|
|
await Promise.resolve(response);
|
|
|
|
expect(response).toBe(true);
|
|
expect(sendResponse).toHaveBeenCalledWith(pageDetails);
|
|
});
|
|
|
|
describe("extension message handlers", () => {
|
|
beforeEach(() => {
|
|
autofillInit.init();
|
|
});
|
|
|
|
describe("collectPageDetails", () => {
|
|
it("sends the collected page details for autofill using a background script message", async () => {
|
|
const pageDetails: AutofillPageDetails = {
|
|
title: "title",
|
|
url: "http://example.com",
|
|
documentUrl: "documentUrl",
|
|
forms: {},
|
|
fields: [],
|
|
collectedTimestamp: 0,
|
|
};
|
|
const message = {
|
|
command: "collectPageDetails",
|
|
sender: "sender",
|
|
tab: mock<chrome.tabs.Tab>(),
|
|
};
|
|
jest
|
|
.spyOn(autofillInit["collectAutofillContentService"], "getPageDetails")
|
|
.mockResolvedValue(pageDetails);
|
|
|
|
sendExtensionRuntimeMessage(message, sender, sendResponse);
|
|
await flushPromises();
|
|
|
|
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
|
|
command: "collectPageDetailsResponse",
|
|
tab: message.tab,
|
|
details: pageDetails,
|
|
sender: message.sender,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("collectPageDetailsImmediately", () => {
|
|
it("returns collected page details for autofill if set to send the details in the response", async () => {
|
|
const pageDetails: AutofillPageDetails = {
|
|
title: "title",
|
|
url: "http://example.com",
|
|
documentUrl: "documentUrl",
|
|
forms: {},
|
|
fields: [],
|
|
collectedTimestamp: 0,
|
|
};
|
|
jest
|
|
.spyOn(autofillInit["collectAutofillContentService"], "getPageDetails")
|
|
.mockResolvedValue(pageDetails);
|
|
|
|
sendExtensionRuntimeMessage(
|
|
{ command: "collectPageDetailsImmediately" },
|
|
sender,
|
|
sendResponse,
|
|
);
|
|
await flushPromises();
|
|
|
|
expect(autofillInit["collectAutofillContentService"].getPageDetails).toHaveBeenCalled();
|
|
expect(sendResponse).toBeCalledWith(pageDetails);
|
|
expect(chrome.runtime.sendMessage).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("fillForm", () => {
|
|
let fillScript: AutofillScript;
|
|
beforeEach(() => {
|
|
fillScript = mock<AutofillScript>();
|
|
jest.spyOn(autofillInit["insertAutofillContentService"], "fillForm").mockImplementation();
|
|
});
|
|
|
|
it("skips calling the InsertAutofillContentService and does not fill the form if the url to fill is not equal to the current tab url", async () => {
|
|
const fillScript = mock<AutofillScript>();
|
|
const message = {
|
|
command: "fillForm",
|
|
fillScript,
|
|
pageDetailsUrl: "https://a-different-url.com",
|
|
};
|
|
|
|
sendExtensionRuntimeMessage(message);
|
|
await flushPromises();
|
|
|
|
expect(autofillInit["insertAutofillContentService"].fillForm).not.toHaveBeenCalledWith(
|
|
fillScript,
|
|
);
|
|
});
|
|
|
|
it("calls the InsertAutofillContentService to fill the form", async () => {
|
|
sendExtensionRuntimeMessage({
|
|
command: "fillForm",
|
|
fillScript,
|
|
pageDetailsUrl: window.location.href,
|
|
});
|
|
await flushPromises();
|
|
|
|
expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith(
|
|
fillScript,
|
|
);
|
|
});
|
|
|
|
it("removes the overlay when filling the form", async () => {
|
|
const blurAndRemoveOverlaySpy = jest.spyOn(autofillInit as any, "blurAndRemoveOverlay");
|
|
sendExtensionRuntimeMessage({
|
|
command: "fillForm",
|
|
fillScript,
|
|
pageDetailsUrl: window.location.href,
|
|
});
|
|
await flushPromises();
|
|
|
|
expect(blurAndRemoveOverlaySpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it("updates the isCurrentlyFilling property of the overlay to true after filling", async () => {
|
|
jest.useFakeTimers();
|
|
jest.spyOn(autofillInit as any, "updateOverlayIsCurrentlyFilling");
|
|
jest
|
|
.spyOn(autofillInit["autofillOverlayContentService"], "focusMostRecentOverlayField")
|
|
.mockImplementation();
|
|
|
|
sendExtensionRuntimeMessage({
|
|
command: "fillForm",
|
|
fillScript,
|
|
pageDetailsUrl: window.location.href,
|
|
});
|
|
await flushPromises();
|
|
jest.advanceTimersByTime(300);
|
|
|
|
expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(1, true);
|
|
expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith(
|
|
fillScript,
|
|
);
|
|
expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(2, false);
|
|
});
|
|
|
|
it("skips attempting to focus the most recent field if the autofillOverlayContentService is not present", async () => {
|
|
jest.useFakeTimers();
|
|
const newAutofillInit = new AutofillInit(undefined);
|
|
newAutofillInit.init();
|
|
jest.spyOn(newAutofillInit as any, "updateOverlayIsCurrentlyFilling");
|
|
jest
|
|
.spyOn(newAutofillInit["insertAutofillContentService"], "fillForm")
|
|
.mockImplementation();
|
|
|
|
sendExtensionRuntimeMessage({
|
|
command: "fillForm",
|
|
fillScript,
|
|
pageDetailsUrl: window.location.href,
|
|
});
|
|
await flushPromises();
|
|
jest.advanceTimersByTime(300);
|
|
|
|
expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(
|
|
1,
|
|
true,
|
|
);
|
|
expect(newAutofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith(
|
|
fillScript,
|
|
);
|
|
expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).not.toHaveBeenNthCalledWith(
|
|
2,
|
|
false,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("openAutofillOverlay", () => {
|
|
const message = {
|
|
command: "openAutofillOverlay",
|
|
data: {
|
|
isFocusingFieldElement: true,
|
|
isOpeningFullOverlay: true,
|
|
authStatus: AuthenticationStatus.Unlocked,
|
|
},
|
|
};
|
|
|
|
it("skips attempting to open the autofill overlay if the autofillOverlayContentService is not present", () => {
|
|
const newAutofillInit = new AutofillInit(undefined);
|
|
newAutofillInit.init();
|
|
|
|
sendExtensionRuntimeMessage(message);
|
|
|
|
expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
|
|
});
|
|
|
|
it("opens the autofill overlay", () => {
|
|
sendExtensionRuntimeMessage(message);
|
|
|
|
expect(
|
|
autofillInit["autofillOverlayContentService"].openAutofillOverlay,
|
|
).toHaveBeenCalledWith({
|
|
isFocusingFieldElement: message.data.isFocusingFieldElement,
|
|
isOpeningFullOverlay: message.data.isOpeningFullOverlay,
|
|
authStatus: message.data.authStatus,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("closeAutofillOverlay", () => {
|
|
beforeEach(() => {
|
|
autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = false;
|
|
autofillInit["autofillOverlayContentService"].isCurrentlyFilling = false;
|
|
});
|
|
|
|
it("skips attempting to remove the overlay if the autofillOverlayContentService is not present", () => {
|
|
const newAutofillInit = new AutofillInit(undefined);
|
|
newAutofillInit.init();
|
|
jest.spyOn(newAutofillInit as any, "removeAutofillOverlay");
|
|
|
|
sendExtensionRuntimeMessage({
|
|
command: "closeAutofillOverlay",
|
|
data: { forceCloseOverlay: false },
|
|
});
|
|
|
|
expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
|
|
});
|
|
|
|
it("removes the autofill overlay if the message flags a forced closure", () => {
|
|
sendExtensionRuntimeMessage({
|
|
command: "closeAutofillOverlay",
|
|
data: { forceCloseOverlay: true },
|
|
});
|
|
|
|
expect(
|
|
autofillInit["autofillOverlayContentService"].removeAutofillOverlay,
|
|
).toHaveBeenCalled();
|
|
});
|
|
|
|
it("ignores the message if a field is currently focused", () => {
|
|
autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = true;
|
|
|
|
sendExtensionRuntimeMessage({ command: "closeAutofillOverlay" });
|
|
|
|
expect(
|
|
autofillInit["autofillOverlayContentService"].removeAutofillOverlayList,
|
|
).not.toHaveBeenCalled();
|
|
expect(
|
|
autofillInit["autofillOverlayContentService"].removeAutofillOverlay,
|
|
).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("removes the autofill overlay list if the overlay is currently filling", () => {
|
|
autofillInit["autofillOverlayContentService"].isCurrentlyFilling = true;
|
|
|
|
sendExtensionRuntimeMessage({ command: "closeAutofillOverlay" });
|
|
|
|
expect(
|
|
autofillInit["autofillOverlayContentService"].removeAutofillOverlayList,
|
|
).toHaveBeenCalled();
|
|
expect(
|
|
autofillInit["autofillOverlayContentService"].removeAutofillOverlay,
|
|
).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("removes the entire overlay if the overlay is not currently filling", () => {
|
|
sendExtensionRuntimeMessage({ command: "closeAutofillOverlay" });
|
|
|
|
expect(
|
|
autofillInit["autofillOverlayContentService"].removeAutofillOverlayList,
|
|
).not.toHaveBeenCalled();
|
|
expect(
|
|
autofillInit["autofillOverlayContentService"].removeAutofillOverlay,
|
|
).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("addNewVaultItemFromOverlay", () => {
|
|
it("will not add a new vault item if the autofillOverlayContentService is not present", () => {
|
|
const newAutofillInit = new AutofillInit(undefined);
|
|
newAutofillInit.init();
|
|
|
|
sendExtensionRuntimeMessage({ command: "addNewVaultItemFromOverlay" });
|
|
|
|
expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
|
|
});
|
|
|
|
it("will add a new vault item", () => {
|
|
sendExtensionRuntimeMessage({ command: "addNewVaultItemFromOverlay" });
|
|
|
|
expect(autofillInit["autofillOverlayContentService"].addNewVaultItem).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("redirectOverlayFocusOut", () => {
|
|
const message = {
|
|
command: "redirectOverlayFocusOut",
|
|
data: {
|
|
direction: RedirectFocusDirection.Next,
|
|
},
|
|
};
|
|
|
|
it("ignores the message to redirect focus if the autofillOverlayContentService does not exist", () => {
|
|
const newAutofillInit = new AutofillInit(undefined);
|
|
newAutofillInit.init();
|
|
|
|
sendExtensionRuntimeMessage(message);
|
|
|
|
expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
|
|
});
|
|
|
|
it("redirects the overlay focus", () => {
|
|
sendExtensionRuntimeMessage(message);
|
|
|
|
expect(
|
|
autofillInit["autofillOverlayContentService"].redirectOverlayFocusOut,
|
|
).toHaveBeenCalledWith(message.data.direction);
|
|
});
|
|
});
|
|
|
|
describe("updateIsOverlayCiphersPopulated", () => {
|
|
const message = {
|
|
command: "updateIsOverlayCiphersPopulated",
|
|
data: {
|
|
isOverlayCiphersPopulated: true,
|
|
},
|
|
};
|
|
|
|
it("skips updating whether the ciphers are populated if the autofillOverlayContentService does note exist", () => {
|
|
const newAutofillInit = new AutofillInit(undefined);
|
|
newAutofillInit.init();
|
|
|
|
sendExtensionRuntimeMessage(message);
|
|
|
|
expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
|
|
});
|
|
|
|
it("updates whether the overlay ciphers are populated", () => {
|
|
sendExtensionRuntimeMessage(message);
|
|
|
|
expect(autofillInit["autofillOverlayContentService"].isOverlayCiphersPopulated).toEqual(
|
|
message.data.isOverlayCiphersPopulated,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("bgUnlockPopoutOpened", () => {
|
|
it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => {
|
|
const newAutofillInit = new AutofillInit(undefined);
|
|
newAutofillInit.init();
|
|
jest.spyOn(newAutofillInit as any, "removeAutofillOverlay");
|
|
|
|
sendExtensionRuntimeMessage({ command: "bgUnlockPopoutOpened" });
|
|
|
|
expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
|
|
expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("blurs the most recently focused feel and remove the autofill overlay", () => {
|
|
jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField");
|
|
jest.spyOn(autofillInit as any, "removeAutofillOverlay");
|
|
|
|
sendExtensionRuntimeMessage({ command: "bgUnlockPopoutOpened" });
|
|
|
|
expect(
|
|
autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField,
|
|
).toHaveBeenCalled();
|
|
expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("bgVaultItemRepromptPopoutOpened", () => {
|
|
it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => {
|
|
const newAutofillInit = new AutofillInit(undefined);
|
|
newAutofillInit.init();
|
|
jest.spyOn(newAutofillInit as any, "removeAutofillOverlay");
|
|
|
|
sendExtensionRuntimeMessage({ command: "bgVaultItemRepromptPopoutOpened" });
|
|
|
|
expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
|
|
expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("blurs the most recently focused feel and remove the autofill overlay", () => {
|
|
jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField");
|
|
jest.spyOn(autofillInit as any, "removeAutofillOverlay");
|
|
|
|
sendExtensionRuntimeMessage({ command: "bgVaultItemRepromptPopoutOpened" });
|
|
|
|
expect(
|
|
autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField,
|
|
).toHaveBeenCalled();
|
|
expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("updateAutofillOverlayVisibility", () => {
|
|
beforeEach(() => {
|
|
autofillInit["autofillOverlayContentService"].autofillOverlayVisibility =
|
|
AutofillOverlayVisibility.OnButtonClick;
|
|
});
|
|
|
|
it("skips attempting to update the overlay visibility if the autofillOverlayVisibility data value is not present", () => {
|
|
sendExtensionRuntimeMessage({
|
|
command: "updateAutofillOverlayVisibility",
|
|
data: {},
|
|
});
|
|
|
|
expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual(
|
|
AutofillOverlayVisibility.OnButtonClick,
|
|
);
|
|
});
|
|
|
|
it("updates the overlay visibility value", () => {
|
|
const message = {
|
|
command: "updateAutofillOverlayVisibility",
|
|
data: {
|
|
autofillOverlayVisibility: AutofillOverlayVisibility.Off,
|
|
},
|
|
};
|
|
|
|
sendExtensionRuntimeMessage(message);
|
|
|
|
expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual(
|
|
message.data.autofillOverlayVisibility,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("destroy", () => {
|
|
it("clears the timeout used to collect page details on load", () => {
|
|
jest.spyOn(window, "clearTimeout");
|
|
|
|
autofillInit.init();
|
|
autofillInit.destroy();
|
|
|
|
expect(window.clearTimeout).toHaveBeenCalledWith(
|
|
autofillInit["collectPageDetailsOnLoadTimeout"],
|
|
);
|
|
});
|
|
|
|
it("removes the extension message listeners", () => {
|
|
autofillInit.destroy();
|
|
|
|
expect(chrome.runtime.onMessage.removeListener).toHaveBeenCalledWith(
|
|
autofillInit["handleExtensionMessage"],
|
|
);
|
|
});
|
|
|
|
it("destroys the collectAutofillContentService", () => {
|
|
jest.spyOn(autofillInit["collectAutofillContentService"], "destroy");
|
|
|
|
autofillInit.destroy();
|
|
|
|
expect(autofillInit["collectAutofillContentService"].destroy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|