585 lines
20 KiB
TypeScript
585 lines
20 KiB
TypeScript
import { mock } from "jest-mock-extended";
|
|
|
|
import { BrowserApi } from "./browser-api";
|
|
|
|
describe("BrowserApi", () => {
|
|
const executeScriptResult = ["value"];
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe("isManifestVersion", () => {
|
|
beforeEach(() => {
|
|
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
|
});
|
|
|
|
it("returns true if the manifest version matches the provided version", () => {
|
|
const result = BrowserApi.isManifestVersion(3);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("returns false if the manifest version does not match the provided version", () => {
|
|
const result = BrowserApi.isManifestVersion(2);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("getWindow", () => {
|
|
it("will get the current window if a window id is not provided", () => {
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
BrowserApi.getWindow();
|
|
|
|
expect(chrome.windows.getCurrent).toHaveBeenCalledWith({ populate: true }, expect.anything());
|
|
});
|
|
|
|
it("will get the window with the provided id if one is provided", () => {
|
|
const windowId = 1;
|
|
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
BrowserApi.getWindow(windowId);
|
|
|
|
expect(chrome.windows.get).toHaveBeenCalledWith(
|
|
windowId,
|
|
{ populate: true },
|
|
expect.anything(),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("getCurrentWindow", () => {
|
|
it("will get the current window", () => {
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
BrowserApi.getCurrentWindow();
|
|
|
|
expect(chrome.windows.getCurrent).toHaveBeenCalledWith({ populate: true }, expect.anything());
|
|
});
|
|
});
|
|
|
|
describe("getWindowById", () => {
|
|
it("will get the window associated with the passed window id", () => {
|
|
const windowId = 1;
|
|
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
BrowserApi.getWindowById(windowId);
|
|
|
|
expect(chrome.windows.get).toHaveBeenCalledWith(
|
|
windowId,
|
|
{ populate: true },
|
|
expect.anything(),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("removeWindow", () => {
|
|
it("removes the window based on the passed window id", () => {
|
|
const windowId = 10;
|
|
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
BrowserApi.removeWindow(windowId);
|
|
|
|
expect(chrome.windows.remove).toHaveBeenCalledWith(windowId, expect.anything());
|
|
});
|
|
});
|
|
|
|
describe("updateWindowProperties", () => {
|
|
it("will update the window with the provided window options", () => {
|
|
const windowId = 1;
|
|
const windowOptions: chrome.windows.UpdateInfo = {
|
|
focused: true,
|
|
};
|
|
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
BrowserApi.updateWindowProperties(windowId, windowOptions);
|
|
|
|
expect(chrome.windows.update).toHaveBeenCalledWith(
|
|
windowId,
|
|
windowOptions,
|
|
expect.anything(),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("focusWindow", () => {
|
|
it("will focus the window with the provided window id", () => {
|
|
const windowId = 1;
|
|
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
BrowserApi.focusWindow(windowId);
|
|
|
|
expect(chrome.windows.update).toHaveBeenCalledWith(
|
|
windowId,
|
|
{ focused: true },
|
|
expect.anything(),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("getTab", () => {
|
|
it("returns `null` if the tabId is a falsy value", async () => {
|
|
const result = await BrowserApi.getTab(null);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("returns the tab within manifest v3", async () => {
|
|
const tabId = 1;
|
|
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
|
(chrome.tabs.get as jest.Mock).mockImplementation(
|
|
(tabId) => ({ id: tabId }) as chrome.tabs.Tab,
|
|
);
|
|
|
|
const result = await BrowserApi.getTab(tabId);
|
|
|
|
expect(result).toEqual({ id: tabId });
|
|
});
|
|
|
|
it("returns the tab within manifest v2", async () => {
|
|
const tabId = 1;
|
|
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(2);
|
|
(chrome.tabs.get as jest.Mock).mockImplementation((tabId, callback) =>
|
|
callback({ id: tabId } as chrome.tabs.Tab),
|
|
);
|
|
|
|
const result = BrowserApi.getTab(tabId);
|
|
|
|
await expect(result).resolves.toEqual({ id: tabId });
|
|
});
|
|
});
|
|
|
|
describe("getBackgroundPage", () => {
|
|
it("returns a null value if the `getBackgroundPage` method is not available", () => {
|
|
chrome.extension.getBackgroundPage = undefined;
|
|
|
|
const result = BrowserApi.getBackgroundPage();
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("returns the background page if the `getBackgroundPage` method is available", () => {
|
|
chrome.extension.getBackgroundPage = jest.fn().mockReturnValue(window);
|
|
|
|
const result = BrowserApi.getBackgroundPage();
|
|
|
|
expect(result).toEqual(window);
|
|
});
|
|
});
|
|
|
|
describe("isBackgroundPage", () => {
|
|
it("returns false if the passed window is `undefined`", () => {
|
|
const result = BrowserApi.isBackgroundPage(undefined);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("returns false if the current window is not the background page", () => {
|
|
chrome.extension.getBackgroundPage = jest.fn().mockReturnValue(null);
|
|
|
|
const result = BrowserApi.isBackgroundPage(window);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("returns true if the current window is the background page", () => {
|
|
chrome.extension.getBackgroundPage = jest.fn().mockReturnValue(window);
|
|
|
|
const result = BrowserApi.isBackgroundPage(window);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("getExtensionViews", () => {
|
|
it("returns an empty array if the `getViews` method is not available", () => {
|
|
chrome.extension.getViews = undefined;
|
|
|
|
const result = BrowserApi.getExtensionViews();
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("returns the extension views if the `getViews` method is available", () => {
|
|
const views = [window];
|
|
chrome.extension.getViews = jest.fn().mockReturnValue(views);
|
|
|
|
const result = BrowserApi.getExtensionViews();
|
|
|
|
expect(result).toEqual(views);
|
|
});
|
|
});
|
|
|
|
describe("isPopupOpen", () => {
|
|
it("returns true if the popup is open", async () => {
|
|
chrome.extension.getViews = jest.fn().mockReturnValue([window]);
|
|
|
|
const result = await BrowserApi.isPopupOpen();
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("returns false if the popup is not open", async () => {
|
|
chrome.extension.getViews = jest.fn().mockReturnValue([]);
|
|
|
|
const result = await BrowserApi.isPopupOpen();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("reloadExtension", () => {
|
|
it("reloads the window location if the passed globalContext is for the window", () => {
|
|
const windowMock = mock<Window>({
|
|
location: { reload: jest.fn() },
|
|
}) as unknown as Window & typeof globalThis;
|
|
|
|
BrowserApi.reloadExtension(windowMock);
|
|
|
|
expect(windowMock.location.reload).toHaveBeenCalled();
|
|
});
|
|
|
|
it("reloads the extension runtime if the passed globalContext is not for the window", () => {
|
|
const globalMock = mock<typeof globalThis>({}) as any;
|
|
BrowserApi.reloadExtension(globalMock);
|
|
|
|
expect(chrome.runtime.reload).toHaveBeenCalled();
|
|
});
|
|
|
|
it("reloads the extension runtime if a null value is passed as the globalContext", () => {
|
|
BrowserApi.reloadExtension(null);
|
|
|
|
expect(chrome.runtime.reload).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("reloadOpenWindows", () => {
|
|
const href = window.location.href;
|
|
const reload = window.location.reload;
|
|
|
|
afterEach(() => {
|
|
window.location.href = href;
|
|
window.location.reload = reload;
|
|
});
|
|
|
|
it("skips reloading any windows if no views can be found", () => {
|
|
Object.defineProperty(window, "location", {
|
|
value: { reload: jest.fn(), href: "chrome-extension://id-value/background.html" },
|
|
writable: true,
|
|
});
|
|
chrome.extension.getViews = jest.fn().mockReturnValue([]);
|
|
|
|
BrowserApi.reloadOpenWindows();
|
|
|
|
expect(window.location.reload).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reloads all open windows", () => {
|
|
Object.defineProperty(window, "location", {
|
|
value: { reload: jest.fn(), href: "chrome-extension://id-value/index.html" },
|
|
writable: true,
|
|
});
|
|
const views = [window];
|
|
chrome.extension.getViews = jest.fn().mockReturnValue(views);
|
|
|
|
BrowserApi.reloadOpenWindows();
|
|
|
|
expect(window.location.reload).toHaveBeenCalledTimes(views.length);
|
|
});
|
|
|
|
it("skips reloading the background page", () => {
|
|
Object.defineProperty(window, "location", {
|
|
value: { reload: jest.fn(), href: "chrome-extension://id-value/background.html" },
|
|
writable: true,
|
|
});
|
|
const views = [window];
|
|
chrome.extension.getViews = jest.fn().mockReturnValue(views);
|
|
chrome.extension.getBackgroundPage = jest.fn().mockReturnValue(window);
|
|
|
|
BrowserApi.reloadOpenWindows();
|
|
|
|
expect(window.location.reload).toHaveBeenCalledTimes(0);
|
|
});
|
|
|
|
it("skips reloading the current href if it is exempt", () => {
|
|
Object.defineProperty(window, "location", {
|
|
value: { reload: jest.fn(), href: "chrome-extension://id-value/index.html" },
|
|
writable: true,
|
|
});
|
|
const mockWindow = mock<Window>({
|
|
location: {
|
|
href: "chrome-extension://id-value/sidebar.html",
|
|
reload: jest.fn(),
|
|
},
|
|
});
|
|
const views = [window, mockWindow];
|
|
chrome.extension.getViews = jest.fn().mockReturnValue(views);
|
|
window.location.href = "chrome-extension://id-value/index.html";
|
|
|
|
BrowserApi.reloadOpenWindows(true);
|
|
|
|
expect(window.location.reload).toHaveBeenCalledTimes(0);
|
|
expect(mockWindow.location.reload).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe("getBrowserAction", () => {
|
|
it("returns the `chrome.action` API if the extension manifest is for version 3", () => {
|
|
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
|
|
|
const result = BrowserApi.getBrowserAction();
|
|
|
|
expect(result).toEqual(chrome.action);
|
|
});
|
|
|
|
it("returns the `chrome.browserAction` API if the extension manifest is for version 2", () => {
|
|
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(2);
|
|
|
|
const result = BrowserApi.getBrowserAction();
|
|
|
|
expect(result).toEqual(chrome.browserAction);
|
|
});
|
|
});
|
|
|
|
describe("executeScriptInTab", () => {
|
|
it("calls to the extension api to execute a script within the give tabId", async () => {
|
|
const tabId = 1;
|
|
const injectDetails = mock<chrome.tabs.InjectDetails>();
|
|
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(2);
|
|
(chrome.tabs.executeScript as jest.Mock).mockImplementation(
|
|
(tabId, injectDetails, callback) => callback(executeScriptResult),
|
|
);
|
|
|
|
const result = await BrowserApi.executeScriptInTab(tabId, injectDetails);
|
|
|
|
expect(chrome.tabs.executeScript).toHaveBeenCalledWith(
|
|
tabId,
|
|
injectDetails,
|
|
expect.any(Function),
|
|
);
|
|
expect(result).toEqual(executeScriptResult);
|
|
});
|
|
|
|
it("calls the manifest v3 scripting API if the extension manifest is for v3", async () => {
|
|
const tabId = 1;
|
|
const injectDetails = mock<chrome.tabs.InjectDetails>({
|
|
file: "file.js",
|
|
allFrames: true,
|
|
runAt: "document_start",
|
|
frameId: null,
|
|
});
|
|
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
|
(chrome.scripting.executeScript as jest.Mock).mockResolvedValue(executeScriptResult);
|
|
|
|
const result = await BrowserApi.executeScriptInTab(tabId, injectDetails);
|
|
|
|
expect(chrome.scripting.executeScript).toHaveBeenCalledWith({
|
|
target: {
|
|
tabId: tabId,
|
|
allFrames: injectDetails.allFrames,
|
|
frameIds: null,
|
|
},
|
|
files: [injectDetails.file],
|
|
injectImmediately: true,
|
|
world: "ISOLATED",
|
|
});
|
|
expect(result).toEqual(executeScriptResult);
|
|
});
|
|
|
|
it("injects the script into a specified frameId when the extension is built for manifest v3", async () => {
|
|
const tabId = 1;
|
|
const frameId = 2;
|
|
const injectDetails = mock<chrome.tabs.InjectDetails>({
|
|
file: "file.js",
|
|
allFrames: true,
|
|
runAt: "document_start",
|
|
frameId,
|
|
});
|
|
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
|
(chrome.scripting.executeScript as jest.Mock).mockResolvedValue(executeScriptResult);
|
|
|
|
await BrowserApi.executeScriptInTab(tabId, injectDetails);
|
|
|
|
expect(chrome.scripting.executeScript).toHaveBeenCalledWith({
|
|
target: {
|
|
tabId: tabId,
|
|
allFrames: injectDetails.allFrames,
|
|
frameIds: [frameId],
|
|
},
|
|
files: [injectDetails.file],
|
|
injectImmediately: true,
|
|
world: "ISOLATED",
|
|
});
|
|
});
|
|
|
|
it("injects the script into the MAIN world context when injecting a script for manifest v3", async () => {
|
|
const tabId = 1;
|
|
const injectDetails = mock<chrome.tabs.InjectDetails>({
|
|
file: null,
|
|
allFrames: true,
|
|
runAt: "document_start",
|
|
frameId: null,
|
|
});
|
|
const scriptingApiDetails = { world: "MAIN" as chrome.scripting.ExecutionWorld };
|
|
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
|
(chrome.scripting.executeScript as jest.Mock).mockResolvedValue(executeScriptResult);
|
|
|
|
const result = await BrowserApi.executeScriptInTab(tabId, injectDetails, scriptingApiDetails);
|
|
|
|
expect(chrome.scripting.executeScript).toHaveBeenCalledWith({
|
|
target: {
|
|
tabId: tabId,
|
|
allFrames: injectDetails.allFrames,
|
|
frameIds: null,
|
|
},
|
|
files: null,
|
|
injectImmediately: true,
|
|
world: "MAIN",
|
|
});
|
|
expect(result).toEqual(executeScriptResult);
|
|
});
|
|
});
|
|
|
|
describe("browserAutofillSettingsOverridden", () => {
|
|
it("returns true if the browser autofill settings are overridden", async () => {
|
|
const expectedDetails = {
|
|
value: false,
|
|
levelOfControl: "controlled_by_this_extension",
|
|
} as chrome.types.ChromeSettingGetResultDetails;
|
|
chrome.privacy.services.autofillAddressEnabled.get = jest.fn((details, callback) =>
|
|
callback(expectedDetails),
|
|
);
|
|
chrome.privacy.services.autofillCreditCardEnabled.get = jest.fn((details, callback) =>
|
|
callback(expectedDetails),
|
|
);
|
|
chrome.privacy.services.passwordSavingEnabled.get = jest.fn((details, callback) =>
|
|
callback(expectedDetails),
|
|
);
|
|
|
|
const result = await BrowserApi.browserAutofillSettingsOverridden();
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("returns false if the browser autofill settings are not overridden", async () => {
|
|
const expectedDetails = {
|
|
value: true,
|
|
levelOfControl: "controlled_by_this_extension",
|
|
} as chrome.types.ChromeSettingGetResultDetails;
|
|
chrome.privacy.services.autofillAddressEnabled.get = jest.fn((details, callback) =>
|
|
callback(expectedDetails),
|
|
);
|
|
chrome.privacy.services.autofillCreditCardEnabled.get = jest.fn((details, callback) =>
|
|
callback(expectedDetails),
|
|
);
|
|
chrome.privacy.services.passwordSavingEnabled.get = jest.fn((details, callback) =>
|
|
callback(expectedDetails),
|
|
);
|
|
|
|
const result = await BrowserApi.browserAutofillSettingsOverridden();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("returns false if the browser autofill settings are not controlled by the extension", async () => {
|
|
const expectedDetails = {
|
|
value: false,
|
|
levelOfControl: "controlled_by_other_extensions",
|
|
} as chrome.types.ChromeSettingGetResultDetails;
|
|
chrome.privacy.services.autofillAddressEnabled.get = jest.fn((details, callback) =>
|
|
callback(expectedDetails),
|
|
);
|
|
chrome.privacy.services.autofillCreditCardEnabled.get = jest.fn((details, callback) =>
|
|
callback(expectedDetails),
|
|
);
|
|
chrome.privacy.services.passwordSavingEnabled.get = jest.fn((details, callback) =>
|
|
callback(expectedDetails),
|
|
);
|
|
|
|
const result = await BrowserApi.browserAutofillSettingsOverridden();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("updateDefaultBrowserAutofillSettings", () => {
|
|
it("updates the default browser autofill settings", async () => {
|
|
await BrowserApi.updateDefaultBrowserAutofillSettings(false);
|
|
|
|
expect(chrome.privacy.services.autofillAddressEnabled.set).toHaveBeenCalledWith({
|
|
value: false,
|
|
});
|
|
expect(chrome.privacy.services.autofillCreditCardEnabled.set).toHaveBeenCalledWith({
|
|
value: false,
|
|
});
|
|
expect(chrome.privacy.services.passwordSavingEnabled.set).toHaveBeenCalledWith({
|
|
value: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("createOffscreenDocument", () => {
|
|
it("creates the offscreen document with the supplied reasons and justification", async () => {
|
|
const reasons = [chrome.offscreen.Reason.CLIPBOARD];
|
|
const justification = "justification";
|
|
|
|
await BrowserApi.createOffscreenDocument(reasons, justification);
|
|
|
|
expect(chrome.offscreen.createDocument).toHaveBeenCalledWith({
|
|
url: "offscreen-document/index.html",
|
|
reasons,
|
|
justification,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("closeOffscreenDocument", () => {
|
|
it("closes the offscreen document", () => {
|
|
const callbackMock = jest.fn();
|
|
|
|
BrowserApi.closeOffscreenDocument(callbackMock);
|
|
|
|
expect(chrome.offscreen.closeDocument).toHaveBeenCalled();
|
|
expect(callbackMock).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("registerContentScriptsMv2", () => {
|
|
const details: browser.contentScripts.RegisteredContentScriptOptions = {
|
|
matches: ["<all_urls>"],
|
|
js: [{ file: "content/fido2/page-script.js" }],
|
|
};
|
|
|
|
it("registers content scripts through the `browser.contentScripts` API when the API is available", async () => {
|
|
globalThis.browser = mock<typeof browser>({
|
|
contentScripts: { register: jest.fn() },
|
|
});
|
|
|
|
await BrowserApi.registerContentScriptsMv2(details);
|
|
|
|
expect(browser.contentScripts.register).toHaveBeenCalledWith(details);
|
|
});
|
|
|
|
it("registers content scripts through the `registerContentScriptsPolyfill` when the `browser.contentScripts.register` API is not available", async () => {
|
|
globalThis.browser = mock<typeof browser>({
|
|
contentScripts: { register: undefined },
|
|
});
|
|
jest.spyOn(BrowserApi, "addListener");
|
|
|
|
await BrowserApi.registerContentScriptsMv2(details);
|
|
|
|
expect(BrowserApi.addListener).toHaveBeenCalledWith(
|
|
chrome.webNavigation.onCommitted,
|
|
expect.any(Function),
|
|
);
|
|
});
|
|
});
|
|
});
|