233 lines
7.3 KiB
JavaScript
233 lines
7.3 KiB
JavaScript
/* 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<value>} 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<Value>}
|
|
* 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,
|
|
};
|
|
}
|