271 lines
8.1 KiB
JavaScript
271 lines
8.1 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
/* exported attachConsole, attachConsoleToTab, attachConsoleToWorker,
|
|
closeDebugger, checkConsoleAPICalls, checkRawHeaders, runTests, nextTest, Ci, Cc,
|
|
withActiveServiceWorker, Services, consoleAPICall, createCommandsForTab, FRACTIONAL_NUMBER_REGEX, DevToolsServer */
|
|
|
|
const { require } = ChromeUtils.importESModule(
|
|
"resource://devtools/shared/loader/Loader.sys.mjs"
|
|
);
|
|
const {
|
|
DevToolsServer,
|
|
} = require("resource://devtools/server/devtools-server.js");
|
|
const {
|
|
CommandsFactory,
|
|
} = require("resource://devtools/shared/commands/commands-factory.js");
|
|
|
|
// timeStamp are the result of a number in microsecond divided by 1000.
|
|
// so we can't expect a precise number of decimals, or even if there would
|
|
// be decimals at all.
|
|
const FRACTIONAL_NUMBER_REGEX = /^\d+(\.\d{1,3})?$/;
|
|
|
|
function attachConsole(listeners) {
|
|
return _attachConsole(listeners);
|
|
}
|
|
function attachConsoleToTab(listeners) {
|
|
return _attachConsole(listeners, true);
|
|
}
|
|
function attachConsoleToWorker(listeners) {
|
|
return _attachConsole(listeners, true, true);
|
|
}
|
|
|
|
var _attachConsole = async function (listeners, attachToTab, attachToWorker) {
|
|
try {
|
|
function waitForMessage(target) {
|
|
return new Promise(resolve => {
|
|
target.addEventListener("message", resolve, { once: true });
|
|
});
|
|
}
|
|
|
|
// Fetch the console actor out of the expected target
|
|
// ParentProcessTarget / WorkerTarget / FrameTarget
|
|
let commands, target, worker;
|
|
if (!attachToTab) {
|
|
commands = await CommandsFactory.forMainProcess();
|
|
target = await commands.descriptorFront.getTarget();
|
|
} else {
|
|
commands = await CommandsFactory.forCurrentTabInChromeMochitest();
|
|
// Descriptor's getTarget will only work if the TargetCommand watches for the first top target
|
|
await commands.targetCommand.startListening();
|
|
target = await commands.descriptorFront.getTarget();
|
|
if (attachToWorker) {
|
|
const workerName = "console-test-worker.js#" + new Date().getTime();
|
|
worker = new Worker(workerName);
|
|
await waitForMessage(worker);
|
|
|
|
const { workers } = await target.listWorkers();
|
|
target = workers.filter(w => w.url == workerName)[0];
|
|
if (!target) {
|
|
console.error(
|
|
"listWorkers failed. Unable to find the worker actor\n"
|
|
);
|
|
return null;
|
|
}
|
|
// This is still important to attach workers as target is still a descriptor front
|
|
// which "becomes" a target when calling this method:
|
|
await target.morphWorkerDescriptorIntoWorkerTarget();
|
|
}
|
|
}
|
|
|
|
const webConsoleFront = await target.getFront("console");
|
|
|
|
// By default the console isn't listening for anything,
|
|
// request listeners from here
|
|
const response = await webConsoleFront.startListeners(listeners);
|
|
return {
|
|
state: {
|
|
dbgClient: commands.client,
|
|
webConsoleFront,
|
|
actor: webConsoleFront.actor,
|
|
// Keep a strong reference to the Worker to avoid it being
|
|
// GCd during the test (bug 1237492).
|
|
// eslint-disable-next-line camelcase
|
|
_worker_ref: worker,
|
|
},
|
|
response,
|
|
};
|
|
} catch (error) {
|
|
console.error(
|
|
`attachConsole failed: ${error.error} ${error.message} - ` + error.stack
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
async function createCommandsForTab() {
|
|
const commands = await CommandsFactory.forMainProcess();
|
|
await commands.targetCommand.startListening();
|
|
return commands;
|
|
}
|
|
|
|
function closeDebugger(state, callback) {
|
|
const onClose = state.dbgClient.close();
|
|
|
|
state.dbgClient = null;
|
|
state.client = null;
|
|
|
|
if (typeof callback === "function") {
|
|
onClose.then(callback);
|
|
}
|
|
return onClose;
|
|
}
|
|
|
|
function checkConsoleAPICalls(consoleCalls, expectedConsoleCalls) {
|
|
is(
|
|
consoleCalls.length,
|
|
expectedConsoleCalls.length,
|
|
"received correct number of console calls"
|
|
);
|
|
expectedConsoleCalls.forEach(function (message, index) {
|
|
info("checking received console call #" + index);
|
|
checkConsoleAPICall(consoleCalls[index], expectedConsoleCalls[index]);
|
|
});
|
|
}
|
|
|
|
function checkConsoleAPICall(call, expected) {
|
|
is(
|
|
call.arguments?.length || 0,
|
|
expected.arguments?.length || 0,
|
|
"number of arguments"
|
|
);
|
|
|
|
checkObject(call, expected);
|
|
}
|
|
|
|
function checkObject(object, expected) {
|
|
if (object && object.getGrip) {
|
|
object = object.getGrip();
|
|
}
|
|
|
|
for (const name of Object.keys(expected)) {
|
|
const expectedValue = expected[name];
|
|
const value = object[name];
|
|
checkValue(name, value, expectedValue);
|
|
}
|
|
}
|
|
|
|
function checkValue(name, value, expected) {
|
|
if (expected === null) {
|
|
ok(!value, "'" + name + "' is null");
|
|
} else if (value === undefined) {
|
|
ok(false, "'" + name + "' is undefined");
|
|
} else if (value === null) {
|
|
ok(false, "'" + name + "' is null");
|
|
} else if (
|
|
typeof expected == "string" ||
|
|
typeof expected == "number" ||
|
|
typeof expected == "boolean"
|
|
) {
|
|
is(value, expected, "property '" + name + "'");
|
|
} else if (expected instanceof RegExp) {
|
|
ok(expected.test(value), name + ": " + expected + " matched " + value);
|
|
} else if (Array.isArray(expected)) {
|
|
info("checking array for property '" + name + "'");
|
|
checkObject(value, expected);
|
|
} else if (typeof expected == "object") {
|
|
info("checking object for property '" + name + "'");
|
|
checkObject(value, expected);
|
|
}
|
|
}
|
|
|
|
function checkHeadersOrCookies(array, expected) {
|
|
const foundHeaders = {};
|
|
|
|
for (const elem of array) {
|
|
if (!(elem.name in expected)) {
|
|
continue;
|
|
}
|
|
foundHeaders[elem.name] = true;
|
|
info("checking value of header " + elem.name);
|
|
checkValue(elem.name, elem.value, expected[elem.name]);
|
|
}
|
|
|
|
for (const header in expected) {
|
|
if (!(header in foundHeaders)) {
|
|
ok(false, header + " was not found");
|
|
}
|
|
}
|
|
}
|
|
|
|
function checkRawHeaders(text, expected) {
|
|
const headers = text.split(/\r\n|\n|\r/);
|
|
const arr = [];
|
|
for (const header of headers) {
|
|
const index = header.indexOf(": ");
|
|
if (index < 0) {
|
|
continue;
|
|
}
|
|
arr.push({
|
|
name: header.substr(0, index),
|
|
value: header.substr(index + 2),
|
|
});
|
|
}
|
|
|
|
checkHeadersOrCookies(arr, expected);
|
|
}
|
|
|
|
var gTestState = {};
|
|
|
|
function runTests(tests, endCallback) {
|
|
function* driver() {
|
|
let lastResult, sendToNext;
|
|
for (let i = 0; i < tests.length; i++) {
|
|
gTestState.index = i;
|
|
const fn = tests[i];
|
|
info("will run test #" + i + ": " + fn.name);
|
|
lastResult = fn(sendToNext, lastResult);
|
|
sendToNext = yield lastResult;
|
|
}
|
|
yield endCallback(sendToNext, lastResult);
|
|
}
|
|
gTestState.driver = driver();
|
|
return gTestState.driver.next();
|
|
}
|
|
|
|
function nextTest(message) {
|
|
return gTestState.driver.next(message);
|
|
}
|
|
|
|
function withActiveServiceWorker(win, url, scope) {
|
|
const opts = {};
|
|
if (scope) {
|
|
opts.scope = scope;
|
|
}
|
|
return win.navigator.serviceWorker.register(url, opts).then(swr => {
|
|
if (swr.active) {
|
|
return swr;
|
|
}
|
|
|
|
// Unfortunately we can't just use navigator.serviceWorker.ready promise
|
|
// here. If the service worker is for a scope that does not cover the window
|
|
// then the ready promise will never resolve. Instead monitor the service
|
|
// workers state change events to determine when its activated.
|
|
return new Promise(resolve => {
|
|
const sw = swr.waiting || swr.installing;
|
|
sw.addEventListener("statechange", function stateHandler() {
|
|
if (sw.state === "activated") {
|
|
sw.removeEventListener("statechange", stateHandler);
|
|
resolve(swr);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Front} consoleFront
|
|
* @param {Function} consoleCall: A function which calls the consoleAPI, e.g. :
|
|
* `() => top.console.log("test")`.
|
|
* @returns {Promise} A promise that will be resolved with the packet sent by the server
|
|
* in response to the consoleAPI call.
|
|
*/
|
|
function consoleAPICall(consoleFront, consoleCall) {
|
|
const onConsoleAPICall = consoleFront.once("consoleAPICall");
|
|
consoleCall();
|
|
return onConsoleAPICall;
|
|
}
|