summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_api_events_listener_calls_exceptions.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/extensions/test/xpcshell/test_ext_api_events_listener_calls_exceptions.js
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_api_events_listener_calls_exceptions.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_api_events_listener_calls_exceptions.js369
1 files changed, 369 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_api_events_listener_calls_exceptions.js b/toolkit/components/extensions/test/xpcshell/test_ext_api_events_listener_calls_exceptions.js
new file mode 100644
index 0000000000..44ff592d83
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_api_events_listener_calls_exceptions.js
@@ -0,0 +1,369 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ExtensionStorageIDB } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionStorageIDB.sys.mjs"
+);
+
+// Detect if the current build is still using the legacy storage.sync Kinto-based backend
+// (currently only GeckoView builds does have that still enabled).
+//
+// TODO(Bug 1625257): remove this once the rust-based storage.sync backend has been enabled
+// also on GeckoView build and the legacy Kinto-based backend has been ripped off.
+const storageSyncKintoEnabled = Services.prefs.getBoolPref(
+ "webextensions.storage.sync.kinto"
+);
+
+const server = createHttpServer({ hosts: ["example.com"] });
+server.registerDirectory("/data/", do_get_file("data"));
+server.registerPathHandler("/test-page.html", (req, res) => {
+ res.setHeader("Content-Type", "text/html", false);
+ res.write(`<!DOCTYPE html>
+ <html><body><script>
+ window.onerror = (evt) => {
+ browser.test.log("webpage page got error event, error property set to: " + String(evt.error) + "::" +
+ evt.error?.stack + "\\n");
+ window.postMessage(
+ {
+ message: evt.message,
+ sourceName: evt.filename,
+ lineNumber: evt.lineno,
+ columnNumber: evt.colno,
+ errorIsDefined: !!evt.error,
+ },
+ "*"
+ );
+ };
+ window.errorListenerReady = true;
+ </script></body></html>
+ `);
+});
+
+add_task(async function test_api_listener_call_exception() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "storage",
+ "webRequest",
+ "webRequestBlocking",
+ "http://example.com/*",
+ ],
+ content_scripts: [
+ {
+ js: ["contentscript.js"],
+ matches: ["http://example.com/test-page.html"],
+ run_at: "document_start",
+ },
+ ],
+ },
+ files: {
+ "contentscript.js": () => {
+ window.onload = () => {
+ browser.test.assertEq(
+ window.wrappedJSObject.errorListenerReady,
+ true,
+ "Got an onerror listener on the content page side"
+ );
+ browser.test.sendMessage("contentscript-attached");
+ };
+ // eslint-disable-next-line mozilla/balanced-listeners
+ window.addEventListener("message", evt => {
+ browser.test.fail(
+ `Webpage got notified on an exception raised from the content script: ${JSON.stringify(
+ evt.data
+ )}`
+ );
+ });
+ // eslint-disable-next-line mozilla/balanced-listeners
+ window.addEventListener("error", evt => {
+ const errorDetails = {
+ message: evt.message,
+ sourceName: evt.filename,
+ lineNumber: evt.lineno,
+ columnNumber: evt.colno,
+ errorIsDefined: !!evt.error,
+ };
+ browser.test.fail(
+ `Webpage got notified on an exception raised from the content script: ${JSON.stringify(
+ errorDetails
+ )}`
+ );
+ });
+ const throwAnError = () => {
+ throw new Error("test-contentscript-error");
+ };
+ browser.storage.sync.onChanged.addListener(() => {
+ throwAnError();
+ });
+
+ browser.storage.local.onChanged.addListener(() => {
+ throw undefined; // eslint-disable-line no-throw-literal
+ });
+ },
+ "extpage.html": `<!DOCTYPE html><script src="extpage.js"></script>`,
+ "extpage.js": () => {
+ // eslint-disable-next-line mozilla/balanced-listeners
+ window.addEventListener("error", evt => {
+ browser.test.log(
+ `Extension page got error event, error property set to: ${evt.error} :: ${evt.error?.stack}\n`
+ );
+ const errorDetails = {
+ message: evt.message,
+ sourceName: evt.filename,
+ lineNumber: evt.lineno,
+ columnNumber: evt.colno,
+ errorIsDefined: !!evt.error,
+ };
+
+ // Theoretically the exception thrown by a listener registered
+ // from an extension webpage should be emitting an error event
+ // (e.g. like for a DOM Event listener in a similar scenario),
+ // but we never emitted it and so it would be better to only emit
+ // it after have explicitly accepted the slightly change in behavior.
+ browser.test.log(
+ `extension page got notified on an exception raised from the API event listener: ${JSON.stringify(
+ errorDetails
+ )}`
+ );
+ });
+ browser.webRequest.onBeforeRequest.addListener(
+ () => {
+ throw new Error(`Mock webRequest listener exception`);
+ },
+ { urls: ["http://example.com/data/*"] },
+ ["blocking"]
+ );
+
+ // An object with a custom getter for the `message` property and a custom
+ // toString method, both are triggering a test failure to make sure we do
+ // catch with a failure if we are running the extension code as a side effect
+ // of logging the error to the console service.
+ const nonError = {
+ get message() {
+ browser.test.fail(`Unexpected extension code executed`);
+ },
+
+ toString() {
+ browser.test.fail(`Unexpected extension code executed`);
+ },
+ };
+ browser.storage.sync.onChanged.addListener(() => {
+ throw nonError;
+ });
+
+ // Throwing undefined or null is also allowed and so we cover that here as well
+ // to confirm we are not making any assumption about the value being raised to
+ // be always defined.
+ browser.storage.local.onChanged.addListener(() => {
+ throw undefined; // eslint-disable-line no-throw-literal
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+
+ const page = await ExtensionTestUtils.loadContentPage(
+ extension.extension.baseURI.resolve("extpage.html"),
+ { extension }
+ );
+
+ // Prepare to collect the error reported for the exception being triggered
+ // by the test itself.
+ const prepareWaitForConsoleMessage = () => {
+ this.content.waitForConsoleMessage = new Promise(resolve => {
+ const currInnerWindowID = this.content.windowGlobalChild?.innerWindowId;
+ const consoleListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener"]),
+ observe: message => {
+ if (
+ message instanceof Ci.nsIScriptError &&
+ message.innerWindowID === currInnerWindowID
+ ) {
+ resolve({
+ message: message.message,
+ category: message.category,
+ sourceName: message.sourceName,
+ hasStack: !!message.stack,
+ });
+ Services.console.unregisterListener(consoleListener);
+ }
+ },
+ };
+ Services.console.registerListener(consoleListener);
+ });
+ };
+
+ const notifyStorageSyncListener = extensionTestWrapper => {
+ // The notifyListeners method from ExtensionStorageSyncKinto does use
+ // the Extension class instance as the key for the storage.sync listeners
+ // map, whereas ExtensionStorageSync does use the extension id instead.
+ //
+ // TODO(Bug 1625257): remove this once the rust-based storage.sync backend has been enabled
+ // also on GeckoView build and the legacy Kinto-based backend has been ripped off.
+ let listenersMapKey = storageSyncKintoEnabled
+ ? extensionTestWrapper.extension
+ : extensionTestWrapper.id;
+ ok(
+ ExtensionParent.apiManager.global.extensionStorageSync.listeners.has(
+ listenersMapKey
+ ),
+ "Got a storage.sync onChanged listener for the test extension"
+ );
+ ExtensionParent.apiManager.global.extensionStorageSync.notifyListeners(
+ listenersMapKey,
+ {}
+ );
+ };
+
+ // Retrieve the message collected from the previously created promise.
+ const asyncAssertConsoleMessage = async ({
+ targetPage,
+ expectedErrorRegExp,
+ expectedSourceName,
+ shouldIncludeStack,
+ }) => {
+ const { message, category, sourceName, hasStack } = await targetPage.spawn(
+ [],
+ () => this.content.waitForConsoleMessage
+ );
+
+ ok(
+ expectedErrorRegExp.test(message),
+ `Got the expected error message: ${message}`
+ );
+
+ Assert.deepEqual(
+ { category, sourceName, hasStack },
+ {
+ category: "content javascript",
+ sourceName: expectedSourceName,
+ hasStack: shouldIncludeStack,
+ },
+ "Expected category and sourceName are set on the nsIScriptError"
+ );
+ };
+
+ {
+ info("Test exception raised by webRequest listener");
+ const expectedErrorRegExp = new RegExp(
+ `Error: Mock webRequest listener exception`
+ );
+ const expectedSourceName =
+ extension.extension.baseURI.resolve("extpage.js");
+ await page.spawn([], prepareWaitForConsoleMessage);
+ await ExtensionTestUtils.fetch(
+ "http://example.com",
+ "http://example.com/data/file_sample.html"
+ );
+ await asyncAssertConsoleMessage({
+ targetPage: page,
+ expectedErrorRegExp,
+ expectedSourceName,
+ // TODO(Bug 1810582): this should be expected to be true.
+ shouldIncludeStack: false,
+ });
+ }
+
+ {
+ info("Test exception raised by storage.sync listener");
+ // The listener has throw an object that isn't an Error instance and
+ // it also has a getter for the message property, we expect it to be
+ // logged using the string returned by the native toString method.
+ const expectedErrorRegExp = new RegExp(
+ `uncaught exception: \\[object Object\\]`
+ );
+ // TODO(Bug 1810582): this should be expected to be the script url
+ // where the exception has been originated from.
+ const expectedSourceName =
+ extension.extension.baseURI.resolve("extpage.html");
+
+ await page.spawn([], prepareWaitForConsoleMessage);
+ notifyStorageSyncListener(extension);
+ await asyncAssertConsoleMessage({
+ targetPage: page,
+ expectedErrorRegExp,
+ expectedSourceName,
+ // TODO(Bug 1810582): this should be expected to be true.
+ shouldIncludeStack: false,
+ });
+ }
+
+ {
+ info("Test exception raised by storage.local listener");
+ // The listener has throw an object that isn't an Error instance and
+ // it also has a getter for the message property, we expect it to be
+ // logged using the string returned by the native toString method.
+ const expectedErrorRegExp = new RegExp(`uncaught exception: undefined`);
+ // TODO(Bug 1810582): this should be expected to be the script url
+ // where the exception has been originated from.
+ const expectedSourceName =
+ extension.extension.baseURI.resolve("extpage.html");
+ await page.spawn([], prepareWaitForConsoleMessage);
+ ExtensionStorageIDB.notifyListeners(extension.id, {});
+ await asyncAssertConsoleMessage({
+ targetPage: page,
+ expectedErrorRegExp,
+ expectedSourceName,
+ // TODO(Bug 1810582): this should be expected to be true.
+ shouldIncludeStack: false,
+ });
+ }
+
+ await page.close();
+
+ info("Test content script API event listeners exception");
+
+ const contentPage = await ExtensionTestUtils.loadContentPage(
+ "http://example.com/test-page.html"
+ );
+
+ await extension.awaitMessage("contentscript-attached");
+
+ {
+ info("Test exception raised by content script storage.sync listener");
+ // The listener has throw an object that isn't an Error instance and
+ // it also has a getter for the message property, we expect it to be
+ // logged using the string returned by the native toString method.
+ const expectedErrorRegExp = new RegExp(`Error: test-contentscript-error`);
+ const expectedSourceName =
+ extension.extension.baseURI.resolve("contentscript.js");
+
+ await contentPage.spawn([], prepareWaitForConsoleMessage);
+ notifyStorageSyncListener(extension);
+ await asyncAssertConsoleMessage({
+ targetPage: contentPage,
+ expectedErrorRegExp,
+ expectedSourceName,
+ // TODO(Bug 1810582): this should be expected to be true.
+ shouldIncludeStack: false,
+ });
+ }
+
+ {
+ info("Test exception raised by content script storage.local listener");
+ // The listener has throw an object that isn't an Error instance and
+ // it also has a getter for the message property, we expect it to be
+ // logged using the string returned by the native toString method.
+ const expectedErrorRegExp = new RegExp(`uncaught exception: undefined`);
+ // TODO(Bug 1810582): this should be expected to be the script url
+ // where the exception has been originated from.
+ const expectedSourceName = extension.extension.baseURI.resolve("/");
+
+ await contentPage.spawn([], prepareWaitForConsoleMessage);
+ ExtensionStorageIDB.notifyListeners(extension.id, {});
+ await asyncAssertConsoleMessage({
+ targetPage: contentPage,
+ expectedErrorRegExp,
+ expectedSourceName,
+ // TODO(Bug 1810582): this should be expected to be true.
+ shouldIncludeStack: false,
+ });
+ }
+
+ await contentPage.close();
+
+ await extension.unload();
+});