diff --git a/.eslintrc b/.eslintrc index 7dd2ffe..7ef4a4c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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": [ diff --git a/package.json b/package.json index 832ef2c..5669c48 100644 --- a/package.json +++ b/package.json @@ -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" ] } } diff --git a/source/test/background.ts b/source/test/background.ts index 1efc4fc..1233cb6 100644 --- a/source/test/background.ts +++ b/source/test/background.ts @@ -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(); diff --git a/source/test/contentscript/api.test.ts b/source/test/contentscript/api.test.ts index 2e55137..62d7320 100644 --- a/source/test/contentscript/api.test.ts +++ b/source/test/contentscript/api.test.ts @@ -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, @@ -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"; @@ -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; @@ -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 { @@ -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", diff --git a/source/test/helpers.ts b/source/test/helpers.ts index 0f5a69d..e4d2da8 100644 --- a/source/test/helpers.ts +++ b/source/test/helpers.ts @@ -1,4 +1,5 @@ import type test from "tape"; +import { type Sender } from "webext-messenger"; export async function expectRejection( t: test.Test, @@ -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, + ); +} diff --git a/source/test/manifest.json b/source/test/manifest.json index 31b197c..fed14c1 100644 --- a/source/test/manifest.json +++ b/source/test/manifest.json @@ -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", @@ -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": [ diff --git a/source/test/offscreen.html b/source/test/offscreen.html new file mode 100644 index 0000000..4281c34 --- /dev/null +++ b/source/test/offscreen.html @@ -0,0 +1,5 @@ + + +Offscreen page + + diff --git a/source/test/offscreen/addFrame.ts b/source/test/offscreen/addFrame.ts new file mode 100644 index 0000000..364c597 --- /dev/null +++ b/source/test/offscreen/addFrame.ts @@ -0,0 +1,5 @@ +export function addFrame(): void { + const frame = document.createElement("iframe"); + frame.src = "https://example.com"; + document.body.append(frame); +} diff --git a/source/test/offscreen/api.test.ts b/source/test/offscreen/api.test.ts new file mode 100644 index 0000000..a36b818 --- /dev/null +++ b/source/test/offscreen/api.test.ts @@ -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", + ); +}); diff --git a/source/test/offscreen/api.ts b/source/test/offscreen/api.ts new file mode 100644 index 0000000..6563481 --- /dev/null +++ b/source/test/offscreen/api.ts @@ -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); diff --git a/source/test/offscreen/getLocation.ts b/source/test/offscreen/getLocation.ts new file mode 100644 index 0000000..6ce6ddd --- /dev/null +++ b/source/test/offscreen/getLocation.ts @@ -0,0 +1,3 @@ +export function getLocation(): string { + return location.href; +} diff --git a/source/test/offscreen/getTrace.ts b/source/test/offscreen/getTrace.ts new file mode 100644 index 0000000..a23a5f4 --- /dev/null +++ b/source/test/offscreen/getTrace.ts @@ -0,0 +1,5 @@ +import { type MessengerMeta, type Sender } from "webext-messenger"; + +export async function getTrace(this: MessengerMeta): Promise { + return this.trace; +} diff --git a/source/test/offscreen/registration.ts b/source/test/offscreen/registration.ts new file mode 100644 index 0000000..b2fe14c --- /dev/null +++ b/source/test/offscreen/registration.ts @@ -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, +}); diff --git a/source/thisTarget.ts b/source/thisTarget.ts index 05473fb..3b2d69c 100644 --- a/source/thisTarget.ts +++ b/source/thisTarget.ts @@ -2,6 +2,7 @@ import { getContextName, isBackground, isExtensionContext, + isOffscreenDocument, } from "webext-detect"; import { messenger } from "./sender.js"; import { registerMethods } from "./receiver.js"; @@ -39,23 +40,25 @@ import { pEvent } from "p-event"; // 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"; export function getTabDataStatus(): typeof tabDataStatus { return tabDataStatus;