Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add offscreen support #276

Merged
merged 9 commits into from
Dec 2, 2024
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
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
}
}
],
"unicorn/no-nested-ternary": "off", // Prettier conflict
"@typescript-eslint/consistent-type-definitions": "off" // Unconvinced by its utility; may be counterproductive
},
"overrides": [
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@
"run": {
"startUrl": [
"https://fregante.github.io/pixiebrix-testing-ground/Will-call-background-methods",
"https://fregante.github.io/pixiebrix-testing-ground/Will-call-other-CS-via-background"
"https://fregante.github.io/pixiebrix-testing-ground/Will-call-other-CS-via-background",
"https://fregante.github.io/pixiebrix-testing-ground/Will-call-offscreen-methods"
]
}
}
Expand Down
18 changes: 18 additions & 0 deletions source/test/background.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
import "./webextensionPolyfill.js";
import "./background/registration.ts";
import "./contentscript/api.test.ts";

async function init() {
try {
await chrome.offscreen.createDocument({
url: "offscreen.html",
// @ts-expect-error wrong?
reasons: ["DOM_PARSER"],
justification: "testing",
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
if (!(error as any).message.includes("Only a single offscreen")) {
throw error;
}
}
}

void init();
35 changes: 6 additions & 29 deletions source/test/contentscript/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from "tape";
import { isBackground, isContentScript, isWebPage } from "webext-detect";
import { type PageTarget, type Sender, type Target } from "webext-messenger";
import { type PageTarget, type Target } from "webext-messenger";
import {
errorTabDoesntExist,
errorTargetClosedEarly,
Expand All @@ -12,6 +12,8 @@ import {
sleep,
trackSettleTime,
expectDuration,
senderIsBackground,
senderIsCurrentPage,
} from "../helpers.js";
import * as backgroundContext from "../background/api.js";
import * as localContext from "../background/testingApi.js";
Expand All @@ -30,31 +32,6 @@ import {
} from "./api.js";
import { MessengerError } from "../../shared.js";

const extensionUrl = new URL(chrome.runtime.getURL(""));

function senderIsCurrentPage(
t: test.Test,
sender: Sender | undefined,
message: string,
) {
t.equal(sender?.url, location.href, message);
}

function senderisBackground(
t: test.Test,
sender: Sender | undefined,
message: string,
) {
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing -- It's an OR on falsy values */
t.true(
sender?.origin === extensionUrl.origin || // Chrome
sender?.origin === "null" || // Chrome, old
sender?.url?.includes("/background.") ||
sender?.url?.endsWith("/_generated_background_page.html"), // Firefox
message,
);
}

const { openTab, createTargets, ensureScripts, closeTab } = isBackground()
? localContext
: backgroundContext;
Expand Down Expand Up @@ -149,9 +126,9 @@ function runOnTarget(target: Target | PageTarget, expectedTitle: string) {
const directSender = trace.at(-1);

if (isBackground()) {
senderisBackground(
senderIsBackground(
t,
directSender,
originalSender,
"Messages should mention the current page (background) in trace[0]",
);
} else {
Expand All @@ -163,7 +140,7 @@ function runOnTarget(target: Target | PageTarget, expectedTitle: string) {
}

if (!("page" in target && isContentScript())) {
senderisBackground(
senderIsBackground(
t,
directSender,
"Messages originated in content scripts or background pages must come directly from the background page",
Expand Down
26 changes: 26 additions & 0 deletions source/test/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type test from "tape";
import { type Sender } from "webext-messenger";

export async function expectRejection(
t: test.Test,
Expand Down Expand Up @@ -65,3 +66,28 @@ export function expectDuration(
);
}
}

const extensionUrl = new URL(chrome.runtime.getURL(""));

export function senderIsCurrentPage(
t: test.Test,
sender: Sender | undefined,
message: string,
) {
t.equal(sender?.url, location.href, message);
}

export function senderIsBackground(
t: test.Test,
sender: Sender | undefined,
message: string,
) {
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing -- It's an OR on falsy values */
t.true(
sender?.origin === extensionUrl.origin || // Chrome
sender?.origin === "null" || // Chrome, old
sender?.url?.includes("/background.") ||
sender?.url?.endsWith("/_generated_background_page.html"), // Firefox
message,
);
}
8 changes: 7 additions & 1 deletion source/test/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "webext-messenger",
"version": "0.0.0",
"manifest_version": 3,
"permissions": ["webNavigation", "scripting", "storage"],
"permissions": ["webNavigation", "scripting", "storage", "offscreen"],
"host_permissions": ["https://fregante.github.io/*"],
"background": {
"type": "module",
Expand All @@ -21,6 +21,12 @@
],
"js": ["webextensionPolyfill.ts", "background/api.test.ts"]
},
{
"matches": [
"https://fregante.github.io/pixiebrix-testing-ground/Will-call-offscreen-methods"
],
"js": ["webextensionPolyfill.ts", "offscreen/api.test.ts"]
},
{
"all_frames": true,
"matches": [
Expand Down
5 changes: 5 additions & 0 deletions source/test/offscreen.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!doctype html>
<meta charset="UTF-8" />
<title>Offscreen page</title>
<script type="module" src="webextensionPolyfill.ts"></script>
<script type="module" src="offscreen/registration.ts"></script>
5 changes: 5 additions & 0 deletions source/test/offscreen/addFrame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function addFrame(): void {
const frame = document.createElement("iframe");
frame.src = "https://example.com";
document.body.append(frame);
}
31 changes: 31 additions & 0 deletions source/test/offscreen/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import test from "tape";
import { getLocation, addFrame, getTrace } from "./api.js";
import { senderIsCurrentPage } from "../helpers.js";

test("should get a value from the offscreen document", async (t) => {
t.equal(await getLocation(), chrome.runtime.getURL("offscreen.html"));
});

test("notification should return undefined", async (t) => {
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -- Testing for this specifically
t.equals(addFrame(), undefined);
});

test("should receive trace", async (t) => {
const trace = await getTrace();
t.true(Array.isArray(trace));

const originalSender = trace[0];

senderIsCurrentPage(
t,
originalSender,
"Messages should mention the current page in trace[0]",
);

t.equal(
trace.length,
1,
"The offscreen page can and should only be messaged directly",
);
});
7 changes: 7 additions & 0 deletions source/test/offscreen/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getNotifier, getMethod } from "webext-messenger";

const target = { page: "offscreen" };

export const getLocation = getMethod("getLocation", target);
export const addFrame = getNotifier("addFrame", target);
export const getTrace = getMethod("getTrace", target);
3 changes: 3 additions & 0 deletions source/test/offscreen/getLocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getLocation(): string {
return location.href;
}
5 changes: 5 additions & 0 deletions source/test/offscreen/getTrace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { type MessengerMeta, type Sender } from "webext-messenger";

export async function getTrace(this: MessengerMeta): Promise<Sender[]> {
return this.trace;
}
26 changes: 26 additions & 0 deletions source/test/offscreen/registration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { isOffscreenDocument } from "webext-detect";
import { registerMethods } from "webext-messenger";

import { addFrame } from "./addFrame.js";
import { getLocation } from "./getLocation.js";
import { getTrace } from "./getTrace.js";

declare global {
interface MessengerMethods {
addFrame: typeof addFrame;
getLocation: typeof getLocation;
getTrace: typeof getTrace;
}
}

if (!isOffscreenDocument()) {
throw new Error(
"This file must only be run in the offscreen document, which is the receiving end",
);
}

registerMethods({
addFrame,
getLocation,
getTrace,
});
33 changes: 18 additions & 15 deletions source/thisTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
getContextName,
isBackground,
isExtensionContext,
isOffscreenDocument,
} from "webext-detect";
import { messenger } from "./sender.js";
import { registerMethods } from "./receiver.js";
Expand Down Expand Up @@ -39,23 +40,25 @@
// Soft warning: Race conditions are possible.
// This CANNOT be awaited because waiting for it means "I will handle the message."
// If a message is received before this is ready, it will just have to be ignored.
export const thisTarget: KnownTarget = isBackground()
? { page: "background" }
: {
get page(): string {
// Extension pages have relative URLs to simplify comparison
const origin = location.protocol.startsWith("http")
? location.origin
: "";

// Don't use the hash
return origin + location.pathname + location.search;
},
};
export const thisTarget: KnownTarget = (() => {
if (isBackground()) return { page: "background" };
if (isOffscreenDocument()) return { page: "offscreen" };
return {
get page(): string {
// Extension pages have relative URLs to simplify comparison
const origin = location.protocol.startsWith("http")
? location.origin
: "";

// Don't use the hash
return origin + location.pathname + location.search;
},
};
})();

let tabDataStatus: "needed" | "pending" | "received" | "not-needed" | "error" =
// The background page doesn't have a tab
isBackground() ? "not-needed" : "needed";
// Exclude contexts that don't have a tab associated to them
isBackground() || isOffscreenDocument() ? "not-needed" : "needed";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: update comment


export function getTabDataStatus(): typeof tabDataStatus {
return tabDataStatus;
Expand Down Expand Up @@ -92,7 +95,7 @@
return { tabId: this.trace[0]?.tab?.id, frameId: this.trace[0]?.frameId };
}

// TODO: Add tests

Check warning on line 98 in source/thisTarget.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected 'todo' comment: 'TODO: Add tests'
export async function getThisFrame(): Promise<FrameTarget> {
await storeTabData(); // It should already have been called but we still need to await it

Expand Down Expand Up @@ -125,7 +128,7 @@
// Improve DX by informing the developer that it's being loaded the wrong way
// https://github.com/pixiebrix/webext-messenger/issues/88
if (globalThis.__webextMessenger) {
// TODO: Use Error#cause after https://bugs.chromium.org/p/chromium/issues/detail?id=1211260

Check warning on line 131 in source/thisTarget.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected 'todo' comment: 'TODO: Use Error#cause after...'
console.log(
globalThis.__webextMessenger.replace(/^Error/, "webext-messenger"),
);
Expand Down
Loading