From 30c6fdff7c8c798a3a6c3c00cc4a0d487e5a2716 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Sun, 3 May 2026 22:32:48 +0300 Subject: [PATCH] feat(offscreen): enhance API with new utility methods and tests - Add `getOffscreenContext` to retrieve the current document context. - Add `getOffscreenUrl` and `getOffscreenPath` to fetch document URL and path. - Add `hasOffscreenUrl` and `hasOffscreenPath` to validate document URL/path. - Update `docs/offscreen.md` with new API methods and usage examples. - Add comprehensive test suite (`offscreen.test.ts`) for new functionalities. --- docs/offscreen.md | 55 ++++++++++++ src/offscreen.test.ts | 198 ++++++++++++++++++++++++++++++++++++++++++ src/offscreen.ts | 34 ++++++++ 3 files changed, 287 insertions(+) create mode 100644 src/offscreen.test.ts diff --git a/docs/offscreen.md b/docs/offscreen.md index 3423493..e5983f5 100644 --- a/docs/offscreen.md +++ b/docs/offscreen.md @@ -9,6 +9,11 @@ A promise-based wrapper for the Chrome `offscreen` API to create and manage offs - [createOffscreen(parameters)](#createOffscreen) - [closeOffscreen()](#closeOffscreen) - [hasOffscreen()](#hasOffscreen) +- [getOffscreenContext()](#getOffscreenContext) +- [getOffscreenUrl()](#getOffscreenUrl) +- [getOffscreenPath()](#getOffscreenPath) +- [hasOffscreenUrl(url)](#hasOffscreenUrl) +- [hasOffscreenPath(path)](#hasOffscreenPath) --- @@ -41,3 +46,53 @@ hasOffscreen(): Promise ``` Checks whether an offscreen document is currently open. + + + +### getOffscreenContext + +``` +getOffscreenContext(): Promise +``` + +Returns the current offscreen document context, if one is open. + + + +### getOffscreenUrl + +``` +getOffscreenUrl(): Promise +``` + +Returns the current offscreen document URL, if one is open. + + + +### getOffscreenPath + +``` +getOffscreenPath(): Promise +``` + +Returns the current offscreen document path within the extension, if one is open. Query parameters and hash fragments are not included. + + + +### hasOffscreenUrl + +``` +hasOffscreenUrl(url: string): Promise +``` + +Checks whether the current offscreen document matches the given URL. + + + +### hasOffscreenPath + +``` +hasOffscreenPath(path: string): Promise +``` + +Checks whether the current offscreen document matches the given extension path. Query parameters and hash fragments are ignored. diff --git a/src/offscreen.test.ts b/src/offscreen.test.ts new file mode 100644 index 0000000..c39f4d5 --- /dev/null +++ b/src/offscreen.test.ts @@ -0,0 +1,198 @@ +import {afterEach, beforeEach, describe, expect, jest, test} from "@jest/globals"; +import { + closeOffscreen, + createOffscreen, + getOffscreenContext, + getOffscreenPath, + getOffscreenUrl, + hasOffscreen, + hasOffscreenPath, + hasOffscreenUrl, +} from "./offscreen"; + +describe("offscreen", () => { + let originalChrome: any; + let contexts: chrome.runtime.ExtensionContext[]; + + beforeEach(() => { + originalChrome = globalThis.chrome; + contexts = []; + + globalThis.chrome = { + runtime: { + id: "extension-id", + lastError: undefined, + getContexts: jest.fn((filter: chrome.runtime.ContextFilter, cb: (result: any) => void) => { + const result = contexts.filter(context => { + if (filter.contextTypes && !filter.contextTypes.includes(context.contextType)) { + return false; + } + + return !( + filter.documentUrls && + (!context.documentUrl || !filter.documentUrls.includes(context.documentUrl)) + ); + }); + + cb(result); + }), + getURL: jest.fn((path: string) => `chrome-extension://extension-id/${path.replace(/^\/+/, "")}`), + }, + offscreen: { + closeDocument: jest.fn((cb: () => void) => cb()), + createDocument: jest.fn((_parameters: chrome.offscreen.CreateParameters, cb: () => void) => cb()), + hasDocument: jest.fn((cb: (result: boolean) => void) => cb(contexts.length !== 0)), + }, + } as any; + }); + + afterEach(() => { + globalThis.chrome = originalChrome; + jest.resetAllMocks(); + }); + + test("should close the current offscreen document", async () => { + await expect(closeOffscreen()).resolves.toBeUndefined(); + expect(globalThis.chrome.offscreen.closeDocument).toHaveBeenCalledWith(expect.any(Function)); + }); + + test("should create an offscreen document", async () => { + const parameters: chrome.offscreen.CreateParameters = { + justification: "Process audio in an offscreen document", + reasons: ["AUDIO_PLAYBACK"], + url: "offscreen.html", + }; + + await expect(createOffscreen(parameters)).resolves.toBeUndefined(); + expect(globalThis.chrome.offscreen.createDocument).toHaveBeenCalledWith(parameters, expect.any(Function)); + }); + + test("should check whether an offscreen document exists", async () => { + await expect(hasOffscreen()).resolves.toBe(false); + + contexts = [ + { + contextId: "context-id", + contextType: "OFFSCREEN_DOCUMENT", + documentUrl: "chrome-extension://extension-id/offscreen.html", + frameId: 0, + incognito: false, + tabId: -1, + windowId: -1, + }, + ]; + + await expect(hasOffscreen()).resolves.toBe(true); + expect(globalThis.chrome.offscreen.hasDocument).toHaveBeenCalledWith(expect.any(Function)); + }); + + test("should return the current offscreen context", async () => { + const offscreenContext = { + contextId: "context-id", + contextType: "OFFSCREEN_DOCUMENT", + documentUrl: "chrome-extension://extension-id/offscreen.html", + frameId: 0, + incognito: false, + tabId: -1, + windowId: -1, + } as chrome.runtime.ExtensionContext; + + contexts = [ + { + ...offscreenContext, + contextId: "popup-id", + contextType: "POPUP", + }, + offscreenContext, + ]; + + await expect(getOffscreenContext()).resolves.toBe(offscreenContext); + expect(globalThis.chrome.runtime.getContexts).toHaveBeenCalledWith( + {contextTypes: ["OFFSCREEN_DOCUMENT"]}, + expect.any(Function) + ); + }); + + test("should return the current offscreen url", async () => { + contexts = [ + { + contextId: "context-id", + contextType: "OFFSCREEN_DOCUMENT", + documentUrl: "chrome-extension://extension-id/offscreen.html", + frameId: 0, + incognito: false, + tabId: -1, + windowId: -1, + }, + ]; + + await expect(getOffscreenUrl()).resolves.toBe("chrome-extension://extension-id/offscreen.html"); + }); + + test("should return the current offscreen pathname", async () => { + contexts = [ + { + contextId: "context-id", + contextType: "OFFSCREEN_DOCUMENT", + documentUrl: "chrome-extension://extension-id/offscreen.html?mode=audio#ready", + frameId: 0, + incognito: false, + tabId: -1, + windowId: -1, + }, + ]; + + await expect(getOffscreenPath()).resolves.toBe("/offscreen.html"); + }); + + test("should return undefined path for non-extension urls", async () => { + contexts = [ + { + contextId: "context-id", + contextType: "OFFSCREEN_DOCUMENT", + documentUrl: "https://example.com/offscreen.html", + frameId: 0, + incognito: false, + tabId: -1, + windowId: -1, + }, + ]; + + await expect(getOffscreenPath()).resolves.toBeUndefined(); + }); + + test("should check the current offscreen url", async () => { + contexts = [ + { + contextId: "context-id", + contextType: "OFFSCREEN_DOCUMENT", + documentUrl: "chrome-extension://extension-id/offscreen.html", + frameId: 0, + incognito: false, + tabId: -1, + windowId: -1, + }, + ]; + + await expect(hasOffscreenUrl("chrome-extension://extension-id/offscreen.html")).resolves.toBe(true); + await expect(hasOffscreenUrl("chrome-extension://extension-id/other.html")).resolves.toBe(false); + }); + + test("should check the current offscreen path by pathname", async () => { + contexts = [ + { + contextId: "context-id", + contextType: "OFFSCREEN_DOCUMENT", + documentUrl: "chrome-extension://extension-id/offscreen.html?mode=audio#ready", + frameId: 0, + incognito: false, + tabId: -1, + windowId: -1, + }, + ]; + + await expect(hasOffscreenPath("/offscreen.html")).resolves.toBe(true); + await expect(hasOffscreenPath("/offscreen.html?mode=video#other")).resolves.toBe(true); + await expect(hasOffscreenPath("other.html")).resolves.toBe(false); + }); +}); diff --git a/src/offscreen.ts b/src/offscreen.ts index 7178cbb..38fd9ae 100644 --- a/src/offscreen.ts +++ b/src/offscreen.ts @@ -1,7 +1,9 @@ import {browser} from "./browser"; +import {getContexts, getUrl} from "./runtime"; import {callWithPromise} from "./utils"; type CreateParameters = chrome.offscreen.CreateParameters; +type ExtensionContext = chrome.runtime.ExtensionContext; const offscreen = () => browser().offscreen; @@ -12,3 +14,35 @@ export const createOffscreen = (parameters: CreateParameters): Promise => callWithPromise(cb => offscreen().createDocument(parameters, cb)); export const hasOffscreen = (): Promise => callWithPromise(cb => offscreen().hasDocument(cb)); + +export const getOffscreenContext = async (): Promise => { + return (await getContexts({contextTypes: ["OFFSCREEN_DOCUMENT"]}))[0]; +}; + +export const getOffscreenUrl = async (): Promise => { + return (await getOffscreenContext())?.documentUrl; +}; + +export const getOffscreenPath = async (): Promise => { + const url = await getOffscreenUrl(); + + if (!url) { + return undefined; + } + + const documentUrl = new URL(url); + + if (documentUrl.origin !== new URL(getUrl("")).origin) { + return undefined; + } + + return documentUrl.pathname; +}; + +export const hasOffscreenUrl = async (url: string): Promise => { + return (await getOffscreenUrl()) === url; +}; + +export const hasOffscreenPath = async (path: string): Promise => { + return (await getOffscreenPath()) === new URL(getUrl(path)).pathname; +};