/* 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(); } } // Attach the Target and the target thread in order to instantiate the console client. await target.attachThread(); 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(evt) { 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; }