tor-browser

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

head.js (58517B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 /**
      5 * This file (head.js) is injected into all other test contexts within
      6 * this directory, allowing one to utilize the functions here in said
      7 * tests without referencing head.js explicitly.
      8 */
      9 
     10 /* exported Toolbox, restartNetMonitor, teardown, waitForExplicitFinish,
     11   verifyRequestItemTarget, waitFor, waitForDispatch, testFilterButtons,
     12   performRequestsInContent, waitForNetworkEvents, selectIndexAndWaitForSourceEditor,
     13   testColumnsAlignment, hideColumn, showColumn, performRequests, waitForRequestData,
     14   toggleBlockedUrl, registerFaviconNotifier, clickOnSidebarTab */
     15 
     16 "use strict";
     17 
     18 // The below file (shared-head.js) handles imports, constants, and
     19 // utility functions, and is loaded into this context.
     20 Services.scriptloader.loadSubScript(
     21  "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
     22  this
     23 );
     24 
     25 const { LinkHandlerParent } = ChromeUtils.importESModule(
     26  "resource:///actors/LinkHandlerParent.sys.mjs"
     27 );
     28 
     29 const {
     30  getFormattedIPAndPort,
     31  getFormattedTime,
     32 } = require("resource://devtools/client/netmonitor/src/utils/format-utils.js");
     33 
     34 const {
     35  getSortedRequests,
     36  getRequestById,
     37 } = require("resource://devtools/client/netmonitor/src/selectors/index.js");
     38 
     39 const {
     40  getUnicodeUrl,
     41  getUnicodeHostname,
     42 } = require("resource://devtools/client/shared/unicode-url.js");
     43 const {
     44  getFormattedProtocol,
     45  getUrlHost,
     46  getUrlScheme,
     47 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
     48 const {
     49  EVENTS,
     50  TEST_EVENTS,
     51 } = require("resource://devtools/client/netmonitor/src/constants.js");
     52 const {
     53  L10N,
     54 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
     55 
     56 /* eslint-disable no-unused-vars, max-len */
     57 const EXAMPLE_URL =
     58  "http://example.com/browser/devtools/client/netmonitor/test/";
     59 const EXAMPLE_ORG_URL =
     60  "http://example.org/browser/devtools/client/netmonitor/test/";
     61 const HTTPS_EXAMPLE_URL =
     62  "https://example.com/browser/devtools/client/netmonitor/test/";
     63 const HTTPS_EXAMPLE_ORG_URL =
     64  "https://example.org/browser/devtools/client/netmonitor/test/";
     65 /* Since the test server will proxy `ws://example.com` to websocket server on 9988,
     66 so we must sepecify the port explicitly */
     67 const WS_URL = "ws://127.0.0.1:8888/browser/devtools/client/netmonitor/test/";
     68 const WS_HTTP_URL =
     69  "http://127.0.0.1:8888/browser/devtools/client/netmonitor/test/websockets/";
     70 
     71 const WS_BASE_URL =
     72  "http://mochi.test:8888/browser/devtools/client/netmonitor/test/websockets/";
     73 const WS_PAGE_URL = WS_BASE_URL + "html_ws-test-page.html";
     74 const WS_PAGE_EARLY_CONNECTION_URL =
     75  WS_BASE_URL + "html_ws-early-connection-page.html";
     76 const API_CALLS_URL = HTTPS_EXAMPLE_URL + "html_api-calls-test-page.html";
     77 const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html";
     78 const HTTPS_SIMPLE_URL = HTTPS_EXAMPLE_URL + "html_simple-test-page.html";
     79 const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html";
     80 const CONTENT_TYPE_WITHOUT_CACHE_URL =
     81  EXAMPLE_URL + "html_content-type-without-cache-test-page.html";
     82 const CONTENT_TYPE_WITHOUT_CACHE_REQUESTS = 8;
     83 const CYRILLIC_URL = EXAMPLE_URL + "html_cyrillic-test-page.html";
     84 const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html";
     85 const HTTPS_STATUS_CODES_URL =
     86  HTTPS_EXAMPLE_URL + "html_status-codes-test-page.html";
     87 const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html";
     88 const POST_ARRAY_DATA_URL = EXAMPLE_URL + "html_post-array-data-test-page.html";
     89 const POST_JSON_URL = EXAMPLE_URL + "html_post-json-test-page.html";
     90 const POST_RAW_URL = EXAMPLE_URL + "html_post-raw-test-page.html";
     91 const POST_RAW_URL_WITH_HASH = EXAMPLE_URL + "html_header-test-page.html";
     92 const POST_RAW_WITH_HEADERS_URL =
     93  EXAMPLE_URL + "html_post-raw-with-headers-test-page.html";
     94 const PARAMS_URL = EXAMPLE_URL + "html_params-test-page.html";
     95 const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html";
     96 const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html";
     97 const JSON_MALFORMED_URL = EXAMPLE_URL + "html_json-malformed-test-page.html";
     98 const JSON_CUSTOM_MIME_URL =
     99  EXAMPLE_URL + "html_json-custom-mime-test-page.html";
    100 const JSON_TEXT_MIME_URL = EXAMPLE_URL + "html_json-text-mime-test-page.html";
    101 const JSON_B64_URL = EXAMPLE_URL + "html_json-b64.html";
    102 const JSON_BASIC_URL = EXAMPLE_URL + "html_json-basic.html";
    103 const JSON_EMPTY_URL = EXAMPLE_URL + "html_json-empty.html";
    104 const JSON_XSSI_PROTECTION_URL = EXAMPLE_URL + "html_json-xssi-protection.html";
    105 const FONTS_URL = EXAMPLE_URL + "html_fonts-test-page.html";
    106 const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html";
    107 const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html";
    108 const HTTPS_FILTERING_URL = HTTPS_EXAMPLE_URL + "html_filter-test-page.html";
    109 const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html";
    110 const CUSTOM_GET_URL = EXAMPLE_URL + "html_custom-get-page.html";
    111 const HTTPS_CUSTOM_GET_URL = HTTPS_EXAMPLE_URL + "html_custom-get-page.html";
    112 const SINGLE_GET_URL = EXAMPLE_URL + "html_single-get-page.html";
    113 const HTTPS_SINGLE_GET_URL = HTTPS_EXAMPLE_URL + "html_single-get-page.html";
    114 const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html";
    115 const STATISTICS_EDGE_CASE_URL =
    116  EXAMPLE_URL + "html_statistics-edge-case-page.html";
    117 const CURL_URL = EXAMPLE_URL + "html_copy-as-curl.html";
    118 const HTTPS_CURL_URL = HTTPS_EXAMPLE_URL + "html_copy-as-curl.html";
    119 const HTTPS_CURL_UTILS_URL = HTTPS_EXAMPLE_URL + "html_curl-utils.html";
    120 const SEND_BEACON_URL = EXAMPLE_URL + "html_send-beacon.html";
    121 const CORS_URL = EXAMPLE_URL + "html_cors-test-page.html";
    122 const HTTPS_CORS_URL = HTTPS_EXAMPLE_URL + "html_cors-test-page.html";
    123 const PAUSE_URL = EXAMPLE_URL + "html_pause-test-page.html";
    124 const OPEN_REQUEST_IN_TAB_URL = EXAMPLE_URL + "html_open-request-in-tab.html";
    125 const CSP_URL = EXAMPLE_URL + "html_csp-test-page.html";
    126 const CSP_RESEND_URL = EXAMPLE_URL + "html_csp-resend-test-page.html";
    127 const IMAGE_CACHE_URL = HTTPS_EXAMPLE_URL + "html_image-cache.html";
    128 const STYLESHEET_CACHE_URL = HTTPS_EXAMPLE_URL + "html_stylesheet-cache.html";
    129 const SCRIPT_CACHE_URL = HTTPS_EXAMPLE_URL + "html_script-cache.html";
    130 const SOURCEMAP_URL = HTTPS_EXAMPLE_URL + "html_maps-test-page.html";
    131 const MODULE_SCRIPT_CACHE_URL =
    132  HTTPS_EXAMPLE_URL + "html_module-script-cache.html";
    133 const SLOW_REQUESTS_URL = EXAMPLE_URL + "html_slow-requests-test-page.html";
    134 const HTTPS_SLOW_REQUESTS_URL =
    135  HTTPS_EXAMPLE_URL + "html_slow-requests-test-page.html";
    136 
    137 const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
    138 const HTTPS_SIMPLE_SJS = HTTPS_EXAMPLE_URL + "sjs_simple-test-server.sjs";
    139 const SIMPLE_UNSORTED_COOKIES_SJS =
    140  EXAMPLE_URL + "sjs_simple-unsorted-cookies-test-server.sjs";
    141 const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
    142 const WS_CONTENT_TYPE_SJS = WS_HTTP_URL + "sjs_content-type-test-server.sjs";
    143 const WS_WS_CONTENT_TYPE_SJS = WS_URL + "sjs_content-type-test-server.sjs";
    144 const HTTPS_CONTENT_TYPE_SJS =
    145  HTTPS_EXAMPLE_URL + "sjs_content-type-test-server.sjs";
    146 const SERVER_TIMINGS_TYPE_SJS =
    147  HTTPS_EXAMPLE_URL + "sjs_timings-test-server.sjs";
    148 const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
    149 const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
    150 const HTTPS_REDIRECT_SJS = EXAMPLE_URL + "sjs_https-redirect-test-server.sjs";
    151 const CORS_SJS_PATH =
    152  "/browser/devtools/client/netmonitor/test/sjs_cors-test-server.sjs";
    153 const HSTS_SJS = EXAMPLE_URL + "sjs_hsts-test-server.sjs";
    154 const METHOD_SJS = EXAMPLE_URL + "sjs_method-test-server.sjs";
    155 const HTTPS_SLOW_SJS = HTTPS_EXAMPLE_URL + "sjs_slow-test-server.sjs";
    156 const DELAY_SJS = HTTPS_EXAMPLE_URL + "sjs_delay-test-server.sjs";
    157 const SET_COOKIE_SAME_SITE_SJS = EXAMPLE_URL + "sjs_set-cookie-same-site.sjs";
    158 const SEARCH_SJS = EXAMPLE_URL + "sjs_search-test-server.sjs";
    159 const HTTPS_SEARCH_SJS = HTTPS_EXAMPLE_URL + "sjs_search-test-server.sjs";
    160 
    161 const HSTS_BASE_URL = EXAMPLE_URL;
    162 const HSTS_PAGE_URL = CUSTOM_GET_URL;
    163 
    164 const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
    165 const TEST_IMAGE_DATA_URI =
    166  "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
    167 
    168 const SETTINGS_MENU_ITEMS = {
    169  "persist-logs": ".netmonitor-settings-persist-item",
    170  "import-har": ".netmonitor-settings-import-har-item",
    171  "save-har": ".netmonitor-settings-import-save-item",
    172  "copy-har": ".netmonitor-settings-import-copy-item",
    173 };
    174 
    175 /* eslint-enable no-unused-vars, max-len */
    176 
    177 // All tests are asynchronous.
    178 waitForExplicitFinish();
    179 
    180 const gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
    181 // To enable logging for try runs, just set the pref to true.
    182 Services.prefs.setBoolPref("devtools.debugger.log", false);
    183 
    184 // Uncomment this pref to dump all devtools emitted events to the console.
    185 // Services.prefs.setBoolPref("devtools.dump.emit", true);
    186 
    187 // Always reset some prefs to their original values after the test finishes.
    188 const gDefaultFilters = Services.prefs.getCharPref(
    189  "devtools.netmonitor.filters"
    190 );
    191 const gDefaultRequestFilter = Services.prefs.getCharPref(
    192  "devtools.netmonitor.requestfilter"
    193 );
    194 
    195 // Reveal many columns for test
    196 Services.prefs.setCharPref(
    197  "devtools.netmonitor.visibleColumns",
    198  '["initiator","contentSize","cookies","domain","duration",' +
    199    '"endTime","file","url","latency","method","protocol",' +
    200    '"remoteip","responseTime","scheme","setCookies",' +
    201    '"startTime","status","transferred","type","waterfall"]'
    202 );
    203 
    204 Services.prefs.setCharPref(
    205  "devtools.netmonitor.columnsData",
    206  '[{"name":"override","minWidth":20,"width":2},' +
    207    '{"name":"status","minWidth":30,"width":5},' +
    208    '{"name":"method","minWidth":30,"width":5},' +
    209    '{"name":"domain","minWidth":30,"width":10},' +
    210    '{"name":"file","minWidth":30,"width":25},' +
    211    '{"name":"url","minWidth":30,"width":25},' +
    212    '{"name":"initiator","minWidth":30,"width":20},' +
    213    '{"name":"type","minWidth":30,"width":5},' +
    214    '{"name":"transferred","minWidth":30,"width":10},' +
    215    '{"name":"contentSize","minWidth":30,"width":5},' +
    216    '{"name":"waterfall","minWidth":150,"width":15}]'
    217 );
    218 
    219 registerCleanupFunction(() => {
    220  info("finish() was called, cleaning up...");
    221  Services.cookies.removeAll();
    222 });
    223 
    224 async function disableCacheAndReload(toolbox, waitForLoad) {
    225  // Disable the cache for any toolbox that it is opened from this point on.
    226  Services.prefs.setBoolPref("devtools.cache.disabled", true);
    227 
    228  await toolbox.commands.targetConfigurationCommand.updateConfiguration({
    229    cacheDisabled: true,
    230  });
    231 
    232  // If the page which is reloaded is not found, this will likely cause
    233  // reloadTopLevelTarget to not return so let not wait for it.
    234  if (waitForLoad) {
    235    await toolbox.commands.targetCommand.reloadTopLevelTarget();
    236  } else {
    237    toolbox.commands.targetCommand.reloadTopLevelTarget();
    238  }
    239 }
    240 
    241 async function enableCacheAndReload(toolbox, waitForLoad) {
    242  // Disable the cache for any toolbox that it is opened from this point on.
    243  Services.prefs.setBoolPref("devtools.cache.disabled", false);
    244 
    245  await toolbox.commands.targetConfigurationCommand.updateConfiguration({
    246    cacheDisabled: false,
    247  });
    248 
    249  // If the page which is reloaded is not found, this will likely cause
    250  // reloadTopLevelTarget to not return so let not wait for it.
    251  if (waitForLoad) {
    252    await toolbox.commands.targetCommand.reloadTopLevelTarget();
    253  } else {
    254    toolbox.commands.targetCommand.reloadTopLevelTarget();
    255  }
    256 }
    257 
    258 /**
    259 * Wait for 2 markers during document load.
    260 */
    261 function waitForTimelineMarkers(monitor) {
    262  return new Promise(resolve => {
    263    const markers = [];
    264 
    265    function handleTimelineEvent(marker) {
    266      info(`Got marker: ${marker.name}`);
    267      markers.push(marker);
    268      if (markers.length == 2) {
    269        monitor.panelWin.api.off(
    270          TEST_EVENTS.TIMELINE_EVENT,
    271          handleTimelineEvent
    272        );
    273        info("Got two timeline markers, done waiting");
    274        resolve(markers);
    275      }
    276    }
    277 
    278    monitor.panelWin.api.on(TEST_EVENTS.TIMELINE_EVENT, handleTimelineEvent);
    279  });
    280 }
    281 
    282 let finishedQueue = {};
    283 const updatingTypes = [
    284  "NetMonitor:NetworkEventUpdating:RequestCookies",
    285  "NetMonitor:NetworkEventUpdating:ResponseCookies",
    286  "NetMonitor:NetworkEventUpdating:RequestHeaders",
    287  "NetMonitor:NetworkEventUpdating:ResponseHeaders",
    288  "NetMonitor:NetworkEventUpdating:RequestPostData",
    289  "NetMonitor:NetworkEventUpdating:ResponseContent",
    290  "NetMonitor:NetworkEventUpdating:SecurityInfo",
    291  "NetMonitor:NetworkEventUpdating:EventTimings",
    292 ];
    293 const updatedTypes = [
    294  "NetMonitor:NetworkEventUpdated:RequestCookies",
    295  "NetMonitor:NetworkEventUpdated:ResponseCookies",
    296  "NetMonitor:NetworkEventUpdated:RequestHeaders",
    297  "NetMonitor:NetworkEventUpdated:ResponseHeaders",
    298  "NetMonitor:NetworkEventUpdated:RequestPostData",
    299  "NetMonitor:NetworkEventUpdated:ResponseContent",
    300  "NetMonitor:NetworkEventUpdated:SecurityInfo",
    301  "NetMonitor:NetworkEventUpdated:EventTimings",
    302 ];
    303 
    304 // Start collecting all networkEventUpdate events when the panel is opened.
    305 // removeTab() should be called once all corresponded RECEIVED_* events finished.
    306 function startNetworkEventUpdateObserver(panelWin) {
    307  updatingTypes.forEach(type =>
    308    panelWin.api.on(type, actor => {
    309      const key = actor + "-" + updatedTypes[updatingTypes.indexOf(type)];
    310      finishedQueue[key] = finishedQueue[key] ? finishedQueue[key] + 1 : 1;
    311    })
    312  );
    313 
    314  updatedTypes.forEach(type =>
    315    panelWin.api.on(type, payload => {
    316      const key = payload.from + "-" + type;
    317      finishedQueue[key] = finishedQueue[key] ? finishedQueue[key] - 1 : -1;
    318    })
    319  );
    320 
    321  panelWin.api.on("clear-network-resources", () => {
    322    finishedQueue = {};
    323  });
    324 }
    325 
    326 async function waitForAllNetworkUpdateEvents() {
    327  function checkNetworkEventUpdateState() {
    328    for (const key in finishedQueue) {
    329      if (finishedQueue[key] > 0) {
    330        return false;
    331      }
    332    }
    333    return true;
    334  }
    335  info("Wait for completion of all NetworkUpdateEvents packets...");
    336  await waitUntil(() => checkNetworkEventUpdateState());
    337  finishedQueue = {};
    338 }
    339 
    340 function initNetMonitor(
    341  url,
    342  {
    343    requestCount,
    344    expectedEventTimings,
    345    waitForLoad = true,
    346    enableCache = false,
    347    openInPrivateWindow = false,
    348  }
    349 ) {
    350  info("Initializing a network monitor pane.");
    351 
    352  if (!requestCount && !enableCache) {
    353    ok(
    354      false,
    355      "initNetMonitor should be given a number of requests the page will perform"
    356    );
    357  }
    358 
    359  return (async function () {
    360    let tab = null;
    361    let privateWindow = null;
    362 
    363    if (openInPrivateWindow) {
    364      privateWindow = await BrowserTestUtils.openNewBrowserWindow({
    365        private: true,
    366      });
    367      ok(
    368        PrivateBrowsingUtils.isContentWindowPrivate(privateWindow),
    369        "window is private"
    370      );
    371      tab = BrowserTestUtils.addTab(privateWindow.gBrowser, url);
    372    } else {
    373      tab = await addTab(url, { waitForLoad });
    374    }
    375 
    376    info("Net tab added successfully: " + url);
    377 
    378    const toolbox = await gDevTools.showToolboxForTab(tab, {
    379      toolId: "netmonitor",
    380    });
    381    info("Network monitor pane shown successfully.");
    382 
    383    const monitor = toolbox.getCurrentPanel();
    384 
    385    startNetworkEventUpdateObserver(monitor.panelWin);
    386 
    387    if (!enableCache) {
    388      info("Disabling cache and reloading page.");
    389 
    390      const allComplete = [];
    391      allComplete.push(
    392        waitForNetworkEvents(monitor, requestCount, {
    393          expectedEventTimings,
    394        })
    395      );
    396 
    397      if (waitForLoad) {
    398        allComplete.push(waitForTimelineMarkers(monitor));
    399      }
    400      await disableCacheAndReload(toolbox, waitForLoad);
    401      await Promise.all(allComplete);
    402      await clearNetworkEvents(monitor);
    403    } else if (Services.prefs.getBoolPref("devtools.cache.disabled")) {
    404      info("Enabling cache and reloading page.");
    405 
    406      const allComplete = [];
    407      allComplete.push(
    408        waitForNetworkEvents(monitor, requestCount, {
    409          expectedEventTimings,
    410        })
    411      );
    412 
    413      if (waitForLoad) {
    414        allComplete.push(waitForTimelineMarkers(monitor));
    415      }
    416      await enableCacheAndReload(toolbox, waitForLoad);
    417      await Promise.all(allComplete);
    418      await clearNetworkEvents(monitor);
    419    }
    420 
    421    return { tab, monitor, toolbox, privateWindow };
    422  })();
    423 }
    424 
    425 function restartNetMonitor(monitor, { requestCount }) {
    426  info("Restarting the specified network monitor.");
    427 
    428  return (async function () {
    429    const tab = monitor.commands.descriptorFront.localTab;
    430    const url = tab.linkedBrowser.currentURI.spec;
    431 
    432    await waitForAllNetworkUpdateEvents();
    433    info("All pending requests finished.");
    434 
    435    const onDestroyed = monitor.once("destroyed");
    436    await removeTab(tab);
    437    await onDestroyed;
    438 
    439    return initNetMonitor(url, { requestCount });
    440  })();
    441 }
    442 
    443 /**
    444 * Clears the network requests in the UI
    445 *
    446 * @param {object} monitor
    447 *         The netmonitor instance used for retrieving a context menu element.
    448 */
    449 async function clearNetworkEvents(monitor) {
    450  const { store, windowRequire } = monitor.panelWin;
    451  const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
    452 
    453  await waitForAllNetworkUpdateEvents();
    454 
    455  info("Clearing the network requests in the UI");
    456  store.dispatch(Actions.clearRequests({ isExplicitClear: true }));
    457 }
    458 
    459 function teardown(monitor, privateWindow) {
    460  info("Destroying the specified network monitor.");
    461 
    462  return (async function () {
    463    const tab = monitor.commands.descriptorFront.localTab;
    464 
    465    await waitForAllNetworkUpdateEvents();
    466    info("All pending requests finished.");
    467 
    468    await monitor.toolbox.destroy();
    469    await removeTab(tab);
    470 
    471    if (privateWindow) {
    472      const closed = BrowserTestUtils.windowClosed(privateWindow);
    473      privateWindow.BrowserCommands.tryToCloseWindow();
    474      await closed;
    475    }
    476  })();
    477 }
    478 
    479 /**
    480 * Wait for the request(s) to be fully notified to the frontend.
    481 *
    482 * @param {object} monitor
    483 *        The netmonitor instance used for retrieving a context menu element.
    484 * @param {number} getRequests
    485 *        The number of request to wait for
    486 * @param {object} options (optional)
    487 *        - expectedEventTimings {Number} Number of EVENT_TIMINGS events to wait for.
    488 *        In case of filtering, we get less of such events.
    489 */
    490 function waitForNetworkEvents(monitor, getRequests, options = {}) {
    491  return new Promise(resolve => {
    492    const panel = monitor.panelWin;
    493    let networkEvent = 0;
    494    let payloadReady = 0;
    495    let eventTimings = 0;
    496 
    497    // Use a set to monitor blocked events, because a network resource might
    498    // only receive its blockedReason in onPayloadReady.
    499    let nonBlockedNetworkEvents = new Set();
    500 
    501    function onNetworkEvent(resource) {
    502      networkEvent++;
    503      if (!resource.blockedReason) {
    504        nonBlockedNetworkEvents.add(resource.actor);
    505      }
    506      maybeResolve(TEST_EVENTS.NETWORK_EVENT, resource.actor);
    507    }
    508 
    509    function onPayloadReady(resource) {
    510      payloadReady++;
    511      if (resource.blockedReason) {
    512        nonBlockedNetworkEvents.delete(resource.actor);
    513      }
    514      maybeResolve(EVENTS.PAYLOAD_READY, resource.actor);
    515    }
    516 
    517    function onEventTimings(response) {
    518      eventTimings++;
    519      maybeResolve(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
    520    }
    521 
    522    function onClearNetworkResources() {
    523      // Reset all counters.
    524      networkEvent = 0;
    525      nonBlockedNetworkEvents = new Set();
    526      payloadReady = 0;
    527      eventTimings = 0;
    528    }
    529 
    530    function maybeResolve(event, actor) {
    531      const { document } = monitor.panelWin;
    532      // Wait until networkEvent, payloadReady and event timings finish for each request.
    533      // The UI won't fetch timings when:
    534      // * hidden in background,
    535      // * for any blocked request,
    536      let expectedEventTimings =
    537        document.visibilityState == "hidden" ? 0 : nonBlockedNetworkEvents.size;
    538      let expectedPayloadReady = getRequests;
    539      // Typically ignore this option if it is undefined or null
    540      if (typeof options?.expectedEventTimings == "number") {
    541        expectedEventTimings = options.expectedEventTimings;
    542      }
    543      if (typeof options?.expectedPayloadReady == "number") {
    544        expectedPayloadReady = options.expectedPayloadReady;
    545      }
    546      info(
    547        "> Network event progress: " +
    548          "NetworkEvent: " +
    549          networkEvent +
    550          "/" +
    551          getRequests +
    552          ", " +
    553          "PayloadReady: " +
    554          payloadReady +
    555          "/" +
    556          expectedPayloadReady +
    557          ", " +
    558          "EventTimings: " +
    559          eventTimings +
    560          "/" +
    561          expectedEventTimings +
    562          ", " +
    563          "got " +
    564          event +
    565          " for " +
    566          actor
    567      );
    568 
    569      if (
    570        networkEvent >= getRequests &&
    571        payloadReady >= expectedPayloadReady &&
    572        eventTimings >= expectedEventTimings
    573      ) {
    574        panel.api.off(TEST_EVENTS.NETWORK_EVENT, onNetworkEvent);
    575        panel.api.off(EVENTS.PAYLOAD_READY, onPayloadReady);
    576        panel.api.off(EVENTS.RECEIVED_EVENT_TIMINGS, onEventTimings);
    577        panel.api.off("clear-network-resources", onClearNetworkResources);
    578        executeSoon(resolve);
    579      }
    580    }
    581 
    582    panel.api.on(TEST_EVENTS.NETWORK_EVENT, onNetworkEvent);
    583    panel.api.on(EVENTS.PAYLOAD_READY, onPayloadReady);
    584    panel.api.on(EVENTS.RECEIVED_EVENT_TIMINGS, onEventTimings);
    585    panel.api.on("clear-network-resources", onClearNetworkResources);
    586  });
    587 }
    588 
    589 async function verifyRequestItemTarget(
    590  document,
    591  requestList,
    592  requestItem,
    593  method,
    594  url,
    595  data = {}
    596 ) {
    597  info("> Verifying: " + method + " " + url + " " + data.toSource());
    598 
    599  const visibleIndex = requestList.findIndex(
    600    needle => needle.id === requestItem.id
    601  );
    602 
    603  isnot(visibleIndex, -1, "The requestItem exists");
    604  info("Visible index of item: " + visibleIndex);
    605 
    606  const {
    607    fuzzyUrl,
    608    status,
    609    statusText,
    610    cause,
    611    type,
    612    fullMimeType,
    613    transferred,
    614    size,
    615    time,
    616    displayedStatus,
    617  } = data;
    618 
    619  const target = document.querySelectorAll(".request-list-item")[visibleIndex];
    620 
    621  // Bug 1414981 - Request URL should not show #hash
    622  const unicodeUrl = getUnicodeUrl(url.split("#")[0]);
    623  const ORIGINAL_FILE_URL = L10N.getFormatStr(
    624    "netRequest.originalFileURL.tooltip",
    625    url
    626  );
    627  const DECODED_FILE_URL = L10N.getFormatStr(
    628    "netRequest.decodedFileURL.tooltip",
    629    unicodeUrl
    630  );
    631  const fileToolTip =
    632    url === unicodeUrl ? url : ORIGINAL_FILE_URL + "\n\n" + DECODED_FILE_URL;
    633  const requestedFile = requestItem.urlDetails.baseNameWithQuery;
    634  const host = getUnicodeHostname(getUrlHost(url));
    635  const scheme = getUrlScheme(url);
    636  const {
    637    remoteAddress,
    638    remotePort,
    639    totalTime,
    640    eventTimings = { timings: {} },
    641  } = requestItem;
    642  const formattedIPPort = getFormattedIPAndPort(remoteAddress, remotePort);
    643  const remoteIP = remoteAddress ? `${formattedIPPort}` : "unknown";
    644  // TODO Bug 1959359: timing columns duration and latency use a custom formatting for now for undefined/NaN values
    645  const duration =
    646    totalTime === undefined || isNaN(totalTime)
    647      ? ""
    648      : getFormattedTime(totalTime);
    649  const latency =
    650    eventTimings.timings.wait === undefined || isNaN(eventTimings.timings.wait)
    651      ? ""
    652      : getFormattedTime(eventTimings.timings.wait);
    653  const protocol = getFormattedProtocol(requestItem);
    654 
    655  if (fuzzyUrl) {
    656    ok(
    657      requestItem.method.startsWith(method),
    658      "The attached method is correct."
    659    );
    660    ok(requestItem.url.startsWith(url), "The attached url is correct.");
    661  } else {
    662    is(requestItem.method, method, "The attached method is correct.");
    663    is(requestItem.url, url.split("#")[0], "The attached url is correct.");
    664  }
    665 
    666  is(
    667    target.querySelector(".requests-list-method").textContent,
    668    method,
    669    "The displayed method is correct."
    670  );
    671 
    672  if (fuzzyUrl) {
    673    ok(
    674      target
    675        .querySelector(".requests-list-file")
    676        .textContent.startsWith(requestedFile),
    677      "The displayed file is correct."
    678    );
    679    ok(
    680      target
    681        .querySelector(".requests-list-file")
    682        .getAttribute("title")
    683        .startsWith(fileToolTip),
    684      "The tooltip file is correct."
    685    );
    686  } else {
    687    is(
    688      target.querySelector(".requests-list-file").textContent,
    689      requestedFile,
    690      "The displayed file is correct."
    691    );
    692    is(
    693      target.querySelector(".requests-list-file").getAttribute("title"),
    694      fileToolTip,
    695      "The tooltip file is correct."
    696    );
    697  }
    698 
    699  is(
    700    target.querySelector(".requests-list-protocol").textContent,
    701    protocol,
    702    "The displayed protocol is correct."
    703  );
    704 
    705  is(
    706    target.querySelector(".requests-list-protocol").getAttribute("title"),
    707    protocol,
    708    "The tooltip protocol is correct."
    709  );
    710 
    711  is(
    712    target.querySelector(".requests-list-domain").textContent,
    713    host,
    714    "The displayed domain is correct."
    715  );
    716 
    717  const domainTooltip =
    718    host + (remoteAddress ? " (" + formattedIPPort + ")" : "");
    719  is(
    720    target.querySelector(".requests-list-domain").getAttribute("title"),
    721    domainTooltip,
    722    "The tooltip domain is correct."
    723  );
    724 
    725  is(
    726    target.querySelector(".requests-list-remoteip").textContent,
    727    remoteIP,
    728    "The displayed remote IP is correct."
    729  );
    730 
    731  is(
    732    target.querySelector(".requests-list-remoteip").getAttribute("title"),
    733    remoteIP,
    734    "The tooltip remote IP is correct."
    735  );
    736 
    737  is(
    738    target.querySelector(".requests-list-scheme").textContent,
    739    scheme,
    740    "The displayed scheme is correct."
    741  );
    742 
    743  is(
    744    target.querySelector(".requests-list-scheme").getAttribute("title"),
    745    scheme,
    746    "The tooltip scheme is correct."
    747  );
    748 
    749  is(
    750    target.querySelector(".requests-list-duration-time").textContent,
    751    duration,
    752    "The displayed duration is correct."
    753  );
    754 
    755  is(
    756    target.querySelector(".requests-list-duration-time").getAttribute("title"),
    757    duration,
    758    "The tooltip duration is correct."
    759  );
    760 
    761  is(
    762    target.querySelector(".requests-list-latency-time").textContent,
    763    latency,
    764    "The displayed latency is correct."
    765  );
    766 
    767  is(
    768    target.querySelector(".requests-list-latency-time").getAttribute("title"),
    769    latency,
    770    "The tooltip latency is correct."
    771  );
    772 
    773  if (status !== undefined) {
    774    info("Wait for the request status to be updated");
    775    await waitFor(
    776      () =>
    777        target.querySelector(".requests-list-status-code").textContent == status
    778    );
    779 
    780    const value = target
    781      .querySelector(".requests-list-status-code")
    782      .getAttribute("data-status-code");
    783    is(
    784      `${value}`,
    785      displayedStatus ? `${displayedStatus}` : `${status}`,
    786      `The displayed status "${value}" is correct.`
    787    );
    788 
    789    const codeValue = target.querySelector(
    790      ".requests-list-status-code"
    791    ).textContent;
    792    is(
    793      `${codeValue}`,
    794      `${status}`,
    795      `The displayed status code "${codeValue}" is correct.`
    796    );
    797 
    798    const tooltip = target
    799      .querySelector(".requests-list-status-code")
    800      .getAttribute("title");
    801    is(
    802      tooltip,
    803      status + " " + statusText,
    804      `The tooltip status "${tooltip}" is correct.`
    805    );
    806  }
    807  if (cause !== undefined) {
    808    const value = Array.from(
    809      target.querySelector(".requests-list-initiator").childNodes
    810    )
    811      .filter(node => node.nodeType === Node.ELEMENT_NODE)
    812      .map(({ textContent }) => textContent)
    813      .join("");
    814    const tooltip = target
    815      .querySelector(".requests-list-initiator")
    816      .getAttribute("title");
    817    info("Displayed cause: " + value);
    818    info("Tooltip cause: " + tooltip);
    819    ok(value.includes(cause.type), "The displayed cause is correct.");
    820    ok(tooltip.includes(cause.type), "The tooltip cause is correct.");
    821  }
    822  if (type !== undefined) {
    823    const value = target.querySelector(".requests-list-type").textContent;
    824    let tooltip = target
    825      .querySelector(".requests-list-type")
    826      .getAttribute("title");
    827    info("Displayed type: " + value);
    828    info("Tooltip type: " + tooltip);
    829    is(value, type, "The displayed type is correct.");
    830    if (Object.is(tooltip, null)) {
    831      tooltip = undefined;
    832    }
    833    is(tooltip, fullMimeType, "The tooltip type is correct.");
    834  }
    835  if (transferred !== undefined) {
    836    let transferedValue;
    837    info("Wait for the transfered value to get updated");
    838    const trnsOk = await waitFor(() => {
    839      transferedValue = target.querySelector(
    840        ".requests-list-transferred"
    841      ).textContent;
    842      return transferedValue == transferred;
    843    });
    844    ok(
    845      trnsOk,
    846      `The displayed transferred size "${transferedValue}" is correct.`
    847    );
    848 
    849    const tooltip = target
    850      .querySelector(".requests-list-transferred")
    851      .getAttribute("title");
    852    is(
    853      tooltip,
    854      transferred,
    855      `The tooltip transferred size "${tooltip}" is correct.`
    856    );
    857  }
    858  if (size !== undefined) {
    859    let sizeValue;
    860    info("Wait for the size to get updated");
    861    const sizeOk = await waitFor(() => {
    862      sizeValue = target.querySelector(".requests-list-size").textContent;
    863      return sizeValue == size;
    864    });
    865    ok(sizeOk, `The displayed size "${sizeValue}" is correct.`);
    866 
    867    const tooltip = target
    868      .querySelector(".requests-list-size")
    869      .getAttribute("title");
    870    is(tooltip, size, `The tooltip size "${tooltip}" is correct.`);
    871  }
    872  if (time !== undefined) {
    873    info("Wait for timings total to get updated");
    874    await waitFor(() => target.querySelector(".requests-list-timings-total"));
    875    const value = target.querySelector(
    876      ".requests-list-timings-total"
    877    ).textContent;
    878    const tooltip = target
    879      .querySelector(".requests-list-timings-total")
    880      .getAttribute("title");
    881    info("Displayed time: " + value);
    882    info("Tooltip time: " + tooltip);
    883    Assert.greaterOrEqual(
    884      ~~value.match(/[0-9]+/),
    885      0,
    886      "The displayed time is correct."
    887    );
    888    Assert.greaterOrEqual(
    889      ~~tooltip.match(/[0-9]+/),
    890      0,
    891      "The tooltip time is correct."
    892    );
    893  }
    894 
    895  if (visibleIndex !== -1) {
    896    if (visibleIndex % 2 === 0) {
    897      ok(target.classList.contains("even"), "Item should have 'even' class.");
    898      ok(!target.classList.contains("odd"), "Item shouldn't have 'odd' class.");
    899    } else {
    900      ok(
    901        !target.classList.contains("even"),
    902        "Item shouldn't have 'even' class."
    903      );
    904      ok(target.classList.contains("odd"), "Item should have 'odd' class.");
    905    }
    906  }
    907 }
    908 
    909 /**
    910 * Tests if a button for a filter of given type is the only one checked.
    911 *
    912 * @param string filterType
    913 *        The type of the filter that should be the only one checked.
    914 */
    915 function testFilterButtons(monitor, filterType) {
    916  const doc = monitor.panelWin.document;
    917  const target = doc.querySelector(
    918    ".requests-list-filter-" + filterType + "-button"
    919  );
    920  ok(target, `Filter button '${filterType}' was found`);
    921  const buttons = [
    922    ...doc.querySelectorAll(".requests-list-filter-buttons button"),
    923  ];
    924  ok(!!buttons.length, "More than zero filter buttons were found");
    925 
    926  // Only target should be checked.
    927  const checkStatus = buttons.map(button => (button == target ? 1 : 0));
    928  testFilterButtonsCustom(monitor, checkStatus);
    929 }
    930 
    931 /**
    932 * Tests if filter buttons have 'checked' attributes set correctly.
    933 *
    934 * @param array aIsChecked
    935 *        An array specifying if a button at given index should have a
    936 *        'checked' attribute. For example, if the third item of the array
    937 *        evaluates to true, the third button should be checked.
    938 */
    939 function testFilterButtonsCustom(monitor, isChecked) {
    940  const doc = monitor.panelWin.document;
    941  const buttons = doc.querySelectorAll(".requests-list-filter-buttons button");
    942  for (let i = 0; i < isChecked.length; i++) {
    943    const button = buttons[i];
    944    if (isChecked[i]) {
    945      is(
    946        button.getAttribute("aria-pressed"),
    947        "true",
    948        "The " + button.id + " button should set 'aria-pressed' = true."
    949      );
    950    } else {
    951      is(
    952        button.getAttribute("aria-pressed"),
    953        "false",
    954        "The " + button.id + " button should set 'aria-pressed' = false."
    955      );
    956    }
    957  }
    958 }
    959 
    960 /**
    961 * Performs a single XMLHttpRequest and returns a promise that resolves once
    962 * the request has loaded.
    963 *
    964 * @param Object data
    965 *        { method: the request method (default: "GET"),
    966 *          url: the url to request (default: content.location.href),
    967 *          body: the request body to send (default: ""),
    968 *          nocache: append an unique token to the query string (default: true),
    969 *          requestHeaders: set request headers (default: none)
    970 *        }
    971 *
    972 * @return Promise A promise that's resolved with object
    973 *         { status: XMLHttpRequest.status,
    974 *           response: XMLHttpRequest.response }
    975 */
    976 function promiseXHR(data) {
    977  return new Promise(resolve => {
    978    const xhr = new content.XMLHttpRequest();
    979 
    980    const method = data.method || "GET";
    981    let url = data.url || content.location.href;
    982    const body = data.body || "";
    983 
    984    if (data.nocache) {
    985      url += "?devtools-cachebust=" + Math.random();
    986    }
    987 
    988    xhr.addEventListener(
    989      "loadend",
    990      function () {
    991        resolve({ status: xhr.status, response: xhr.response });
    992      },
    993      { once: true }
    994    );
    995 
    996    xhr.open(method, url);
    997 
    998    // Set request headers
    999    if (data.requestHeaders) {
   1000      data.requestHeaders.forEach(header => {
   1001        xhr.setRequestHeader(header.name, header.value);
   1002      });
   1003    }
   1004 
   1005    xhr.send(body);
   1006  });
   1007 }
   1008 
   1009 /**
   1010 * Performs a single websocket request and returns a promise that resolves once
   1011 * the request has loaded.
   1012 *
   1013 * @param Object data
   1014 *        { url: the url to request (default: content.location.href),
   1015 *          nocache: append an unique token to the query string (default: true),
   1016 *        }
   1017 *
   1018 * @return Promise A promise that's resolved with object
   1019 *         { status: websocket status(101),
   1020 *           response: empty string }
   1021 */
   1022 function promiseWS(data) {
   1023  return new Promise(resolve => {
   1024    let url = data.url;
   1025 
   1026    if (data.nocache) {
   1027      url += "?devtools-cachebust=" + Math.random();
   1028    }
   1029 
   1030    /* Create websocket instance */
   1031    const socket = new content.WebSocket(url);
   1032 
   1033    /* Since we only use HTTP server to mock websocket, so just ignore the error */
   1034    socket.onclose = () => {
   1035      socket.close();
   1036      resolve({
   1037        status: 101,
   1038        response: "",
   1039      });
   1040    };
   1041 
   1042    socket.onerror = () => {
   1043      socket.close();
   1044      resolve({
   1045        status: 101,
   1046        response: "",
   1047      });
   1048    };
   1049  });
   1050 }
   1051 
   1052 /**
   1053 * Perform the specified requests in the context of the page content.
   1054 *
   1055 * @param Array requests
   1056 *        An array of objects specifying the requests to perform. See
   1057 *        shared/test/frame-script-utils.js for more information.
   1058 *
   1059 * @return A promise that resolves once the requests complete.
   1060 */
   1061 async function performRequestsInContent(requests) {
   1062  if (!Array.isArray(requests)) {
   1063    requests = [requests];
   1064  }
   1065 
   1066  const responses = [];
   1067 
   1068  info("Performing requests in the context of the content.");
   1069 
   1070  for (const request of requests) {
   1071    const requestFn = request.ws ? promiseWS : promiseXHR;
   1072    const response = await SpecialPowers.spawn(
   1073      gBrowser.selectedBrowser,
   1074      [request],
   1075      requestFn
   1076    );
   1077    responses.push(response);
   1078  }
   1079 }
   1080 
   1081 function testColumnsAlignment(headers, requestList) {
   1082  const firstRequestLine = requestList.childNodes[0];
   1083 
   1084  // Find number of columns
   1085  const numberOfColumns = headers.childElementCount;
   1086  for (let i = 0; i < numberOfColumns; i++) {
   1087    const headerColumn = headers.childNodes[i];
   1088    const requestColumn = firstRequestLine.childNodes[i];
   1089    is(
   1090      headerColumn.getBoundingClientRect().left,
   1091      requestColumn.getBoundingClientRect().left,
   1092      "Headers for columns number " + i + " are aligned."
   1093    );
   1094  }
   1095 }
   1096 
   1097 async function hideColumn(monitor, column) {
   1098  const { document } = monitor.panelWin;
   1099 
   1100  info(`Clicking context-menu item for ${column}`);
   1101  EventUtils.sendMouseEvent(
   1102    { type: "contextmenu" },
   1103    document.querySelector(".requests-list-headers")
   1104  );
   1105 
   1106  const onHeaderRemoved = waitForDOM(
   1107    document,
   1108    `#requests-list-${column}-button`,
   1109    0
   1110  );
   1111  await selectContextMenuItem(monitor, `request-list-header-${column}-toggle`);
   1112  await onHeaderRemoved;
   1113 
   1114  ok(
   1115    !document.querySelector(`#requests-list-${column}-button`),
   1116    `Column ${column} should be hidden`
   1117  );
   1118 }
   1119 
   1120 async function showColumn(monitor, column) {
   1121  const { document } = monitor.panelWin;
   1122 
   1123  info(`Clicking context-menu item for ${column}`);
   1124  EventUtils.sendMouseEvent(
   1125    { type: "contextmenu" },
   1126    document.querySelector(".requests-list-headers")
   1127  );
   1128 
   1129  const onHeaderAdded = waitForDOM(
   1130    document,
   1131    `#requests-list-${column}-button`,
   1132    1
   1133  );
   1134  await selectContextMenuItem(monitor, `request-list-header-${column}-toggle`);
   1135  await onHeaderAdded;
   1136 
   1137  ok(
   1138    document.querySelector(`#requests-list-${column}-button`),
   1139    `Column ${column} should be visible`
   1140  );
   1141 }
   1142 
   1143 /**
   1144 * Select a request and switch to its response panel.
   1145 *
   1146 * @param {number} index The request index to be selected
   1147 */
   1148 async function selectIndexAndWaitForSourceEditor(monitor, index) {
   1149  const { document } = monitor.panelWin;
   1150  const onResponseContent = monitor.panelWin.api.once(
   1151    TEST_EVENTS.RECEIVED_RESPONSE_CONTENT
   1152  );
   1153  // Select the request first, as it may try to fetch whatever is the current request's
   1154  // responseContent if we select the ResponseTab first.
   1155  EventUtils.sendMouseEvent(
   1156    { type: "mousedown" },
   1157    document.querySelectorAll(".request-list-item")[index]
   1158  );
   1159  // We may already be on the ResponseTab, so only select it if needed.
   1160  const editor = document.querySelector("#response-panel .cm-content");
   1161  if (!editor) {
   1162    const waitDOM = waitForDOM(document, "#response-panel .cm-content");
   1163    document.querySelector("#response-tab").click();
   1164    await waitDOM;
   1165  }
   1166  await onResponseContent;
   1167 }
   1168 
   1169 /**
   1170 * Helper function for executing XHRs on a test page.
   1171 *
   1172 * @param {object} monitor
   1173 * @param {object} tab - The current browser tab
   1174 * @param {number} count - Number of requests to be executed.
   1175 */
   1176 async function performRequests(monitor, tab, count) {
   1177  const wait = waitForNetworkEvents(monitor, count);
   1178  await ContentTask.spawn(tab.linkedBrowser, count, requestCount => {
   1179    content.wrappedJSObject.performRequests(requestCount);
   1180  });
   1181  await wait;
   1182 }
   1183 
   1184 function getCMEditor(monitor) {
   1185  return monitor.panelWin.codeMirrorSourceEditorTestInstance;
   1186 }
   1187 
   1188 /**
   1189 * Helper function for retrieving the editor content
   1190 */
   1191 function getCodeMirrorValue(monitor) {
   1192  return getCMEditor(monitor).getText();
   1193 }
   1194 
   1195 /**
   1196 * Waits for the currently triggered editor scroll to complete
   1197 *
   1198 * @param {*} monitor
   1199 * @returns {Promise}
   1200 */
   1201 async function waitForEditorScrolling(monitor) {
   1202  return getCMEditor(monitor).once("cm-editor-scrolled");
   1203 }
   1204 
   1205 /**
   1206 * Helper function opening the options menu
   1207 */
   1208 function openSettingsMenu(monitor) {
   1209  const { document } = monitor.panelWin;
   1210  document.querySelector(".netmonitor-settings-menu-button").click();
   1211 }
   1212 
   1213 function clickSettingsMenuItem(monitor, itemKey) {
   1214  openSettingsMenu(monitor);
   1215  const node = getSettingsMenuItem(monitor, itemKey);
   1216  node.click();
   1217 }
   1218 
   1219 function getSettingsMenuItem(monitor, itemKey) {
   1220  // The settings menu is injected into the toolbox document,
   1221  // so we must use the panelWin parent to query for items
   1222  const { parent } = monitor.panelWin;
   1223  const { document } = parent;
   1224 
   1225  return document.querySelector(SETTINGS_MENU_ITEMS[itemKey]);
   1226 }
   1227 
   1228 /**
   1229 * Wait for lazy fields to be loaded in a request.
   1230 *
   1231 * @param {object} Store - redux store containing request list.
   1232 * @param {Array} fields - array of strings which contain field names to be checked
   1233 * @param {number} id - The id of the request whose data we need to wait for
   1234 * @param {number} index - The position of the request in the sorted request list.
   1235 */
   1236 function waitForRequestData(store, fields, id, index = 0) {
   1237  return waitUntil(() => {
   1238    let item;
   1239    if (id) {
   1240      item = getRequestById(store.getState(), id);
   1241    } else {
   1242      item = getSortedRequests(store.getState())[index];
   1243    }
   1244    if (!item) {
   1245      return false;
   1246    }
   1247    for (const field of fields) {
   1248      if (item[field] == undefined) {
   1249        return false;
   1250      }
   1251    }
   1252    return item;
   1253  });
   1254 }
   1255 
   1256 // Telemetry
   1257 
   1258 /**
   1259 * Helper for verifying telemetry event.
   1260 *
   1261 * @param Object expectedEvent object representing expected event data.
   1262 * @param Object query fields specifying category, method and object
   1263 *                     of the target telemetry event.
   1264 */
   1265 function checkTelemetryEvent(expectedEvent, query) {
   1266  const events = queryTelemetryEvents(query);
   1267  is(events.length, 1, "There was only 1 event logged");
   1268 
   1269  const [event] = events;
   1270  Assert.greater(
   1271    Number(event.session_id),
   1272    0,
   1273    "There is a valid session_id in the logged event"
   1274  );
   1275 
   1276  const f = e => JSON.stringify(e, null, 2);
   1277  is(
   1278    f(event),
   1279    f({
   1280      ...expectedEvent,
   1281      session_id: event.session_id,
   1282    }),
   1283    "The event has the expected data"
   1284  );
   1285 }
   1286 
   1287 function queryTelemetryEvents(query) {
   1288  const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS;
   1289  const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true);
   1290  const category = query.category || "devtools.main";
   1291  const object = query.object || "netmonitor";
   1292 
   1293  const filtersChangedEvents = snapshot.parent.filter(
   1294    event =>
   1295      event[1] === category && event[2] === query.method && event[3] === object
   1296  );
   1297 
   1298  // Return the `extra` field (which is event[5]e).
   1299  return filtersChangedEvents.map(event => event[5]);
   1300 }
   1301 /**
   1302 * Check that the provided requests match the requests displayed in the netmonitor.
   1303 *
   1304 * @param {Array} requests
   1305 *     The expected requests.
   1306 * @param {object} monitor
   1307 *     The netmonitor instance.
   1308 * @param {object=} options
   1309 * @param {boolean} allowDifferentOrder
   1310 *     When set to true, requests are allowed to be in a different order in the
   1311 *     netmonitor than in the expected requests array. Defaults to false.
   1312 */
   1313 async function validateRequests(requests, monitor, options = {}) {
   1314  const { allowDifferentOrder } = options;
   1315  const { document, store, windowRequire } = monitor.panelWin;
   1316 
   1317  const { getDisplayedRequests } = windowRequire(
   1318    "devtools/client/netmonitor/src/selectors/index"
   1319  );
   1320  const sortedRequests = getSortedRequests(store.getState());
   1321 
   1322  for (const [i, spec] of requests.entries()) {
   1323    const { method, url, causeType, causeUri, stack } = spec;
   1324 
   1325    let requestItem;
   1326    if (allowDifferentOrder) {
   1327      requestItem = sortedRequests.find(r => r.url === url);
   1328    } else {
   1329      requestItem = sortedRequests[i];
   1330    }
   1331 
   1332    await verifyRequestItemTarget(
   1333      document,
   1334      getDisplayedRequests(store.getState()),
   1335      requestItem,
   1336      method,
   1337      url,
   1338      { cause: { type: causeType, loadingDocumentUri: causeUri } }
   1339    );
   1340 
   1341    const { stacktrace } = requestItem;
   1342    const stackLen = stacktrace ? stacktrace.length : 0;
   1343 
   1344    if (stack) {
   1345      ok(stacktrace, `Request #${i} has a stacktrace`);
   1346      Assert.greater(
   1347        stackLen,
   1348        0,
   1349        `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items`
   1350      );
   1351 
   1352      // if "stack" is array, check the details about the top stack frames
   1353      if (Array.isArray(stack)) {
   1354        stack.forEach((frame, j) => {
   1355          let value = stacktrace[j].functionName;
   1356          if (Object.is(value, null)) {
   1357            value = undefined;
   1358          }
   1359          is(
   1360            value,
   1361            frame.fn,
   1362            `Request #${i} has the correct function on JS stack frame #${j}`
   1363          );
   1364          is(
   1365            stacktrace[j].filename.split("/").pop(),
   1366            frame.file.split("/").pop(),
   1367            `Request #${i} has the correct file on JS stack frame #${j}`
   1368          );
   1369          is(
   1370            stacktrace[j].lineNumber,
   1371            frame.line,
   1372            `Request #${i} has the correct line number on JS stack frame #${j}`
   1373          );
   1374          value = stacktrace[j].asyncCause;
   1375          if (Object.is(value, null)) {
   1376            value = undefined;
   1377          }
   1378          is(
   1379            value,
   1380            frame.asyncCause,
   1381            `Request #${i} has the correct async cause on JS stack frame #${j}`
   1382          );
   1383        });
   1384      }
   1385    } else {
   1386      is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`);
   1387    }
   1388  }
   1389 }
   1390 
   1391 /**
   1392 * @see getNetmonitorContextMenuItem in shared-head.js
   1393 */
   1394 function getContextMenuItem(monitor, id) {
   1395  return getNetmonitorContextMenuItem(monitor, id);
   1396 }
   1397 
   1398 /**
   1399 * Hides the provided netmonitor context menu
   1400 *
   1401 * @param {XULPopupElement} popup
   1402 *        The popup to hide.
   1403 */
   1404 async function hideContextMenu(popup) {
   1405  if (popup.state !== "open") {
   1406    return;
   1407  }
   1408  const onPopupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
   1409  popup.hidePopup();
   1410  await onPopupHidden;
   1411 }
   1412 
   1413 /**
   1414 * @see selectNetmonitorContextMenuItem in shared-head.js
   1415 */
   1416 async function selectContextMenuItem(monitor, id) {
   1417  return selectNetmonitorContextMenuItem(monitor, id);
   1418 }
   1419 
   1420 /**
   1421 * Wait for DOM being in specific state. But, do not wait
   1422 * for change if it's in the expected state already.
   1423 */
   1424 async function waitForDOMIfNeeded(target, selector, expectedLength = 1) {
   1425  return new Promise(resolve => {
   1426    const elements = target.querySelectorAll(selector);
   1427    if (elements.length == expectedLength) {
   1428      resolve(elements);
   1429    } else {
   1430      waitForDOM(target, selector, expectedLength).then(elems => {
   1431        resolve(elems);
   1432      });
   1433    }
   1434  });
   1435 }
   1436 
   1437 /**
   1438 * Helper for blocking or unblocking a request via the list item's context menu.
   1439 *
   1440 * @param {Element} element
   1441 *        Target request list item to be right clicked to bring up its context menu.
   1442 * @param {object} monitor
   1443 *        The netmonitor instance used for retrieving a context menu element.
   1444 * @param {object} store
   1445 *        The redux store (wait-service middleware required).
   1446 * @param {string} action
   1447 *        The action, block or unblock, to construct a corresponding context menu id.
   1448 */
   1449 async function toggleBlockedUrl(element, monitor, store, action = "block") {
   1450  EventUtils.sendMouseEvent({ type: "contextmenu" }, element);
   1451  const contextMenuId = `request-list-context-${action}-url`;
   1452  const onRequestComplete = waitForDispatch(
   1453    store,
   1454    "REQUEST_BLOCKING_UPDATE_COMPLETE"
   1455  );
   1456  await selectContextMenuItem(monitor, contextMenuId);
   1457 
   1458  info(`Wait for selected request to be ${action}ed`);
   1459  await onRequestComplete;
   1460  info(`Selected request is now ${action}ed`);
   1461 }
   1462 
   1463 /**
   1464 * Find and click an element
   1465 *
   1466 * @param {Element} element
   1467 *        Target element to be clicked
   1468 * @param {object} monitor
   1469 *        The netmonitor instance used for retrieving the window.
   1470 */
   1471 
   1472 function clickElement(element, monitor) {
   1473  EventUtils.synthesizeMouseAtCenter(element, {}, monitor.panelWin);
   1474 }
   1475 
   1476 /**
   1477 * Register a listener to be notified when a favicon finished loading and
   1478 * dispatch a "devtools:test:favicon" event to the favicon's link element.
   1479 *
   1480 * @param {Browser} browser
   1481 *        Target browser to observe the favicon load.
   1482 */
   1483 function registerFaviconNotifier(browser) {
   1484  const listener = async name => {
   1485    if (name == "SetIcon" || name == "SetFailedIcon") {
   1486      await SpecialPowers.spawn(browser, [], async () => {
   1487        content.document
   1488          .querySelector("link[rel='icon']")
   1489          .dispatchEvent(new content.CustomEvent("devtools:test:favicon"));
   1490      });
   1491      LinkHandlerParent.removeListenerForTests(listener);
   1492    }
   1493  };
   1494  LinkHandlerParent.addListenerForTests(listener);
   1495 }
   1496 
   1497 /**
   1498 * Predicates used when sorting items.
   1499 *
   1500 * @param object first
   1501 *        The first item used in the comparison.
   1502 * @param object second
   1503 *        The second item used in the comparison.
   1504 * @return number
   1505 *         <0 to sort first to a lower index than second
   1506 *         =0 to leave first and second unchanged with respect to each other
   1507 *         >0 to sort second to a lower index than first
   1508 */
   1509 
   1510 function compareValues(first, second) {
   1511  if (first === second) {
   1512    return 0;
   1513  }
   1514  return first > second ? 1 : -1;
   1515 }
   1516 
   1517 /**
   1518 * Click on the "Response" tab to open "Response" panel in the sidebar.
   1519 *
   1520 * @param {Document} doc
   1521 *        Network panel document.
   1522 * @param {string} name
   1523 *        Network panel sidebar tab name.
   1524 */
   1525 const clickOnSidebarTab = (doc, name) => {
   1526  AccessibilityUtils.setEnv({
   1527    // Keyboard accessibility is handled on the sidebar tabs container level
   1528    // (nav). Users can use arrow keys to navigate between and select tabs.
   1529    nonNegativeTabIndexRule: false,
   1530  });
   1531  EventUtils.sendMouseEvent(
   1532    { type: "click" },
   1533    doc.querySelector(`#${name}-tab`)
   1534  );
   1535  AccessibilityUtils.resetEnv();
   1536 };
   1537 
   1538 /**
   1539 * Add a new blocked request URL pattern. The request blocking sidepanel should
   1540 * already be opened.
   1541 *
   1542 * @param {string} pattern
   1543 *     The URL pattern to add to block requests.
   1544 * @param {object} monitor
   1545 *     The netmonitor instance.
   1546 */
   1547 async function addBlockedRequest(pattern, monitor) {
   1548  info("Add a blocked request for the URL pattern " + pattern);
   1549  const doc = monitor.panelWin.document;
   1550 
   1551  const addRequestForm = await waitFor(() =>
   1552    doc.querySelector(
   1553      "#network-action-bar-blocked-panel .request-blocking-add-form"
   1554    )
   1555  );
   1556  ok(!!addRequestForm, "The request blocking side panel is not available");
   1557 
   1558  info("Wait for the add input to get focus");
   1559  await waitFor(() =>
   1560    addRequestForm.querySelector("input.devtools-searchinput:focus")
   1561  );
   1562 
   1563  typeInNetmonitor(pattern, monitor);
   1564  EventUtils.synthesizeKey("KEY_Enter");
   1565 }
   1566 
   1567 /**
   1568 * Check if the provided .request-list-item element corresponds to a blocked
   1569 * request.
   1570 *
   1571 * @param {Element}
   1572 *     The request's DOM element.
   1573 * @returns {boolean}
   1574 *     True if the request is displayed as blocked, false otherwise.
   1575 */
   1576 function checkRequestListItemBlocked(item) {
   1577  return item.className.includes("blocked");
   1578 }
   1579 
   1580 /**
   1581 * Type the provided string the netmonitor window. The correct input should be
   1582 * focused prior to using this helper.
   1583 *
   1584 * @param {string} string
   1585 *     The string to type.
   1586 * @param {object} monitor
   1587 *     The netmonitor instance used to type the string.
   1588 */
   1589 function typeInNetmonitor(string, monitor) {
   1590  for (const ch of string) {
   1591    EventUtils.synthesizeKey(ch, {}, monitor.panelWin);
   1592  }
   1593 }
   1594 
   1595 /**
   1596 * Opens/ closes the URL preview in the headers side panel
   1597 *
   1598 * @param {boolean} shouldExpand
   1599 * @param {NetMonitorPanel} monitor
   1600 * @returns
   1601 */
   1602 async function toggleUrlPreview(shouldExpand, monitor) {
   1603  const { document } = monitor.panelWin;
   1604  const wait = waitUntil(() => {
   1605    const rowSize = document.querySelectorAll(
   1606      "#headers-panel .url-preview tr.treeRow"
   1607    ).length;
   1608    return shouldExpand ? rowSize > 1 : rowSize == 1;
   1609  });
   1610 
   1611  clickElement(
   1612    document.querySelector(
   1613      "#headers-panel .url-preview tr:first-child span.treeIcon.theme-twisty"
   1614    ),
   1615    monitor
   1616  );
   1617  return wait;
   1618 }
   1619 
   1620 /**
   1621 * Wait for the eager evaluated result from the split console
   1622 *
   1623 * @param {object} hud
   1624 * @param {string} text - expected evaluation result
   1625 */
   1626 async function waitForEagerEvaluationResult(hud, text) {
   1627  await waitUntil(() => {
   1628    const elem = hud.ui.outputNode.querySelector(".eager-evaluation-result");
   1629    if (elem) {
   1630      if (text instanceof RegExp) {
   1631        return text.test(elem.innerText);
   1632      }
   1633      return elem.innerText == text;
   1634    }
   1635    return false;
   1636  });
   1637  ok(true, `Got eager evaluation result ${text}`);
   1638 }
   1639 
   1640 /**
   1641 * Assert the contents of the filter urls autocomplete box
   1642 *
   1643 * @param {Array} expected
   1644 * @param {object} document
   1645 */
   1646 function testAutocompleteContents(expected, document) {
   1647  expected.forEach(function (item, i) {
   1648    is(
   1649      document.querySelector(
   1650        `.devtools-autocomplete-listbox .autocomplete-item:nth-child(${i + 1})`
   1651      ).textContent,
   1652      item,
   1653      `${expected[i]} found`
   1654    );
   1655  });
   1656 }
   1657 
   1658 /**
   1659 * Check if a valid numerical size is displayed in the request column for the
   1660 * provided request.
   1661 *
   1662 * @param {Element} request
   1663 *     A request element from the netmonitor requests list.
   1664 * @return {boolean}
   1665 *     True if the size column contains a valid size, false otherwise.
   1666 */
   1667 function hasValidSize(request) {
   1668  const VALID_SIZE_RE = /^\d+(\.\d+)? \w+/;
   1669  return VALID_SIZE_RE.test(
   1670    request.querySelector(".requests-list-size").innerText
   1671  );
   1672 }
   1673 
   1674 function getThrottleProfileItem(monitor, profileId) {
   1675  const toolboxDoc = monitor.toolbox.doc;
   1676 
   1677  const popup = toolboxDoc.querySelector("#network-throttling-menu");
   1678  const menuItems = [...popup.querySelectorAll(".menuitem > .command")];
   1679  return menuItems.find(menuItem => menuItem.id == profileId);
   1680 }
   1681 
   1682 async function selectThrottle(monitor, profileId) {
   1683  const panelDoc = monitor.panelWin.document;
   1684  const toolboxDoc = monitor.toolbox.doc;
   1685 
   1686  info("Opening the throttling menu");
   1687 
   1688  const onShown = BrowserTestUtils.waitForPopupEvent(toolboxDoc, "shown");
   1689  panelDoc.getElementById("network-throttling").click();
   1690 
   1691  info("Waiting for the throttling menu to be displayed");
   1692  await onShown;
   1693 
   1694  const profileItem = getThrottleProfileItem(monitor, profileId);
   1695  ok(profileItem, "Found a profile throttling menu item for id " + profileId);
   1696 
   1697  info(`Selecting the '${profileId}' profile`);
   1698  profileItem.click();
   1699 
   1700  info(`Waiting for the '${profileId}' profile to be applied`);
   1701  await monitor.panelWin.api.once(TEST_EVENTS.THROTTLING_CHANGED);
   1702 }
   1703 
   1704 /**
   1705 * Resize a netmonitor column.
   1706 *
   1707 * @param {Element} columnHeader
   1708 * @param {number} newPercent
   1709 * @param {number} parentWidth
   1710 * @param {string} dir
   1711 */
   1712 function resizeColumn(columnHeader, newPercent, parentWidth, dir = "ltr") {
   1713  const newWidthInPixels = (newPercent * parentWidth) / 100;
   1714  const win = columnHeader.ownerDocument.defaultView;
   1715  const currentWidth = columnHeader.getBoundingClientRect().width;
   1716  const mouseDown = dir === "rtl" ? 0 : currentWidth;
   1717  const mouseMove =
   1718    dir === "rtl" ? currentWidth - newWidthInPixels : newWidthInPixels;
   1719 
   1720  EventUtils.synthesizeMouse(
   1721    columnHeader,
   1722    mouseDown,
   1723    1,
   1724    { type: "mousedown" },
   1725    win
   1726  );
   1727  EventUtils.synthesizeMouse(
   1728    columnHeader,
   1729    mouseMove,
   1730    1,
   1731    { type: "mousemove" },
   1732    win
   1733  );
   1734  EventUtils.synthesizeMouse(
   1735    columnHeader,
   1736    mouseMove,
   1737    1,
   1738    { type: "mouseup" },
   1739    win
   1740  );
   1741 }
   1742 
   1743 /**
   1744 * Resize the waterfall netmonitor column.
   1745 * Uses slightly different logic than for the other columns.
   1746 *
   1747 * @param {Element} columnHeader
   1748 * @param {number} newPercent
   1749 * @param {number} parentWidth
   1750 * @param {string} dir
   1751 */
   1752 function resizeWaterfallColumn(
   1753  columnHeader,
   1754  newPercent,
   1755  parentWidth,
   1756  dir = "ltr"
   1757 ) {
   1758  const newWidthInPixels = (newPercent * parentWidth) / 100;
   1759  const win = columnHeader.ownerDocument.defaultView;
   1760  const mouseDown =
   1761    dir === "rtl"
   1762      ? columnHeader.getBoundingClientRect().right
   1763      : columnHeader.getBoundingClientRect().left;
   1764  const mouseMove =
   1765    dir === "rtl"
   1766      ? mouseDown +
   1767        (newWidthInPixels - columnHeader.getBoundingClientRect().width)
   1768      : mouseDown +
   1769        (columnHeader.getBoundingClientRect().width - newWidthInPixels);
   1770 
   1771  EventUtils.synthesizeMouse(
   1772    columnHeader.parentElement,
   1773    mouseDown,
   1774    1,
   1775    { type: "mousedown" },
   1776    win
   1777  );
   1778  EventUtils.synthesizeMouse(
   1779    columnHeader.parentElement,
   1780    mouseMove,
   1781    1,
   1782    { type: "mousemove" },
   1783    win
   1784  );
   1785  EventUtils.synthesizeMouse(
   1786    columnHeader.parentElement,
   1787    mouseMove,
   1788    1,
   1789    { type: "mouseup" },
   1790    win
   1791  );
   1792 }
   1793 
   1794 function getCurrentVisibleColumns(monitor) {
   1795  const { store, windowRequire } = monitor.panelWin;
   1796  const { getColumns, getVisibleColumns, hasOverride } = windowRequire(
   1797    "devtools/client/netmonitor/src/selectors/index"
   1798  );
   1799  const hasOverrideState = hasOverride(monitor.toolbox.store.getState());
   1800  const visibleColumns = getVisibleColumns(
   1801    getColumns(store.getState(), hasOverrideState)
   1802  );
   1803 
   1804  // getVisibleColumns returns an array of arrays [name, isVisible=true], flatten
   1805  // to return name.
   1806  return visibleColumns.map(([name]) => name);
   1807 }
   1808 
   1809 function findRequestByInitiator(document, initiator) {
   1810  for (const request of document.querySelectorAll(".request-list-item")) {
   1811    if (
   1812      request.querySelector(".requests-list-initiator").getAttribute("title") ==
   1813      initiator
   1814    ) {
   1815      return request;
   1816    }
   1817  }
   1818  return null;
   1819 }
   1820 
   1821 /**
   1822 * Click on the "save response as" context menu item for the provided request
   1823 * element in the provided netmonitor panel.
   1824 *
   1825 * Resolves when the context menu is closed.
   1826 *
   1827 * @param {object} monitor
   1828 *     The netmonitor instance
   1829 * @param {HTMLElement} request
   1830 *     The request item in the netmonitor table
   1831 */
   1832 async function triggerSaveResponseAs(monitor, request) {
   1833  EventUtils.sendMouseEvent({ type: "mousedown" }, request);
   1834  EventUtils.sendMouseEvent({ type: "contextmenu" }, request);
   1835 
   1836  info("Open the save dialog");
   1837  await selectContextMenuItem(monitor, "request-list-context-save-response-as");
   1838 }
   1839 
   1840 /**
   1841 * Wait until the provided path has a non-zero size on the file system.
   1842 *
   1843 * @param {string} path
   1844 *     The path to wait for.
   1845 */
   1846 async function waitForFileSavedToDisk(path) {
   1847  info("Wait for the downloaded file to be fully saved to disk: " + path);
   1848  await TestUtils.waitForCondition(async () => {
   1849    if (!(await IOUtils.exists(path))) {
   1850      return false;
   1851    }
   1852    const { size } = await IOUtils.stat(path);
   1853    return size > 0;
   1854  });
   1855 }
   1856 
   1857 /**
   1858 * Create a temporary directory to save files for a test.
   1859 * Register a cleanup function to delete the directory after the test.
   1860 *
   1861 * @returns {nsIFile}
   1862 *     The created temporary directory.
   1863 */
   1864 function createTemporarySaveDirectory() {
   1865  const saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
   1866  saveDir.append("testsavedir");
   1867 
   1868  if (!saveDir.exists()) {
   1869    saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
   1870  }
   1871 
   1872  registerCleanupFunction(function () {
   1873    saveDir.remove(true);
   1874  });
   1875  return saveDir;
   1876 }