// 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");
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();
// 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 }) { returnnew 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) { thrownew Error("typeSelector property is required");
} if (!typeSelector.startsWith(".")) { thrownew 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;
}
}
/** * 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) { returnfalse;
}
// 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;
}
returnfalse;
});
}
/** * 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");
}
/** * 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`); returnnull;
}
/** * 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();
}
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 = {}) { returnnew 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);
// 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}")`);
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;
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`
);
}
/** * 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");
/** * 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;
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();
/** * 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 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;
} returnfalse;
});
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");
/** * 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) { returnfalse;
} 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]);
}
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) { returntrue;
} 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("|")) { returnfalse;
} 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"
);
}
/** * 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();
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}`
);
}
/** * 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);
/** * 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"));
}
/** * 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;
}
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);
// 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.nsIInputStream)) { thrownew 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);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.