summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/test/browser/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webconsole/test/browser/head.js')
-rw-r--r--devtools/client/webconsole/test/browser/head.js1916
1 files changed, 1916 insertions, 0 deletions
diff --git a/devtools/client/webconsole/test/browser/head.js b/devtools/client/webconsole/test/browser/head.js
new file mode 100644
index 0000000000..1a74f2deee
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/head.js
@@ -0,0 +1,1916 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"use strict";
+
+/* globals Task, openToolboxForTab, gBrowser */
+
+// shared-head.js handles imports, constants, and utility functions
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+// Import helpers for the new debugger
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/shared-head.js",
+ this
+);
+
+var {
+ BrowserConsoleManager,
+} = require("resource://devtools/client/webconsole/browser-console-manager.js");
+
+var WCUL10n = require("resource://devtools/client/webconsole/utils/l10n.js");
+const DOCS_GA_PARAMS = `?${new URLSearchParams({
+ utm_source: "mozilla",
+ utm_medium: "firefox-console-errors",
+ utm_campaign: "default",
+})}`;
+const GA_PARAMS = `?${new URLSearchParams({
+ utm_source: "mozilla",
+ utm_medium: "devtools-webconsole",
+ utm_campaign: "default",
+})}`;
+
+const wcActions = require("resource://devtools/client/webconsole/actions/index.js");
+
+registerCleanupFunction(async function () {
+ // Reset all cookies, tests loading sjs_slow-response-test-server.sjs will
+ // set a foo cookie which might have side effects on other tests.
+ Services.cookies.removeAll();
+
+ Services.prefs.clearUserPref("devtools.webconsole.ui.filterbar");
+
+ // Reset all filter prefs between tests. First flushPrefEnv in case one of the
+ // filter prefs has been pushed for the test
+ await SpecialPowers.flushPrefEnv();
+ Services.prefs.getChildList("devtools.webconsole.filter").forEach(pref => {
+ Services.prefs.clearUserPref(pref);
+ });
+});
+
+/**
+ * Add a new tab and open the toolbox in it, and select the webconsole.
+ *
+ * @param string url
+ * The URL for the tab to be opened.
+ * @param Boolean clearJstermHistory
+ * true (default) if the jsterm history should be cleared.
+ * @param String hostId (optional)
+ * The type of toolbox host to be used.
+ * @return Promise
+ * Resolves when the tab has been added, loaded and the toolbox has been opened.
+ * Resolves to the hud.
+ */
+async function openNewTabAndConsole(url, clearJstermHistory = true, hostId) {
+ const toolbox = await openNewTabAndToolbox(url, "webconsole", hostId);
+ const hud = toolbox.getCurrentPanel().hud;
+
+ if (clearJstermHistory) {
+ // Clearing history that might have been set in previous tests.
+ await hud.ui.wrapper.dispatchClearHistory();
+ }
+
+ return hud;
+}
+
+/**
+ * Add a new tab with iframes, open the toolbox in it, and select the webconsole.
+ *
+ * @param string url
+ * The URL for the tab to be opened.
+ * @param Arra<string> iframes
+ * An array of URLs that will be added to the top document.
+ * @return Promise
+ * Resolves when the tab has been added, loaded, iframes loaded, and the toolbox
+ * has been opened. Resolves to the hud.
+ */
+async function openNewTabWithIframesAndConsole(tabUrl, iframes) {
+ // We need to add the tab and the iframes before opening the console in case we want
+ // to handle remote frames (we don't support creating frames target when the toolbox
+ // is already open).
+ await addTab(tabUrl);
+ await ContentTask.spawn(
+ gBrowser.selectedBrowser,
+ iframes,
+ async function (urls) {
+ const iframesLoadPromises = urls.map((url, i) => {
+ const iframe = content.document.createElement("iframe");
+ iframe.classList.add(`iframe-${i + 1}`);
+ const onLoadIframe = new Promise(resolve => {
+ iframe.addEventListener("load", resolve, { once: true });
+ });
+ content.document.body.append(iframe);
+ iframe.src = url;
+ return onLoadIframe;
+ });
+
+ await Promise.all(iframesLoadPromises);
+ }
+ );
+
+ return openConsole();
+}
+
+/**
+ * Open a new window with a tab,open the toolbox, and select the webconsole.
+ *
+ * @param string url
+ * The URL for the tab to be opened.
+ * @return Promise<{win, hud, tab}>
+ * Resolves when the tab has been added, loaded and the toolbox has been opened.
+ * Resolves to the toolbox.
+ */
+async function openNewWindowAndConsole(url) {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ const tab = await addTab(url, { window: win });
+ win.gBrowser.selectedTab = tab;
+ const hud = await openConsole(tab);
+ return { win, hud, tab };
+}
+
+/**
+ * Subscribe to the store and log out stringinfied versions of messages.
+ * This is a helper function for debugging, to make is easier to see what
+ * happened during the test in the log.
+ *
+ * @param object hud
+ */
+function logAllStoreChanges(hud) {
+ const store = hud.ui.wrapper.getStore();
+ // Adding logging each time the store is modified in order to check
+ // the store state in case of failure.
+ store.subscribe(() => {
+ const messages = [
+ ...store.getState().messages.mutableMessagesById.values(),
+ ];
+ const debugMessages = messages.map(
+ ({ id, type, parameters, messageText }) => {
+ return { id, type, parameters, messageText };
+ }
+ );
+ info(
+ "messages : " +
+ JSON.stringify(debugMessages, function (key, value) {
+ if (value && value.getGrip) {
+ return value.getGrip();
+ }
+ return value;
+ })
+ );
+ });
+}
+
+/**
+ * Wait for messages with given message type in the web console output,
+ * resolving once they are received.
+ *
+ * @param object options
+ * - hud: the webconsole
+ * - messages: Array[Object]. An array of messages to match.
+ * Current supported options:
+ * - text: {String} Partial text match in .message-body
+ * - typeSelector: {String} A part of selector for the message, to
+ * specify the message type.
+ * @return promise
+ * A promise that is resolved to an array of the message nodes
+ */
+function waitForMessagesByType({ hud, messages }) {
+ return new Promise(resolve => {
+ const matchedMessages = [];
+ hud.ui.on("new-messages", function messagesReceived(newMessages) {
+ for (const message of messages) {
+ if (message.matched) {
+ continue;
+ }
+
+ const typeSelector = message.typeSelector;
+ if (!typeSelector) {
+ throw new Error("typeSelector property is required");
+ }
+ if (!typeSelector.startsWith(".")) {
+ throw new Error(
+ "typeSelector property start with a dot e.g. `.result`"
+ );
+ }
+ const selector = ".message" + typeSelector;
+
+ for (const newMessage of newMessages) {
+ const messageBody = newMessage.node.querySelector(`.message-body`);
+ if (
+ messageBody &&
+ newMessage.node.matches(selector) &&
+ messageBody.textContent.includes(message.text)
+ ) {
+ matchedMessages.push(newMessage);
+ message.matched = true;
+ const messagesLeft = messages.length - matchedMessages.length;
+ info(
+ `Matched a message with text: "${message.text}", ` +
+ (messagesLeft > 0
+ ? `still waiting for ${messagesLeft} messages.`
+ : `all messages received.`)
+ );
+ break;
+ }
+ }
+
+ if (matchedMessages.length === messages.length) {
+ hud.ui.off("new-messages", messagesReceived);
+ resolve(matchedMessages);
+ return;
+ }
+ }
+ });
+ });
+}
+
+/**
+ * Wait for a message with the provided text and showing the provided repeat count.
+ *
+ * @param {Object} hud : the webconsole
+ * @param {String} text : text included in .message-body
+ * @param {String} typeSelector : A part of selector for the message, to
+ * specify the message type.
+ * @param {Number} repeat : expected repeat count in .message-repeats
+ */
+function waitForRepeatedMessageByType(hud, text, typeSelector, repeat) {
+ return waitFor(() => {
+ // Wait for a message matching the provided text.
+ const node = findMessageByType(hud, text, typeSelector);
+ if (!node) {
+ return false;
+ }
+
+ // Check if there is a repeat node with the expected count.
+ const repeatNode = node.querySelector(".message-repeats");
+ if (repeatNode && parseInt(repeatNode.textContent, 10) === repeat) {
+ return node;
+ }
+
+ return false;
+ });
+}
+
+/**
+ * Wait for a single message with given message type in the web console output,
+ * resolving with the first message that matches the query once it is received.
+ *
+ * @param {Object} hud : the webconsole
+ * @param {String} text : text included in .message-body
+ * @param {String} typeSelector : A part of selector for the message, to
+ * specify the message type.
+ * @return promise
+ * A promise that is resolved to the message node
+ */
+async function waitForMessageByType(hud, text, typeSelector) {
+ const messages = await waitForMessagesByType({
+ hud,
+ messages: [{ text, typeSelector }],
+ });
+ return messages[0];
+}
+
+/**
+ * Execute an input expression.
+ *
+ * @param {Object} hud : The webconsole.
+ * @param {String} input : The input expression to execute.
+ */
+function execute(hud, input) {
+ return hud.ui.wrapper.dispatchEvaluateExpression(input);
+}
+
+/**
+ * Execute an input expression and wait for a message with the expected text
+ * with given message type to be displayed in the output.
+ *
+ * @param {Object} hud : The webconsole.
+ * @param {String} input : The input expression to execute.
+ * @param {String} matchingText : A string that should match the message body content.
+ * @param {String} typeSelector : A part of selector for the message, to
+ * specify the message type.
+ */
+function executeAndWaitForMessageByType(
+ hud,
+ input,
+ matchingText,
+ typeSelector
+) {
+ const onMessage = waitForMessageByType(hud, matchingText, typeSelector);
+ execute(hud, input);
+ return onMessage;
+}
+
+/**
+ * Type-specific wrappers for executeAndWaitForMessageByType
+ *
+ * @param {Object} hud : The webconsole.
+ * @param {String} input : The input expression to execute.
+ * @param {String} matchingText : A string that should match the message body
+ * content.
+ */
+function executeAndWaitForResultMessage(hud, input, matchingText) {
+ return executeAndWaitForMessageByType(hud, input, matchingText, ".result");
+}
+
+function executeAndWaitForErrorMessage(hud, input, matchingText) {
+ return executeAndWaitForMessageByType(hud, input, matchingText, ".error");
+}
+
+/**
+ * Set the input value, simulates the right keyboard event to evaluate it,
+ * depending on if the console is in editor mode or not, and wait for a message
+ * with the expected text with given message type to be displayed in the output.
+ *
+ * @param {Object} hud : The webconsole.
+ * @param {String} input : The input expression to execute.
+ * @param {String} matchingText : A string that should match the message body
+ * content.
+ * @param {String} typeSelector : A part of selector for the message, to
+ * specify the message type.
+ */
+function keyboardExecuteAndWaitForMessageByType(
+ hud,
+ input,
+ matchingText,
+ typeSelector
+) {
+ hud.jsterm.focus();
+ setInputValue(hud, input);
+ const onMessage = waitForMessageByType(hud, matchingText, typeSelector);
+ if (isEditorModeEnabled(hud)) {
+ EventUtils.synthesizeKey("KEY_Enter", {
+ [Services.appinfo.OS === "Darwin" ? "metaKey" : "ctrlKey"]: true,
+ });
+ } else {
+ EventUtils.synthesizeKey("VK_RETURN");
+ }
+ return onMessage;
+}
+
+/**
+ * Type-specific wrappers for keyboardExecuteAndWaitForMessageByType
+ *
+ * @param {Object} hud : The webconsole.
+ * @param {String} input : The input expression to execute.
+ * @param {String} matchingText : A string that should match the message body
+ * content.
+ */
+function keyboardExecuteAndWaitForResultMessage(hud, input, matchingText) {
+ return keyboardExecuteAndWaitForMessageByType(
+ hud,
+ input,
+ matchingText,
+ ".result"
+ );
+}
+
+/**
+ * Wait for a message to be logged and ensure it is logged only once.
+ *
+ * @param object hud
+ * The web console.
+ * @param string text
+ * A substring that can be found in the message.
+ * @param string typeSelector
+ * A part of selector for the message, to specify the message type.
+ * @return {Node} the node corresponding the found message
+ */
+async function checkUniqueMessageExists(hud, msg, typeSelector) {
+ info(`Checking "${msg}" was logged`);
+ let messages;
+ try {
+ messages = await waitFor(async () => {
+ const msgs = await findMessagesVirtualizedByType({
+ hud,
+ text: msg,
+ typeSelector,
+ });
+ return msgs.length ? msgs : null;
+ });
+ } catch (e) {
+ ok(false, `Message "${msg}" wasn't logged\n`);
+ return null;
+ }
+
+ is(messages.length, 1, `"${msg}" was logged once`);
+ const [messageEl] = messages;
+ const repeatNode = messageEl.querySelector(".message-repeats");
+ is(repeatNode, null, `"${msg}" wasn't repeated`);
+ return messageEl;
+}
+
+/**
+ * Simulate a context menu event on the provided element, and wait for the console context
+ * menu to open. Returns a promise that resolves the menu popup element.
+ *
+ * @param object hud
+ * The web console.
+ * @param element element
+ * The dom element on which the context menu event should be synthesized.
+ * @return promise
+ */
+async function openContextMenu(hud, element) {
+ const onConsoleMenuOpened = hud.ui.wrapper.once("menu-open");
+ synthesizeContextMenuEvent(element);
+ await onConsoleMenuOpened;
+ return _getContextMenu(hud);
+}
+
+/**
+ * Hide the webconsole context menu popup. Returns a promise that will resolve when the
+ * context menu popup is hidden or immediately if the popup can't be found.
+ *
+ * @param object hud
+ * The web console.
+ * @return promise
+ */
+function hideContextMenu(hud) {
+ const popup = _getContextMenu(hud);
+ if (!popup || popup.state == "hidden") {
+ return Promise.resolve();
+ }
+
+ const onPopupHidden = once(popup, "popuphidden");
+ popup.hidePopup();
+ return onPopupHidden;
+}
+
+function _getContextMenu(hud) {
+ const toolbox = hud.toolbox;
+ const doc = toolbox ? toolbox.topWindow.document : hud.chromeWindow.document;
+ return doc.getElementById("webconsole-menu");
+}
+
+/**
+ * Toggle Enable network monitoring setting
+ *
+ * @param object hud
+ * The web console.
+ * @param boolean shouldBeSwitchedOn
+ * The expected state the setting should be in after the toggle.
+ */
+async function toggleNetworkMonitoringConsoleSetting(hud, shouldBeSwitchedOn) {
+ const selector =
+ ".webconsole-console-settings-menu-item-enableNetworkMonitoring";
+ const settingChanged = waitFor(() => {
+ const el = getConsoleSettingElement(hud, selector);
+ return shouldBeSwitchedOn
+ ? el.getAttribute("aria-checked") === "true"
+ : el.getAttribute("aria-checked") !== "true";
+ });
+ await toggleConsoleSetting(hud, selector);
+ await settingChanged;
+}
+
+async function toggleConsoleSetting(hud, selector) {
+ const toolbox = hud.toolbox;
+ const doc = toolbox ? toolbox.doc : hud.chromeWindow.document;
+
+ const menuItem = doc.querySelector(selector);
+ menuItem.click();
+}
+
+function getConsoleSettingElement(hud, selector) {
+ const toolbox = hud.toolbox;
+ const doc = toolbox ? toolbox.doc : hud.chromeWindow.document;
+
+ return doc.querySelector(selector);
+}
+
+function checkConsoleSettingState(hud, selector, enabled) {
+ const el = getConsoleSettingElement(hud, selector);
+ const checked = el.getAttribute("aria-checked") === "true";
+
+ if (enabled) {
+ ok(checked, "setting is enabled");
+ } else {
+ ok(!checked, "setting is disabled");
+ }
+}
+
+/**
+ * Returns a promise that resolves when the node passed as an argument mutate
+ * according to the passed configuration.
+ *
+ * @param {Node} node - The node to observe mutations on.
+ * @param {Object} observeConfig - A configuration object for MutationObserver.observe.
+ * @returns {Promise}
+ */
+function waitForNodeMutation(node, observeConfig = {}) {
+ return new Promise(resolve => {
+ const observer = new MutationObserver(mutations => {
+ resolve(mutations);
+ observer.disconnect();
+ });
+ observer.observe(node, observeConfig);
+ });
+}
+
+/**
+ * Search for a given message. When found, simulate a click on the
+ * message's location, checking to make sure that the debugger opens
+ * the corresponding URL. If the message was generated by a logpoint,
+ * check if the corresponding logpoint editing panel is opened.
+ *
+ * @param {Object} hud
+ * The webconsole
+ * @param {Object} options
+ * - text: {String} The text to search for. This should be contained in
+ * the message. The searching is done with
+ * @see findMessageByType.
+ * - typeSelector: {string} A part of selector for the message, to
+ * specify the message type.
+ * - expectUrl: {boolean} Whether the URL in the opened source should
+ * match the link, or whether it is expected to
+ * be null.
+ * - expectLine: {boolean} It indicates if there is the need to check
+ * the line.
+ * - expectColumn: {boolean} It indicates if there is the need to check
+ * the column.
+ * - logPointExpr: {String} The logpoint expression
+ */
+async function testOpenInDebugger(
+ hud,
+ {
+ text,
+ typeSelector,
+ expectUrl = true,
+ expectLine = true,
+ expectColumn = true,
+ logPointExpr = undefined,
+ }
+) {
+ info(`Finding message for open-in-debugger test; text is "${text}"`);
+ const messageNode = await waitFor(() =>
+ findMessageByType(hud, text, typeSelector)
+ );
+ const locationNode = messageNode.querySelector(".message-location");
+ ok(locationNode, "The message does have a location link");
+ await checkClickOnNode(
+ hud,
+ hud.toolbox,
+ locationNode,
+ expectUrl,
+ expectLine,
+ expectColumn,
+ logPointExpr
+ );
+}
+
+/**
+ * Helper function for testOpenInDebugger.
+ */
+async function checkClickOnNode(
+ hud,
+ toolbox,
+ frameLinkNode,
+ expectUrl,
+ expectLine,
+ expectColumn,
+ logPointExpr
+) {
+ info("checking click on node location");
+
+ // If the debugger hasn't fully loaded yet and breakpoints are still being
+ // added when we click on the logpoint link, the logpoint panel might not
+ // render. Work around this for now, see bug 1592854.
+ await waitForTime(1000);
+
+ const onSourceInDebuggerOpened = once(hud, "source-in-debugger-opened");
+
+ EventUtils.sendMouseEvent(
+ { type: "click" },
+ frameLinkNode.querySelector(".frame-link-filename")
+ );
+
+ await onSourceInDebuggerOpened;
+
+ const dbg = toolbox.getPanel("jsdebugger");
+
+ // Wait for the source to finish loading, if it is pending.
+ await waitFor(
+ () =>
+ !!dbg._selectors.getSelectedSource(dbg._getState()) &&
+ !!dbg._selectors.getSelectedLocation(dbg._getState())
+ );
+
+ if (expectUrl) {
+ const url = frameLinkNode.getAttribute("data-url");
+ ok(url, `source url found ("${url}")`);
+
+ is(
+ dbg._selectors.getSelectedSource(dbg._getState()).url,
+ url,
+ "expected source url"
+ );
+ }
+ if (expectLine) {
+ const line = frameLinkNode.getAttribute("data-line");
+ ok(line, `source line found ("${line}")`);
+
+ is(
+ parseInt(dbg._selectors.getSelectedLocation(dbg._getState()).line, 10),
+ parseInt(line, 10),
+ "expected source line"
+ );
+ }
+ if (expectColumn) {
+ const column = frameLinkNode.getAttribute("data-column");
+ ok(column, `source column found ("${column}")`);
+
+ is(
+ parseInt(dbg._selectors.getSelectedLocation(dbg._getState()).column, 10),
+ parseInt(column, 10),
+ "expected source column"
+ );
+ }
+
+ if (logPointExpr !== undefined && logPointExpr !== "") {
+ const inputEl = dbg.panelWin.document.activeElement;
+ is(
+ inputEl.tagName,
+ "TEXTAREA",
+ "The textarea of logpoint panel is focused"
+ );
+
+ const inputValue = inputEl.parentElement.parentElement.innerText.trim();
+ is(
+ inputValue,
+ logPointExpr,
+ "The input in the open logpoint panel matches the logpoint expression"
+ );
+ }
+}
+
+/**
+ * Returns true if the give node is currently focused.
+ */
+function hasFocus(node) {
+ return (
+ node.ownerDocument.activeElement == node && node.ownerDocument.hasFocus()
+ );
+}
+
+/**
+ * Get the value of the console input .
+ *
+ * @param {WebConsole} hud: The webconsole
+ * @returns {String}: The value of the console input.
+ */
+function getInputValue(hud) {
+ return hud.jsterm._getValue();
+}
+
+/**
+ * Set the value of the console input .
+ *
+ * @param {WebConsole} hud: The webconsole
+ * @param {String} value : The value to set the console input to.
+ */
+function setInputValue(hud, value) {
+ const onValueSet = hud.jsterm.once("set-input-value");
+ hud.jsterm._setValue(value);
+ return onValueSet;
+}
+
+/**
+ * Set the value of the console input and its caret position, and wait for the
+ * autocompletion to be updated.
+ *
+ * @param {WebConsole} hud: The webconsole
+ * @param {String} value : The value to set the jsterm to.
+ * @param {Integer} caretPosition : The index where to place the cursor. A negative
+ * number will place the caret at (value.length - offset) position.
+ * Default to value.length (caret set at the end).
+ * @returns {Promise} resolves when the jsterm is completed.
+ */
+async function setInputValueForAutocompletion(
+ hud,
+ value,
+ caretPosition = value.length
+) {
+ const { jsterm } = hud;
+
+ const initialPromises = [];
+ if (jsterm.autocompletePopup.isOpen) {
+ initialPromises.push(jsterm.autocompletePopup.once("popup-closed"));
+ }
+ setInputValue(hud, "");
+ await Promise.all(initialPromises);
+
+ // Wait for next tick. Tooltip tests sometimes fail to successively hide and
+ // show tooltips on Win32 debug.
+ await waitForTick();
+
+ jsterm.focus();
+
+ const updated = jsterm.once("autocomplete-updated");
+ EventUtils.sendString(value, hud.iframeWindow);
+ await updated;
+
+ // Wait for next tick. Tooltip tests sometimes fail to successively hide and
+ // show tooltips on Win32 debug.
+ await waitForTick();
+
+ if (caretPosition < 0) {
+ caretPosition = value.length + caretPosition;
+ }
+
+ if (Number.isInteger(caretPosition)) {
+ jsterm.editor.setCursor(jsterm.editor.getPosition(caretPosition));
+ }
+}
+
+/**
+ * Set the value of the console input and wait for the confirm dialog to be displayed.
+ *
+ * @param {Toolbox} toolbox
+ * @param {WebConsole} hud
+ * @param {String} value : The value to set the jsterm to.
+ * Default to value.length (caret set at the end).
+ * @returns {Promise<HTMLElement>} resolves with dialog element when it is opened.
+ */
+async function setInputValueForGetterConfirmDialog(toolbox, hud, value) {
+ await setInputValueForAutocompletion(hud, value);
+ await waitFor(() => isConfirmDialogOpened(toolbox));
+ ok(true, "The confirm dialog is displayed");
+ return getConfirmDialog(toolbox);
+}
+
+/**
+ * Checks if the console input has the expected completion value.
+ *
+ * @param {WebConsole} hud
+ * @param {String} expectedValue
+ * @param {String} assertionInfo: Description of the assertion passed to `is`.
+ */
+function checkInputCompletionValue(hud, expectedValue, assertionInfo) {
+ const completionValue = getInputCompletionValue(hud);
+ if (completionValue === null) {
+ ok(false, "Couldn't retrieve the completion value");
+ }
+
+ info(`Expects "${expectedValue}", is "${completionValue}"`);
+ is(completionValue, expectedValue, assertionInfo);
+}
+
+/**
+ * Checks if the cursor on console input is at expected position.
+ *
+ * @param {WebConsole} hud
+ * @param {Integer} expectedCursorIndex
+ * @param {String} assertionInfo: Description of the assertion passed to `is`.
+ */
+function checkInputCursorPosition(hud, expectedCursorIndex, assertionInfo) {
+ const { jsterm } = hud;
+ is(jsterm.editor.getCursor().ch, expectedCursorIndex, assertionInfo);
+}
+
+/**
+ * Checks the console input value and the cursor position given an expected string
+ * containing a "|" to indicate the expected cursor position.
+ *
+ * @param {WebConsole} hud
+ * @param {String} expectedStringWithCursor:
+ * String with a "|" to indicate the expected cursor position.
+ * For example, this is how you assert an empty value with the focus "|",
+ * and this indicates the value should be "test" and the cursor at the
+ * end of the input: "test|".
+ * @param {String} assertionInfo: Description of the assertion passed to `is`.
+ */
+function checkInputValueAndCursorPosition(
+ hud,
+ expectedStringWithCursor,
+ assertionInfo
+) {
+ info(`Checking jsterm state: \n${expectedStringWithCursor}`);
+ if (!expectedStringWithCursor.includes("|")) {
+ ok(
+ false,
+ `expectedStringWithCursor must contain a "|" char to indicate cursor position`
+ );
+ }
+
+ const inputValue = expectedStringWithCursor.replace("|", "");
+ const { jsterm } = hud;
+
+ is(getInputValue(hud), inputValue, "console input has expected value");
+ const lines = expectedStringWithCursor.split("\n");
+ const lineWithCursor = lines.findIndex(line => line.includes("|"));
+ const { ch, line } = jsterm.editor.getCursor();
+ is(line, lineWithCursor, assertionInfo + " - correct line");
+ is(ch, lines[lineWithCursor].indexOf("|"), assertionInfo + " - correct ch");
+}
+
+/**
+ * Returns the console input completion value.
+ *
+ * @param {WebConsole} hud
+ * @returns {String}
+ */
+function getInputCompletionValue(hud) {
+ const { jsterm } = hud;
+ return jsterm.editor.getAutoCompletionText();
+}
+
+function closeAutocompletePopup(hud) {
+ const { jsterm } = hud;
+
+ if (!jsterm.autocompletePopup.isOpen) {
+ return Promise.resolve();
+ }
+
+ const onPopupClosed = jsterm.autocompletePopup.once("popup-closed");
+ const onAutocompleteUpdated = jsterm.once("autocomplete-updated");
+ EventUtils.synthesizeKey("KEY_Escape");
+ return Promise.all([onPopupClosed, onAutocompleteUpdated]);
+}
+
+/**
+ * Returns a boolean indicating if the console input is focused.
+ *
+ * @param {WebConsole} hud
+ * @returns {Boolean}
+ */
+function isInputFocused(hud) {
+ const { jsterm } = hud;
+ const document = hud.ui.outputNode.ownerDocument;
+ const documentIsFocused = document.hasFocus();
+ return documentIsFocused && jsterm.editor.hasFocus();
+}
+
+/**
+ * Open the JavaScript debugger.
+ *
+ * @param object options
+ * Options for opening the debugger:
+ * - tab: the tab you want to open the debugger for.
+ * @return object
+ * A promise that is resolved once the debugger opens, or rejected if
+ * the open fails. The resolution callback is given one argument, an
+ * object that holds the following properties:
+ * - target: the Target object for the Tab.
+ * - toolbox: the Toolbox instance.
+ * - panel: the jsdebugger panel instance.
+ */
+async function openDebugger(options = {}) {
+ if (!options.tab) {
+ options.tab = gBrowser.selectedTab;
+ }
+
+ let toolbox = gDevTools.getToolboxForTab(options.tab);
+ const dbgPanelAlreadyOpen = toolbox && toolbox.getPanel("jsdebugger");
+ if (dbgPanelAlreadyOpen) {
+ await toolbox.selectTool("jsdebugger");
+
+ return {
+ target: toolbox.target,
+ toolbox,
+ panel: toolbox.getCurrentPanel(),
+ };
+ }
+
+ toolbox = await gDevTools.showToolboxForTab(options.tab, {
+ toolId: "jsdebugger",
+ });
+ const panel = toolbox.getCurrentPanel();
+
+ await toolbox.threadFront.getSources();
+
+ return { target: toolbox.target, toolbox, panel };
+}
+
+async function openInspector(options = {}) {
+ if (!options.tab) {
+ options.tab = gBrowser.selectedTab;
+ }
+
+ const toolbox = await gDevTools.showToolboxForTab(options.tab, {
+ toolId: "inspector",
+ });
+
+ return toolbox.getCurrentPanel();
+}
+
+/**
+ * Open the netmonitor for the given tab, or the current one if none given.
+ *
+ * @param Element tab
+ * Optional tab element for which you want open the netmonitor.
+ * Defaults to current selected tab.
+ * @return Promise
+ * A promise that is resolved with the netmonitor panel once the netmonitor is open.
+ */
+async function openNetMonitor(tab) {
+ tab = tab || gBrowser.selectedTab;
+ let toolbox = gDevTools.getToolboxForTab(tab);
+ if (!toolbox) {
+ toolbox = await gDevTools.showToolboxForTab(tab);
+ }
+ await toolbox.selectTool("netmonitor");
+ return toolbox.getCurrentPanel();
+}
+
+/**
+ * Open the Web Console for the given tab, or the current one if none given.
+ *
+ * @param Element tab
+ * Optional tab element for which you want open the Web Console.
+ * Defaults to current selected tab.
+ * @return Promise
+ * A promise that is resolved with the console hud once the web console is open.
+ */
+async function openConsole(tab) {
+ tab = tab || gBrowser.selectedTab;
+ const toolbox = await gDevTools.showToolboxForTab(tab, {
+ toolId: "webconsole",
+ });
+ return toolbox.getCurrentPanel().hud;
+}
+
+/**
+ * Close the Web Console for the given tab.
+ *
+ * @param Element [tab]
+ * Optional tab element for which you want close the Web Console.
+ * Defaults to current selected tab.
+ * @return object
+ * A promise that is resolved once the web console is closed.
+ */
+async function closeConsole(tab = gBrowser.selectedTab) {
+ const toolbox = gDevTools.getToolboxForTab(tab);
+ if (toolbox) {
+ await toolbox.destroy();
+ }
+}
+
+/**
+ * Open a network request logged in the webconsole in the netmonitor panel.
+ *
+ * @param {Object} toolbox
+ * @param {Object} hud
+ * @param {String} url
+ * URL of the request as logged in the netmonitor.
+ * @param {String} urlInConsole
+ * (optional) Use if the logged URL in webconsole is different from the real URL.
+ */
+async function openMessageInNetmonitor(toolbox, hud, url, urlInConsole) {
+ // By default urlInConsole should be the same as the complete url.
+ urlInConsole = urlInConsole || url;
+
+ const message = await waitFor(() =>
+ findMessageByType(hud, urlInConsole, ".network")
+ );
+
+ const onNetmonitorSelected = toolbox.once(
+ "netmonitor-selected",
+ (event, panel) => {
+ return panel;
+ }
+ );
+
+ const menuPopup = await openContextMenu(hud, message);
+ const openInNetMenuItem = menuPopup.querySelector(
+ "#console-menu-open-in-network-panel"
+ );
+ ok(openInNetMenuItem, "open in network panel item is enabled");
+ menuPopup.activateItem(openInNetMenuItem);
+
+ const { panelWin } = await onNetmonitorSelected;
+ ok(
+ true,
+ "The netmonitor panel is selected when clicking on the network message"
+ );
+
+ const { store, windowRequire } = panelWin;
+ const nmActions = windowRequire(
+ "devtools/client/netmonitor/src/actions/index"
+ );
+ const { getSelectedRequest } = windowRequire(
+ "devtools/client/netmonitor/src/selectors/index"
+ );
+
+ store.dispatch(nmActions.batchEnable(false));
+
+ await waitFor(() => {
+ const selected = getSelectedRequest(store.getState());
+ return selected && selected.url === url;
+ }, `network entry for the URL "${url}" wasn't found`);
+
+ ok(true, "The attached url is correct.");
+
+ info(
+ "Wait for the netmonitor headers panel to appear as it spawns RDP requests"
+ );
+ await waitFor(() =>
+ panelWin.document.querySelector("#headers-panel .headers-overview")
+ );
+}
+
+function selectNode(hud, node) {
+ const outputContainer = hud.ui.outputNode.querySelector(".webconsole-output");
+
+ // We must first blur the input or else we can't select anything.
+ outputContainer.ownerDocument.activeElement.blur();
+
+ const selection = outputContainer.ownerDocument.getSelection();
+ const range = document.createRange();
+ range.selectNodeContents(node);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ return selection;
+}
+
+async function waitForBrowserConsole() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(subject) {
+ Services.obs.removeObserver(observer, "web-console-created");
+ subject.QueryInterface(Ci.nsISupportsString);
+
+ const hud = BrowserConsoleManager.getBrowserConsole();
+ ok(hud, "browser console is open");
+ is(subject.data, hud.hudId, "notification hudId is correct");
+
+ executeSoon(() => resolve(hud));
+ }, "web-console-created");
+ });
+}
+
+/**
+ * Get the state of a console filter.
+ *
+ * @param {Object} hud
+ */
+async function getFilterState(hud) {
+ const { outputNode } = hud.ui;
+ const filterBar = outputNode.querySelector(".webconsole-filterbar-secondary");
+ const buttons = filterBar.querySelectorAll("button");
+ const result = {};
+
+ for (const button of buttons) {
+ result[button.dataset.category] =
+ button.getAttribute("aria-pressed") === "true";
+ }
+
+ return result;
+}
+
+/**
+ * Return the filter input element.
+ *
+ * @param {Object} hud
+ * @return {HTMLInputElement}
+ */
+function getFilterInput(hud) {
+ return hud.ui.outputNode.querySelector(".devtools-searchbox input");
+}
+
+/**
+ * Set the state of a console filter.
+ *
+ * @param {Object} hud
+ * @param {Object} settings
+ * Category settings in the following format:
+ * {
+ * error: true,
+ * warn: true,
+ * log: true,
+ * info: true,
+ * debug: true,
+ * css: false,
+ * netxhr: false,
+ * net: false,
+ * text: ""
+ * }
+ */
+async function setFilterState(hud, settings) {
+ const { outputNode } = hud.ui;
+ const filterBar = outputNode.querySelector(".webconsole-filterbar-secondary");
+
+ for (const category in settings) {
+ const value = settings[category];
+ const button = filterBar.querySelector(`[data-category="${category}"]`);
+
+ if (category === "text") {
+ const filterInput = getFilterInput(hud);
+ filterInput.focus();
+ filterInput.select();
+ const win = outputNode.ownerDocument.defaultView;
+ if (!value) {
+ EventUtils.synthesizeKey("KEY_Delete", {}, win);
+ } else {
+ EventUtils.sendString(value, win);
+ }
+ await waitFor(() => filterInput.value === value);
+ continue;
+ }
+
+ if (!button) {
+ ok(
+ false,
+ `setFilterState() called with a category of ${category}, ` +
+ `which doesn't exist.`
+ );
+ }
+
+ info(
+ `Setting the ${category} category to ${value ? "checked" : "disabled"}`
+ );
+
+ const isPressed = button.getAttribute("aria-pressed");
+
+ if ((!value && isPressed === "true") || (value && isPressed !== "true")) {
+ button.click();
+
+ await waitFor(() => {
+ const pressed = button.getAttribute("aria-pressed");
+ if (!value) {
+ return pressed === "false" || pressed === null;
+ }
+ return pressed === "true";
+ });
+ }
+ }
+}
+
+/**
+ * Reset the filters at the end of a test that has changed them. This is
+ * important when using the `--verify` test option as when it is used you need
+ * to manually reset the filters.
+ *
+ * The css, netxhr and net filters are disabled by default.
+ *
+ * @param {Object} hud
+ */
+async function resetFilters(hud) {
+ info("Resetting filters to their default state");
+
+ const store = hud.ui.wrapper.getStore();
+ store.dispatch(wcActions.filtersClear());
+}
+
+/**
+ * Open the reverse search input by simulating the appropriate keyboard shortcut.
+ *
+ * @param {Object} hud
+ * @returns {DOMNode} The reverse search dom node.
+ */
+async function openReverseSearch(hud) {
+ info("Open the reverse search UI with a keyboard shortcut");
+ const onReverseSearchUiOpen = waitFor(() => getReverseSearchElement(hud));
+ const isMacOS = AppConstants.platform === "macosx";
+ if (isMacOS) {
+ EventUtils.synthesizeKey("r", { ctrlKey: true });
+ } else {
+ EventUtils.synthesizeKey("VK_F9");
+ }
+
+ const element = await onReverseSearchUiOpen;
+ return element;
+}
+
+function getReverseSearchElement(hud) {
+ const { outputNode } = hud.ui;
+ return outputNode.querySelector(".reverse-search");
+}
+
+function getReverseSearchInfoElement(hud) {
+ const reverseSearchElement = getReverseSearchElement(hud);
+ if (!reverseSearchElement) {
+ return null;
+ }
+
+ return reverseSearchElement.querySelector(".reverse-search-info");
+}
+
+/**
+ * Returns a boolean indicating if the reverse search input is focused.
+ *
+ * @param {WebConsole} hud
+ * @returns {Boolean}
+ */
+function isReverseSearchInputFocused(hud) {
+ const { outputNode } = hud.ui;
+ const document = outputNode.ownerDocument;
+ const documentIsFocused = document.hasFocus();
+ const reverseSearchInput = outputNode.querySelector(".reverse-search-input");
+
+ return document.activeElement == reverseSearchInput && documentIsFocused;
+}
+
+function getEagerEvaluationElement(hud) {
+ return hud.ui.outputNode.querySelector(".eager-evaluation-result");
+}
+
+async function waitForEagerEvaluationResult(hud, text) {
+ await waitUntil(() => {
+ const elem = getEagerEvaluationElement(hud);
+ if (elem) {
+ if (text instanceof RegExp) {
+ return text.test(elem.innerText);
+ }
+ return elem.innerText == text;
+ }
+ return false;
+ });
+ ok(true, `Got eager evaluation result ${text}`);
+}
+
+// This just makes sure the eager evaluation result disappears. This will pass
+// even for inputs which eventually have a result because nothing will be shown
+// while the evaluation happens. Waiting here does make sure that a previous
+// input was processed and sent down to the server for evaluating.
+async function waitForNoEagerEvaluationResult(hud) {
+ await waitUntil(() => {
+ const elem = getEagerEvaluationElement(hud);
+ return elem && elem.innerText == "";
+ });
+ ok(true, `Eager evaluation result disappeared`);
+}
+
+/**
+ * Selects a node in the inspector.
+ *
+ * @param {Object} toolbox
+ * @param {String} selector: The selector for the node we want to select.
+ */
+async function selectNodeWithPicker(toolbox, selector) {
+ const inspector = toolbox.getPanel("inspector");
+
+ const onPickerStarted = toolbox.nodePicker.once("picker-started");
+ toolbox.nodePicker.start();
+ await onPickerStarted;
+
+ info(
+ `Picker mode started, now clicking on "${selector}" to select that node`
+ );
+ const onPickerStopped = toolbox.nodePicker.once("picker-stopped");
+ const onInspectorUpdated = inspector.once("inspector-updated");
+
+ await safeSynthesizeMouseEventAtCenterInContentPage(selector);
+
+ await onPickerStopped;
+ await onInspectorUpdated;
+}
+
+/**
+ * Clicks on the arrow of a single object inspector node if it exists.
+ *
+ * @param {HTMLElement} node: Object inspector node (.tree-node)
+ */
+function expandObjectInspectorNode(node) {
+ const arrow = getObjectInspectorNodeArrow(node);
+ if (!arrow) {
+ ok(false, "Node can't be expanded");
+ return;
+ }
+ arrow.click();
+}
+
+/**
+ * Retrieve the arrow of a single object inspector node.
+ *
+ * @param {HTMLElement} node: Object inspector node (.tree-node)
+ * @return {HTMLElement|null} the arrow element
+ */
+function getObjectInspectorNodeArrow(node) {
+ return node.querySelector(".arrow");
+}
+
+/**
+ * Check if a single object inspector node is expandable.
+ *
+ * @param {HTMLElement} node: Object inspector node (.tree-node)
+ * @return {Boolean} true if the node can be expanded
+ */
+function isObjectInspectorNodeExpandable(node) {
+ return !!getObjectInspectorNodeArrow(node);
+}
+
+/**
+ * Retrieve the nodes for a given object inspector element.
+ *
+ * @param {HTMLElement} oi: Object inspector element
+ * @return {NodeList} the object inspector nodes
+ */
+function getObjectInspectorNodes(oi) {
+ return oi.querySelectorAll(".tree-node");
+}
+
+/**
+ * Retrieve the "children" nodes for a given object inspector node.
+ *
+ * @param {HTMLElement} node: Object inspector node (.tree-node)
+ * @return {Array<HTMLElement>} the direct children (i.e. the ones that are one level
+ * deeper than the passed node)
+ */
+function getObjectInspectorChildrenNodes(node) {
+ const getLevel = n => parseInt(n.getAttribute("aria-level"), 10);
+ const level = getLevel(node);
+ const childLevel = level + 1;
+ const children = [];
+
+ let currentNode = node;
+ while (
+ currentNode.nextSibling &&
+ getLevel(currentNode.nextSibling) === childLevel
+ ) {
+ currentNode = currentNode.nextSibling;
+ children.push(currentNode);
+ }
+
+ return children;
+}
+
+/**
+ * Retrieve the invoke getter button for a given object inspector node.
+ *
+ * @param {HTMLElement} node: Object inspector node (.tree-node)
+ * @return {HTMLElement|null} the invoke button element
+ */
+function getObjectInspectorInvokeGetterButton(node) {
+ return node.querySelector(".invoke-getter");
+}
+
+/**
+ * Retrieve the first node that match the passed node label, for a given object inspector
+ * element.
+ *
+ * @param {HTMLElement} oi: Object inspector element
+ * @param {String} nodeLabel: label of the searched node
+ * @return {HTMLElement|null} the Object inspector node with the matching label
+ */
+function findObjectInspectorNode(oi, nodeLabel) {
+ return [...oi.querySelectorAll(".tree-node")].find(node => {
+ const label = node.querySelector(".object-label");
+ if (!label) {
+ return false;
+ }
+ return label.textContent === nodeLabel;
+ });
+}
+
+/**
+ * Return an array of the label of the autocomplete popup items.
+ *
+ * @param {AutocompletPopup} popup
+ * @returns {Array<String>}
+ */
+function getAutocompletePopupLabels(popup) {
+ return popup.getItems().map(item => item.label);
+}
+
+/**
+ * Check if the retrieved list of autocomplete labels of the specific popup
+ * includes all of the expected labels.
+ *
+ * @param {AutocompletPopup} popup
+ * @param {Array<String>} expected the array of expected labels
+ */
+function hasExactPopupLabels(popup, expected) {
+ return hasPopupLabels(popup, expected, true);
+}
+
+/**
+ * Check if the expected label is included in the list of autocomplete labels
+ * of the specific popup.
+ *
+ * @param {AutocompletPopup} popup
+ * @param {String} label the label to check
+ */
+function hasPopupLabel(popup, label) {
+ return hasPopupLabels(popup, [label]);
+}
+
+/**
+ * Validate the expected labels against the autocomplete labels.
+ *
+ * @param {AutocompletPopup} popup
+ * @param {Array<String>} expectedLabels
+ * @param {Boolean} checkAll
+ */
+function hasPopupLabels(popup, expectedLabels, checkAll = false) {
+ const autocompleteLabels = getAutocompletePopupLabels(popup);
+ if (checkAll) {
+ return (
+ autocompleteLabels.length === expectedLabels.length &&
+ autocompleteLabels.every((autoLabel, idx) => {
+ return expectedLabels.indexOf(autoLabel) === idx;
+ })
+ );
+ }
+ return expectedLabels.every(expectedLabel => {
+ return autocompleteLabels.includes(expectedLabel);
+ });
+}
+
+/**
+ * Return the "Confirm Dialog" element.
+ *
+ * @param toolbox
+ * @returns {HTMLElement|null}
+ */
+function getConfirmDialog(toolbox) {
+ const { doc } = toolbox;
+ return doc.querySelector(".invoke-confirm");
+}
+
+/**
+ * Returns true if the Confirm Dialog is opened.
+ * @param toolbox
+ * @returns {Boolean}
+ */
+function isConfirmDialogOpened(toolbox) {
+ const tooltip = getConfirmDialog(toolbox);
+ if (!tooltip) {
+ return false;
+ }
+
+ return tooltip.classList.contains("tooltip-visible");
+}
+
+async function selectFrame(dbg, frame) {
+ const onScopes = waitForDispatch(dbg.store, "ADD_SCOPES");
+ await dbg.actions.selectFrame(frame);
+ await onScopes;
+}
+
+async function pauseDebugger(dbg, options = { shouldWaitForLoadScopes: true }) {
+ info("Waiting for debugger to pause");
+ const onPaused = waitForPaused(dbg, null, options);
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.wrappedJSObject.firstCall();
+ }).catch(() => {});
+ await onPaused;
+}
+
+/**
+ * Check that the passed HTMLElement vertically overflows.
+ * @param {HTMLElement} container
+ * @returns {Boolean}
+ */
+function hasVerticalOverflow(container) {
+ return container.scrollHeight > container.clientHeight;
+}
+
+/**
+ * Check that the passed HTMLElement is scrolled to the bottom.
+ * @param {HTMLElement} container
+ * @returns {Boolean}
+ */
+function isScrolledToBottom(container) {
+ if (!container.lastChild) {
+ return true;
+ }
+ const lastNodeHeight = container.lastChild.clientHeight;
+ return (
+ container.scrollTop + container.clientHeight >=
+ container.scrollHeight - lastNodeHeight / 2
+ );
+}
+
+/**
+ *
+ * @param {WebConsole} hud
+ * @param {Array<String>} expectedMessages: An array of string representing the messages
+ * from the output. This can only be a part of the string of the
+ * message.
+ * Start the string with "▶︎⚠ " or "▼⚠ " to indicate that the
+ * message is a warningGroup (with respectively an open or
+ * collapsed arrow).
+ * Start the string with "|︎ " to indicate that the message is
+ * inside a group and should be indented.
+ */
+async function checkConsoleOutputForWarningGroup(hud, expectedMessages) {
+ const messages = await findAllMessagesVirtualized(hud);
+ is(
+ messages.length,
+ expectedMessages.length,
+ "Got the expected number of messages"
+ );
+
+ const isInWarningGroup = index => {
+ const message = expectedMessages[index];
+ if (!message.startsWith("|")) {
+ return false;
+ }
+ const groups = expectedMessages
+ .slice(0, index)
+ .reverse()
+ .filter(m => !m.startsWith("|"));
+ if (groups.length === 0) {
+ ok(false, "Unexpected structure: an indented message isn't in a group");
+ }
+
+ return groups[0].startsWith("▼︎⚠");
+ };
+
+ for (let [i, expectedMessage] of expectedMessages.entries()) {
+ // Refresh the reference to the message, as it may have been scrolled out of existence.
+ const message = await findMessageVirtualizedById({
+ hud,
+ messageId: messages[i].getAttribute("data-message-id"),
+ });
+ info(`Checking "${expectedMessage}"`);
+
+ // Collapsed Warning group
+ if (expectedMessage.startsWith("▶︎⚠")) {
+ is(
+ message.querySelector(".arrow").getAttribute("aria-expanded"),
+ "false",
+ "There's a collapsed arrow"
+ );
+ is(
+ message.getAttribute("data-indent"),
+ "0",
+ "The warningGroup has the expected indent"
+ );
+ expectedMessage = expectedMessage.replace("▶︎⚠ ", "");
+ }
+
+ // Expanded Warning group
+ if (expectedMessage.startsWith("▼︎⚠")) {
+ is(
+ message.querySelector(".arrow").getAttribute("aria-expanded"),
+ "true",
+ "There's an expanded arrow"
+ );
+ is(
+ message.getAttribute("data-indent"),
+ "0",
+ "The warningGroup has the expected indent"
+ );
+ expectedMessage = expectedMessage.replace("▼︎⚠ ", "");
+ }
+
+ // Collapsed console.group
+ if (expectedMessage.startsWith("▶︎")) {
+ is(
+ message.querySelector(".arrow").getAttribute("aria-expanded"),
+ "false",
+ "There's a collapsed arrow"
+ );
+ expectedMessage = expectedMessage.replace("▶︎ ", "");
+ }
+
+ // Expanded console.group
+ if (expectedMessage.startsWith("▼")) {
+ is(
+ message.querySelector(".arrow").getAttribute("aria-expanded"),
+ "true",
+ "There's an expanded arrow"
+ );
+ expectedMessage = expectedMessage.replace("▼ ", "");
+ }
+
+ // In-group message
+ if (expectedMessage.startsWith("|")) {
+ if (isInWarningGroup(i)) {
+ ok(
+ message.querySelector(".warning-indent"),
+ "The message has the expected indent"
+ );
+ }
+
+ expectedMessage = expectedMessage.replace("| ", "");
+ } else {
+ is(
+ message.getAttribute("data-indent"),
+ "0",
+ "The message has the expected indent"
+ );
+ }
+
+ ok(
+ message.textContent.trim().includes(expectedMessage.trim()),
+ `Message includes ` +
+ `the expected "${expectedMessage}" content - "${message.textContent.trim()}"`
+ );
+ }
+}
+
+/**
+ * Check that there is a message with the specified text that has the specified
+ * stack information. Self-hosted frames are ignored.
+ * @param {WebConsole} hud
+ * @param {string} text
+ * message substring to look for
+ * @param {Array<number>} expectedFrameLines
+ * line numbers of the frames expected in the stack
+ */
+async function checkMessageStack(hud, text, expectedFrameLines) {
+ info(`Checking message stack for "${text}"`);
+ const msgNode = await waitFor(
+ () => findErrorMessage(hud, text),
+ `Couln't find message including "${text}"`
+ );
+ ok(!msgNode.classList.contains("open"), `Error logged not expanded`);
+
+ const button = await waitFor(
+ () => msgNode.querySelector(".collapse-button"),
+ `Couldn't find the expand button on "${text}" message`
+ );
+ button.click();
+
+ const framesNode = await waitFor(
+ () => msgNode.querySelector(".message-body-wrapper > .stacktrace .frames"),
+ `Couldn't find stacktrace frames on "${text}" message`
+ );
+ const frameNodes = Array.from(framesNode.querySelectorAll(".frame")).filter(
+ el => {
+ const fileName = el.querySelector(".filename").textContent;
+ return (
+ fileName !== "self-hosted" &&
+ !fileName.startsWith("chrome:") &&
+ !fileName.startsWith("resource:")
+ );
+ }
+ );
+
+ for (let i = 0; i < frameNodes.length; i++) {
+ const frameNode = frameNodes[i];
+ is(
+ frameNode.querySelector(".line").textContent,
+ expectedFrameLines[i].toString(),
+ `Found line ${expectedFrameLines[i]} for frame #${i}`
+ );
+ }
+
+ is(
+ frameNodes.length,
+ expectedFrameLines.length,
+ `Found ${frameNodes.length} frames`
+ );
+}
+
+/**
+ * Reload the content page.
+ * @returns {Promise} A promise that will return when the page is fully loaded (i.e., the
+ * `load` event was fired).
+ */
+function reloadPage() {
+ const onLoad = BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "load",
+ true
+ );
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.location.reload();
+ });
+ return onLoad;
+}
+
+/**
+ * Check if the editor mode is enabled (i.e. .webconsole-app has the expected class).
+ *
+ * @param {WebConsole} hud
+ * @returns {Boolean}
+ */
+function isEditorModeEnabled(hud) {
+ const { outputNode } = hud.ui;
+ const appNode = outputNode.querySelector(".webconsole-app");
+ return appNode.classList.contains("jsterm-editor");
+}
+
+/**
+ * Toggle the layout between in-line and editor.
+ *
+ * @param {WebConsole} hud
+ * @returns {Promise} A promise that resolves once the layout change was rendered.
+ */
+function toggleLayout(hud) {
+ const isMacOS = Services.appinfo.OS === "Darwin";
+ const enabled = isEditorModeEnabled(hud);
+
+ EventUtils.synthesizeKey("b", {
+ [isMacOS ? "metaKey" : "ctrlKey"]: true,
+ });
+ return waitFor(() => isEditorModeEnabled(hud) === !enabled);
+}
+
+/**
+ * Wait until all lazily fetch requests in netmonitor get finished.
+ * Otherwise test will be shutdown too early and cause failure.
+ */
+async function waitForLazyRequests(toolbox) {
+ const ui = toolbox.getCurrentPanel().hud.ui;
+ return waitUntil(() => {
+ return (
+ !ui.networkDataProvider.lazyRequestData.size &&
+ // Make sure that batched request updates are all complete
+ // as they trigger late lazy data requests.
+ !ui.wrapper.queuedRequestUpdates.length
+ );
+ });
+}
+
+/**
+ * Clear the console output and wait for eventual object actors to be released.
+ *
+ * @param {WebConsole} hud
+ * @param {Object} An options object with the following properties:
+ * - {Boolean} keepStorage: true to prevent clearing the messages storage.
+ */
+async function clearOutput(hud, { keepStorage = false } = {}) {
+ const { ui } = hud;
+ const promises = [ui.once("messages-cleared")];
+
+ // If there's an object inspector, we need to wait for the actors to be released.
+ if (ui.outputNode.querySelector(".object-inspector")) {
+ promises.push(ui.once("fronts-released"));
+ }
+
+ ui.clearOutput(!keepStorage);
+ await Promise.all(promises);
+}
+
+/**
+ * Retrieve all the items of the context selector menu.
+ *
+ * @param {WebConsole} hud
+ * @return Array<Element>
+ */
+function getContextSelectorItems(hud) {
+ const toolbox = hud.toolbox;
+ const doc = toolbox ? toolbox.doc : hud.chromeWindow.document;
+ const list = doc.getElementById(
+ "webconsole-console-evaluation-context-selector-menu-list"
+ );
+ return Array.from(list.querySelectorAll("li.menuitem button, hr"));
+}
+
+/**
+ * Check that the evaluation context selector menu has the expected item, in the expected
+ * state.
+ *
+ * @param {WebConsole} hud
+ * @param {Array<Object>} expected: An array of object (see checkContextSelectorMenuItemAt
+ * for expected properties)
+ */
+function checkContextSelectorMenu(hud, expected) {
+ const items = getContextSelectorItems(hud);
+
+ is(
+ items.length,
+ expected.length,
+ "The context selector menu has the expected number of items"
+ );
+
+ expected.forEach((expectedItem, i) => {
+ checkContextSelectorMenuItemAt(hud, i, expectedItem);
+ });
+}
+
+/**
+ * Check that the evaluation context selector menu has the expected item at the specified index.
+ *
+ * @param {WebConsole} hud
+ * @param {Number} index
+ * @param {Object} expected
+ * @param {String} expected.label: The label of the target
+ * @param {String} expected.tooltip: The tooltip of the target element in the menu
+ * @param {Boolean} expected.checked: if the target should be selected or not
+ * @param {Boolean} expected.separator: if the element is a simple separator
+ */
+function checkContextSelectorMenuItemAt(hud, index, expected) {
+ const el = getContextSelectorItems(hud).at(index);
+
+ if (expected.separator === true) {
+ is(el.getAttribute("role"), "menuseparator", "The element is a separator");
+ return;
+ }
+
+ const elChecked = el.getAttribute("aria-checked") === "true";
+ const elTooltip = el.getAttribute("title");
+ const elLabel = el.querySelector(".label").innerText;
+
+ is(elLabel, expected.label, `The item has the expected label`);
+ is(elTooltip, expected.tooltip, `Item "${elLabel}" has the expected tooltip`);
+ is(
+ elChecked,
+ expected.checked,
+ `Item "${elLabel}" is ${expected.checked ? "checked" : "unchecked"}`
+ );
+}
+
+/**
+ * Select a target in the context selector.
+ *
+ * @param {WebConsole} hud
+ * @param {String} targetLabel: The label of the target to select.
+ */
+function selectTargetInContextSelector(hud, targetLabel) {
+ const items = getContextSelectorItems(hud);
+ const itemToSelect = items.find(
+ item => item.querySelector(".label")?.innerText === targetLabel
+ );
+ if (!itemToSelect) {
+ ok(false, `Couldn't find target with "${targetLabel}" label`);
+ return;
+ }
+
+ itemToSelect.click();
+}
+
+/**
+ * A helper that returns the size of the image that was just put into the clipboard by the
+ * :screenshot command.
+ * @return The {width, height} dimension object.
+ */
+async function getImageSizeFromClipboard() {
+ const clipid = Ci.nsIClipboard;
+ const clip = Cc["@mozilla.org/widget/clipboard;1"].getService(clipid);
+ const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ const flavor = "image/png";
+ trans.init(null);
+ trans.addDataFlavor(flavor);
+
+ clip.getData(
+ trans,
+ clipid.kGlobalClipboard,
+ SpecialPowers.wrap(window).browsingContext.currentWindowContext
+ );
+ const data = {};
+ trans.getTransferData(flavor, data);
+
+ ok(data.value, "screenshot exists");
+
+ let image = data.value;
+
+ // Due to the differences in how images could be stored in the clipboard the
+ // checks below are needed. The clipboard could already provide the image as
+ // byte streams or as image container. If it's not possible obtain a
+ // byte stream, the function throws.
+
+ if (image instanceof Ci.imgIContainer) {
+ image = Cc["@mozilla.org/image/tools;1"]
+ .getService(Ci.imgITools)
+ .encodeImage(image, flavor);
+ }
+
+ if (!(image instanceof Ci.nsIInputStream)) {
+ throw new Error("Unable to read image data");
+ }
+
+ const binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ binaryStream.setInputStream(image);
+ const available = binaryStream.available();
+ const buffer = new ArrayBuffer(available);
+ is(
+ binaryStream.readArrayBuffer(available, buffer),
+ available,
+ "Read expected amount of data"
+ );
+
+ // We are going to load the image in the content page to measure its size.
+ // We don't want to insert the image directly in the browser's document
+ // (which is value of the global `document` here). Doing so might push the
+ // toolbox upwards, shrink the content page and fail the fullpage screenshot
+ // test.
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [buffer],
+ async function (_buffer) {
+ const img = content.document.createElement("img");
+ const loaded = new Promise(r => {
+ img.addEventListener("load", r, { once: true });
+ });
+
+ // Build a URL from the buffer passed to the ContentTask
+ const url = content.URL.createObjectURL(
+ new Blob([_buffer], { type: "image/png" })
+ );
+
+ // Load the image
+ img.src = url;
+ content.document.documentElement.appendChild(img);
+
+ info("Waiting for the clipboard image to load in the content page");
+ await loaded;
+
+ // Remove the image and revoke the URL.
+ img.remove();
+ content.URL.revokeObjectURL(url);
+
+ return {
+ width: img.width,
+ height: img.height,
+ };
+ }
+ );
+}