517 lines
18 KiB
JavaScript
517 lines
18 KiB
JavaScript
/* 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);
|
|
}
|
|
function findTracerMessages(hud, text, extraSelector = "") {
|
|
return findMessagesByType(hud, text, ".jstracer" + extraSelector);
|
|
}
|