Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions docs/offscreen.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down Expand Up @@ -41,3 +46,53 @@ hasOffscreen(): Promise<boolean>
```

Checks whether an offscreen document is currently open.

<a name="getOffscreenContext"></a>

### getOffscreenContext

```
getOffscreenContext(): Promise<chrome.runtime.ExtensionContext | undefined>
```

Returns the current offscreen document context, if one is open.

<a name="getOffscreenUrl"></a>

### getOffscreenUrl

```
getOffscreenUrl(): Promise<string | undefined>
```

Returns the current offscreen document URL, if one is open.

<a name="getOffscreenPath"></a>

### getOffscreenPath

```
getOffscreenPath(): Promise<string | undefined>
```

Returns the current offscreen document path within the extension, if one is open. Query parameters and hash fragments are not included.

<a name="hasOffscreenUrl"></a>

### hasOffscreenUrl

```
hasOffscreenUrl(url: string): Promise<boolean>
```

Checks whether the current offscreen document matches the given URL.

<a name="hasOffscreenPath"></a>

### hasOffscreenPath

```
hasOffscreenPath(path: string): Promise<boolean>
```

Checks whether the current offscreen document matches the given extension path. Query parameters and hash fragments are ignored.
198 changes: 198 additions & 0 deletions src/offscreen.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
34 changes: 34 additions & 0 deletions src/offscreen.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -12,3 +14,35 @@ export const createOffscreen = (parameters: CreateParameters): Promise<void> =>
callWithPromise(cb => offscreen().createDocument(parameters, cb));

export const hasOffscreen = (): Promise<boolean> => callWithPromise(cb => offscreen().hasDocument(cb));

export const getOffscreenContext = async (): Promise<ExtensionContext | undefined> => {
return (await getContexts({contextTypes: ["OFFSCREEN_DOCUMENT"]}))[0];
};

export const getOffscreenUrl = async (): Promise<string | undefined> => {
return (await getOffscreenContext())?.documentUrl;
};

export const getOffscreenPath = async (): Promise<string | undefined> => {
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<boolean> => {
return (await getOffscreenUrl()) === url;
};

export const hasOffscreenPath = async (path: string): Promise<boolean> => {
return (await getOffscreenPath()) === new URL(getUrl(path)).pathname;
};
Loading