mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 04:41:11 +00:00
Bug 1911836 - Implement onUserScriptMessage r=zombie
Differential Revision: https://phabricator.services.mozilla.com/D229386
This commit is contained in:
parent
d37e90a463
commit
d99a5fd35f
@ -330,6 +330,14 @@ export class Messenger {
|
||||
this.onMessageEx = new MessageEvent(context, "runtime.onMessageExternal");
|
||||
}
|
||||
|
||||
get onUserScriptMessage() {
|
||||
return redefineGetter(
|
||||
this,
|
||||
"onUserScriptMessage",
|
||||
new MessageEvent(this.context, "runtime.onUserScriptMessage")
|
||||
);
|
||||
}
|
||||
|
||||
sendNativeMessage(nativeApp, json) {
|
||||
let holder = holdMessage(
|
||||
`Messenger/${this.context.extension.id}/sendNativeMessage/${nativeApp}`,
|
||||
@ -340,7 +348,11 @@ export class Messenger {
|
||||
return this.conduit.queryNativeMessage({ nativeApp, holder });
|
||||
}
|
||||
|
||||
sendRuntimeMessage({ extensionId, message, callback, ...args }) {
|
||||
sendRuntimeMessage({ context, extensionId, message, callback, ...args }) {
|
||||
// this.context is usually used, except with user scripts, where we pass a
|
||||
// custom context to ensure that the return value is cloned into the right
|
||||
// USER_SCRIPT world.
|
||||
context ??= this.context;
|
||||
let response = this.conduit.queryRuntimeMessage({
|
||||
extensionId: extensionId || this.context.extension.id,
|
||||
holder: holdMessage(
|
||||
@ -352,7 +364,7 @@ export class Messenger {
|
||||
});
|
||||
// If |response| is a rejected promise, the value will be sanitized by
|
||||
// wrapPromise, according to the rules of context.normalizeError.
|
||||
return this.context.wrapPromise(response, callback);
|
||||
return context.wrapPromise(response, callback);
|
||||
}
|
||||
|
||||
connect({ name, native, ...args }) {
|
||||
@ -372,8 +384,12 @@ export class Messenger {
|
||||
}
|
||||
}
|
||||
|
||||
recvRuntimeMessage({ extensionId, holder, sender }) {
|
||||
recvRuntimeMessage({ extensionId, holder, sender, userScriptWorldId }) {
|
||||
let event = sender.id === extensionId ? this.onMessage : this.onMessageEx;
|
||||
if (typeof userScriptWorldId == "string") {
|
||||
sender = { ...sender, userScriptWorldId };
|
||||
return this.onUserScriptMessage.emit(holder, sender);
|
||||
}
|
||||
return event.emit(holder, sender);
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,17 @@
|
||||
*/
|
||||
|
||||
import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
|
||||
import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
|
||||
|
||||
const { DefaultMap, DefaultWeakMap } = ExtensionUtils;
|
||||
/** @type {Lazy} */
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
Schemas: "resource://gre/modules/Schemas.sys.mjs",
|
||||
});
|
||||
|
||||
const { DefaultMap, DefaultWeakMap, ExtensionError } = ExtensionUtils;
|
||||
const { BaseContext, redefineGetter } = ExtensionCommon;
|
||||
|
||||
class WorldConfigHolder {
|
||||
/** @type {Map<ExtensionChild,WorldConfigHolder>} */
|
||||
@ -38,6 +47,130 @@ class WorldConfigHolder {
|
||||
this.defaultCSP
|
||||
);
|
||||
}
|
||||
|
||||
isMessagingEnabledForWorldId(worldId) {
|
||||
return (
|
||||
this.configs.get(worldId)?.messaging ??
|
||||
this.configs.get("")?.messaging ??
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A light wrapper around a ContentScriptContextChild to serve as a BaseContext
|
||||
* instance to support APIs exposed to USER_SCRIPT worlds. Such contexts are
|
||||
* usually heavier due to the need to track the document lifetime, but because
|
||||
* all user script worlds and a content script for a document (and extension)
|
||||
* shares the same lifetime, we delegate to the only ContentScriptContextChild
|
||||
* that exists for the document+extension.
|
||||
*/
|
||||
class UserScriptContext extends BaseContext {
|
||||
/**
|
||||
* @param {ContentScriptContextChild} contentContext
|
||||
* @param {Sandbox} sandbox
|
||||
* @param {string} worldId
|
||||
* @param {boolean} messaging
|
||||
*/
|
||||
constructor(contentContext, sandbox, worldId, messaging) {
|
||||
// Note: envType "userscript_child" is currently not recognized elsewhere.
|
||||
// In particular ParentAPIManager.recvCreateProxyContext refuses to create
|
||||
// ProxyContextParent instances, which is desirable because an extension
|
||||
// can create many user script worlds, and we do not want the overhead of
|
||||
// a new ProxyContextParent for each USER_SCRIPT worldId.
|
||||
super("userscript_child", contentContext.extension);
|
||||
|
||||
this.contentContext = contentContext;
|
||||
this.#forwardGetterToOwnerContext("active");
|
||||
this.#forwardGetterToOwnerContext("incognito");
|
||||
this.#forwardGetterToOwnerContext("messageManager");
|
||||
this.#forwardGetterToOwnerContext("contentWindow");
|
||||
this.#forwardGetterToOwnerContext("innerWindowID");
|
||||
this.cloneScopeError = sandbox.Error;
|
||||
this.cloneScopePromise = sandbox.Promise;
|
||||
|
||||
this.sandbox = sandbox;
|
||||
Object.defineProperty(this, "principal", {
|
||||
value: Cu.getObjectPrincipal(sandbox),
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
this.worldId = worldId;
|
||||
this.enableMessaging = messaging;
|
||||
|
||||
contentContext.callOnClose(this);
|
||||
}
|
||||
|
||||
close() {
|
||||
super.close();
|
||||
this.contentContext = null;
|
||||
this.sandbox = null;
|
||||
}
|
||||
|
||||
get cloneScope() {
|
||||
return this.sandbox;
|
||||
}
|
||||
|
||||
#forwardGetterToOwnerContext(name) {
|
||||
Object.defineProperty(this, name, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
return this.contentContext[name];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get browserObj() {
|
||||
const browser = {};
|
||||
// The set of APIs exposed to user scripts is minimal. For simplicity and
|
||||
// minimizing overhead, we do not use Schemas-generated bindings.
|
||||
|
||||
const wrapF = func => {
|
||||
return (...args) => {
|
||||
try {
|
||||
return func.apply(this, args);
|
||||
} catch (e) {
|
||||
throw this.normalizeError(e);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if (this.enableMessaging) {
|
||||
browser.runtime = {};
|
||||
browser.runtime.sendMessage = wrapF(this.runtimeSendMessage);
|
||||
}
|
||||
const value = Cu.cloneInto(browser, this.sandbox, { cloneFunctions: true });
|
||||
return redefineGetter(this, "browserObj", value);
|
||||
}
|
||||
|
||||
runtimeSendMessage(...args) {
|
||||
// Simplified version of parseBonkersArgs in child/ext-runtime.js
|
||||
let callback = typeof args[args.length - 1] === "function" && args.pop();
|
||||
|
||||
// The extensionId and options parameters are an optional part of the
|
||||
// runtime.sendMessage() interface, but not supported in user scripts:
|
||||
// runtime.sendMessage() will only trigger runtime.onUserScriptMessage and
|
||||
// never runtime.onMessage nor runtime.onMessageExternal.
|
||||
if (!args.length) {
|
||||
throw new ExtensionError(
|
||||
"runtime.sendMessage's message argument is missing"
|
||||
);
|
||||
} else if (args.length > 1) {
|
||||
throw new ExtensionError(
|
||||
"runtime.sendMessage received too many arguments"
|
||||
);
|
||||
}
|
||||
|
||||
let [message] = args;
|
||||
|
||||
return this.contentContext.messenger.sendRuntimeMessage({
|
||||
context: this,
|
||||
userScriptWorldId: this.worldId,
|
||||
message,
|
||||
callback,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class WorldCollection {
|
||||
@ -103,7 +236,19 @@ class WorldCollection {
|
||||
originAttributes: docPrincipal.originAttributes,
|
||||
});
|
||||
|
||||
// TODO bug 1911836: Expose APIs when messaging is true.
|
||||
let messaging = this.configHolder.isMessagingEnabledForWorldId(worldId);
|
||||
if (messaging) {
|
||||
let userScriptContext = new UserScriptContext(
|
||||
this.context,
|
||||
sandbox,
|
||||
worldId,
|
||||
messaging
|
||||
);
|
||||
|
||||
const getBrowserObj = () => userScriptContext.browserObj;
|
||||
lazy.Schemas.exportLazyGetter(sandbox, "browser", getBrowserObj);
|
||||
lazy.Schemas.exportLazyGetter(sandbox, "chrome", getBrowserObj);
|
||||
}
|
||||
|
||||
return sandbox;
|
||||
}
|
||||
|
@ -63,6 +63,14 @@ this.runtime = class extends ExtensionAPI {
|
||||
onConnectExternal: context.messenger.onConnectEx.api(),
|
||||
onMessageExternal: context.messenger.onMessageEx.api(),
|
||||
|
||||
get onUserScriptMessage() {
|
||||
return ExtensionCommon.redefineGetter(
|
||||
this,
|
||||
"onUserScriptMessage",
|
||||
context.messenger.onUserScriptMessage.api()
|
||||
);
|
||||
},
|
||||
|
||||
connect(extensionId, options) {
|
||||
let name = options?.name ?? "";
|
||||
return context.messenger.connect({ name, extensionId });
|
||||
|
@ -35,6 +35,7 @@ this.runtime = class extends ExtensionAPIPersistent {
|
||||
// - runtime.onConnectExternal
|
||||
// - runtime.onMessage
|
||||
// - runtime.onMessageExternal
|
||||
// - runtime.onUserScriptMessage
|
||||
// For details, see bug 1852317 and test_ext_eventpage_messaging_wakeup.js.
|
||||
|
||||
onInstalled({ fire }) {
|
||||
|
@ -170,6 +170,11 @@
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "The TLS channel ID of the page or frame that opened the connection, if requested by the extension or app, and if available."
|
||||
},
|
||||
"userScriptWorldId": {
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "The worldId of the USER_SCRIPT world that sent the message. Only present on onUserScriptMessage events."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -800,6 +805,31 @@
|
||||
"description": "Return true from the event listener if you wish to call <code>sendResponse</code> after the event listener returns."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "onUserScriptMessage",
|
||||
"type": "function",
|
||||
"description": "Fired when a message is sent from a USER_SCRIPT world registered through the userScripts API.",
|
||||
"permissions": ["userScripts"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "message",
|
||||
"type": "any",
|
||||
"optional": true,
|
||||
"description": "The message sent by the calling script."
|
||||
},
|
||||
{ "name": "sender", "$ref": "MessageSender" },
|
||||
{
|
||||
"name": "sendResponse",
|
||||
"type": "function",
|
||||
"description": "Function to call (at most once) when you have a response. The argument should be any JSON-ifiable object. If you have more than one <code>onMessage</code> listener in the same document, then only one may send a response. This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until <code>sendResponse</code> is called)."
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "Return true from the event listener if you wish to call <code>sendResponse</code> after the event listener returns."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "onRestartRequired",
|
||||
"unsupported": true,
|
||||
|
@ -230,6 +230,12 @@
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "The world's Content Security Policy. Defaults to the CSP of regular content scripts, which prohibits dynamic code execution such as eval."
|
||||
},
|
||||
"messaging": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"default": false,
|
||||
"description": "Whether the runtime.sendMessage and runtime.connect methods are exposed. Defaults to not exposing these messaging APIs."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -69,6 +69,11 @@ async function startEvalTesterExtension() {
|
||||
await browser.userScripts.resetWorldConfiguration(args);
|
||||
} else if (msg === "getWorldConfigurations") {
|
||||
let res = await browser.userScripts.getWorldConfigurations();
|
||||
for (let properties of res) {
|
||||
// We are only interested in the worldId / csp properties, so drop
|
||||
// all other properties so we can keep the assertions simple.
|
||||
delete properties.messaging;
|
||||
}
|
||||
browser.test.sendMessage("getWorldConfigurations:done", res);
|
||||
return;
|
||||
} else {
|
||||
|
@ -0,0 +1,359 @@
|
||||
"use strict";
|
||||
|
||||
const { ExtensionTestCommon } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/ExtensionTestCommon.sys.mjs"
|
||||
);
|
||||
|
||||
const { ExtensionUserScripts } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/ExtensionUserScripts.sys.mjs"
|
||||
);
|
||||
|
||||
const server = createHttpServer({ hosts: ["example.com", "example.net"] });
|
||||
server.registerPathHandler("/dummy", () => {});
|
||||
|
||||
AddonTestUtils.init(this);
|
||||
AddonTestUtils.overrideCertDB();
|
||||
|
||||
add_setup(async () => {
|
||||
Services.prefs.setBoolPref("extensions.userScripts.mv3.enabled", true);
|
||||
await ExtensionTestUtils.startAddonManager();
|
||||
});
|
||||
|
||||
add_task(async function test_runtime_messaging_errors() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
useAddonManager: "permanent",
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
permissions: ["userScripts"],
|
||||
host_permissions: ["*://example.com/*"],
|
||||
content_scripts: [
|
||||
{
|
||||
js: ["contentscript_expose_test.js"],
|
||||
matches: ["*://example.com/dummy"],
|
||||
run_at: "document_start",
|
||||
},
|
||||
],
|
||||
},
|
||||
files: {
|
||||
"contentscript_expose_test.js": () => {
|
||||
// userscript.js does not have browser.test, export it from here.
|
||||
// eslint-disable-next-line no-undef
|
||||
window.wrappedJSObject.contentscriptTest = cloneInto(
|
||||
browser.test,
|
||||
window,
|
||||
{ cloneFunctions: true }
|
||||
);
|
||||
},
|
||||
"userscript.js": async () => {
|
||||
// Note: this is the browser.test namespace from the content script.
|
||||
// We can only pass primitive values here, because Xrays prevent the
|
||||
// content script from receiving functions, etc.
|
||||
const contentscriptTest = window.wrappedJSObject.contentscriptTest;
|
||||
function assertThrows(cb, expectedError, desc) {
|
||||
let actualErrorMessage;
|
||||
try {
|
||||
cb();
|
||||
actualErrorMessage = "Unexpectedly not thrown";
|
||||
} catch (e) {
|
||||
actualErrorMessage = e.message;
|
||||
}
|
||||
contentscriptTest.assertEq(expectedError, actualErrorMessage, desc);
|
||||
}
|
||||
async function assertRejects(promise, expectedError, desc) {
|
||||
let actualErrorMessage;
|
||||
try {
|
||||
await promise;
|
||||
actualErrorMessage = "Unexpectedly not rejected";
|
||||
} catch (e) {
|
||||
actualErrorMessage = e.message;
|
||||
}
|
||||
contentscriptTest.assertEq(expectedError, actualErrorMessage, desc);
|
||||
}
|
||||
|
||||
try {
|
||||
assertThrows(
|
||||
() => browser.runtime.sendMessage(),
|
||||
"runtime.sendMessage's message argument is missing",
|
||||
"sendMessage without params"
|
||||
);
|
||||
assertThrows(
|
||||
() => browser.runtime.sendMessage("extensionId@", "message"),
|
||||
"runtime.sendMessage received too many arguments",
|
||||
"sendMessage with unsupported extensionId parameter"
|
||||
);
|
||||
assertThrows(
|
||||
() => browser.runtime.sendMessage("message", {}),
|
||||
"runtime.sendMessage received too many arguments",
|
||||
"sendMessage with unsupported options parameter"
|
||||
);
|
||||
assertThrows(
|
||||
() => browser.runtime.sendMessage(location),
|
||||
"Location object could not be cloned.",
|
||||
"sendMessage with non-cloneable message"
|
||||
);
|
||||
await assertRejects(
|
||||
browser.runtime.sendMessage("msg"),
|
||||
"Could not establish connection. Receiving end does not exist.",
|
||||
"Expected error when there is no onUserScriptMessage handler"
|
||||
);
|
||||
} catch (e) {
|
||||
contentscriptTest.fail(`Unexpected error in userscript.js: ${e}`);
|
||||
}
|
||||
contentscriptTest.sendMessage("done");
|
||||
},
|
||||
},
|
||||
async background() {
|
||||
await browser.userScripts.configureWorld({ messaging: true });
|
||||
await browser.userScripts.register([
|
||||
{
|
||||
id: "error checker",
|
||||
matches: ["*://example.com/dummy"],
|
||||
js: [{ file: "userscript.js" }],
|
||||
},
|
||||
]);
|
||||
browser.test.sendMessage("registered");
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("registered");
|
||||
|
||||
let contentPage = await ExtensionTestUtils.loadContentPage(
|
||||
"http://example.com/dummy"
|
||||
);
|
||||
await extension.awaitMessage("done");
|
||||
await contentPage.close();
|
||||
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
// This tests that runtime.sendMessage works when called from a user script.
|
||||
// And that the messaging flag persists across restarts.
|
||||
// Moreover, that runtime.sendMessage can wake up an event page.
|
||||
add_task(async function test_onUserScriptMessage() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
useAddonManager: "permanent",
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
permissions: ["userScripts"],
|
||||
host_permissions: ["*://example.com/*"],
|
||||
},
|
||||
files: {
|
||||
"userscript.js": async () => {
|
||||
// browser.runtime.sendMessage should be available because we call
|
||||
// userScripts.configureWorld({ messaging: true }) before page load.
|
||||
let responses = [
|
||||
await browser.runtime.sendMessage("expectPromiseResult"),
|
||||
await browser.runtime.sendMessage("expectSendResponse"),
|
||||
await browser.runtime.sendMessage("expectSendResponseAsync"),
|
||||
await browser.runtime.sendMessage("expectDefaultResponse"),
|
||||
];
|
||||
browser.runtime.sendMessage(responses);
|
||||
},
|
||||
},
|
||||
background() {
|
||||
// Set up message listeners. The user script will send multiple messages,
|
||||
// and ultimately send a message will all responses received so far.
|
||||
// NOTE: To make sure that the functionality is independent of other
|
||||
// messaging APIs, we only register runtime.onUserScriptMessage here,
|
||||
// and no other extension messaging APIs.
|
||||
browser.runtime.onUserScriptMessage.addListener(
|
||||
(msg, sender, sendResponse) => {
|
||||
// worldId defaults to "" when not specified in userScripts.register
|
||||
// and userScripts.configureWorld. That default value should appear
|
||||
// here as sender.userScriptWorldId (also an empty string).
|
||||
browser.test.assertEq(
|
||||
"",
|
||||
sender.userScriptWorldId,
|
||||
`Expected userScriptWorldId in onUserScriptMessage for: ${msg}`
|
||||
);
|
||||
switch (msg) {
|
||||
case "expectPromiseResult":
|
||||
return Promise.resolve("Promise");
|
||||
case "expectSendResponse":
|
||||
sendResponse("sendResponse");
|
||||
return;
|
||||
case "expectSendResponseAsync":
|
||||
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
|
||||
setTimeout(() => sendResponse("sendResponseAsync"), 50);
|
||||
return true;
|
||||
case "expectDefaultResponse":
|
||||
return;
|
||||
default:
|
||||
browser.test.assertDeepEq(
|
||||
["Promise", "sendResponse", "sendResponseAsync", undefined],
|
||||
msg,
|
||||
"All sendMessage calls got the expected response"
|
||||
);
|
||||
browser.test.sendMessage("testRuntimeSendMessage:done");
|
||||
}
|
||||
}
|
||||
);
|
||||
browser.runtime.onInstalled.addListener(async () => {
|
||||
await browser.userScripts.configureWorld({ messaging: true });
|
||||
await browser.userScripts.register([
|
||||
{
|
||||
id: "messaging checker",
|
||||
matches: ["*://example.com/dummy"],
|
||||
js: [{ file: "userscript.js" }],
|
||||
},
|
||||
]);
|
||||
browser.test.sendMessage("registered");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("registered");
|
||||
|
||||
async function testRuntimeSendMessage() {
|
||||
let contentPage = await ExtensionTestUtils.loadContentPage(
|
||||
"http://example.com/dummy"
|
||||
);
|
||||
await extension.awaitMessage("testRuntimeSendMessage:done");
|
||||
await contentPage.close();
|
||||
}
|
||||
info("Loading page that should trigger runtime.sendMessage");
|
||||
await testRuntimeSendMessage();
|
||||
|
||||
await AddonTestUtils.promiseShutdownManager();
|
||||
ExtensionUserScripts._getStoreForTesting()._uninitForTesting();
|
||||
await AddonTestUtils.promiseStartupManager();
|
||||
|
||||
// Because the background has a persistent listener (runtime.onInstalled), it
|
||||
// stays suspended after a restart.
|
||||
await extension.awaitStartup();
|
||||
ExtensionTestCommon.testAssertions.assertBackgroundStatusStopped(extension);
|
||||
|
||||
// We expect that the load of the page that calls runtime.sendMessage from a
|
||||
// user script will wake it to fire runtime.onUserScriptMessage.
|
||||
info("Loading page that should load user script and wake event page");
|
||||
await testRuntimeSendMessage();
|
||||
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
// This test tests the following:
|
||||
// - configureWorld() with messaging=false does not affect existing worlds.
|
||||
// - after reloading the page, that the user script is run again but without
|
||||
// access to messaging APIs due to messaging=false.
|
||||
// - Moreover, this also verifies that runtime.sendMessage from a user script
|
||||
// does not trigger runtime.onMessage / runtime.onMessageExternal, and that
|
||||
// even if runtime.onMessage is triggered from a content script, that it does
|
||||
// not have the userScripts-specific "sender.userScriptWorldId" field.
|
||||
add_task(async function test_configureWorld_messaging_existing_world() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
useAddonManager: "permanent",
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
permissions: ["userScripts"],
|
||||
host_permissions: ["*://example.com/*"],
|
||||
content_scripts: [
|
||||
{
|
||||
js: ["contentscript_result_reporter.js"],
|
||||
matches: ["*://example.com/dummy"],
|
||||
run_at: "document_start",
|
||||
},
|
||||
],
|
||||
},
|
||||
files: {
|
||||
"contentscript_result_reporter.js": () => {
|
||||
// exportFunction is defined in the scope of a content script.
|
||||
// eslint-disable-next-line no-undef
|
||||
window.wrappedJSObject.reportResultViaContentScript = exportFunction(
|
||||
msg => browser.runtime.sendMessage(msg),
|
||||
window
|
||||
);
|
||||
},
|
||||
"userscript.js": async () => {
|
||||
try {
|
||||
dump("Trying to call sendMessage(initial_message)\n");
|
||||
await browser.runtime.sendMessage("initial_message");
|
||||
dump("Trying to call sendMessage(after_messaging_false)\n");
|
||||
await browser.runtime.sendMessage("after_messaging_false");
|
||||
// ^ we expect runtime.sendMessage() to succeed despite configuring
|
||||
// messaging to false, because we only check the flag when the APIs
|
||||
// are initialized in the sandbox. This is consistent with Chrome's
|
||||
// behavior. Note that the specification permits this behavior (but
|
||||
// it also allows the implementation to fail if desired):
|
||||
// https://github.com/w3c/webextensions/blob/d16807376b/proposals/multiple_user_script_worlds.md#L191-L193
|
||||
|
||||
dump("Reloading page\n");
|
||||
location.reload();
|
||||
// ^ after reloading the page, we expect the messaging=false flag to
|
||||
// be enforced, and the first runtime.sendMessage() call should fail
|
||||
// and fall through to the catch below.
|
||||
} catch (e) {
|
||||
window.wrappedJSObject.reportResultViaContentScript(`Result:${e}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
async background() {
|
||||
let msgCount = 0;
|
||||
browser.runtime.onUserScriptMessage.addListener(async (msg, sender) => {
|
||||
++msgCount;
|
||||
browser.test.assertEq(
|
||||
"non_default_worldId",
|
||||
sender.userScriptWorldId,
|
||||
"Expected userScriptWorldId in onUserScriptMessage"
|
||||
);
|
||||
if (msgCount === 1) {
|
||||
browser.test.assertEq("initial_message", msg, "Initial message");
|
||||
browser.test.log("Calling configureWorld with messaging=false");
|
||||
await browser.userScripts.configureWorld({
|
||||
worldId: "non_default_worldId",
|
||||
messaging: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (msgCount === 2) {
|
||||
browser.test.assertEq("after_messaging_false", msg, "Second message");
|
||||
return;
|
||||
}
|
||||
// After reload.
|
||||
browser.test.fail(`Unexpected onUserScriptMessage ${msgCount}: ${msg}`);
|
||||
});
|
||||
browser.runtime.onMessage.addListener((msg, sender) => {
|
||||
browser.test.assertFalse(
|
||||
"userScriptWorldId" in sender,
|
||||
"No userScriptWorldId in runtime.onMessage"
|
||||
);
|
||||
browser.test.assertEq(
|
||||
2,
|
||||
msgCount,
|
||||
"Should reach reportResultViaContentScript after reloading page"
|
||||
);
|
||||
browser.test.assertEq(
|
||||
"Result:ReferenceError: browser is not defined",
|
||||
msg,
|
||||
"Expected (error) message after reload when messaging=false"
|
||||
);
|
||||
browser.test.sendMessage("done");
|
||||
});
|
||||
browser.runtime.onMessageExternal.addListener(msg => {
|
||||
browser.test.fail(`Unexpected message: ${msg}`);
|
||||
});
|
||||
await browser.userScripts.configureWorld({ messaging: true });
|
||||
await browser.userScripts.register([
|
||||
{
|
||||
id: "Test effect of configureWorld(messaging=false) and reload",
|
||||
matches: ["*://example.com/dummy"],
|
||||
js: [{ file: "userscript.js" }],
|
||||
worldId: "non_default_worldId",
|
||||
},
|
||||
]);
|
||||
browser.test.sendMessage("registered");
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("registered");
|
||||
|
||||
let contentPage = await ExtensionTestUtils.loadContentPage(
|
||||
"http://example.com/dummy"
|
||||
);
|
||||
await extension.awaitMessage("done");
|
||||
await contentPage.close();
|
||||
|
||||
await extension.unload();
|
||||
});
|
@ -621,6 +621,8 @@ run-sequentially = "very high failure rate in parallel"
|
||||
|
||||
["test_ext_userScripts_mv3_injection.js"]
|
||||
|
||||
["test_ext_userScripts_mv3_messaging.js"]
|
||||
|
||||
["test_ext_userScripts_mv3_persistence.js"]
|
||||
|
||||
["test_ext_userScripts_mv3_worlds.js"]
|
||||
|
Loading…
Reference in New Issue
Block a user