407 lines
14 KiB
TypeScript
407 lines
14 KiB
TypeScript
import { EVENTS } from "../../constants";
|
|
import { createPortSpyMock } from "../../jest/autofill-mocks";
|
|
import {
|
|
flushPromises,
|
|
sendPortMessage,
|
|
triggerPortOnDisconnectEvent,
|
|
} from "../../jest/testing-utils";
|
|
import { AutofillOverlayPort } from "../../utils/autofill-overlay.enum";
|
|
|
|
import AutofillOverlayIframeService from "./autofill-overlay-iframe.service";
|
|
|
|
describe("AutofillOverlayIframeService", () => {
|
|
const iframePath = "overlay/list.html";
|
|
let autofillOverlayIframeService: AutofillOverlayIframeService;
|
|
let portSpy: chrome.runtime.Port;
|
|
let shadowAppendSpy: jest.SpyInstance;
|
|
let handlePortDisconnectSpy: jest.SpyInstance;
|
|
let handlePortMessageSpy: jest.SpyInstance;
|
|
let handleWindowMessageSpy: jest.SpyInstance;
|
|
|
|
beforeEach(() => {
|
|
const shadow = document.createElement("div").attachShadow({ mode: "open" });
|
|
autofillOverlayIframeService = new AutofillOverlayIframeService(
|
|
iframePath,
|
|
AutofillOverlayPort.Button,
|
|
shadow,
|
|
);
|
|
shadowAppendSpy = jest.spyOn(shadow, "appendChild");
|
|
handlePortDisconnectSpy = jest.spyOn(
|
|
autofillOverlayIframeService as any,
|
|
"handlePortDisconnect",
|
|
);
|
|
handlePortMessageSpy = jest.spyOn(autofillOverlayIframeService as any, "handlePortMessage");
|
|
handleWindowMessageSpy = jest.spyOn(autofillOverlayIframeService as any, "handleWindowMessage");
|
|
chrome.runtime.connect = jest.fn((connectInfo: chrome.runtime.ConnectInfo) =>
|
|
createPortSpyMock(connectInfo.name),
|
|
) as unknown as typeof chrome.runtime.connect;
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe("initOverlayIframe", () => {
|
|
it("sets up the iframe's attributes", () => {
|
|
autofillOverlayIframeService.initOverlayIframe({ height: "0px" }, "title");
|
|
|
|
expect(autofillOverlayIframeService["iframe"]).toMatchSnapshot();
|
|
});
|
|
|
|
it("appends the iframe to the shadowDom", () => {
|
|
jest.spyOn(autofillOverlayIframeService["shadow"], "appendChild");
|
|
|
|
autofillOverlayIframeService.initOverlayIframe({}, "title");
|
|
|
|
expect(autofillOverlayIframeService["shadow"].appendChild).toBeCalledWith(
|
|
autofillOverlayIframeService["iframe"],
|
|
);
|
|
});
|
|
|
|
it("creates an aria alert element if the ariaAlert param is passed", () => {
|
|
const ariaAlert = "aria alert";
|
|
jest.spyOn(autofillOverlayIframeService as any, "createAriaAlertElement");
|
|
|
|
autofillOverlayIframeService.initOverlayIframe({}, "title", ariaAlert);
|
|
|
|
expect(autofillOverlayIframeService["createAriaAlertElement"]).toBeCalledWith(ariaAlert);
|
|
expect(autofillOverlayIframeService["ariaAlertElement"]).toMatchSnapshot();
|
|
});
|
|
|
|
describe("on load of the iframe source", () => {
|
|
beforeEach(() => {
|
|
autofillOverlayIframeService.initOverlayIframe({ height: "0px" }, "title", "ariaAlert");
|
|
});
|
|
|
|
it("sets up and connects the port message listener to the extension background", () => {
|
|
jest.spyOn(globalThis, "addEventListener");
|
|
|
|
autofillOverlayIframeService["iframe"].dispatchEvent(new Event(EVENTS.LOAD));
|
|
portSpy = autofillOverlayIframeService["port"];
|
|
|
|
expect(chrome.runtime.connect).toBeCalledWith({ name: AutofillOverlayPort.Button });
|
|
expect(portSpy.onDisconnect.addListener).toBeCalledWith(handlePortDisconnectSpy);
|
|
expect(portSpy.onMessage.addListener).toBeCalledWith(handlePortMessageSpy);
|
|
expect(globalThis.addEventListener).toBeCalledWith(EVENTS.MESSAGE, handleWindowMessageSpy);
|
|
});
|
|
|
|
it("skips announcing the aria alert if the aria alert element is not populated", () => {
|
|
jest.spyOn(globalThis, "setTimeout");
|
|
autofillOverlayIframeService["ariaAlertElement"] = undefined;
|
|
|
|
autofillOverlayIframeService["iframe"].dispatchEvent(new Event(EVENTS.LOAD));
|
|
|
|
expect(globalThis.setTimeout).not.toBeCalled();
|
|
});
|
|
|
|
it("announces the aria alert if the aria alert element is populated", () => {
|
|
jest.useFakeTimers();
|
|
jest.spyOn(globalThis, "setTimeout");
|
|
autofillOverlayIframeService["ariaAlertElement"] = document.createElement("div");
|
|
autofillOverlayIframeService["ariaAlertTimeout"] = setTimeout(jest.fn(), 2000);
|
|
|
|
autofillOverlayIframeService["iframe"].dispatchEvent(new Event(EVENTS.LOAD));
|
|
|
|
expect(globalThis.setTimeout).toBeCalled();
|
|
jest.advanceTimersByTime(2000);
|
|
|
|
expect(shadowAppendSpy).toBeCalledWith(autofillOverlayIframeService["ariaAlertElement"]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("event listeners", () => {
|
|
beforeEach(() => {
|
|
autofillOverlayIframeService.initOverlayIframe({ height: "0px" }, "title", "ariaAlert");
|
|
autofillOverlayIframeService["iframe"].dispatchEvent(new Event(EVENTS.LOAD));
|
|
Object.defineProperty(autofillOverlayIframeService["iframe"], "contentWindow", {
|
|
value: {
|
|
postMessage: jest.fn(),
|
|
},
|
|
writable: true,
|
|
});
|
|
jest.spyOn(autofillOverlayIframeService["iframe"].contentWindow, "postMessage");
|
|
portSpy = autofillOverlayIframeService["port"];
|
|
});
|
|
|
|
describe("handlePortDisconnect", () => {
|
|
it("ignores ports that do not have the correct port name", () => {
|
|
portSpy.name = "wrong-port-name";
|
|
triggerPortOnDisconnectEvent(portSpy);
|
|
|
|
expect(autofillOverlayIframeService["port"]).not.toBeNull();
|
|
});
|
|
|
|
it("resets the iframe element's opacity, height, and display styles", () => {
|
|
triggerPortOnDisconnectEvent(portSpy);
|
|
|
|
expect(autofillOverlayIframeService["iframe"].style.opacity).toBe("0");
|
|
expect(autofillOverlayIframeService["iframe"].style.height).toBe("0px");
|
|
expect(autofillOverlayIframeService["iframe"].style.display).toBe("block");
|
|
});
|
|
|
|
it("removes the global message listener", () => {
|
|
jest.spyOn(globalThis, "removeEventListener");
|
|
|
|
triggerPortOnDisconnectEvent(portSpy);
|
|
|
|
expect(globalThis.removeEventListener).toBeCalledWith(
|
|
EVENTS.MESSAGE,
|
|
handleWindowMessageSpy,
|
|
);
|
|
});
|
|
|
|
it("removes the port's onMessage listener", () => {
|
|
triggerPortOnDisconnectEvent(portSpy);
|
|
|
|
expect(portSpy.onMessage.removeListener).toBeCalledWith(handlePortMessageSpy);
|
|
});
|
|
|
|
it("removes the port's onDisconnect listener", () => {
|
|
triggerPortOnDisconnectEvent(portSpy);
|
|
|
|
expect(portSpy.onDisconnect.removeListener).toBeCalledWith(handlePortDisconnectSpy);
|
|
});
|
|
|
|
it("disconnects the port", () => {
|
|
triggerPortOnDisconnectEvent(portSpy);
|
|
|
|
expect(portSpy.disconnect).toBeCalled();
|
|
expect(autofillOverlayIframeService["port"]).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("handlePortMessage", () => {
|
|
it("ignores port messages that do not correlate to the correct port name", () => {
|
|
portSpy.name = "wrong-port-name";
|
|
sendPortMessage(portSpy, {});
|
|
|
|
expect(autofillOverlayIframeService["iframe"].contentWindow.postMessage).not.toBeCalled();
|
|
});
|
|
|
|
it("passes on the message to the iframe if the message is not registered with the message handlers", () => {
|
|
const message = { command: "unregisteredMessage" };
|
|
|
|
sendPortMessage(portSpy, message);
|
|
|
|
expect(autofillOverlayIframeService["iframe"].contentWindow.postMessage).toBeCalledWith(
|
|
message,
|
|
"*",
|
|
);
|
|
});
|
|
|
|
it("handles port messages that are registered with the message handlers and does not pass the message on to the iframe", () => {
|
|
jest.spyOn(autofillOverlayIframeService as any, "updateIframePosition");
|
|
|
|
sendPortMessage(portSpy, { command: "updateIframePosition" });
|
|
|
|
expect(autofillOverlayIframeService["iframe"].contentWindow.postMessage).not.toBeCalled();
|
|
});
|
|
|
|
describe("initializing the overlay list", () => {
|
|
let updateElementStylesSpy: jest.SpyInstance;
|
|
|
|
beforeEach(() => {
|
|
updateElementStylesSpy = jest.spyOn(
|
|
autofillOverlayIframeService as any,
|
|
"updateElementStyles",
|
|
);
|
|
});
|
|
|
|
it("passed the message on to the iframe element", () => {
|
|
const message = {
|
|
command: "initAutofillOverlayList",
|
|
theme: "theme_light",
|
|
};
|
|
|
|
sendPortMessage(portSpy, message);
|
|
|
|
expect(updateElementStylesSpy).not.toBeCalled();
|
|
expect(autofillOverlayIframeService["iframe"].contentWindow.postMessage).toBeCalledWith(
|
|
message,
|
|
"*",
|
|
);
|
|
});
|
|
|
|
it("updates the border to match the `dark` theme", () => {
|
|
const message = {
|
|
command: "initAutofillOverlayList",
|
|
theme: "theme_dark",
|
|
};
|
|
|
|
sendPortMessage(portSpy, message);
|
|
|
|
expect(updateElementStylesSpy).toBeCalledWith(autofillOverlayIframeService["iframe"], {
|
|
borderColor: "#4c525f",
|
|
});
|
|
});
|
|
|
|
it("updates the border to match the `nord` theme", () => {
|
|
const message = {
|
|
command: "initAutofillOverlayList",
|
|
theme: "theme_nord",
|
|
};
|
|
|
|
sendPortMessage(portSpy, message);
|
|
|
|
expect(updateElementStylesSpy).toBeCalledWith(autofillOverlayIframeService["iframe"], {
|
|
borderColor: "#2E3440",
|
|
});
|
|
});
|
|
|
|
it("updates the border to match the `solarizedDark` theme", () => {
|
|
const message = {
|
|
command: "initAutofillOverlayList",
|
|
theme: "theme_solarizedDark",
|
|
};
|
|
|
|
sendPortMessage(portSpy, message);
|
|
|
|
expect(updateElementStylesSpy).toBeCalledWith(autofillOverlayIframeService["iframe"], {
|
|
borderColor: "#073642",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("updating the iframe's position", () => {
|
|
beforeEach(() => {
|
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
|
});
|
|
|
|
it("ignores updating the iframe position if the document does not have focus", () => {
|
|
jest.spyOn(autofillOverlayIframeService as any, "updateElementStyles");
|
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
|
|
|
sendPortMessage(portSpy, {
|
|
command: "updateIframePosition",
|
|
styles: { top: 100, left: 100 },
|
|
});
|
|
|
|
expect(autofillOverlayIframeService["updateElementStyles"]).not.toBeCalled();
|
|
});
|
|
|
|
it("updates the iframe position if the document has focus", () => {
|
|
const styles = { top: "100px", left: "100px" };
|
|
|
|
sendPortMessage(portSpy, {
|
|
command: "updateIframePosition",
|
|
styles,
|
|
});
|
|
|
|
expect(autofillOverlayIframeService["iframe"].style.top).toBe(styles.top);
|
|
expect(autofillOverlayIframeService["iframe"].style.left).toBe(styles.left);
|
|
});
|
|
|
|
it("fades the iframe element in after positioning the element", () => {
|
|
jest.useFakeTimers();
|
|
const styles = { top: "100px", left: "100px" };
|
|
|
|
sendPortMessage(portSpy, {
|
|
command: "updateIframePosition",
|
|
styles,
|
|
});
|
|
|
|
expect(autofillOverlayIframeService["iframe"].style.opacity).toBe("0");
|
|
jest.advanceTimersByTime(10);
|
|
expect(autofillOverlayIframeService["iframe"].style.opacity).toBe("1");
|
|
});
|
|
|
|
it("announces the opening of the iframe using an aria alert", () => {
|
|
jest.useFakeTimers();
|
|
const styles = { top: "100px", left: "100px" };
|
|
|
|
sendPortMessage(portSpy, {
|
|
command: "updateIframePosition",
|
|
styles,
|
|
});
|
|
|
|
jest.advanceTimersByTime(2000);
|
|
expect(shadowAppendSpy).toBeCalledWith(autofillOverlayIframeService["ariaAlertElement"]);
|
|
});
|
|
});
|
|
|
|
it("updates the visibility of the iframe", () => {
|
|
sendPortMessage(portSpy, {
|
|
command: "updateOverlayHidden",
|
|
styles: { display: "none" },
|
|
});
|
|
|
|
expect(autofillOverlayIframeService["iframe"].style.display).toBe("none");
|
|
});
|
|
});
|
|
|
|
describe("handleWindowMessage", () => {
|
|
it("ignores window messages when the port is not set", () => {
|
|
autofillOverlayIframeService["port"] = null;
|
|
|
|
globalThis.dispatchEvent(new MessageEvent("message", { data: {} }));
|
|
|
|
expect(autofillOverlayIframeService["port"]).toBeNull();
|
|
});
|
|
|
|
it("ignores window messages whose source is not the iframe's content window", () => {
|
|
globalThis.dispatchEvent(
|
|
new MessageEvent("message", {
|
|
data: {},
|
|
source: window,
|
|
}),
|
|
);
|
|
|
|
expect(portSpy.postMessage).not.toBeCalled();
|
|
});
|
|
|
|
it("ignores window messages whose origin is not from the extension origin", () => {
|
|
globalThis.dispatchEvent(
|
|
new MessageEvent("message", {
|
|
data: {},
|
|
source: autofillOverlayIframeService["iframe"].contentWindow,
|
|
origin: "https://www.google.com",
|
|
}),
|
|
);
|
|
|
|
expect(portSpy.postMessage).not.toBeCalled();
|
|
});
|
|
|
|
it("passes the window message from an iframe element to the background port", () => {
|
|
globalThis.dispatchEvent(
|
|
new MessageEvent("message", {
|
|
data: { command: "not-a-handled-command" },
|
|
source: autofillOverlayIframeService["iframe"].contentWindow,
|
|
origin: "chrome-extension://id",
|
|
}),
|
|
);
|
|
|
|
expect(portSpy.postMessage).toBeCalledWith({ command: "not-a-handled-command" });
|
|
});
|
|
|
|
it("updates the overlay list height", () => {
|
|
globalThis.dispatchEvent(
|
|
new MessageEvent("message", {
|
|
data: { command: "updateAutofillOverlayListHeight", styles: { height: "300px" } },
|
|
source: autofillOverlayIframeService["iframe"].contentWindow,
|
|
origin: "chrome-extension://id",
|
|
}),
|
|
);
|
|
|
|
expect(autofillOverlayIframeService["iframe"].style.height).toBe("300px");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("mutation observer", () => {
|
|
beforeEach(() => {
|
|
autofillOverlayIframeService.initOverlayIframe({ height: "0px" }, "title", "ariaAlert");
|
|
autofillOverlayIframeService["iframe"].dispatchEvent(new Event(EVENTS.LOAD));
|
|
});
|
|
|
|
it("reverts any styles changes made directly to the iframe", async () => {
|
|
jest.useFakeTimers();
|
|
|
|
autofillOverlayIframeService["iframe"].style.visibility = "hidden";
|
|
await flushPromises();
|
|
|
|
expect(autofillOverlayIframeService["iframe"].style.visibility).toBe("visible");
|
|
});
|
|
});
|
|
});
|