forked from mirrors/gecko-dev
Bug 1634872 - Improve worker import tests. r=dom-worker-reviewers,smaug
Differential Revision: https://phabricator.services.mozilla.com/D84096
This commit is contained in:
parent
d65329ee39
commit
e48ed13952
6 changed files with 702 additions and 162 deletions
4
dom/workers/test/call_throws.js
Normal file
4
dom/workers/test/call_throws.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
function workerMethod() {
|
||||
console.log("workerMethod about to throw...");
|
||||
throw new Error("Method-Throw-Payload");
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -58,6 +59,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 +68,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",
|
||||
|
|
|
|||
22
dom/workers/test/redirect_with_query_args.sjs
Normal file
22
dom/workers/test/redirect_with_query_args.sjs
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -14,123 +14,560 @@
|
|||
|
||||
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 });
|
||||
},
|
||||
|
||||
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 });
|
||||
},
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
worker.postMessage({ url: sameOriginURL, test: 'eventListener', 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 via onerror!");
|
||||
next();
|
||||
};
|
||||
worker.onerror = function(event) {
|
||||
event.preventDefault();
|
||||
/**
|
||||
* - `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."
|
||||
}
|
||||
|
||||
worker.postMessage({ url: sameOriginURL, test: 'onerror', nested: false });
|
||||
},
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
worker.postMessage({ url: sameOriginURL, test: 'none', nested: false });
|
||||
{
|
||||
// What happens if the script is a 404?
|
||||
// This test case primarily exists to document what we expect to happen in
|
||||
// this case.
|
||||
name: 'Nonexistent script',
|
||||
script: 'script_does_not_exist.js',
|
||||
sameOrigin: {
|
||||
exceptionName: 'NetworkError',
|
||||
thrownFile: 'importing-worker-script',
|
||||
isDOMException: true,
|
||||
code: DOMException.NETWORK_ERR,
|
||||
message: x => `WorkerGlobalScope.importScripts: Failed to load worker script at ${x.importUrl} (nsresult = 0x80530013)`,
|
||||
},
|
||||
crossOrigin: {
|
||||
exceptionName: 'NetworkError',
|
||||
thrownFile: 'importing-worker-script',
|
||||
isDOMException: true,
|
||||
code: DOMException.NETWORK_ERR,
|
||||
message: x => `WorkerGlobalScope.importScripts: Failed to load worker script at ${x.importUrl} (nsresult = 0x80530013)`,
|
||||
}
|
||||
},
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
worker.postMessage({ url: sameOriginURL, test: 'none', nested: false });
|
||||
{
|
||||
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."
|
||||
}
|
||||
},
|
||||
|
||||
function() {
|
||||
var worker = new Worker("importScripts_3rdParty_worker.js");
|
||||
worker.onerror = function(event) {
|
||||
ok(false, "No error should be received!");
|
||||
};
|
||||
|
||||
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 });
|
||||
{
|
||||
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 url = URL.createObjectURL(new Blob(["%&%^&%^"]));
|
||||
var worker = new Worker(url);
|
||||
worker.onerror = function(event) {
|
||||
event.preventDefault();
|
||||
ok(event instanceof Event, "Event received.");
|
||||
next();
|
||||
};
|
||||
/**
|
||||
* 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 next() {
|
||||
if (!tests.length) {
|
||||
SimpleTest.finish();
|
||||
return;
|
||||
// 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;
|
||||
}
|
||||
|
||||
var test = tests.shift();
|
||||
test();
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
next();
|
||||
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}`);
|
||||
|
||||
// Add some whitespace in our output.
|
||||
info("");
|
||||
}
|
||||
|
||||
function checkErrorEvent(label, expectation, context, event, viaTask=false) {
|
||||
info(`## Checking error event: ${JSON.stringify(event)}`);
|
||||
|
||||
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(event.filename, expectedFile,
|
||||
`${label}: Filename from ${expectation.thrownFile} is ${expectedFile}`);
|
||||
|
||||
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}`;
|
||||
|
||||
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) {
|
||||
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.");
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
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>
|
||||
|
|
|
|||
1
dom/workers/test/toplevel_throws.js
Normal file
1
dom/workers/test/toplevel_throws.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
throw new Error("Toplevel-Throw-Payload");
|
||||
Loading…
Reference in a new issue