From a1745b2dae4c594eb022008b3c393771bc486908 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Wed, 7 Feb 2024 15:20:53 -0600 Subject: [PATCH] [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 --- .../src/autofill/content/notification-bar.ts | 2 +- .../src/platform/browser/browser-api.spec.ts | 137 ++++++++++++++++++ .../src/platform/browser/browser-api.ts | 52 ++++++- .../browser-platform-utils.service.spec.ts | 48 +++++- .../browser-platform-utils.service.ts | 4 +- apps/browser/test.setup.ts | 6 + 6 files changed, 236 insertions(+), 13 deletions(-) diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index e5b2fcb4af..f4f7c74ed7 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -870,7 +870,7 @@ async function loadNotificationBar() { return; } - const barPageUrl: string = chrome.extension.getURL(barPage); + const barPageUrl: string = chrome.runtime.getURL(barPage); const iframe = document.createElement("iframe"); iframe.style.cssText = "height: 42px; width: 100%; border: 0; min-height: initial;"; diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts index 1d11edb92e..bb1aa5ff1d 100644 --- a/apps/browser/src/platform/browser/browser-api.spec.ts +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -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({ + 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", () => { it("calls to the extension api to execute a script within the give tabId", async () => { const tabId = 1; diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index b4c53363cc..83cfaaa9e7 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -199,20 +199,54 @@ export class BrowserApi { 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 { + if (typeof chrome.extension.getBackgroundPage === "undefined") { + return null; + } + 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 { - return window === chrome.extension.getBackgroundPage(); + return typeof window !== "undefined" && window === BrowserApi.getBackgroundPage(); } static getApplicationVersion(): string { 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 { - 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 { @@ -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) { - const currentHref = window.location.href; - const views = chrome.extension.getViews() as Window[]; + const currentHref = window?.location.href; + const views = BrowserApi.getExtensionViews(); views .filter((w) => w.location.href != null && !w.location.href.includes("background.html")) .filter((w) => !exemptCurrentHref || w.location.href !== currentHref) - .forEach((w) => { - w.location.reload(); - }); + .forEach((w) => w.location.reload()); } static connectNative(application: string): browser.runtime.Port | chrome.runtime.Port { diff --git a/apps/browser/src/platform/services/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/browser-platform-utils.service.spec.ts index 964200dfce..11afabed64 100644 --- a/apps/browser/src/platform/services/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/browser-platform-utils.service.spec.ts @@ -3,9 +3,14 @@ import { DeviceType } from "@bitwarden/common/enums"; import BrowserPlatformUtilsService from "./browser-platform-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", () => { const originalUserAgent = navigator.userAgent; - // Reset the userAgent. afterAll(() => { Object.defineProperty(navigator, "userAgent", { @@ -13,10 +18,8 @@ describe("Browser Utils Service", () => { }); }); - let browserPlatformUtilsService: BrowserPlatformUtilsService; beforeEach(() => { (window as any).matchMedia = jest.fn().mockReturnValueOnce({}); - browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null, window); }); afterEach(() => { @@ -86,6 +89,45 @@ describe("Browser Utils Service", () => { 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", () => { diff --git a/apps/browser/src/platform/services/browser-platform-utils.service.ts b/apps/browser/src/platform/services/browser-platform-utils.service.ts index 733e121ee1..63f5b0345c 100644 --- a/apps/browser/src/platform/services/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/browser-platform-utils.service.ts @@ -160,12 +160,12 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService } // 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; } // 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; } diff --git a/apps/browser/test.setup.ts b/apps/browser/test.setup.ts index 9f787d8109..58a54475c6 100644 --- a/apps/browser/test.setup.ts +++ b/apps/browser/test.setup.ts @@ -105,6 +105,11 @@ const privacy = { }, }; +const extension = { + getBackgroundPage: jest.fn(), + getViews: jest.fn(), +}; + // set chrome global.chrome = { i18n, @@ -116,4 +121,5 @@ global.chrome = { windows, port, privacy, + extension, } as any;