[PM-5189] Implementing jest tests for the OverlayBackground

This commit is contained in:
Cesar Gonzalez 2024-06-05 09:34:13 -05:00
parent 4a6853bdaf
commit 81e2cd3e1f
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
5 changed files with 296 additions and 23 deletions

View File

@ -94,7 +94,7 @@ export type OverlayBackgroundExtensionMessageHandlers = {
[key: string]: CallableFunction;
autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
checkIsOverlayLoginCiphersPopulated: ({ sender }: BackgroundSenderParam) => void;
checkIsInlineMenuCiphersPopulated: ({ sender }: BackgroundSenderParam) => void;
updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
updateIsFieldCurrentlyFocused: ({ message }: BackgroundMessageParam) => void;
checkIsFieldCurrentlyFocused: () => boolean;

View File

@ -32,9 +32,14 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { BrowserApi } from "../../platform/browser/browser-api";
import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service";
import { AutofillOverlayElement } from "../enums/autofill-overlay.enum";
import { AutofillOverlayElement, AutofillOverlayPort } from "../enums/autofill-overlay.enum";
import { AutofillService } from "../services/abstractions/autofill.service";
import { createChromeTabMock, createAutofillPageDetailsMock } from "../spec/autofill-mocks";
import {
createChromeTabMock,
createAutofillPageDetailsMock,
createPortSpyMock,
createFocusedFieldDataMock,
} from "../spec/autofill-mocks";
import { flushPromises, sendMockExtensionMessage } from "../spec/testing-utils";
import {
@ -47,6 +52,7 @@ import { OverlayBackground } from "./overlay.background";
describe("OverlayBackground", () => {
const mockUserId = Utils.newGuid() as UserId;
const sendResponse = jest.fn();
let accountService: FakeAccountService;
let fakeStateProvider: FakeStateProvider;
let showFaviconsMock$: BehaviorSubject<boolean>;
@ -70,8 +76,26 @@ describe("OverlayBackground", () => {
let subFrameOffsetsSpy: SubFrameOffsetsForTab;
let getFrameDetailsSpy: jest.SpyInstance;
let tabsSendMessageSpy: jest.SpyInstance;
let sendMessageSpy: jest.SpyInstance;
let getTabFromCurrentWindowIdSpy: jest.SpyInstance;
let buttonPortSpy: chrome.runtime.Port;
let listPortSpy: chrome.runtime.Port;
let getFrameCounter: number = 2;
async function initOverlayElementPorts(options = { initList: true, initButton: true }) {
const { initList, initButton } = options;
if (initButton) {
await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button));
buttonPortSpy = overlayBackground["inlineMenuButtonPort"];
}
if (initList) {
await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List));
listPortSpy = overlayBackground["inlineMenuListPort"];
}
return { buttonPortSpy, listPortSpy };
}
beforeEach(() => {
accountService = mockAccountServiceWith(mockUserId);
@ -125,6 +149,7 @@ describe("OverlayBackground", () => {
});
});
tabsSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage");
sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage");
getTabFromCurrentWindowIdSpy = jest.spyOn(BrowserApi, "getTabFromCurrentWindowId");
void overlayBackground.init();
@ -349,11 +374,17 @@ describe("OverlayBackground", () => {
beforeEach(() => {
sender = mock<chrome.runtime.MessageSender>({ tab, frameId: middleFrameId });
jest.useFakeTimers();
overlayBackground["isFieldCurrentlyFocused"] = true;
sendMockExtensionMessage({
command: "updateIsFieldCurrentlyFocused",
isFieldCurrentlyFocused: true,
});
});
it("skips updating the position of either inline menu element if a field is not currently focused", async () => {
overlayBackground["isFieldCurrentlyFocused"] = false;
sendMockExtensionMessage({
command: "updateIsFieldCurrentlyFocused",
isFieldCurrentlyFocused: false,
});
sendMockExtensionMessage({ command: "rebuildSubFrameOffsets" }, sender);
await flushInlineMenuUpdatePromises();
@ -430,7 +461,7 @@ describe("OverlayBackground", () => {
});
});
describe("updateOverlayCiphers", () => {
describe("updating the overlay ciphers", () => {
const url = "https://jest-testing-website.com";
const tab = createChromeTabMock({ url });
const cipher1 = mock<CipherView>({
@ -480,7 +511,7 @@ describe("OverlayBackground", () => {
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url);
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual(
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
new Map([
["overlay-cipher-0", cipher2],
["overlay-cipher-1", cipher1],
@ -535,4 +566,230 @@ describe("OverlayBackground", () => {
});
});
});
describe("extension message handlers", () => {
describe("autofillOverlayElementClosed message handler", () => {
beforeEach(async () => {
await initOverlayElementPorts();
});
it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => {
const port1 = mock<chrome.runtime.Port>();
const port2 = mock<chrome.runtime.Port>();
overlayBackground["expiredPorts"] = [port1, port2];
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 });
sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData });
sendMockExtensionMessage(
{
command: "autofillOverlayElementClosed",
overlayElement: AutofillOverlayElement.Button,
},
sender,
);
expect(port1.disconnect).toHaveBeenCalled();
expect(port2.disconnect).toHaveBeenCalled();
});
it("disconnects the button element port", () => {
sendMockExtensionMessage({
command: "autofillOverlayElementClosed",
overlayElement: AutofillOverlayElement.Button,
});
expect(buttonPortSpy.disconnect).toHaveBeenCalled();
});
it("disconnects the list element port", () => {
sendMockExtensionMessage({
command: "autofillOverlayElementClosed",
overlayElement: AutofillOverlayElement.List,
});
expect(listPortSpy.disconnect).toHaveBeenCalled();
});
});
describe("autofillOverlayAddNewVaultItem message handler", () => {
let sender: chrome.runtime.MessageSender;
let openAddEditVaultItemPopoutSpy: jest.SpyInstance;
beforeEach(() => {
sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
openAddEditVaultItemPopoutSpy = jest
.spyOn(overlayBackground as any, "openAddEditVaultItemPopout")
.mockImplementation();
});
it("will not open the add edit popout window if the message does not have a login cipher provided", () => {
sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender);
expect(cipherService.setAddEditCipherInfo).not.toHaveBeenCalled();
expect(openAddEditVaultItemPopoutSpy).not.toHaveBeenCalled();
});
it("will open the add edit popout window after creating a new cipher", async () => {
sendMockExtensionMessage(
{
command: "autofillOverlayAddNewVaultItem",
login: {
uri: "https://tacos.com",
hostname: "",
username: "username",
password: "password",
},
},
sender,
);
await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher");
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled();
});
});
describe("checkIsInlineMenuCiphersPopulated message handler", () => {
let focusedFieldData: FocusedFieldData;
beforeEach(() => {
focusedFieldData = createFocusedFieldDataMock();
sendMockExtensionMessage(
{ command: "updateFocusedFieldData", focusedFieldData },
mock<chrome.runtime.MessageSender>({ tab: { id: 2 }, frameId: 0 }),
);
});
it("returns false if the sender's tab id is not equal to the focused field's tab id", async () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
sendMockExtensionMessage(
{ command: "checkIsInlineMenuCiphersPopulated" },
sender,
sendResponse,
);
await flushPromises();
expect(sendResponse).toHaveBeenCalledWith(false);
});
it("returns false if the overlay login cipher are not populated", () => {});
it("returns true if the overlay login ciphers are populated", async () => {
overlayBackground["inlineMenuCiphers"] = new Map([
["overlay-cipher-0", mock<CipherView>()],
]);
sendMockExtensionMessage(
{ command: "checkIsInlineMenuCiphersPopulated" },
mock<chrome.runtime.MessageSender>({ tab: { id: 2 } }),
sendResponse,
);
await flushPromises();
expect(sendResponse).toHaveBeenCalledWith(true);
});
});
describe("checkIsFieldCurrentlyFocused message handler", () => {
it("returns true when a form field is currently focused", async () => {
sendMockExtensionMessage({
command: "updateIsFieldCurrentlyFocused",
isFieldCurrentlyFocused: true,
});
sendMockExtensionMessage(
{ command: "checkIsFieldCurrentlyFocused" },
mock<chrome.runtime.MessageSender>(),
sendResponse,
);
await flushPromises();
expect(sendResponse).toHaveBeenCalledWith(true);
});
});
describe("checkIsFieldCurrentlyFilling message handler", () => {
it("returns true if autofill is currently running", async () => {
sendMockExtensionMessage({
command: "updateIsFieldCurrentlyFilling",
isFieldCurrentlyFilling: true,
});
sendMockExtensionMessage(
{ command: "checkIsFieldCurrentlyFilling" },
mock<chrome.runtime.MessageSender>(),
sendResponse,
);
await flushPromises();
expect(sendResponse).toHaveBeenCalledWith(true);
});
});
describe("getAutofillInlineMenuVisibility message handler", () => {
it("returns the current inline menu visibility setting", async () => {
sendMockExtensionMessage(
{ command: "getAutofillInlineMenuVisibility" },
mock<chrome.runtime.MessageSender>(),
sendResponse,
);
await flushPromises();
expect(sendResponse).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus);
});
});
describe("openAutofillInlineMenu", () => {
let sender: chrome.runtime.MessageSender;
beforeEach(() => {
sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
getTabFromCurrentWindowIdSpy.mockResolvedValue(sender.tab);
tabsSendMessageSpy.mockImplementation();
});
it("opens the autofill inline menu by sending a message to the current tab", async () => {
sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender);
await flushPromises();
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
sender.tab,
{
command: "openAutofillInlineMenu",
isFocusingFieldElement: false,
isOpeningFullAutofillInlineMenu: false,
authStatus: 2,
},
{ frameId: 0 },
);
});
it("sends the open menu message to the focused field's frameId", async () => {
sender.frameId = 10;
sendMockExtensionMessage({ command: "updateFocusedFieldData" }, sender);
await flushPromises();
sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender);
await flushPromises();
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
sender.tab,
{
command: "openAutofillInlineMenu",
isFocusingFieldElement: false,
isOpeningFullAutofillInlineMenu: false,
authStatus: 2,
},
{ frameId: 10 },
);
});
});
});
describe("inline menu button message handlers", () => {});
describe("inline menu list message handlers", () => {});
});

