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

[PM-5189] Implementing jest tests for AutofillInlineMenuContentService

This commit is contained in:
Cesar Gonzalez
2024-06-11 13:30:32 -05:00
parent 8cea459d44
commit eaeb4e46e9
3 changed files with 196 additions and 17 deletions

View File

@@ -2,7 +2,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
import { InlineMenuCipherData } from "../../../background/abstractions/overlay.background";
type AutofillInlineMenuContainerMessage = {
export type AutofillInlineMenuContainerMessage = {
command: string;
portKey: string;
};
@@ -18,8 +18,14 @@ export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMe
portName: string;
};
export type AutofillInlineMenuContainerWindowMessage = AutofillInlineMenuContainerMessage &
Record<string, unknown>;
export type AutofillInlineMenuContainerPortMessage = AutofillInlineMenuContainerMessage &
Record<string, unknown>;
export type AutofillInlineMenuContainerWindowMessageHandlers = {
[key: string]: CallableFunction;
initAutofillInlineMenuList: (message: InitAutofillInlineMenuElementMessage) => void;
initAutofillInlineMenuButton: (message: InitAutofillInlineMenuElementMessage) => void;
initAutofillInlineMenuList: (message: InitAutofillInlineMenuElementMessage) => void;
};

View File

@@ -0,0 +1,130 @@
import { AutofillOverlayPort } from "../../../../enums/autofill-overlay.enum";
import { createPortSpyMock } from "../../../../spec/autofill-mocks";
import { postWindowMessage } from "../../../../spec/testing-utils";
import { AutofillInlineMenuContainer } from "./autofill-inline-menu-container";
describe("AutofillInlineMenuContainer", () => {
const portKey = "testPortKey";
const iframeUrl = "https://example.com";
const pageTitle = "Example";
let autofillInlineMenuContainer: AutofillInlineMenuContainer;
beforeEach(() => {
autofillInlineMenuContainer = new AutofillInlineMenuContainer();
});
afterEach(() => {
jest.clearAllMocks();
});
describe("initializing the inline menu iframe", () => {
it("sets the default iframe attributes to the message values", () => {
const message = {
command: "initAutofillInlineMenuList",
iframeUrl,
pageTitle,
portKey,
portName: AutofillOverlayPort.List,
};
postWindowMessage(message);
expect(autofillInlineMenuContainer["defaultIframeAttributes"].src).toBe(message.iframeUrl);
expect(autofillInlineMenuContainer["defaultIframeAttributes"].title).toBe(message.pageTitle);
expect(autofillInlineMenuContainer["portName"]).toBe(message.portName);
});
it("sets up a onLoad listener on the iframe that sets up the background port message listener", async () => {
const message = {
command: "initAutofillInlineMenuButton",
iframeUrl,
pageTitle,
portKey,
portName: AutofillOverlayPort.Button,
};
postWindowMessage(message);
jest.spyOn(autofillInlineMenuContainer["inlineMenuPageIframe"].contentWindow, "postMessage");
autofillInlineMenuContainer["inlineMenuPageIframe"].dispatchEvent(new Event("load"));
expect(chrome.runtime.connect).toHaveBeenCalledWith({ name: message.portName });
expect(
autofillInlineMenuContainer["inlineMenuPageIframe"].contentWindow.postMessage,
).toHaveBeenCalledWith(message, "*");
});
});
describe("handling window messages", () => {
let iframe: HTMLIFrameElement;
let port: chrome.runtime.Port;
beforeEach(() => {
const message = {
command: "initAutofillInlineMenuButton",
iframeUrl,
pageTitle,
portKey,
portName: AutofillOverlayPort.Button,
};
postWindowMessage(message);
iframe = autofillInlineMenuContainer["inlineMenuPageIframe"];
jest.spyOn(iframe.contentWindow, "postMessage");
port = createPortSpyMock(AutofillOverlayPort.Button);
autofillInlineMenuContainer["port"] = port;
});
it("ignores messages that do not contain a portKey", () => {
const message = { command: "checkInlineMenuButtonFocused" };
postWindowMessage(message, "*", iframe.contentWindow as any);
expect(port.postMessage).not.toHaveBeenCalled();
});
it("ignores messages if the inline menu iframe has not been created", () => {
autofillInlineMenuContainer["inlineMenuPageIframe"] = null;
const message = { command: "checkInlineMenuButtonFocused", portKey };
postWindowMessage(message, "*", iframe.contentWindow as any);
expect(port.postMessage).not.toHaveBeenCalled();
});
it("ignores messages that do not come from either the parent frame or the inline menu iframe", () => {
const randomIframe = document.createElement("iframe");
const message = { command: "checkInlineMenuButtonFocused", portKey };
postWindowMessage(message, "*", randomIframe.contentWindow as any);
expect(port.postMessage).not.toHaveBeenCalled();
});
it("ignores messages that come from an invalid origin", () => {
const message = { command: "checkInlineMenuButtonFocused", portKey };
postWindowMessage(message, "https://example.com", iframe.contentWindow as any);
expect(port.postMessage).not.toHaveBeenCalled();
});
it("posts a message to the background from the inline menu iframe", () => {
const message = { command: "checkInlineMenuButtonFocused", portKey };
postWindowMessage(message, "null", iframe.contentWindow as any);
expect(port.postMessage).toHaveBeenCalledWith(message);
});
it("posts a message to the inline menu iframe from the parent", () => {
const message = { command: "checkInlineMenuButtonFocused", portKey };
postWindowMessage(message);
expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith(message, "*");
});
});
});

