From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../test/helpers-browser-toolbox.js | 233 +++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js (limited to 'devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js') diff --git a/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js b/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js new file mode 100644 index 0000000000..bc97c20c01 --- /dev/null +++ b/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js @@ -0,0 +1,233 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-unused-vars, no-undef */ + +"use strict"; + +const { BrowserToolboxLauncher } = ChromeUtils.importESModule( + "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs" +); +const { + DevToolsClient, +} = require("resource://devtools/client/devtools-client.js"); + +/** + * Open up a browser toolbox and return a ToolboxTask object for interacting + * with it. ToolboxTask has the following methods: + * + * importFunctions(object) + * + * The object contains functions from this process which should be defined in + * the global evaluation scope of the toolbox. The toolbox cannot load testing + * files directly. + * + * destroy() + * + * Destroy the browser toolbox and make sure it exits cleanly. + * + * @param {Object}: + * - {Function} existingProcessClose: if truth-y, connect to an existing + * browser toolbox process rather than launching a new one and + * connecting to it. The given function is expected to return an + * object containing an `exitCode`, like `{exitCode}`, and will be + * awaited in the returned `destroy()` function. `exitCode` is + * asserted to be 0 (success). + */ +async function initBrowserToolboxTask({ existingProcessClose } = {}) { + if (AppConstants.ASAN) { + ok( + false, + "ToolboxTask cannot be used on ASAN builds. This test should be skipped (Bug 1591064)." + ); + } + + await pushPref("devtools.chrome.enabled", true); + await pushPref("devtools.debugger.remote-enabled", true); + await pushPref("devtools.browsertoolbox.enable-test-server", true); + await pushPref("devtools.debugger.prompt-connection", false); + + // This rejection seems to affect all tests using the browser toolbox. + ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" + ).PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/); + + let process; + let dbgProcess; + if (!existingProcessClose) { + [process, dbgProcess] = await new Promise(resolve => { + BrowserToolboxLauncher.init({ + onRun: (_process, _dbgProcess) => resolve([_process, _dbgProcess]), + overwritePreferences: true, + }); + }); + ok(true, "Browser toolbox started"); + is( + BrowserToolboxLauncher.getBrowserToolboxSessionState(), + true, + "Has session state" + ); + } else { + ok(true, "Connecting to existing browser toolbox"); + } + + // The port of the DevToolsServer installed in the toolbox process is fixed. + // See browser-toolbox/window.js + let transport; + while (true) { + try { + transport = await DevToolsClient.socketConnect({ + host: "localhost", + port: 6001, + webSocket: false, + }); + break; + } catch (e) { + await waitForTime(100); + } + } + ok(true, "Got transport"); + + const client = new DevToolsClient(transport); + await client.connect(); + + const commands = await CommandsFactory.forMainProcess({ client }); + const target = await commands.descriptorFront.getTarget(); + const consoleFront = await target.getFront("console"); + + ok(true, "Connected"); + + await importFunctions({ + info: msg => dump(msg + "\n"), + is: (a, b, description) => { + let msg = + "'" + JSON.stringify(a) + "' is equal to '" + JSON.stringify(b) + "'"; + if (description) { + msg += " - " + description; + } + if (a !== b) { + msg = "FAILURE: " + msg; + dump(msg + "\n"); + throw new Error(msg); + } else { + msg = "SUCCESS: " + msg; + dump(msg + "\n"); + } + }, + ok: (a, description) => { + let msg = "'" + JSON.stringify(a) + "' is true"; + if (description) { + msg += " - " + description; + } + if (!a) { + msg = "FAILURE: " + msg; + dump(msg + "\n"); + throw new Error(msg); + } else { + msg = "SUCCESS: " + msg; + dump(msg + "\n"); + } + }, + }); + + async function evaluateExpression(expression, options = {}) { + const onEvaluationResult = consoleFront.once("evaluationResult"); + await consoleFront.evaluateJSAsync({ text: expression, ...options }); + return onEvaluationResult; + } + + /** + * Invoke the given function and argument(s) within the global evaluation scope + * of the toolbox. The evaluation scope predefines the name "gToolbox" for the + * toolbox itself. + * + * @param {value|Array} arg + * If an Array is passed, we will consider it as the list of arguments + * to pass to `fn`. Otherwise we will consider it as the unique argument + * to pass to it. + * @param {Function} fn + * Function to call in the global scope within the browser toolbox process. + * This function will be stringified and passed to the process via RDP. + * @return {Promise} + * Return the primitive value returned by `fn`. + */ + async function spawn(arg, fn) { + // Use JSON.stringify to ensure that we can pass strings + // as well as any JSON-able object. + const argString = JSON.stringify(Array.isArray(arg) ? arg : [arg]); + const rv = await evaluateExpression(`(${fn}).apply(null,${argString})`, { + // Use the following argument in order to ensure waiting for the completion + // of the promise returned by `fn` (in case this is an async method). + mapped: { await: true }, + }); + if (rv.exceptionMessage) { + throw new Error(`ToolboxTask.spawn failure: ${rv.exceptionMessage}`); + } else if (rv.topLevelAwaitRejected) { + throw new Error(`ToolboxTask.spawn await rejected`); + } + return rv.result; + } + + async function importFunctions(functions) { + for (const [key, fn] of Object.entries(functions)) { + await evaluateExpression(`this.${key} = ${fn}`); + } + } + + async function importScript(script) { + const response = await evaluateExpression(script); + if (response.hasException) { + ok( + false, + "ToolboxTask.spawn exception while importing script: " + + response.exceptionMessage + ); + } + } + + let destroyed = false; + async function destroy() { + // No need to do anything if `destroy` was already called. + if (destroyed) { + return; + } + + const closePromise = existingProcessClose + ? existingProcessClose() + : dbgProcess.wait(); + evaluateExpression("gToolbox.destroy()").catch(e => { + // Ignore connection close as the toolbox destroy may destroy + // everything quickly enough so that evaluate request is still pending + if (!e.message.includes("Connection closed")) { + throw e; + } + }); + + const { exitCode } = await closePromise; + ok(true, "Browser toolbox process closed"); + + is(exitCode, 0, "The remote debugger process died cleanly"); + + if (!existingProcessClose) { + is( + BrowserToolboxLauncher.getBrowserToolboxSessionState(), + false, + "No session state after closing" + ); + } + + await commands.destroy(); + destroyed = true; + } + + // When tests involving using this task fail, the spawned Browser Toolbox is not + // destroyed and might impact the next tests (e.g. pausing the content process before + // the debugger from the content toolbox does). So make sure to cleanup everything. + registerCleanupFunction(destroy); + + return { + importFunctions, + importScript, + spawn, + destroy, + }; +} -- cgit v1.2.3