/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at . */ /** * Helper methods for finding messages in the virtualized output of the * webconsole. This file can be safely required from other panel test * files. */ "use strict"; /* eslint-disable no-unused-vars */ // Assume that shared-head is always imported before this file /* import-globals-from ../../../shared/test/shared-head.js */ /** * Find a message with given messageId in the output, scrolling through the * output from top to bottom in order to make sure the messages are actually * rendered. * * @param object hud * The web console. * @param messageId * A message ID to look for. This could be baked into the selector, but * is provided as a convenience. * @return {Node} the node corresponding the found message */ async function findMessageVirtualizedById({ hud, messageId }) { if (!messageId) { throw new Error("messageId parameter is required"); } const elements = await findMessagesVirtualized({ hud, expectedCount: 1, messageId, }); return elements.at(-1); } /** * Find the last message with given message type in the output, scrolling * through the output from top to bottom in order to make sure the messages are * actually rendered. * * @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 findMessageVirtualizedByType({ hud, text, typeSelector }) { const elements = await findMessagesVirtualizedByType({ hud, text, typeSelector, expectedCount: 1, }); return elements.at(-1); } /** * Find all messages in the output, scrolling through the output from top * to bottom in order to make sure the messages are actually rendered. * * @param object hud * The web console. * @return {Array} all of the message nodes in the console output. Some of * these may be stale from having been scrolled out of view. */ async function findAllMessagesVirtualized(hud) { return findMessagesVirtualized({ hud }); } // This is just a reentrancy guard. Because findMessagesVirtualized mucks // around with the scroll position, if we do something like // let promise1 = findMessagesVirtualized(...); // let promise2 = findMessagesVirtualized(...); // await promise1; // await promise2; // then the two calls will end up messing up each other's expected scroll // position, at which point they could get stuck. This lets us throw an // error when that happens. let gInFindMessagesVirtualized = false; // And this lets us get a little more information in the error - it just holds // the stack of the prior call. let gFindMessagesVirtualizedStack = null; /** * Find multiple messages in the output, scrolling through the output from top * to bottom in order to make sure the messages are actually rendered. * * @param object options * @param object options.hud * The web console. * @param options.text [optional] * A substring that can be found in the message. * @param options.typeSelector * A part of selector for the message, to specify the message type. * @param options.expectedCount [optional] * The number of messages to get. This lets us stop scrolling early if * we find that number of messages. * @return {Array} all of the message nodes in the console output matching the * provided filters. If expectedCount is greater than 1, or equal to -1, * some of these may be stale from having been scrolled out of view. */ async function findMessagesVirtualizedByType({ hud, text, typeSelector, expectedCount, }) { if (!typeSelector) { throw new Error("typeSelector parameter is required"); } if (!typeSelector.startsWith(".")) { throw new Error("typeSelector should start with a dot e.g. `.result`"); } return findMessagesVirtualized({ hud, text, selector: ".message" + typeSelector, expectedCount, }); } /** * Find multiple messages in the output, scrolling through the output from top * to bottom in order to make sure the messages are actually rendered. * * @param object options * @param object options.hud * The web console. * @param options.text [optional] * A substring that can be found in the message. * @param options.selector [optional] * The selector to use in finding the message. * @param options.expectedCount [optional] * The number of messages to get. This lets us stop scrolling early if * we find that number of messages. * @param options.messageId [optional] * A message ID to look for. This could be baked into the selector, but * is provided as a convenience. * @return {Array} all of the message nodes in the console output matching the * provided filters. If expectedCount is greater than 1, or equal to -1, * some of these may be stale from having been scrolled out of view. */ async function findMessagesVirtualized({ hud, text, selector, expectedCount, messageId, }) { if (text === undefined) { text = ""; } if (selector === undefined) { selector = ".message"; } if (expectedCount === undefined) { expectedCount = -1; } const outputNode = hud.ui.outputNode; const scrollport = outputNode.querySelector(".webconsole-output"); function getVisibleMessageIds() { return JSON.parse(scrollport.getAttribute("data-visible-messages")); } function getVisibleMessageMap() { return new Map( JSON.parse(scrollport.getAttribute("data-visible-messages")).map( (id, i) => [id, i] ) ); } function getMessageIndex(message) { return getVisibleMessageIds().indexOf( message.getAttribute("data-message-id") ); } function getNextMessageId(prevMessage) { const visible = getVisibleMessageIds(); let index = 0; if (prevMessage) { const lastId = prevMessage.getAttribute("data-message-id"); index = visible.indexOf(lastId); if (index === -1) { throw new Error( `Tried to get next message ID for message that doesn't exist. Last seen ID: ${lastId}, all visible ids: [${visible.join( ", " )}]` ); } } if (index + 1 >= visible.length) { return null; } return visible[index + 1]; } if (gInFindMessagesVirtualized) { throw new Error( `findMessagesVirtualized was re-entered somehow. This is not allowed. Other stack: [${gFindMessagesVirtualizedStack}]` ); } try { gInFindMessagesVirtualized = true; gFindMessagesVirtualizedStack = new Error().stack; // The console output will automatically scroll to the bottom of the // scrollport in certain circumstances. Because we need to scroll the // output to find all messages, we need to disable this. This attribute // controls that. scrollport.setAttribute("disable-autoscroll", ""); // This array is here purely for debugging purposes. We collect the indices // of every element we see in order to validate that we don't have any gaps // in the list. const allIndices = []; const allElements = []; const seenIds = new Set(); let lastItem = null; while (true) { if (scrollport.scrollHeight > scrollport.clientHeight) { if (!lastItem && scrollport.scrollTop != 0) { // For simplicity's sake, we always start from the top of the output. scrollport.scrollTop = 0; } else if (!lastItem && scrollport.scrollTop == 0) { // We want to make sure that we actually change the scroll position // here, because we're going to wait for an update below regardless, // just to flush out any changes that may have just happened. If we // don't do this, and there were no changes before this function was // called, then we'll just hang on the promise below. scrollport.scrollTop = 1; } else { // This is the core of the loop. Scroll down to the bottom of the // current scrollport, wait until we see the element after the last // one we've seen, and then harvest the messages that are displayed. scrollport.scrollTop = scrollport.scrollTop + scrollport.clientHeight; } // Wait for something to happen in the output before checking for our // expected next message. await new Promise(resolve => hud.ui.once("lazy-message-list-updated-or-noop", resolve) ); try { await waitFor(async () => { const nextMessageId = getNextMessageId(lastItem); if ( nextMessageId === undefined || scrollport.querySelector(`[data-message-id="${nextMessageId}"]`) ) { return true; } // After a scroll, we typically expect to get an updated list of // elements. However, we have some slack at the top of the list, // because we draw elements above and below the actual scrollport to // avoid white flashes when async scrolling. const scrollTarget = scrollport.scrollTop + scrollport.clientHeight; scrollport.scrollTop = scrollTarget; await new Promise(resolve => hud.ui.once("lazy-message-list-updated-or-noop", resolve) ); return false; }); } catch (e) { throw new Error( `Failed waiting for next message ID (${getNextMessageId( lastItem )}) Visible messages: [${[ ...scrollport.querySelectorAll(".message"), ].map(el => el.getAttribute("data-message-id"))}]` ); } } const bottomPlaceholder = scrollport.querySelector( ".lazy-message-list-bottom" ); if (!bottomPlaceholder) { // When there are no messages in the output, there is also no // top/bottom placeholder. There's nothing more to do at this point, // so break and return. break; } lastItem = bottomPlaceholder.previousSibling; // This chunk is just validating that we have no gaps in our output so // far. const indices = [...scrollport.querySelectorAll("[data-message-id]")] .filter( el => el !== scrollport.firstChild && el !== scrollport.lastChild ) .map(el => getMessageIndex(el)); allIndices.push(...indices); allIndices.sort((lhs, rhs) => lhs - rhs); for (let i = 1; i < allIndices.length; i++) { if ( allIndices[i] != allIndices[i - 1] && allIndices[i] != allIndices[i - 1] + 1 ) { throw new Error( `Gap detected in virtualized webconsole output between ${ allIndices[i - 1] } and ${allIndices[i]}. Indices: ${allIndices.join(",")}` ); } } const messages = scrollport.querySelectorAll(selector); const filtered = [...messages].filter( el => // Core user filters: el.textContent.includes(text) && (!messageId || el.getAttribute("data-message-id") === messageId) && // Make sure we don't collect duplicate messages: !seenIds.has(el.getAttribute("data-message-id")) ); allElements.push(...filtered); for (const message of filtered) { seenIds.add(message.getAttribute("data-message-id")); } if (expectedCount >= 0 && allElements.length >= expectedCount) { break; } // If the bottom placeholder has 0 height, it means we've scrolled to the // bottom and output all the items. if (bottomPlaceholder.getBoundingClientRect().height == 0) { break; } await waitForTime(0); } // Finally, we get the map of message IDs to indices within the output, and // sort the message nodes according to that index. They can come in out of // order for a number of reasons (we continue rendering any messages that // have been expanded, and we always render the topmost and bottommost // messages for a11y reasons.) const idsToIndices = getVisibleMessageMap(); allElements.sort( (lhs, rhs) => idsToIndices.get(lhs.getAttribute("data-message-id")) - idsToIndices.get(rhs.getAttribute("data-message-id")) ); return allElements; } finally { scrollport.removeAttribute("disable-autoscroll"); gInFindMessagesVirtualized = false; gFindMessagesVirtualizedStack = null; } } /** * Find the last message with given message type in the output. * * @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, otherwise undefined */ function findMessageByType(hud, text, typeSelector) { const elements = findMessagesByType(hud, text, typeSelector); return elements.at(-1); } /** * Find multiple messages with given message type in the output. * * @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 {Array} The nodes corresponding the found messages */ function findMessagesByType(hud, text, typeSelector) { if (!typeSelector) { throw new Error("typeSelector parameter is required"); } if (!typeSelector.startsWith(".")) { throw new Error("typeSelector should start with a dot e.g. `.result`"); } const selector = ".message" + typeSelector; const messages = hud.ui.outputNode.querySelectorAll(selector); const elements = Array.from(messages).filter(el => el.textContent.includes(text) ); return elements; } /** * Find all messages in the output. * * @param object hud * The web console. * @return {Array} The nodes corresponding the found messages */ function findAllMessages(hud) { const messages = hud.ui.outputNode.querySelectorAll(".message"); return Array.from(messages); } /** * Find a part of the last message with given message type in the output. * * @param object hud * The web console. * @param object options * - text : {String} A substring that can be found in the message. * - typeSelector: {String} A part of selector for the message, * to specify the message type. * - partSelector: {String} A selector for the part of the message. * @return {Node} the node corresponding the found part, otherwise undefined */ function findMessagePartByType(hud, options) { const elements = findMessagePartsByType(hud, options); return elements.at(-1); } /** * Find parts of multiple messages with given message type in the output. * * @param object hud * The web console. * @param object options * - text : {String} A substring that can be found in the message. * - typeSelector: {String} A part of selector for the message, * to specify the message type. * - partSelector: {String} A selector for the part of the message. * @return {Array} The nodes corresponding the found parts */ function findMessagePartsByType(hud, { text, typeSelector, partSelector }) { if (!typeSelector) { throw new Error("typeSelector parameter is required"); } if (!typeSelector.startsWith(".")) { throw new Error("typeSelector should start with a dot e.g. `.result`"); } if (!partSelector) { throw new Error("partSelector parameter is required"); } const selector = ".message" + typeSelector + " " + partSelector; const parts = hud.ui.outputNode.querySelectorAll(selector); const elements = Array.from(parts).filter(el => el.textContent.includes(text) ); return elements; } /** * Type-specific wrappers for findMessageByType and findMessagesByType. * * @param object hud * The web console. * @param string text * A substring that can be found in the message. * @param string extraSelector [optional] * An extra part of selector for the message, in addition to * type-specific selector. * @return {Node|Array} See findMessageByType or findMessagesByType. */ function findEvaluationResultMessage(hud, text, extraSelector = "") { return findMessageByType(hud, text, ".result" + extraSelector); } function findEvaluationResultMessages(hud, text, extraSelector = "") { return findMessagesByType(hud, text, ".result" + extraSelector); } function findErrorMessage(hud, text, extraSelector = "") { return findMessageByType(hud, text, ".error" + extraSelector); } function findErrorMessages(hud, text, extraSelector = "") { return findMessagesByType(hud, text, ".error" + extraSelector); } function findWarningMessage(hud, text, extraSelector = "") { return findMessageByType(hud, text, ".warn" + extraSelector); } function findWarningMessages(hud, text, extraSelector = "") { return findMessagesByType(hud, text, ".warn" + extraSelector); } function findConsoleAPIMessage(hud, text, extraSelector = "") { return findMessageByType(hud, text, ".console-api" + extraSelector); } function findConsoleAPIMessages(hud, text, extraSelector = "") { return findMessagesByType(hud, text, ".console-api" + extraSelector); } function findNetworkMessage(hud, text, extraSelector = "") { return findMessageByType(hud, text, ".network" + extraSelector); } function findNetworkMessages(hud, text, extraSelector = "") { return findMessagesByType(hud, text, ".network" + extraSelector); }