View File

@ -49,7 +49,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private readonly openUnlockPopout = openUnlockPopout;
private readonly openViewVaultItemPopout = openViewVaultItemPopout;
private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout;
private overlayLoginCiphers: Map<string, CipherView> = new Map();
private inlineMenuCiphers: Map<string, CipherView> = new Map();
private pageDetailsForTab: PageDetailsForTab = {};
private subFrameOffsetsForTab: SubFrameOffsetsForTab = {};
private updateInlineMenuPositionTimeout: number | NodeJS.Timeout;
@ -67,8 +67,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
autofillOverlayElementClosed: ({ message, sender }) =>
this.overlayElementClosed(message, sender),
autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender),
checkIsOverlayLoginCiphersPopulated: ({ sender }) =>
this.checkIsOverlayLoginCiphersPopulated(sender),
checkIsInlineMenuCiphersPopulated: ({ sender }) =>
this.checkIsInlineMenuCiphersPopulated(sender),
updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender),
updateIsFieldCurrentlyFocused: ({ message }) => this.updateIsFieldCurrentlyFocused(message),
checkIsFieldCurrentlyFocused: () => this.checkIsFieldCurrentlyFocused(),
@ -179,12 +179,12 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return;
}
this.overlayLoginCiphers = new Map();
this.inlineMenuCiphers = new Map();
const ciphersViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url)).sort(
(a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b),
);
for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) {
this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]);
this.inlineMenuCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]);
}
const ciphers = await this.getOverlayCipherData();
@ -200,7 +200,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
*/
private async getOverlayCipherData(): Promise<OverlayCipherData[]> {
const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$);
const overlayCiphersArray = Array.from(this.overlayLoginCiphers);
const overlayCiphersArray = Array.from(this.inlineMenuCiphers);
const overlayCipherData: OverlayCipherData[] = [];
for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) {
@ -388,7 +388,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
);
if (
mostRecentlyFocusedFieldHasValue &&
(this.checkIsOverlayLoginCiphersPopulated(sender) ||
(this.checkIsInlineMenuCiphersPopulated(sender) ||
(await this.getAuthStatus()) !== AuthenticationStatus.Unlocked)
) {
return;
@ -401,7 +401,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* Triggers autofill for the selected cipher in the inline menu list. Also places
* the selected cipher at the top of the list of ciphers.
*
* @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID.
* @param overlayCipherId - Cipher ID corresponding to the inlineMenuCiphers map. Does not correspond to the actual cipher's ID.
* @param sender - The sender of the port message
*/
private async fillSelectedInlineMenuListItem(
@ -413,7 +413,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return;
}
const cipher = this.overlayLoginCiphers.get(overlayCipherId);
const cipher = this.inlineMenuCiphers.get(overlayCipherId);
if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) {
return;
@ -430,7 +430,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.platformUtilsService.copyToClipboard(totpCode);
}
this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]);
this.inlineMenuCiphers = new Map([[overlayCipherId, cipher], ...this.inlineMenuCiphers]);
}
/**
@ -801,14 +801,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
/**
* Triggers the opening of a vault item popout window associated
* with the passed cipher ID.
* @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID.
* @param overlayCipherId - Cipher ID corresponding to the inlineMenuCiphers map. Does not correspond to the actual cipher's ID.
* @param sender - The sender of the port message
*/
private async viewSelectedCipher(
{ overlayCipherId }: OverlayPortMessage,
{ sender }: chrome.runtime.Port,
) {
const cipher = this.overlayLoginCiphers.get(overlayCipherId);
const cipher = this.inlineMenuCiphers.get(overlayCipherId);
if (!cipher) {
return;
}
@ -945,18 +945,34 @@ export class OverlayBackground implements OverlayBackgroundInterface {
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
}
/**
* Updates the property that identifies if a form field set up for the inline menu is currently focused.
*
* @param message - The message received from the web page
*/
private updateIsFieldCurrentlyFocused(message: OverlayBackgroundExtensionMessage) {
this.isFieldCurrentlyFocused = message.isFieldCurrentlyFocused;
}
/**
* Allows a content script to check if a form field setup for the inline menu is currently focused.
*/
private checkIsFieldCurrentlyFocused() {
return this.isFieldCurrentlyFocused;
}
/**
* Updates the property that identifies if a form field is currently being autofilled.
*
* @param message - The message received from the web page
*/
private updateIsFieldCurrentlyFilling(message: OverlayBackgroundExtensionMessage) {
this.isFieldCurrentlyFilling = message.isFieldCurrentlyFilling;
}
/**
* Allows a content script to check if a form field is currently being autofilled.
*/
private checkIsFieldCurrentlyFilling() {
return this.isFieldCurrentlyFilling;
}
@ -977,8 +993,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
);
}
private checkIsOverlayLoginCiphersPopulated(sender: chrome.runtime.MessageSender) {
return sender.tab.id === this.focusedFieldData.tabId && this.overlayLoginCiphers.size > 0;
private checkIsInlineMenuCiphersPopulated(sender: chrome.runtime.MessageSender) {
return sender.tab.id === this.focusedFieldData.tabId && this.inlineMenuCiphers.size > 0;
}
private updateInlineMenuButtonColorScheme() {

View File

@ -18,7 +18,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
private ariaAlertTimeout: number | NodeJS.Timeout;
private delayedCloseTimeout: number | NodeJS.Timeout;
private readonly fadeInOpacityTransition = "opacity 125ms ease-out 0s";
private readonly fadeOutOpacityTransition = "opacity 75ms ease-in 0s";
private readonly fadeOutOpacityTransition = "opacity 60ms ease-out 0s";
private iframeStyles: Partial<CSSStyleDeclaration> = {
all: "initial",
position: "fixed",

View File

@ -1022,7 +1022,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
private async isInlineMenuCiphersPopulated() {
return (await this.sendExtensionMessage("checkIsOverlayLoginCiphersPopulated")) === true;
return (await this.sendExtensionMessage("checkIsInlineMenuCiphersPopulated")) === true;
}
/**