Source code

Revision control

Copy as Markdown

Other Tools

/* Any copyright is dedicated to the Public Domain.
/* 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(
this
);
// Import helpers for the new debugger
Services.scriptloader.loadSubScript(
this
);
Services.scriptloader.loadSubScript(
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
*/
function checkContextSelectorMenuItemAt(hud, index, expected) {
const el = getContextSelectorItems(hud).at(index);
if (expected.separator === true) {
is(el.getAttribute("role"), "menuseparator", "The element is a separator");
return;
}
const elChecked = el.getAttribute("aria-checked") === "true";
const elTooltip = el.getAttribute("title");
const elLabel = el.querySelector(".label").innerText;
is(elLabel, expected.label, `The item has the expected label`);
is(elTooltip, expected.tooltip, `Item "${elLabel}" has the expected tooltip`);
is(
elChecked,
expected.checked,
`Item "${elLabel}" is ${expected.checked ? "checked" : "unchecked"}`
);
}
/**
* Select a target in the context selector.
*
* @param {WebConsole} hud
* @param {String} targetLabel: The label of the target to select.
*/
function selectTargetInContextSelector(hud, targetLabel) {
const items = getContextSelectorItems(hud);
const itemToSelect = items.find(
item => item.querySelector(".label")?.innerText === targetLabel
);
if (!itemToSelect) {
ok(false, `Couldn't find target with "${targetLabel}" label`);
return;
}
itemToSelect.click();
}
/**
* A helper that returns the size of the image that was just put into the clipboard by the
* :screenshot command.
* @return The {width, height} dimension object.
*/
async function getImageSizeFromClipboard() {
const clipid = Ci.nsIClipboard;
const clip = Cc["@mozilla.org/widget/clipboard;1"].getService(clipid);
const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
Ci.nsITransferable
);
const flavor = "image/png";
trans.init(null);
trans.addDataFlavor(flavor);
clip.getData(
trans,
clipid.kGlobalClipboard,
SpecialPowers.wrap(window).browsingContext.currentWindowContext
);
const data = {};
trans.getTransferData(flavor, data);
ok(data.value, "screenshot exists");
let image = data.value;
// Due to the differences in how images could be stored in the clipboard the
// checks below are needed. The clipboard could already provide the image as
// byte streams or as image container. If it's not possible obtain a
// byte stream, the function throws.
if (image instanceof Ci.imgIContainer) {
image = Cc["@mozilla.org/image/tools;1"]
.getService(Ci.imgITools)
.encodeImage(image, flavor);
}
if (!(image instanceof Ci.nsIInputStream)) {
throw new Error("Unable to read image data");
}
const binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
Ci.nsIBinaryInputStream
);
binaryStream.setInputStream(image);
const available = binaryStream.available();
const buffer = new ArrayBuffer(available);
is(
binaryStream.readArrayBuffer(available, buffer),
available,
"Read expected amount of data"
);
// We are going to load the image in the content page to measure its size.
// We don't want to insert the image directly in the browser's document
// (which is value of the global `document` here). Doing so might push the
// toolbox upwards, shrink the content page and fail the fullpage screenshot
// test.
return SpecialPowers.spawn(
gBrowser.selectedBrowser,
[buffer],
async function (_buffer) {
const img = content.document.createElement("img");
const loaded = new Promise(r => {
img.addEventListener("load", r, { once: true });
});
// Build a URL from the buffer passed to the ContentTask
const url = content.URL.createObjectURL(
new Blob([_buffer], { type: "image/png" })
);
// Load the image
img.src = url;
content.document.documentElement.appendChild(img);
info("Waiting for the clipboard image to load in the content page");
await loaded;
// Remove the image and revoke the URL.
img.remove();
content.URL.revokeObjectURL(url);
return {
width: img.width,
height: img.height,
};
}
);
}