diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
commit | d8bbc7858622b6d9c278469aab701ca0b609cddf (patch) | |
tree | eff41dc61d9f714852212739e6b3738b82a2af87 /dom/workers/test | |
parent | Releasing progress-linux version 125.0.3-1~progress7.99u1. (diff) | |
download | firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip |
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/workers/test')
-rw-r--r-- | dom/workers/test/call_throws.js | 4 | ||||
-rw-r--r-- | dom/workers/test/chrome.toml | 4 | ||||
-rw-r--r-- | dom/workers/test/crashtests/1858809.html | 14 | ||||
-rw-r--r-- | dom/workers/test/crashtests/crashtests.list | 1 | ||||
-rw-r--r-- | dom/workers/test/importScripts_3rdParty_worker.js | 199 | ||||
-rw-r--r-- | dom/workers/test/mime_type_is_csv.js | 1 | ||||
-rw-r--r-- | dom/workers/test/mime_type_is_csv.js^headers^ | 1 | ||||
-rw-r--r-- | dom/workers/test/mochitest.toml | 5 | ||||
-rw-r--r-- | dom/workers/test/redirect_with_query_args.sjs | 22 | ||||
-rw-r--r-- | dom/workers/test/test_importScripts_3rdparty.html | 633 | ||||
-rw-r--r-- | dom/workers/test/test_worker_interfaces.js | 2 | ||||
-rw-r--r-- | dom/workers/test/toplevel_throws.js | 1 |
12 files changed, 730 insertions, 157 deletions
diff --git a/dom/workers/test/call_throws.js b/dom/workers/test/call_throws.js new file mode 100644 index 0000000000..dfde155961 --- /dev/null +++ b/dom/workers/test/call_throws.js @@ -0,0 +1,4 @@ +function workerMethod() { + console.log("workerMethod about to throw..."); + throw new Error("Method-Throw-Payload"); +} diff --git a/dom/workers/test/chrome.toml b/dom/workers/test/chrome.toml index 0b2d68da39..239394d2ea 100644 --- a/dom/workers/test/chrome.toml +++ b/dom/workers/test/chrome.toml @@ -89,10 +89,6 @@ support-files = [ ["test_chromeWorkerJSM.xhtml"] ["test_file.xhtml"] -skip-if = [ - "os == 'linux' && bits == 64 && debug", # Bug 1765445 - "apple_catalina && !debug", # Bug 1765445 -] ["test_fileBlobPosting.xhtml"] diff --git a/dom/workers/test/crashtests/1858809.html b/dom/workers/test/crashtests/1858809.html new file mode 100644 index 0000000000..3a5190c300 --- /dev/null +++ b/dom/workers/test/crashtests/1858809.html @@ -0,0 +1,14 @@ +<!DOCTYPE> +<html> +<head> +<meta charset="UTF-8"> +<script> +document.addEventListener("DOMContentLoaded", () => { + const a = new Worker("", {}) + const b = new Blob([""], {}) + a.terminate() + new RTCRtpScriptTransform(a, b, [{}]) +}) +</script> +</head> +</html> diff --git a/dom/workers/test/crashtests/crashtests.list b/dom/workers/test/crashtests/crashtests.list index 528f4c8a10..26a1fbbf80 100644 --- a/dom/workers/test/crashtests/crashtests.list +++ b/dom/workers/test/crashtests/crashtests.list @@ -6,3 +6,4 @@ load 1228456.html load 1348882.html load 1821066.html load 1819146.html +load 1858809.html diff --git a/dom/workers/test/importScripts_3rdParty_worker.js b/dom/workers/test/importScripts_3rdParty_worker.js index 326d48f77a..e55fdc514b 100644 --- a/dom/workers/test/importScripts_3rdParty_worker.js +++ b/dom/workers/test/importScripts_3rdParty_worker.js @@ -1,18 +1,113 @@ const workerURL = "http://mochi.test:8888/tests/dom/workers/test/importScripts_3rdParty_worker.js"; +/** + * An Error can be a JS Error or a DOMException. The primary difference is that + * JS Errors have a SpiderMonkey specific `fileName` for the filename and + * DOMEXCEPTION uses `filename`. + */ +function normalizeError(err) { + if (!err) { + return null; + } + + const isDOMException = "filename" in err; + + return { + message: err.message, + name: err.name, + isDOMException, + code: err.code, + // normalize to fileName + fileName: isDOMException ? err.filename : err.fileName, + hasFileName: !!err.fileName, + hasFilename: !!err.filename, + lineNumber: err.lineNumber, + columnNumber: err.columnNumber, + stack: err.stack, + stringified: err.toString(), + }; +} + +function normalizeErrorEvent(event) { + if (!event) { + return null; + } + + return { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + error: normalizeError(event.error), + stringified: event.toString(), + }; +} + +/** + * Normalize the `OnErrorEventHandlerNonNull onerror` invocation. The + * special handling in JSEventHandler::HandleEvent ends up spreading out the + * contents of the ErrorEvent into discrete arguments. The one thing lost is + * we can't toString the ScriptEvent itself, but this should be the same as the + * message anyways. + * + * The spec for the invocation is the "special error event handling" logic + * described in step 4 at: + * https://html.spec.whatwg.org/multipage/webappapis.html#the-event-handler-processing-algorithm + * noting that the step somewhat glosses over that it's only "onerror" that is + * OnErrorEventHandlerNonNull and capable of processing 5 arguments and that's + * why an addEventListener "error" listener doesn't get this handling. + * + * Argument names here are made to match the call-site in JSEventHandler. + */ +function normalizeOnError( + msgOrEvent, + fileName, + lineNumber, + columnNumber, + error +) { + return { + message: msgOrEvent, + filename: fileName, + lineno: lineNumber, + colno: columnNumber, + error: normalizeError(error), + stringified: null, + }; +} + +/** + * Helper to postMessage the provided data after a setTimeout(0) so that any + * error event currently being dispatched that will bubble to our parent will + * be delivered before our postMessage. + */ +function delayedPostMessage(data) { + setTimeout(() => { + postMessage(data); + }, 0); +} + onmessage = function (a) { + const args = a.data; + // Messages are either nested (forward to a nested worker) or should be + // processed locally. if (a.data.nested) { - var worker = new Worker(workerURL); + const worker = new Worker(workerURL); + let firstErrorEvent; + + // When the test mode is "catch" + worker.onmessage = function (event) { - postMessage(event.data); + delayedPostMessage({ + nestedMessage: event.data, + errorEvent: firstErrorEvent, + }); }; worker.onerror = function (event) { + firstErrorEvent = normalizeErrorEvent(event); event.preventDefault(); - postMessage({ - error: event instanceof ErrorEvent && event.filename == workerURL, - }); }; a.data.nested = false; @@ -20,69 +115,47 @@ onmessage = function (a) { return; } - // This first URL will use the same origin of this script. - var sameOriginURL = new URL(a.data.url); - var fileName1 = 42; - - // This is cross-origin URL. - var crossOriginURL = new URL(a.data.url); - crossOriginURL.host = "example.com"; - crossOriginURL.port = 80; - var fileName2 = 42; - - if (a.data.test == "none") { - importScripts(crossOriginURL.href); - return; - } - - try { - importScripts(sameOriginURL.href); - } catch (e) { - if (!(e instanceof SyntaxError)) { - postMessage({ result: false }); - return; - } - - fileName1 = e.fileName; - } - - if (fileName1 != sameOriginURL.href || !fileName1) { - postMessage({ result: false }); - return; - } - - if (a.data.test == "try") { - var exception; + // Local test. + if (a.data.mode === "catch") { try { - importScripts(crossOriginURL.href); - } catch (e) { - fileName2 = e.filename; - exception = e; + importScripts(a.data.url); + workerMethod(); + } catch (ex) { + delayedPostMessage({ + args, + error: normalizeError(ex), + }); } - - postMessage({ - result: - fileName2 == workerURL && - exception.name == "NetworkError" && - exception.code == DOMException.NETWORK_ERR, + } else if (a.data.mode === "uncaught") { + const onerrorPromise = new Promise(resolve => { + self.onerror = (...onerrorArgs) => { + resolve(normalizeOnError(...onerrorArgs)); + }; }); - return; - } - - if (a.data.test == "eventListener") { - addEventListener("error", function (event) { - event.preventDefault(); - postMessage({ - result: event instanceof ErrorEvent && event.filename == workerURL, + const listenerPromise = new Promise(resolve => { + self.addEventListener("error", evt => { + resolve(normalizeErrorEvent(evt)); }); }); - } - if (a.data.test == "onerror") { - onerror = function (...args) { - postMessage({ result: args[1] == workerURL }); - }; - } + Promise.all([onerrorPromise, listenerPromise]).then( + ([onerrorEvent, listenerEvent]) => { + delayedPostMessage({ + args, + onerrorEvent, + listenerEvent, + }); + } + ); - importScripts(crossOriginURL.href); + importScripts(a.data.url); + workerMethod(); + // we will have thrown by this point, which will trigger an "error" event + // on our global and then will propagate to our parent (which could be a + // window or a worker, if nested). + // + // To avoid hangs, throw a different error here that will fail equivalence + // tests. + throw new Error("We expected an error and this is a failsafe for hangs."); + } }; diff --git a/dom/workers/test/mime_type_is_csv.js b/dom/workers/test/mime_type_is_csv.js new file mode 100644 index 0000000000..54d3b70689 --- /dev/null +++ b/dom/workers/test/mime_type_is_csv.js @@ -0,0 +1 @@ +throw new Error("This file has a CSV mime type and should not load."); diff --git a/dom/workers/test/mime_type_is_csv.js^headers^ b/dom/workers/test/mime_type_is_csv.js^headers^ new file mode 100644 index 0000000000..0d848b02c2 --- /dev/null +++ b/dom/workers/test/mime_type_is_csv.js^headers^ @@ -0,0 +1 @@ +Content-Type: text/csv diff --git a/dom/workers/test/mochitest.toml b/dom/workers/test/mochitest.toml index 5ae8094b58..a32dbf3bf6 100644 --- a/dom/workers/test/mochitest.toml +++ b/dom/workers/test/mochitest.toml @@ -12,6 +12,7 @@ support-files = [ "bug998474_worker.js", "bug1063538_worker.js", "bug1063538.sjs", + "call_throws.js", "clearTimeouts_worker.js", "clearTimeoutsImplicit_worker.js", "content_worker.js", @@ -42,6 +43,8 @@ support-files = [ "loadEncoding_worker.js", "location_worker.js", "longThread_worker.js", + "mime_type_is_csv.js", + "mime_type_is_csv.js^headers^", "multi_sharedWorker_frame.html", "multi_sharedWorker_sharedWorker.js", "navigator_languages_worker.js", @@ -58,6 +61,7 @@ support-files = [ "recursion_worker.js", "recursiveOnerror_worker.js", "redirect_to_foreign.sjs", + "redirect_with_query_args.sjs", "rvals_worker.js", "sharedWorker_sharedWorker.js", "simpleThread_worker.js", @@ -66,6 +70,7 @@ support-files = [ "terminate_worker.js", "test_csp.html^headers^", "test_csp.js", + "toplevel_throws.js", "referrer_worker.html", "sourcemap_header_iframe.html", "sourcemap_header_worker.js", diff --git a/dom/workers/test/redirect_with_query_args.sjs b/dom/workers/test/redirect_with_query_args.sjs new file mode 100644 index 0000000000..3359367ee0 --- /dev/null +++ b/dom/workers/test/redirect_with_query_args.sjs @@ -0,0 +1,22 @@ +/** + * This file expects a query string that's the upper-cased version of a file to + * be redirected to in the same directory. The redirect will also include + * added "secret data" as a query string. + * + * So if the request is `/path/redirect_with_query_args.sjs?FOO.JS` the redirect + * will be to `/path/foo.js?SECRET_DATA`. + **/ + +function handleRequest(request, response) { + // The secret data to include in the redirect to make the redirect URL + // easily detectable. + const secretData = "SECRET_DATA"; + + let pathBase = request.path.split("/").slice(0, -1).join("/"); + let targetFile = request.queryString.toLowerCase(); + let newUrl = `${pathBase}/${targetFile}?${secretData}`; + + response.setStatusLine(request.httpVersion, 302, "Found"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Location", newUrl, false); +} diff --git a/dom/workers/test/test_importScripts_3rdparty.html b/dom/workers/test/test_importScripts_3rdparty.html index 7f10f23faf..b1e4913eea 100644 --- a/dom/workers/test/test_importScripts_3rdparty.html +++ b/dom/workers/test/test_importScripts_3rdparty.html @@ -14,123 +14,576 @@ const workerURL = 'http://mochi.test:8888/tests/dom/workers/test/importScripts_3rdParty_worker.js'; -const sameOriginURL = 'http://mochi.test:8888/tests/dom/workers/test/invalid.js' +const sameOriginBaseURL = 'http://mochi.test:8888/tests/dom/workers/test'; +const crossOriginBaseURL = "https://example.com/tests/dom/workers/test"; -var tests = [ - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data!"); - next(); - }; +const workerRelativeUrl = 'importScripts_3rdParty_worker.js'; +const workerAbsoluteUrl = `${sameOriginBaseURL}/${workerRelativeUrl}` - worker.postMessage({ url: sameOriginURL, test: 'try', nested: false }); - }, +/** + * This file tests cross-origin error muting in importScripts for workers. In + * particular, we want to test: + * - The errors thrown by the parsing phase of importScripts(). + * - The errors thrown by the top-level evaluation phase of importScripts(). + * - If the error is reported to the parent's Worker binding, including through + * nested workers, as well as the contents of the error. + * - For errors: + * - What type of exception is reported? + * - What fileName is reported on the exception? + * - What are the contents of the stack on the exception? + * + * Relevant specs: + * - https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-worker-imported-script + * - https://html.spec.whatwg.org/multipage/webappapis.html#creating-a-classic-script + * + * The situation and motivation for error muting is: + * - JS scripts are allowed to be loaded cross-origin without CORS for legacy + * reasons. If a script is cross-origin, its "muted errors" is set to true. + * - The fetch will set the "use-URL-credentials" flag + * https://fetch.spec.whatwg.org/#concept-request-use-url-credentials-flag + * but will have the default "credentials" mode of "omit" + * https://fetch.spec.whatwg.org/#concept-request-credentials-mode which + * means that username/password will be propagated. + * - For legacy reasons, JS scripts aren't required to have an explicit JS MIME + * type which allows attacks that attempt to load a known-non JS file as JS + * in order to derive information from the errors or from side-effects to the + * global for code that does parse and evaluate as legal JS. + **/ - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data in nested workers!"); - next(); - }; - worker.postMessage({ url: sameOriginURL, test: 'try', nested: true }); + /** + * - `sameOrigin`: Describes the exception we expect to see for a same-origin + * import. + * - `crossOrigin`: Describes the exception we expect to see for a cross-origin + * import (from example.com while the worker is the mochitest origin). + * + * The exception fields are: + * - `exceptionName`: The `name` of the Error object. + * - `thrownFile`: Describes the filename we expect to see on the error: + * - `importing-worker-script`: The worker script that's doing the importing + * will be the source of the exception, not the imported script. + * - `imported-script-no-redirect`: The (absolute-ified) script as passed to + * importScript(s), regardless of any redirects that occur. + * - `post-redirect-imported-script`: The name of the actual URL that was + * loaded following any redirects. + */ +const scriptPermutations = [ + { + name: 'Invalid script that generates a syntax error', + script: 'invalid.js', + sameOrigin: { + exceptionName: 'SyntaxError', + thrownFile: 'post-redirect-imported-script', + isDOMException: false, + message: "expected expression, got end of script" + }, + crossOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + } }, + { + name: 'Non-JS MIME Type', + script: 'mime_type_is_csv.js', + sameOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + }, + crossOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + } + }, + { + // What happens if the script is a 404? + name: 'Nonexistent script', + script: 'script_does_not_exist.js', + sameOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + }, + crossOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + } + }, + { + name: 'Script that throws during toplevel execution', + script: 'toplevel_throws.js', + sameOrigin: { + exceptionName: 'Error', + thrownFile: 'post-redirect-imported-script', + isDOMException: false, + message: "Toplevel-Throw-Payload", + }, + crossOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + } + }, + { + name: 'Script that exposes a method that throws', + script: 'call_throws.js', + sameOrigin: { + exceptionName: 'Error', + thrownFile: 'post-redirect-imported-script', + isDOMException: false, + message: "Method-Throw-Payload" + }, + crossOrigin: { + exceptionName: 'Error', + thrownFile: 'imported-script-no-redirect', + isDOMException: false, + message: "Method-Throw-Payload" + } + }, +]; - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data via eventListener!"); - next(); - }; - - worker.postMessage({ url: sameOriginURL, test: 'eventListener', nested: false }); +/** + * Special fields: + * - `transformScriptImport`: A function that takes the script name as input and + * produces the actual path to use for import purposes, allowing the addition + * of a redirect. + * - `expectedURLAfterRedirect`: A function that takes the script name as + * input and produces the expected script name post-redirect (if there is a + * redirect). In particular, our `redirect_with_query_args.sjs` helper will + * perform a same-origin redirect and append "?SECRET_DATA" onto the end of + * the redirected URL at this time. + * - `partOfTheURLToNotExposeToJS`: A string snippet that is present in the + * post-redirect contents that should absolutely not show up in the error's + * stack if the redirect isn't exposed. This is a secondary check to the + * result of expectedURLAfterRedirect. + */ +const urlPermutations = [ + { + name: 'No Redirect', + transformScriptImport: x => x, + expectedURLAfterRedirect: x => x, + // No redirect means nothing to be paranoid about. + partOfTheURLToNotExposeToJS: null, }, + { + name: 'Same-Origin Redirect With Query Args', + // We mangle the script into uppercase and the redirector undoes this in + // order to minimize the similarity of the pre-redirect and post-redirect + // strings. + transformScriptImport: x => `redirect_with_query_args.sjs?${x.toUpperCase()}`, + expectedURLAfterRedirect: x => `${x}?SECRET_DATA`, + // The redirect will add this when it formulates the redirected URL, and the + // test wants to make sure this doesn't show up in filenames or stacks + // unless the thrownFile is set to 'post-redirect-imported-script'. + partOfTheURLToNotExposeToJS: 'SECRET_DATA', + } +]; +const nestedPermutations = [ + { + name: 'Window Parent', + nested: false, + }, + { + name: 'Worker Parent', + nested: true, + } +]; - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data in nested workers via eventListener!"); - next(); - }; + // NOTE: These implementations are copied from importScripts_3rdParty_worker.js + // for reasons of minimizing the number of calls to importScripts for + // debugging. + function normalizeError(err) { + if (!err) { + return null; + } - worker.postMessage({ url: sameOriginURL, test: 'eventListener', nested: true }); - }, + const isDOMException = "filename" in err; - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data via onerror!"); - next(); - }; - worker.onerror = function(event) { - event.preventDefault(); + return { + message: err.message, + name: err.name, + isDOMException, + code: err.code, + // normalize to fileName + fileName: isDOMException ? err.filename : err.fileName, + hasFileName: !!err.fileName, + hasFilename: !!err.filename, + lineNumber: err.lineNumber, + columnNumber: err.columnNumber, + stack: err.stack, + stringified: err.toString(), + }; +} + +function normalizeErrorEvent(event) { + if (!event) { + return null; + } + + return { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + error: normalizeError(event.error), + stringified: event.toString(), + }; +} +// End duplicated code. + + +/** + * Validate the received error against our expectations and provided context. + * + * For `expectation`, see the `scriptPermutations` doc-block which documents + * its `sameOrigin` and `crossOrigin` properties which are what we expect here. + * + * The `context` should include: + * - `workerUrl`: The absolute URL of the toplevel worker script that the worker + * is running which is the code that calls `importScripts`. + * - `importUrl`: The absolute URL provided to the call to `importScripts`. + * This is the pre-redirect URL if a redirect is involved. + * - `postRedirectUrl`: The same as `importUrl` unless a redirect is involved, + * in which case this will be a different URL. + * - `isRedirected`: Boolean indicating whether a redirect was involved. This + * is a convenience variable that's derived from the above 2 URL's for now. + * - `shouldNotInclude`: Provided by the URL permutation, this is used to check + * that post-redirect data does not creep into the exception unless the + * expected `thrownFile` is `post-redirect-imported-script`. + */ +function checkError(label, expectation, context, err) { + info(`## Checking error: ${JSON.stringify(err)}`); + is(err.name, expectation.exceptionName, + `${label}: Error name matches "${expectation.exceptionName}"?`); + is(err.isDOMException, expectation.isDOMException, + `${label}: Is a DOM Exception == ${expectation.isDOMException}?`); + if (expectation.code) { + is(err.code, expectation.code, + `${label}: Code matches ${expectation.code}?`); + } + + let expectedFile; + switch (expectation.thrownFile) { + case 'importing-worker-script': + expectedFile = context.workerUrl; + break; + case 'imported-script-no-redirect': + expectedFile = context.importUrl; + break; + case 'post-redirect-imported-script': + expectedFile = context.postRedirectUrl; + break; + default: + ok(false, `Unexpected thrownFile parameter: ${expectation.thrownFile}`); + return; + } + + is(err.fileName, expectedFile, + `${label}: Filename from ${expectation.thrownFile} is ${expectedFile}`); + + + let expMessage = expectation.message; + if (typeof(expMessage) === "function") { + expMessage = expectation.message(context); + } + is(err.message, expMessage, + `${label}: Message is ${expMessage}`); + + // If this is a redirect and we expect the error to not be surfacing any + // post-redirect information and there's a `shouldNotInclude` string, then + // check to make sure it's not present. + if (context.isRedirected && context.shouldNotInclude) { + if (expectation.thrownFile !== 'post-redirect-imported-script') { + ok(!err.stack.includes(context.shouldNotInclude), + `${label}: Stack should not include ${context.shouldNotInclude}:\n${err.stack}`); + ok(!err.stringified.includes(context.shouldNotInclude), + `${label}: Stringified error should not include ${context.shouldNotInclude}:\n${err.stringified}`); + } else if (expectation.exceptionName !== 'SyntaxError') { + // We do expect the shouldNotInclude to be present for + // 'post-redirect-imported-script' as long as the exception isn't a + // SyntaxError. SyntaxError stacks inherently do not include the filename + // of the file with the syntax problem as a stack frame. + ok(err.stack.includes(context.shouldNotInclude), + `${label}: Stack should include ${context.shouldNotInclude}:\n${err.stack}`); } + } + let expStringified = `${err.name}: ${expMessage}`; + is(err.stringified, expStringified, + `${label}: Stringified error should be: ${expStringified}`); - worker.postMessage({ url: sameOriginURL, test: 'onerror', nested: false }); - }, + // Add some whitespace in our output. + info(""); +} - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onerror = function(event) { - event.preventDefault(); - ok(event instanceof ErrorEvent, "ErrorEvent received."); - is(event.filename, workerURL, "ErrorEvent.filename is correct"); - next(); - }; +function checkErrorEvent(label, expectation, context, event, viaTask=false) { + info(`## Checking error event: ${JSON.stringify(event)}`); - worker.postMessage({ url: sameOriginURL, test: 'none', nested: false }); - }, + let expectedFile; + switch (expectation.thrownFile) { + case 'importing-worker-script': + expectedFile = context.workerUrl; + break; + case 'imported-script-no-redirect': + expectedFile = context.importUrl; + break; + case 'post-redirect-imported-script': + expectedFile = context.postRedirectUrl; + break; + default: + ok(false, `Unexpected thrownFile parameter: ${expectation.thrownFile}`); + return; + } - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.addEventListener("error", function(event) { - event.preventDefault(); - ok(event instanceof ErrorEvent, "ErrorEvent received."); - is(event.filename, workerURL, "ErrorEvent.filename is correct"); - next(); - }); + is(event.filename, expectedFile, + `${label}: Filename from ${expectation.thrownFile} is ${expectedFile}`); - worker.postMessage({ url: sameOriginURL, test: 'none', nested: false }); - }, + let expMessage = expectation.message; + if (typeof(expMessage) === "function") { + expMessage = expectation.message(context); + } + // The error event message prepends the exception name to the Error's message. + expMessage = `${expectation.exceptionName}: ${expMessage}`; - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onerror = function(event) { - ok(false, "No error should be received!"); - }; + is(event.message, expMessage, + `${label}: Message is ${expMessage}`); + + // If this is a redirect and we expect the error to not be surfacing any + // post-redirect information and there's a `shouldNotInclude` string, then + // check to make sure it's not present. + // + // Note that `stringified` may not be present for the "onerror" case. + if (context.isRedirected && + expectation.thrownFile !== 'post-redirect-imported-script' && + context.shouldNotInclude && + event.stringified) { + ok(!event.stringified.includes(context.shouldNotInclude), + `${label}: Stringified error should not include ${context.shouldNotInclude}:\n${event.stringified}`); + } + if (event.stringified) { + is(event.stringified, "[object ErrorEvent]", + `${label}: Stringified event should be "[object ErrorEvent]"`); + } + + // If we received the error via a task queued because it was not handled in + // the worker, then per + // https://html.spec.whatwg.org/multipage/workers.html#runtime-script-errors-2 + // the error will be null. + if (viaTask) { + is(event.error, null, + `${label}: Error is null because it came from an HTML 10.2.5 task.`); + } else { + checkError(label, expectation, context, event.error); + } +} + +/** + * Helper to spawn a worker, postMessage it the given args, and return the + * worker's response payload and the first "error" received on the Worker + * binding by the time the message handler resolves. The worker logic makes + * sure to delay its postMessage using setTimeout(0) so error events will always + * arrive before any message that is sent. + * + * If args includes a truthy `nested` value, then the `message` and + * `bindingErrorEvent` are as perceived by the parent worker. + */ +function asyncWorkerImport(args) { + const worker = new Worker(workerRelativeUrl); + const promise = new Promise((resolve, reject) => { + // The first "error" received on the Worker binding. + let firstErrorEvent = null; worker.onmessage = function(event) { - ok("error" in event.data && event.data.error, "The error has been fully received from a nested worker"); - next(); - }; - worker.postMessage({ url: sameOriginURL, test: 'none', nested: true }); - }, + let message = event.data; + // For the nested case, unwrap and normalize things. + if (args.nested) { + firstErrorEvent = message.errorEvent; + message = message.nestedMessage; + // We need to re-set the argument to be nested because it was set to + // false so that only a single level of nesting occurred. + message.args.nested = true; + } + + // Make sure the args we receive from the worker are the same as the ones + // we sent. + is(JSON.stringify(message.args), JSON.stringify(args), + "Worker re-transmitted args match sent args."); - function() { - var url = URL.createObjectURL(new Blob(["%&%^&%^"])); - var worker = new Worker(url); + resolve({ + message, + bindingErrorEvent: firstErrorEvent + }); + worker.terminate(); + }; worker.onerror = function(event) { + // We don't want this to bubble to the window and cause a test failure. event.preventDefault(); - ok(event instanceof Event, "Event received."); - next(); - }; - } -]; -function next() { - if (!tests.length) { - SimpleTest.finish(); - return; - } + if (firstErrorEvent) { + ok(false, "Worker binding received more than one error"); + reject(new Error("multiple error events received")); + return; + } + firstErrorEvent = normalizeErrorEvent(event); + } + }); + info("Sending args to worker: " + JSON.stringify(args)); + worker.postMessage(args); - var test = tests.shift(); - test(); + return promise; } -SimpleTest.waitForExplicitFinish(); -next(); +function makeTestPermutations() { + for (const urlPerm of urlPermutations) { + for (const scriptPerm of scriptPermutations) { + for (const nestedPerm of nestedPermutations) { + const testName = + `${nestedPerm.name}: ${urlPerm.name}: ${scriptPerm.name}`; + const caseFunc = async () => { + // Make the test name much more obvious when viewing logs. + info(`#############################################################`); + info(`### ${testName}`); + let result, errorEvent; + + const scriptName = urlPerm.transformScriptImport(scriptPerm.script); + const redirectedUrl = urlPerm.expectedURLAfterRedirect(scriptPerm.script); + + // ### Same-Origin Import + // ## What does the error look like when caught? + ({ message, bindingErrorEvent } = await asyncWorkerImport( + { + url: `${sameOriginBaseURL}/${scriptName}`, + mode: "catch", + nested: nestedPerm.nested, + })); + + const sameOriginContext = { + workerUrl: workerAbsoluteUrl, + importUrl: message.args.url, + postRedirectUrl: `${sameOriginBaseURL}/${redirectedUrl}`, + isRedirected: message.args.url !== redirectedUrl, + shouldNotInclude: urlPerm.partOfTheURLToNotExposeToJS, + }; + checkError( + `${testName}: Same-Origin Thrown`, + scriptPerm.sameOrigin, + sameOriginContext, + message.error); + + // ## What does the error events look like when not caught? + ({ message, bindingErrorEvent } = await asyncWorkerImport( + { + url: `${sameOriginBaseURL}/${scriptName}`, + mode: "uncaught", + nested: nestedPerm.nested, + })); + + // The worker will have captured the error event twice, once via + // onerror and once via an "error" event listener. It will have not + // invoked preventDefault(), so the worker's parent will also have + // received a copy of the error event as well. + checkErrorEvent( + `${testName}: Same-Origin Worker global onerror handler`, + scriptPerm.sameOrigin, + sameOriginContext, + message.onerrorEvent); + checkErrorEvent( + `${testName}: Same-Origin Worker global error listener`, + scriptPerm.sameOrigin, + sameOriginContext, + message.listenerEvent); + // Binding events + checkErrorEvent( + `${testName}: Same-Origin Parent binding onerror`, + scriptPerm.sameOrigin, + sameOriginContext, + bindingErrorEvent, "via-task"); + + // ### Cross-Origin Import + // ## What does the error look like when caught? + ({ message, bindingErrorEvent } = await asyncWorkerImport( + { + url: `${crossOriginBaseURL}/${scriptName}`, + mode: "catch", + nested: nestedPerm.nested, + })); + + const crossOriginContext = { + workerUrl: workerAbsoluteUrl, + importUrl: message.args.url, + postRedirectUrl: `${crossOriginBaseURL}/${redirectedUrl}`, + isRedirected: message.args.url !== redirectedUrl, + shouldNotInclude: urlPerm.partOfTheURLToNotExposeToJS, + }; + + checkError( + `${testName}: Cross-Origin Thrown`, + scriptPerm.crossOrigin, + crossOriginContext, + message.error); + + // ## What does the error events look like when not caught? + ({ message, bindingErrorEvent } = await asyncWorkerImport( + { + url: `${crossOriginBaseURL}/${scriptName}`, + mode: "uncaught", + nested: nestedPerm.nested, + })); + + // The worker will have captured the error event twice, once via + // onerror and once via an "error" event listener. It will have not + // invoked preventDefault(), so the worker's parent will also have + // received a copy of the error event as well. + checkErrorEvent( + `${testName}: Cross-Origin Worker global onerror handler`, + scriptPerm.crossOrigin, + crossOriginContext, + message.onerrorEvent); + checkErrorEvent( + `${testName}: Cross-Origin Worker global error listener`, + scriptPerm.crossOrigin, + crossOriginContext, + message.listenerEvent); + // Binding events + checkErrorEvent( + `${testName}: Cross-Origin Parent binding onerror`, + scriptPerm.crossOrigin, + crossOriginContext, + bindingErrorEvent, "via-task"); + }; + + // The mochitest framework uses the name of the caseFunc, which by default + // will be inferred and set on the configurable `name` property. It's not + // writable though, so we need to clobber the property. Devtools will + // xray through this name but this works for the test framework. + Object.defineProperty( + caseFunc, + 'name', + { + value: testName, + writable: false + }); + add_task(caseFunc); + } + } + } +} +makeTestPermutations(); </script> </body> </html> diff --git a/dom/workers/test/test_worker_interfaces.js b/dom/workers/test/test_worker_interfaces.js index efd108f85c..c53c0b2b0f 100644 --- a/dom/workers/test/test_worker_interfaces.js +++ b/dom/workers/test/test_worker_interfaces.js @@ -138,6 +138,8 @@ let interfaceNamesInGlobalScope = [ // IMPORTANT: Do not change this list without review from a DOM peer! { name: "AudioDecoder", nightly: true }, // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "AudioEncoder", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! { name: "Blob", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "BroadcastChannel", insecureContext: true }, diff --git a/dom/workers/test/toplevel_throws.js b/dom/workers/test/toplevel_throws.js new file mode 100644 index 0000000000..3efe29d5af --- /dev/null +++ b/dom/workers/test/toplevel_throws.js @@ -0,0 +1 @@ +throw new Error("Toplevel-Throw-Payload"); |