1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-24 21:41:33 +01:00

[PM-5189] Implementing jest tests for the OverlayBackground

This commit is contained in:
Cesar Gonzalez 2024-06-05 15:04:43 -05:00
parent 2efbc5ce51
commit 5e70d042b4
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
3 changed files with 213 additions and 36 deletions

View File

@ -3,7 +3,10 @@ import { BehaviorSubject } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import {
AutofillOverlayVisibility,
SHOW_AUTOFILL_BUTTON,
} from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction as AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service";
import {
DefaultDomainSettingsService,
@ -44,7 +47,14 @@ import {
createPortSpyMock,
createFocusedFieldDataMock,
} from "../spec/autofill-mocks";
import { flushPromises, sendMockExtensionMessage, sendPortMessage } from "../spec/testing-utils";
import {
flushPromises,
sendMockExtensionMessage,
sendPortMessage,
triggerPortOnConnectEvent,
triggerPortOnDisconnectEvent,
triggerPortOnMessageEvent,
} from "../spec/testing-utils";
import {
FocusedFieldData,
@ -93,19 +103,19 @@ describe("OverlayBackground", () => {
async function initOverlayElementPorts(options = { initList: true, initButton: true }) {
const { initList, initButton } = options;
if (initButton) {
await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button));
triggerPortOnConnectEvent(createPortSpyMock(AutofillOverlayPort.Button));
buttonPortSpy = overlayBackground["inlineMenuButtonPort"];
buttonMessageConnectorSpy = createPortSpyMock(AutofillOverlayPort.ButtonMessageConnector);
await overlayBackground["handlePortOnConnect"](buttonMessageConnectorSpy);
triggerPortOnConnectEvent(buttonMessageConnectorSpy);
}
if (initList) {
await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List));
triggerPortOnConnectEvent(createPortSpyMock(AutofillOverlayPort.List));
listPortSpy = overlayBackground["inlineMenuListPort"];
listMessageConnectorSpy = createPortSpyMock(AutofillOverlayPort.ListMessageConnector);
await overlayBackground["handlePortOnConnect"](listMessageConnectorSpy);
triggerPortOnConnectEvent(listMessageConnectorSpy);
}
return { buttonPortSpy, listPortSpy };
@ -1269,6 +1279,25 @@ describe("OverlayBackground", () => {
});
});
describe("handle extension onMessage", () => {
it("will return early if the message command is not present within the extensionMessageHandlers", () => {
const message = {
command: "not-a-command",
};
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const sendResponse = jest.fn();
const returnValue = overlayBackground["handleExtensionMessage"](
message,
sender,
sendResponse,
);
expect(returnValue).toBe(undefined);
expect(sendResponse).not.toHaveBeenCalled();
});
});
describe("inline menu button message handlers", () => {
let sender: chrome.runtime.MessageSender;
const portKey = "inlineMenuButtonPort";
@ -1613,5 +1642,152 @@ describe("OverlayBackground", () => {
);
});
});
describe("viewSelectedCipher message handler", () => {
let openViewVaultItemPopoutSpy: jest.SpyInstance;
beforeEach(() => {
openViewVaultItemPopoutSpy = jest
.spyOn(overlayBackground as any, "openViewVaultItemPopout")
.mockImplementation();
});
it("returns early if the passed cipher ID does not match one of the inline menu ciphers", async () => {
overlayBackground["inlineMenuCiphers"] = new Map([
["overlay-cipher-0", mock<CipherView>({ id: "overlay-cipher-0" })],
]);
sendPortMessage(listMessageConnectorSpy, {
command: "viewSelectedCipher",
overlayCipherId: "overlay-cipher-1",
portKey,
});
await flushPromises();
expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled();
});
it("will open the view vault item popout with the selected cipher", async () => {
const cipher = mock<CipherView>({ id: "overlay-cipher-1" });
overlayBackground["inlineMenuCiphers"] = new Map([
["overlay-cipher-0", mock<CipherView>({ id: "overlay-cipher-0" })],
["overlay-cipher-1", cipher],
]);
sendPortMessage(listMessageConnectorSpy, {
command: "viewSelectedCipher",
overlayCipherId: "overlay-cipher-1",
portKey,
});
await flushPromises();
expect(openViewVaultItemPopoutSpy).toHaveBeenCalledWith(sender.tab, {
cipherId: cipher.id,
action: SHOW_AUTOFILL_BUTTON,
});
});
});
describe("redirectAutofillInlineMenuFocusOut message handler", () => {
it("redirects focus out of the inline menu list", async () => {
sendPortMessage(listMessageConnectorSpy, {
command: "redirectAutofillInlineMenuFocusOut",
direction: RedirectFocusDirection.Next,
portKey,
});
await flushPromises();
expect(tabSendMessageDataSpy).toHaveBeenCalledWith(
sender.tab,
"redirectAutofillInlineMenuFocusOut",
{ direction: RedirectFocusDirection.Next },
);
});
});
describe("updateAutofillInlineMenuListHeight message handler", () => {
it("sends a message to the list port to update the menu iframe position", () => {
sendPortMessage(listMessageConnectorSpy, {
command: "updateAutofillInlineMenuListHeight",
styles: { height: "100px" },
portKey,
});
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateInlineMenuIframePosition",
styles: { height: "100px" },
});
});
});
});
describe("handle port onConnect", () => {
it("skips setting up the overlay port if the port connection is not for an overlay element", async () => {
const port = createPortSpyMock("not-an-overlay-element");
triggerPortOnConnectEvent(port);
await flushPromises();
expect(port.onMessage.addListener).not.toHaveBeenCalled();
expect(port.postMessage).not.toHaveBeenCalled();
});
it("stores an existing overlay port so that it can be disconnected at a later time", async () => {
overlayBackground["inlineMenuButtonPort"] = mock<chrome.runtime.Port>();
await initOverlayElementPorts({ initList: false, initButton: true });
await flushPromises();
expect(overlayBackground["expiredPorts"].length).toBe(1);
});
});
describe("handle overlay element port onMessage", () => {
let sender: chrome.runtime.MessageSender;
const portKey = "inlineMenuListPort";
beforeEach(async () => {
sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
portKeyForTabSpy[sender.tab.id] = portKey;
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
await initOverlayElementPorts();
listMessageConnectorSpy.sender = sender;
openUnlockPopoutSpy.mockImplementation();
});
it("ignores messages that do not contain a valid portKey", async () => {
triggerPortOnMessageEvent(buttonMessageConnectorSpy, {
command: "autofillInlineMenuBlurred",
});
await flushPromises();
expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({
command: "checkAutofillInlineMenuListFocused",
});
});
it("ignores messages from ports that are not listened to", () => {
triggerPortOnMessageEvent(buttonPortSpy, {
command: "autofillInlineMenuBlurred",
portKey,
});
expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({
command: "checkAutofillInlineMenuListFocused",
});
});
});
describe("handle port onDisconnect", () => {
it("sets the disconnected port to a `null` value", async () => {
await initOverlayElementPorts();
triggerPortOnDisconnectEvent(buttonPortSpy);
triggerPortOnDisconnectEvent(listPortSpy);
await flushPromises();
expect(overlayBackground["inlineMenuListPort"]).toBeNull();
expect(overlayBackground["inlineMenuButtonPort"]).toBeNull();
});
});
});

