/* 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;
}