View File

@@ -4,9 +4,12 @@ import { setElementStyles } from "../../../../utils";
import {
InitAutofillInlineMenuElementMessage,
AutofillInlineMenuContainerWindowMessageHandlers,
AutofillInlineMenuContainerWindowMessage,
AutofillInlineMenuContainerPortMessage,
} from "../../abstractions/autofill-inline-menu-container";
export class AutofillInlineMenuContainer {
private setElementStyles = setElementStyles;
private extensionOriginsSet: Set<string>;
private port: chrome.runtime.Port | null = null;
private portName: string;
@@ -37,8 +40,8 @@ export class AutofillInlineMenuContainer {
tabIndex: "-1",
};
private windowMessageHandlers: AutofillInlineMenuContainerWindowMessageHandlers = {
initAutofillInlineMenuList: (message) => this.handleInitInlineMenuIframe(message),
initAutofillInlineMenuButton: (message) => this.handleInitInlineMenuIframe(message),
initAutofillInlineMenuList: (message) => this.handleInitInlineMenuIframe(message),
};
constructor() {
@@ -50,44 +53,67 @@ export class AutofillInlineMenuContainer {
globalThis.addEventListener("message", this.handleWindowMessage);
}
/**
* Handles initialization of the iframe used to display the inline menu.
*
* @param message - The message containing the iframe url and page title.
*/
private handleInitInlineMenuIframe(message: InitAutofillInlineMenuElementMessage) {
this.defaultIframeAttributes.src = message.iframeUrl;
this.defaultIframeAttributes.title = message.pageTitle;
this.portName = message.portName;
this.inlineMenuPageIframe = globalThis.document.createElement("iframe");
setElementStyles(this.inlineMenuPageIframe, this.iframeStyles, true);
this.setElementStyles(this.inlineMenuPageIframe, this.iframeStyles, true);
for (const [attribute, value] of Object.entries(this.defaultIframeAttributes)) {
this.inlineMenuPageIframe.setAttribute(attribute, value);
}
this.inlineMenuPageIframe.addEventListener(EVENTS.LOAD, () =>
this.setupPortMessageListener(message),
);
const handleInlineMenuPageIframeLoad = () => {
this.inlineMenuPageIframe.removeEventListener(EVENTS.LOAD, handleInlineMenuPageIframeLoad);
this.setupPortMessageListener(message);
};
this.inlineMenuPageIframe.addEventListener(EVENTS.LOAD, handleInlineMenuPageIframeLoad);
globalThis.document.body.appendChild(this.inlineMenuPageIframe);
}
/**
* Sets up the port message listener for the inline menu page.
*
* @param message - The message containing the port name.
*/
private setupPortMessageListener = (message: InitAutofillInlineMenuElementMessage) => {
this.port = chrome.runtime.connect({ name: this.portName });
this.postMessageToInlineMenuPage(message);
};
private postMessageToInlineMenuPage(message: any) {
if (!this.inlineMenuPageIframe?.contentWindow) {
return;
}
/**
* Posts a message to the inline menu page iframe.
*
* @param message - The message to post.
*/
private postMessageToInlineMenuPage(message: AutofillInlineMenuContainerWindowMessage) {
if (this.inlineMenuPageIframe?.contentWindow) {
this.inlineMenuPageIframe.contentWindow.postMessage(message, "*");
}
private postMessageToBackground(message: any) {
if (!this.port) {
return;
}
/**
* Posts a message from the inline menu iframe to the background script.
*
* @param message - The message to post.
*/
private postMessageToBackground(message: AutofillInlineMenuContainerPortMessage) {
if (this.port) {
this.port.postMessage(message);
}
}
/**
* Handles window messages, routing them to the appropriate handler.
*
* @param event - The message event.
*/
private handleWindowMessage = (event: MessageEvent) => {
const message = event.data;
if (this.isForeignWindowMessage(event)) {
@@ -107,6 +133,13 @@ export class AutofillInlineMenuContainer {
this.postMessageToBackground(message);
};
/**
* Identifies if the message is from a foreign window. A foreign window message is
* considered as any message that does not have a portKey, is not from the parent window,
* or is not from the inline menu page iframe.
*
* @param event - The message event.
*/
private isForeignWindowMessage(event: MessageEvent) {
if (!event.data.portKey) {
return true;
@@ -119,10 +152,20 @@ export class AutofillInlineMenuContainer {
return !this.isMessageFromInlineMenuPageIframe(event);
}
/**
* Identifies if the message is from the parent window.
*
* @param event - The message event.
*/
private isMessageFromParentWindow(event: MessageEvent): boolean {
return globalThis.parent === event.source;
}
/**
* Identifies if the message is from the inline menu page iframe.
*
* @param event - The message event.
*/
private isMessageFromInlineMenuPageIframe(event: MessageEvent): boolean {
if (!this.inlineMenuPageIframe) {
return false;