summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/test/browser/shared-head.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webconsole/test/browser/shared-head.js')
-rw-r--r--devtools/client/webconsole/test/browser/shared-head.js514
1 files changed, 514 insertions, 0 deletions
diff --git a/devtools/client/webconsole/test/browser/shared-head.js b/devtools/client/webconsole/test/browser/shared-head.js
new file mode 100644
index 0000000000..868a6fccc4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/shared-head.js
@@ -0,0 +1,514 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+/**
+ * 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);
+}