/* 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;
const isPanelFocused = isCm6Enabled
? inputEl.classList.contains(
"cm-content") &&
inputEl.closest(
".conditional-breakpoint-panel.log-point")
: inputEl.tagName ==
"TEXTAREA";
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 = { 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
* @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,
};
}
);
}