View File

@ -1170,7 +1170,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
message: OverlayBackgroundExtensionMessage,
port: chrome.runtime.Port,
) => {
if (this.portKeyForTab[port.sender.tab.id] !== message?.portKey) {
const tabPortKey = this.portKeyForTab[port.sender.tab.id];
if (!tabPortKey || tabPortKey !== message?.portKey) {
return;
}

View File

@ -1,21 +1,21 @@
import { mock } from "jest-mock-extended";
function triggerTestFailure() {
export function triggerTestFailure() {
expect(true).toBe("Test has failed.");
}
const scheduler = typeof setImmediate === "function" ? setImmediate : setTimeout;
function flushPromises() {
export function flushPromises() {
return new Promise(function (resolve) {
scheduler(resolve);
});
}
function postWindowMessage(data: any, origin = "https://localhost/", source = window) {
export function postWindowMessage(data: any, origin = "https://localhost/", source = window) {
globalThis.dispatchEvent(new MessageEvent("message", { data, origin, source }));
}
function sendMockExtensionMessage(
export function sendMockExtensionMessage(
message: any,
sender?: chrome.runtime.MessageSender,
sendResponse?: CallableFunction,
@ -32,7 +32,7 @@ function sendMockExtensionMessage(
);
}
function triggerRuntimeOnConnectEvent(port: chrome.runtime.Port) {
export function triggerRuntimeOnConnectEvent(port: chrome.runtime.Port) {
(chrome.runtime.onConnect.addListener as unknown as jest.SpyInstance).mock.calls.forEach(
(call) => {
const callback = call[0];
@ -41,21 +41,37 @@ function triggerRuntimeOnConnectEvent(port: chrome.runtime.Port) {
);
}
function sendPortMessage(port: chrome.runtime.Port, message: any) {
export function sendPortMessage(port: chrome.runtime.Port, message: any) {
(port.onMessage.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => {
const callback = call[0];
callback(message || {}, port);
});
}
function triggerPortOnDisconnectEvent(port: chrome.runtime.Port) {
export function triggerPortOnConnectEvent(port: chrome.runtime.Port) {
(chrome.runtime.onConnect.addListener as unknown as jest.SpyInstance).mock.calls.forEach(
(call) => {
const callback = call[0];
callback(port);
},
);
}
export function triggerPortOnMessageEvent(port: chrome.runtime.Port, message: any) {
(port.onMessage.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => {
const callback = call[0];
callback(message, port);
});
}
export function triggerPortOnDisconnectEvent(port: chrome.runtime.Port) {
(port.onDisconnect.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => {
const callback = call[0];
callback(port);
});
}
function triggerWindowOnFocusedChangedEvent(windowId: number) {
export function triggerWindowOnFocusedChangedEvent(windowId: number) {
(chrome.windows.onFocusChanged.addListener as unknown as jest.SpyInstance).mock.calls.forEach(
(call) => {
const callback = call[0];
@ -64,7 +80,7 @@ function triggerWindowOnFocusedChangedEvent(windowId: number) {
);
}
function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.TabActiveInfo) {
export function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.TabActiveInfo) {
(chrome.tabs.onActivated.addListener as unknown as jest.SpyInstance).mock.calls.forEach(
(call) => {
const callback = call[0];
@ -73,14 +89,14 @@ function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.TabActiveInfo) {
);
}
function triggerTabOnReplacedEvent(addedTabId: number, removedTabId: number) {
export function triggerTabOnReplacedEvent(addedTabId: number, removedTabId: number) {
(chrome.tabs.onReplaced.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => {
const callback = call[0];
callback(addedTabId, removedTabId);
});
}
function triggerTabOnUpdatedEvent(
export function triggerTabOnUpdatedEvent(
tabId: number,
changeInfo: chrome.tabs.TabChangeInfo,
tab: chrome.tabs.Tab,
@ -91,14 +107,14 @@ function triggerTabOnUpdatedEvent(
});
}
function triggerTabOnRemovedEvent(tabId: number, removeInfo: chrome.tabs.TabRemoveInfo) {
export function triggerTabOnRemovedEvent(tabId: number, removeInfo: chrome.tabs.TabRemoveInfo) {
(chrome.tabs.onRemoved.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => {
const callback = call[0];
callback(tabId, removeInfo);
});
}
function mockQuerySelectorAllDefinedCall() {
export function mockQuerySelectorAllDefinedCall() {
const originalDocumentQuerySelectorAll = document.querySelectorAll;
document.querySelectorAll = function (selector: string) {
return originalDocumentQuerySelectorAll.call(
@ -125,19 +141,3 @@ function mockQuerySelectorAllDefinedCall() {
},
};
}
export {
triggerTestFailure,
flushPromises,
postWindowMessage,
sendMockExtensionMessage,
triggerRuntimeOnConnectEvent,
sendPortMessage,
triggerPortOnDisconnectEvent,
triggerWindowOnFocusedChangedEvent,
triggerTabOnActivatedEvent,
triggerTabOnReplacedEvent,
triggerTabOnUpdatedEvent,
triggerTabOnRemovedEvent,
mockQuerySelectorAllDefinedCall,
};