[PM-5742] Rework Usage of Extension APIs that Cannot be Called with the Background Service Worker (#7667)

* [PM-5742] Rework Usage of Extension APIs that Cannot be Called with the Background Service Worker

* [PM-5742] Implementing jest tests for the updated BrowserApi methods

* [PM-5742] Implementing jest tests to validate logic within added API calls

* [PM-5742] Implementing jest tests to validate logic within added API calls

* [PM-5742] Fixing broken Jest tests

* [PM-5742] Fixing linter error
This commit is contained in:
Cesar Gonzalez 2024-02-07 15:20:53 -06:00 committed by GitHub
parent 2e11fb2a24
commit a1745b2dae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 236 additions and 13 deletions

View File

@ -870,7 +870,7 @@ async function loadNotificationBar() {
return; return;
} }
const barPageUrl: string = chrome.extension.getURL(barPage); const barPageUrl: string = chrome.runtime.getURL(barPage);
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
iframe.style.cssText = "height: 42px; width: 100%; border: 0; min-height: initial;"; iframe.style.cssText = "height: 42px; width: 100%; border: 0; min-height: initial;";

View File

@ -106,6 +106,143 @@ describe("BrowserApi", () => {
}); });
}); });
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("reloadOpenWindows", () => {
const href = window.location.href;
const reload = window.location.reload;
afterEach(() => {
window.location.href = href;
window.location.reload = reload;
});
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("executeScriptInTab", () => { describe("executeScriptInTab", () => {
it("calls to the extension api to execute a script within the give tabId", async () => { it("calls to the extension api to execute a script within the give tabId", async () => {
const tabId = 1; const tabId = 1;

View File

@ -199,20 +199,54 @@ export class BrowserApi {
return chrome.windows.onCreated.addListener(callback); return chrome.windows.onCreated.addListener(callback);
} }
/**
* Gets the background page for the extension. This method is
* not valid within manifest v3 background service workers. As
* a result, it will return null when called from that context.
*/
static getBackgroundPage(): any { static getBackgroundPage(): any {
if (typeof chrome.extension.getBackgroundPage === "undefined") {
return null;
}
return chrome.extension.getBackgroundPage(); return chrome.extension.getBackgroundPage();
} }
/**
* Accepts a window object and determines if it is
* associated with the background page of the extension.
*
* @param window - The window to check.
*/
static isBackgroundPage(window: Window & typeof globalThis): boolean { static isBackgroundPage(window: Window & typeof globalThis): boolean {
return window === chrome.extension.getBackgroundPage(); return typeof window !== "undefined" && window === BrowserApi.getBackgroundPage();
} }
static getApplicationVersion(): string { static getApplicationVersion(): string {
return chrome.runtime.getManifest().version; return chrome.runtime.getManifest().version;
} }
/**
* Gets the extension views that match the given properties. This method is not
* available within background service worker. As a result, it will return an
* empty array when called from that context.
*
* @param fetchProperties - The properties used to filter extension views.
*/
static getExtensionViews(fetchProperties?: chrome.extension.FetchProperties): Window[] {
if (typeof chrome.extension.getViews === "undefined") {
return [];
}
return chrome.extension.getViews(fetchProperties);
}
/**
* Queries all extension views that are of type `popup`
* and returns whether any are currently open.
*/
static async isPopupOpen(): Promise<boolean> { static async isPopupOpen(): Promise<boolean> {
return Promise.resolve(chrome.extension.getViews({ type: "popup" }).length > 0); return Promise.resolve(BrowserApi.getExtensionViews({ type: "popup" }).length > 0);
} }
static createNewTab(url: string, active = true): Promise<chrome.tabs.Tab> { static createNewTab(url: string, active = true): Promise<chrome.tabs.Tab> {
@ -355,15 +389,19 @@ export class BrowserApi {
} }
} }
/**
* Reloads all open extension views, except the background page. Will also
* skip reloading the current window location if exemptCurrentHref is true.
*
* @param exemptCurrentHref - Whether to exempt the current window location from the reload.
*/
static reloadOpenWindows(exemptCurrentHref = false) { static reloadOpenWindows(exemptCurrentHref = false) {
const currentHref = window.location.href; const currentHref = window?.location.href;
const views = chrome.extension.getViews() as Window[]; const views = BrowserApi.getExtensionViews();
views views
.filter((w) => w.location.href != null && !w.location.href.includes("background.html")) .filter((w) => w.location.href != null && !w.location.href.includes("background.html"))
.filter((w) => !exemptCurrentHref || w.location.href !== currentHref) .filter((w) => !exemptCurrentHref || w.location.href !== currentHref)
.forEach((w) => { .forEach((w) => w.location.reload());
w.location.reload();
});
} }
static connectNative(application: string): browser.runtime.Port | chrome.runtime.Port { static connectNative(application: string): browser.runtime.Port | chrome.runtime.Port {

View File

@ -3,9 +3,14 @@ import { DeviceType } from "@bitwarden/common/enums";
import BrowserPlatformUtilsService from "./browser-platform-utils.service"; import BrowserPlatformUtilsService from "./browser-platform-utils.service";
describe("Browser Utils Service", () => { describe("Browser Utils Service", () => {
let browserPlatformUtilsService: BrowserPlatformUtilsService;
beforeEach(() => {
(window as any).matchMedia = jest.fn().mockReturnValueOnce({});
browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null, window);
});
describe("getBrowser", () => { describe("getBrowser", () => {
const originalUserAgent = navigator.userAgent; const originalUserAgent = navigator.userAgent;
// Reset the userAgent. // Reset the userAgent.
afterAll(() => { afterAll(() => {
Object.defineProperty(navigator, "userAgent", { Object.defineProperty(navigator, "userAgent", {
@ -13,10 +18,8 @@ describe("Browser Utils Service", () => {
}); });
}); });
let browserPlatformUtilsService: BrowserPlatformUtilsService;
beforeEach(() => { beforeEach(() => {
(window as any).matchMedia = jest.fn().mockReturnValueOnce({}); (window as any).matchMedia = jest.fn().mockReturnValueOnce({});
browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null, window);
}); });
afterEach(() => { afterEach(() => {
@ -86,6 +89,45 @@ describe("Browser Utils Service", () => {
expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.VivaldiExtension); expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.VivaldiExtension);
}); });
}); });
describe("isViewOpen", () => {
beforeEach(() => {
globalThis.chrome = {
// eslint-disable-next-line
// @ts-ignore
extension: {
getViews: jest.fn(),
},
};
});
it("returns true if the user is on Firefox and the sidebar is open", async () => {
chrome.extension.getViews = jest.fn().mockReturnValueOnce([window]);
jest
.spyOn(browserPlatformUtilsService, "getDevice")
.mockReturnValueOnce(DeviceType.FirefoxExtension);
const result = await browserPlatformUtilsService.isViewOpen();
expect(result).toBe(true);
});
it("returns true if a extension view is open as a tab", async () => {
chrome.extension.getViews = jest.fn().mockReturnValueOnce([window]);
const result = await browserPlatformUtilsService.isViewOpen();
expect(result).toBe(true);
});
it("returns false if no extension view is open", async () => {
chrome.extension.getViews = jest.fn().mockReturnValue([]);
const result = await browserPlatformUtilsService.isViewOpen();
expect(result).toBe(false);
});
});
}); });
describe("Safari Height Fix", () => { describe("Safari Height Fix", () => {

View File

@ -160,12 +160,12 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
} }
// Opera has "sidebar_panel" as a ViewType but doesn't currently work // Opera has "sidebar_panel" as a ViewType but doesn't currently work
if (this.isFirefox() && chrome.extension.getViews({ type: "sidebar" }).length > 0) { if (this.isFirefox() && BrowserApi.getExtensionViews({ type: "sidebar" }).length > 0) {
return true; return true;
} }
// Opera sidebar has type of "tab" (will stick around for a while after closing sidebar) // Opera sidebar has type of "tab" (will stick around for a while after closing sidebar)
const tabOpen = chrome.extension.getViews({ type: "tab" }).length > 0; const tabOpen = BrowserApi.getExtensionViews({ type: "tab" }).length > 0;
return tabOpen; return tabOpen;
} }

View File

@ -105,6 +105,11 @@ const privacy = {
}, },
}; };
const extension = {
getBackgroundPage: jest.fn(),
getViews: jest.fn(),
};
// set chrome // set chrome
global.chrome = { global.chrome = {
i18n, i18n,
@ -116,4 +121,5 @@ global.chrome = {
windows, windows,
port, port,
privacy, privacy,
extension,
} as any; } as any;