1968 lines
58 KiB
JavaScript
1968 lines
58 KiB
JavaScript
/* 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}")`);
|
||
|
||
// Redux location object uses 0-based column, while we display a 1-based one.
|
||
is(
|
||
parseInt(dbg._selectors.getSelectedLocation(dbg._getState()).column, 10) +
|
||
1,
|
||
parseInt(column, 10),
|
||
"expected source column"
|
||
);
|
||
}
|
||
|
||
if (logPointExpr !== undefined && logPointExpr !== "") {
|
||
const inputEl = dbg.panelWin.document.activeElement;
|
||
|
||
const isPanelFocused =
|
||
inputEl.classList.contains("cm-content") &&
|
||
inputEl.closest(".conditional-breakpoint-panel.log-point");
|
||
|
||
ok(isPanelFocused, "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)
|
||
*/
|
||
async function expandObjectInspectorNode(node) {
|
||
if (!node.classList.contains("tree-node")) {
|
||
ok(false, "Node should be a .tree-node");
|
||
return;
|
||
}
|
||
const arrow = getObjectInspectorNodeArrow(node);
|
||
if (!arrow) {
|
||
ok(false, "Node can't be expanded");
|
||
return;
|
||
}
|
||
if (arrow.classList.contains("open")) {
|
||
ok(false, "Node already expanded");
|
||
return;
|
||
}
|
||
const isLongString = node.querySelector(".node > .objectBox-string");
|
||
let onMutation;
|
||
let textContentBeforeExpand;
|
||
if (!isLongString) {
|
||
const objectInspector = node.closest(".object-inspector");
|
||
onMutation = waitForNodeMutation(objectInspector, {
|
||
childList: true,
|
||
});
|
||
} else {
|
||
textContentBeforeExpand = node.textContent;
|
||
}
|
||
arrow.click();
|
||
|
||
// Long strings are not going to be expanded into children element.
|
||
// Instead the tree node will update itself to show the long string.
|
||
// So that we can't wait for the childList mutation.
|
||
if (isLongString) {
|
||
// Reps will expand on click...
|
||
await waitFor(() => arrow.classList.contains("open"));
|
||
// ...but it will fetch the long string content asynchronously after having expanded the TreeNode.
|
||
// So also wait for the string to be updated and be longer.
|
||
await waitFor(
|
||
() => node.textContent.length > textContentBeforeExpand.length
|
||
);
|
||
} else {
|
||
await onMutation;
|
||
// Waiting for the object inspector mutation isn't enough,
|
||
// also wait for the children element, with higher aria-level to be added to the DOM.
|
||
await waitFor(() => !!getObjectInspectorChildrenNodes(node).length);
|
||
}
|
||
|
||
ok(
|
||
arrow.classList.contains("open"),
|
||
"The arrow of the root node of the tree is expanded after clicking on it"
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 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(".theme-twisty");
|
||
}
|
||
|
||
/**
|
||
* 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") || "0", 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) {
|
||
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
|
||
* @param {Boolean} expected.indented: if the element is indented
|
||
*/
|
||
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;
|
||
const indented = el.classList.contains("indented");
|
||
|
||
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"}`
|
||
);
|
||
is(
|
||
indented,
|
||
expected.indented ?? false,
|
||
`Item "${elLabel}" is ${!indented ? " not" : ""} indented`
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 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,
|
||
};
|
||
}
|
||
);
|
||
}
|