tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

shared-head.js (18250B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
      4 
      5 /**
      6 * Helper methods for finding messages in the virtualized output of the
      7 * webconsole. This file can be safely required from other panel test
      8 * files.
      9 */
     10 
     11 "use strict";
     12 
     13 /* eslint-disable no-unused-vars */
     14 
     15 // Assume that shared-head is always imported before this file
     16 /* import-globals-from ../../../shared/test/shared-head.js */
     17 
     18 /**
     19 * Find a message with given messageId in the output, scrolling through the
     20 * output from top to bottom in order to make sure the messages are actually
     21 * rendered.
     22 *
     23 * @param object hud
     24 *        The web console.
     25 * @param messageId
     26 *        A message ID to look for. This could be baked into the selector, but
     27 *        is provided as a convenience.
     28 * @return {Node} the node corresponding the found message
     29 */
     30 async function findMessageVirtualizedById({ hud, messageId }) {
     31  if (!messageId) {
     32    throw new Error("messageId parameter is required");
     33  }
     34 
     35  const elements = await findMessagesVirtualized({
     36    hud,
     37    expectedCount: 1,
     38    messageId,
     39  });
     40  return elements.at(-1);
     41 }
     42 
     43 /**
     44 * Find the last message with given message type in the output, scrolling
     45 * through the output from top to bottom in order to make sure the messages are
     46 * actually rendered.
     47 *
     48 * @param object hud
     49 *        The web console.
     50 * @param string text
     51 *        A substring that can be found in the message.
     52 * @param string typeSelector
     53 *        A part of selector for the message, to specify the message type.
     54 * @return {Node} the node corresponding the found message
     55 */
     56 async function findMessageVirtualizedByType({ hud, text, typeSelector }) {
     57  const elements = await findMessagesVirtualizedByType({
     58    hud,
     59    text,
     60    typeSelector,
     61    expectedCount: 1,
     62  });
     63  return elements.at(-1);
     64 }
     65 
     66 /**
     67 * Find all messages in the output, scrolling through the output from top
     68 * to bottom in order to make sure the messages are actually rendered.
     69 *
     70 * @param object hud
     71 *        The web console.
     72 * @return {Array} all of the message nodes in the console output. Some of
     73 *        these may be stale from having been scrolled out of view.
     74 */
     75 async function findAllMessagesVirtualized(hud) {
     76  return findMessagesVirtualized({ hud });
     77 }
     78 
     79 // This is just a reentrancy guard. Because findMessagesVirtualized mucks
     80 // around with the scroll position, if we do something like
     81 //   let promise1 = findMessagesVirtualized(...);
     82 //   let promise2 = findMessagesVirtualized(...);
     83 //   await promise1;
     84 //   await promise2;
     85 // then the two calls will end up messing up each other's expected scroll
     86 // position, at which point they could get stuck. This lets us throw an
     87 // error when that happens.
     88 let gInFindMessagesVirtualized = false;
     89 // And this lets us get a little more information in the error - it just holds
     90 // the stack of the prior call.
     91 let gFindMessagesVirtualizedStack = null;
     92 
     93 /**
     94 * Find multiple messages in the output, scrolling through the output from top
     95 * to bottom in order to make sure the messages are actually rendered.
     96 *
     97 * @param object options
     98 * @param object options.hud
     99 *        The web console.
    100 * @param options.text [optional]
    101 *        A substring that can be found in the message.
    102 * @param options.typeSelector
    103 *        A part of selector for the message, to specify the message type.
    104 * @param options.expectedCount [optional]
    105 *        The number of messages to get. This lets us stop scrolling early if
    106 *        we find that number of messages.
    107 * @return {Array} all of the message nodes in the console output matching the
    108 *        provided filters. If expectedCount is greater than 1, or equal to -1,
    109 *        some of these may be stale from having been scrolled out of view.
    110 */
    111 async function findMessagesVirtualizedByType({
    112  hud,
    113  text,
    114  typeSelector,
    115  expectedCount,
    116 }) {
    117  if (!typeSelector) {
    118    throw new Error("typeSelector parameter is required");
    119  }
    120  if (!typeSelector.startsWith(".")) {
    121    throw new Error("typeSelector should start with a dot e.g. `.result`");
    122  }
    123 
    124  return findMessagesVirtualized({
    125    hud,
    126    text,
    127    selector: ".message" + typeSelector,
    128    expectedCount,
    129  });
    130 }
    131 
    132 /**
    133 * Find multiple messages in the output, scrolling through the output from top
    134 * to bottom in order to make sure the messages are actually rendered.
    135 *
    136 * @param object options
    137 * @param object options.hud
    138 *        The web console.
    139 * @param options.text [optional]
    140 *        A substring that can be found in the message.
    141 * @param options.selector [optional]
    142 *        The selector to use in finding the message.
    143 * @param options.expectedCount [optional]
    144 *        The number of messages to get. This lets us stop scrolling early if
    145 *        we find that number of messages.
    146 * @param options.messageId [optional]
    147 *        A message ID to look for. This could be baked into the selector, but
    148 *        is provided as a convenience.
    149 * @return {Array} all of the message nodes in the console output matching the
    150 *        provided filters. If expectedCount is greater than 1, or equal to -1,
    151 *        some of these may be stale from having been scrolled out of view.
    152 */
    153 async function findMessagesVirtualized({
    154  hud,
    155  text,
    156  selector,
    157  expectedCount,
    158  messageId,
    159 }) {
    160  if (text === undefined) {
    161    text = "";
    162  }
    163  if (selector === undefined) {
    164    selector = ".message";
    165  }
    166  if (expectedCount === undefined) {
    167    expectedCount = -1;
    168  }
    169 
    170  const outputNode = hud.ui.outputNode;
    171  const scrollport = outputNode.querySelector(".webconsole-output");
    172 
    173  function getVisibleMessageIds() {
    174    return JSON.parse(scrollport.getAttribute("data-visible-messages"));
    175  }
    176 
    177  function getVisibleMessageMap() {
    178    return new Map(
    179      JSON.parse(scrollport.getAttribute("data-visible-messages")).map(
    180        (id, i) => [id, i]
    181      )
    182    );
    183  }
    184 
    185  function getMessageIndex(message) {
    186    return getVisibleMessageIds().indexOf(
    187      message.getAttribute("data-message-id")
    188    );
    189  }
    190 
    191  function getNextMessageId(prevMessage) {
    192    const visible = getVisibleMessageIds();
    193    let index = 0;
    194    if (prevMessage) {
    195      const lastId = prevMessage.getAttribute("data-message-id");
    196      index = visible.indexOf(lastId);
    197      if (index === -1) {
    198        throw new Error(
    199          `Tried to get next message ID for message that doesn't exist. Last seen ID: ${lastId}, all visible ids: [${visible.join(
    200            ", "
    201          )}]`
    202        );
    203      }
    204    }
    205    if (index + 1 >= visible.length) {
    206      return null;
    207    }
    208    return visible[index + 1];
    209  }
    210 
    211  if (gInFindMessagesVirtualized) {
    212    throw new Error(
    213      `findMessagesVirtualized was re-entered somehow. This is not allowed. Other stack: [${gFindMessagesVirtualizedStack}]`
    214    );
    215  }
    216  try {
    217    gInFindMessagesVirtualized = true;
    218    gFindMessagesVirtualizedStack = new Error().stack;
    219    // The console output will automatically scroll to the bottom of the
    220    // scrollport in certain circumstances. Because we need to scroll the
    221    // output to find all messages, we need to disable this. This attribute
    222    // controls that.
    223    scrollport.setAttribute("disable-autoscroll", "");
    224 
    225    // This array is here purely for debugging purposes. We collect the indices
    226    // of every element we see in order to validate that we don't have any gaps
    227    // in the list.
    228    const allIndices = [];
    229 
    230    const allElements = [];
    231    const seenIds = new Set();
    232    let lastItem = null;
    233    while (true) {
    234      if (scrollport.scrollHeight > scrollport.clientHeight) {
    235        if (!lastItem && scrollport.scrollTop != 0) {
    236          // For simplicity's sake, we always start from the top of the output.
    237          scrollport.scrollTop = 0;
    238        } else if (!lastItem && scrollport.scrollTop == 0) {
    239          // We want to make sure that we actually change the scroll position
    240          // here, because we're going to wait for an update below regardless,
    241          // just to flush out any changes that may have just happened. If we
    242          // don't do this, and there were no changes before this function was
    243          // called, then we'll just hang on the promise below.
    244          scrollport.scrollTop = 1;
    245        } else {
    246          // This is the core of the loop. Scroll down to the bottom of the
    247          // current scrollport, wait until we see the element after the last
    248          // one we've seen, and then harvest the messages that are displayed.
    249          scrollport.scrollTop = scrollport.scrollTop + scrollport.clientHeight;
    250        }
    251 
    252        // Wait for something to happen in the output before checking for our
    253        // expected next message.
    254        await new Promise(resolve =>
    255          hud.ui.once("lazy-message-list-updated-or-noop", resolve)
    256        );
    257 
    258        try {
    259          await waitFor(async () => {
    260            const nextMessageId = getNextMessageId(lastItem);
    261            if (
    262              nextMessageId === undefined ||
    263              scrollport.querySelector(`[data-message-id="${nextMessageId}"]`)
    264            ) {
    265              return true;
    266            }
    267 
    268            // After a scroll, we typically expect to get an updated list of
    269            // elements. However, we have some slack at the top of the list,
    270            // because we draw elements above and below the actual scrollport to
    271            // avoid white flashes when async scrolling.
    272            const scrollTarget = scrollport.scrollTop + scrollport.clientHeight;
    273            scrollport.scrollTop = scrollTarget;
    274            await new Promise(resolve =>
    275              hud.ui.once("lazy-message-list-updated-or-noop", resolve)
    276            );
    277            return false;
    278          });
    279        } catch (e) {
    280          throw new Error(
    281            `Failed waiting for next message ID (${getNextMessageId(
    282              lastItem
    283            )}) Visible messages: [${[
    284              ...scrollport.querySelectorAll(".message"),
    285            ].map(el => el.getAttribute("data-message-id"))}]`
    286          );
    287        }
    288      }
    289 
    290      const bottomPlaceholder = scrollport.querySelector(
    291        ".lazy-message-list-bottom"
    292      );
    293      if (!bottomPlaceholder) {
    294        // When there are no messages in the output, there is also no
    295        // top/bottom placeholder. There's nothing more to do at this point,
    296        // so break and return.
    297        break;
    298      }
    299 
    300      lastItem = bottomPlaceholder.previousSibling;
    301 
    302      // This chunk is just validating that we have no gaps in our output so
    303      // far.
    304      const indices = [...scrollport.querySelectorAll("[data-message-id]")]
    305        .filter(
    306          el => el !== scrollport.firstChild && el !== scrollport.lastChild
    307        )
    308        .map(el => getMessageIndex(el));
    309      allIndices.push(...indices);
    310      allIndices.sort((lhs, rhs) => lhs - rhs);
    311      for (let i = 1; i < allIndices.length; i++) {
    312        if (
    313          allIndices[i] != allIndices[i - 1] &&
    314          allIndices[i] != allIndices[i - 1] + 1
    315        ) {
    316          throw new Error(
    317            `Gap detected in virtualized webconsole output between ${
    318              allIndices[i - 1]
    319            } and ${allIndices[i]}. Indices: ${allIndices.join(",")}`
    320          );
    321        }
    322      }
    323 
    324      const messages = scrollport.querySelectorAll(selector);
    325      const filtered = [...messages].filter(
    326        el =>
    327          // Core user filters:
    328          el.textContent.includes(text) &&
    329          (!messageId || el.getAttribute("data-message-id") === messageId) &&
    330          // Make sure we don't collect duplicate messages:
    331          !seenIds.has(el.getAttribute("data-message-id"))
    332      );
    333      allElements.push(...filtered);
    334      for (const message of filtered) {
    335        seenIds.add(message.getAttribute("data-message-id"));
    336      }
    337 
    338      if (expectedCount >= 0 && allElements.length >= expectedCount) {
    339        break;
    340      }
    341 
    342      // If the bottom placeholder has 0 height, it means we've scrolled to the
    343      // bottom and output all the items.
    344      if (bottomPlaceholder.getBoundingClientRect().height == 0) {
    345        break;
    346      }
    347 
    348      await waitForTime(0);
    349    }
    350 
    351    // Finally, we get the map of message IDs to indices within the output, and
    352    // sort the message nodes according to that index. They can come in out of
    353    // order for a number of reasons (we continue rendering any messages that
    354    // have been expanded, and we always render the topmost and bottommost
    355    // messages for a11y reasons.)
    356    const idsToIndices = getVisibleMessageMap();
    357    allElements.sort(
    358      (lhs, rhs) =>
    359        idsToIndices.get(lhs.getAttribute("data-message-id")) -
    360        idsToIndices.get(rhs.getAttribute("data-message-id"))
    361    );
    362    return allElements;
    363  } finally {
    364    scrollport.removeAttribute("disable-autoscroll");
    365    gInFindMessagesVirtualized = false;
    366    gFindMessagesVirtualizedStack = null;
    367  }
    368 }
    369 
    370 /**
    371 * Find the last message with given message type in the output.
    372 *
    373 * @param object hud
    374 *        The web console.
    375 * @param string text
    376 *        A substring that can be found in the message.
    377 * @param string typeSelector
    378 *        A part of selector for the message, to specify the message type.
    379 * @return {Node} the node corresponding the found message, otherwise undefined
    380 */
    381 function findMessageByType(hud, text, typeSelector) {
    382  const elements = findMessagesByType(hud, text, typeSelector);
    383  return elements.at(-1);
    384 }
    385 
    386 /**
    387 * Find multiple messages with given message type in the output.
    388 *
    389 * @param object hud
    390 *        The web console.
    391 * @param string text
    392 *        A substring that can be found in the message.
    393 * @param string typeSelector
    394 *        A part of selector for the message, to specify the message type.
    395 * @return {Array} The nodes corresponding the found messages
    396 */
    397 function findMessagesByType(hud, text, typeSelector) {
    398  if (!typeSelector) {
    399    throw new Error("typeSelector parameter is required");
    400  }
    401  if (!typeSelector.startsWith(".")) {
    402    throw new Error("typeSelector should start with a dot e.g. `.result`");
    403  }
    404 
    405  const selector = ".message" + typeSelector;
    406  const messages = hud.ui.outputNode.querySelectorAll(selector);
    407  const elements = Array.from(messages).filter(el =>
    408    el.textContent.includes(text)
    409  );
    410  return elements;
    411 }
    412 
    413 /**
    414 * Find all messages in the output.
    415 *
    416 * @param object hud
    417 *        The web console.
    418 * @return {Array} The nodes corresponding the found messages
    419 */
    420 function findAllMessages(hud) {
    421  const messages = hud.ui.outputNode.querySelectorAll(".message");
    422  return Array.from(messages);
    423 }
    424 
    425 /**
    426 * Find a part of the last message with given message type in the output.
    427 *
    428 * @param object hud
    429 *        The web console.
    430 * @param object options
    431 *        - text : {String} A substring that can be found in the message.
    432 *        - typeSelector: {String} A part of selector for the message,
    433 *                                 to specify the message type.
    434 *        - partSelector: {String} A selector for the part of the message.
    435 * @return {Node} the node corresponding the found part, otherwise undefined
    436 */
    437 function findMessagePartByType(hud, options) {
    438  const elements = findMessagePartsByType(hud, options);
    439  return elements.at(-1);
    440 }
    441 
    442 /**
    443 * Find parts of multiple messages with given message type in the output.
    444 *
    445 * @param object hud
    446 *        The web console.
    447 * @param object options
    448 *        - text : {String} A substring that can be found in the message.
    449 *        - typeSelector: {String} A part of selector for the message,
    450 *                                 to specify the message type.
    451 *        - partSelector: {String} A selector for the part of the message.
    452 * @return {Array} The nodes corresponding the found parts
    453 */
    454 function findMessagePartsByType(hud, { text, typeSelector, partSelector }) {
    455  if (!typeSelector) {
    456    throw new Error("typeSelector parameter is required");
    457  }
    458  if (!typeSelector.startsWith(".")) {
    459    throw new Error("typeSelector should start with a dot e.g. `.result`");
    460  }
    461  if (!partSelector) {
    462    throw new Error("partSelector parameter is required");
    463  }
    464 
    465  const selector = ".message" + typeSelector + " " + partSelector;
    466  const parts = hud.ui.outputNode.querySelectorAll(selector);
    467  const elements = Array.from(parts).filter(el =>
    468    el.textContent.includes(text)
    469  );
    470  return elements;
    471 }
    472 
    473 /**
    474 * Type-specific wrappers for findMessageByType and findMessagesByType.
    475 *
    476 * @param object hud
    477 *        The web console.
    478 * @param string text
    479 *        A substring that can be found in the message.
    480 * @param string extraSelector [optional]
    481 *        An extra part of selector for the message, in addition to
    482 *        type-specific selector.
    483 * @return {Node|Array} See findMessageByType or findMessagesByType.
    484 */
    485 function findEvaluationResultMessage(hud, text, extraSelector = "") {
    486  return findMessageByType(hud, text, ".result" + extraSelector);
    487 }
    488 function findEvaluationResultMessages(hud, text, extraSelector = "") {
    489  return findMessagesByType(hud, text, ".result" + extraSelector);
    490 }
    491 function findErrorMessage(hud, text, extraSelector = "") {
    492  return findMessageByType(hud, text, ".error" + extraSelector);
    493 }
    494 function findErrorMessages(hud, text, extraSelector = "") {
    495  return findMessagesByType(hud, text, ".error" + extraSelector);
    496 }
    497 function findWarningMessage(hud, text, extraSelector = "") {
    498  return findMessageByType(hud, text, ".warn" + extraSelector);
    499 }
    500 function findWarningMessages(hud, text, extraSelector = "") {
    501  return findMessagesByType(hud, text, ".warn" + extraSelector);
    502 }
    503 function findConsoleAPIMessage(hud, text, extraSelector = "") {
    504  return findMessageByType(hud, text, ".console-api" + extraSelector);
    505 }
    506 function findConsoleAPIMessages(hud, text, extraSelector = "") {
    507  return findMessagesByType(hud, text, ".console-api" + extraSelector);
    508 }
    509 function findNetworkMessage(hud, text, extraSelector = "") {
    510  return findMessageByType(hud, text, ".network" + extraSelector);
    511 }
    512 function findNetworkMessages(hud, text, extraSelector = "") {
    513  return findMessagesByType(hud, text, ".network" + extraSelector);
    514 }
    515 function findTracerMessages(hud, text, extraSelector = "") {
    516  return findMessagesByType(hud, text, ".jstracer" + extraSelector);
    517 }