diff options
Diffstat (limited to 'devtools/shared/webconsole/test/chrome/common.js')
-rw-r--r-- | devtools/shared/webconsole/test/chrome/common.js | 274 |
1 files changed, 274 insertions, 0 deletions
diff --git a/devtools/shared/webconsole/test/chrome/common.js b/devtools/shared/webconsole/test/chrome/common.js new file mode 100644 index 0000000000..62878d7e60 --- /dev/null +++ b/devtools/shared/webconsole/test/chrome/common.js @@ -0,0 +1,274 @@ +/* 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; +} |