tor-browser

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

head.js (32484B)


      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 "use strict";
      6 
      7 /* eslint no-unused-vars: [2, {"vars": "local", caughtErrors: "none"}] */
      8 /* import-globals-from ../../shared/test/shared-head.js */
      9 
     10 // Sometimes HTML pages have a `clear` function that cleans up the storage they
     11 // created. To make sure it's always called, we are registering as a cleanup
     12 // function, but since this needs to run before tabs are closed, we need to
     13 // do this registration before importing `shared-head`, since declaration
     14 // order matters.
     15 registerCleanupFunction(async () => {
     16  Services.cookies.removeAll();
     17 
     18  // Close tabs and force memory collection to happen
     19  while (gBrowser.tabs.length > 1) {
     20    const browser = gBrowser.selectedBrowser;
     21    const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree();
     22    for (const context of contexts) {
     23      await SpecialPowers.spawn(context, [], async () => {
     24        const win = content.wrappedJSObject;
     25 
     26        // Some windows (e.g., about: URLs) don't have storage available
     27        try {
     28          win.localStorage.clear();
     29          win.sessionStorage.clear();
     30        } catch (ex) {
     31          // ignore
     32        }
     33 
     34        if (win.clear) {
     35          // Do not get hung into win.clear() forever
     36          await Promise.race([
     37            new Promise(r => win.setTimeout(r, 10000)),
     38            win.clear(),
     39          ]);
     40        }
     41      });
     42    }
     43 
     44    await closeTabAndToolbox(gBrowser.selectedTab);
     45  }
     46  forceCollections();
     47 });
     48 
     49 // shared-head.js handles imports, constants, and utility functions
     50 Services.scriptloader.loadSubScript(
     51  "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
     52  this
     53 );
     54 
     55 const {
     56  TableWidget,
     57 } = require("resource://devtools/client/shared/widgets/TableWidget.js");
     58 const {
     59  LocalTabCommandsFactory,
     60 } = require("resource://devtools/client/framework/local-tab-commands-factory.js");
     61 const STORAGE_PREF = "devtools.storage.enabled";
     62 const DUMPEMIT_PREF = "devtools.dump.emit";
     63 const DEBUGGERLOG_PREF = "devtools.debugger.log";
     64 
     65 // Allows Cache API to be working on usage `http` test page
     66 const CACHES_ON_HTTP_PREF = "dom.caches.testing.enabled";
     67 const PATH = "browser/devtools/client/storage/test/";
     68 const MAIN_DOMAIN = "http://test1.example.org/" + PATH;
     69 const MAIN_DOMAIN_SECURED = "https://test1.example.org/" + PATH;
     70 const MAIN_DOMAIN_WITH_PORT = "http://test1.example.org:8000/" + PATH;
     71 const ALT_DOMAIN = "http://sectest1.example.org/" + PATH;
     72 const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH;
     73 
     74 // GUID to be used as a separator in compound keys. This must match the same
     75 // constant in devtools/server/actors/resources/storage/index.js,
     76 // devtools/client/storage/ui.js and devtools/server/tests/browser/head.js
     77 const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
     78 
     79 var gToolbox, gPanelWindow, gUI;
     80 
     81 // Services.prefs.setBoolPref(DUMPEMIT_PREF, true);
     82 // Services.prefs.setBoolPref(DEBUGGERLOG_PREF, true);
     83 
     84 Services.prefs.setBoolPref(STORAGE_PREF, true);
     85 Services.prefs.setBoolPref(CACHES_ON_HTTP_PREF, true);
     86 registerCleanupFunction(() => {
     87  gToolbox = gPanelWindow = gUI = null;
     88  Services.prefs.clearUserPref(CACHES_ON_HTTP_PREF);
     89  Services.prefs.clearUserPref(DEBUGGERLOG_PREF);
     90  Services.prefs.clearUserPref(DUMPEMIT_PREF);
     91  Services.prefs.clearUserPref(STORAGE_PREF);
     92 });
     93 
     94 /**
     95 * This generator function opens the given url in a new tab, then sets up the
     96 * page by waiting for all cookies, indexedDB items etc.
     97 *
     98 * @param url {String} The url to be opened in the new tab
     99 * @param options {Object} The tab options for the new tab
    100 *
    101 * @return {Promise} A promise that resolves after the tab is ready
    102 */
    103 async function openTab(url, options = {}) {
    104  const tab = await addTab(url, options);
    105 
    106  const browser = gBrowser.selectedBrowser;
    107  const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree();
    108 
    109  for (const context of contexts) {
    110    await SpecialPowers.spawn(context, [], async () => {
    111      const win = content.wrappedJSObject;
    112      const readyState = win.document.readyState;
    113      info(`Found a window: ${readyState}`);
    114      if (readyState != "complete") {
    115        await new Promise(resolve => {
    116          const onLoad = () => {
    117            win.removeEventListener("load", onLoad);
    118            resolve();
    119          };
    120          win.addEventListener("load", onLoad);
    121        });
    122      }
    123      if (win.setup) {
    124        await win.setup();
    125      }
    126    });
    127  }
    128 
    129  return tab;
    130 }
    131 
    132 /**
    133 * This generator function opens the given url in a new tab, then sets up the
    134 * page by waiting for all cookies, indexedDB items etc. to be created; Then
    135 * opens the storage inspector and waits for the storage tree and table to be
    136 * populated.
    137 *
    138 * @param url {String} The url to be opened in the new tab
    139 * @param options {Object} The tab options for the new tab
    140 *
    141 * @return {Promise} A promise that resolves after storage inspector is ready
    142 */
    143 async function openTabAndSetupStorage(url, options = {}) {
    144  // open tab
    145  await openTab(url, options);
    146 
    147  // open storage inspector
    148  return openStoragePanel();
    149 }
    150 
    151 /**
    152 * Open a toolbox with the storage panel opened by default
    153 * for a given Web Extension.
    154 *
    155 * @param {string} addonId
    156 *        The ID of the Web Extension to debug.
    157 */
    158 var openStoragePanelForAddon = async function (addonId) {
    159  const toolbox = await gDevTools.showToolboxForWebExtension(addonId, {
    160    toolId: "storage",
    161  });
    162 
    163  info("Making sure that the toolbox's frame is focused");
    164  await SimpleTest.promiseFocus(toolbox.win);
    165 
    166  const storage = _setupStoragePanelForTest(toolbox);
    167 
    168  return {
    169    toolbox,
    170    storage,
    171  };
    172 };
    173 
    174 /**
    175 * Open the toolbox, with the storage tool visible.
    176 *
    177 * @param tab {XULTab} Optional, the tab for the toolbox; defaults to selected tab
    178 * @param commands {Object} Optional, the commands for the toolbox; defaults to a tab commands
    179 * @param hostType {Toolbox.HostType} Optional, type of host that will host the toolbox
    180 *
    181 * @return {Promise} a promise that resolves when the storage inspector is ready
    182 */
    183 var openStoragePanel = async function ({ tab, hostType } = {}) {
    184  const toolbox = await openToolboxForTab(
    185    tab || gBrowser.selectedTab,
    186    "storage",
    187    hostType
    188  );
    189 
    190  const storage = _setupStoragePanelForTest(toolbox);
    191 
    192  return {
    193    toolbox,
    194    storage,
    195  };
    196 };
    197 
    198 /**
    199 * Set global variables needed in helper functions
    200 *
    201 * @param toolbox {Toolbox}
    202 * @return {StoragePanel}
    203 */
    204 function _setupStoragePanelForTest(toolbox) {
    205  const storage = toolbox.getPanel("storage");
    206  gPanelWindow = storage.panelWindow;
    207  gUI = storage.UI;
    208  gToolbox = toolbox;
    209 
    210  // The table animation flash causes some timeouts on Linux debug tests,
    211  // so we disable it
    212  gUI.animationsEnabled = false;
    213 
    214  return storage;
    215 }
    216 
    217 /**
    218 * Forces GC, CC and Shrinking GC to get rid of disconnected docshells and
    219 * windows.
    220 */
    221 function forceCollections() {
    222  Cu.forceGC();
    223  Cu.forceCC();
    224  Cu.forceShrinkingGC();
    225 }
    226 
    227 // Sends a click event on the passed DOM node in an async manner
    228 function click(node) {
    229  node.scrollIntoView();
    230 
    231  return new Promise(resolve => {
    232    // We need setTimeout here to allow any scrolling to complete before clicking
    233    // the node.
    234    setTimeout(() => {
    235      node.click();
    236      resolve();
    237    }, 200);
    238  });
    239 }
    240 
    241 /**
    242 * Recursively expand the variables view up to a given property.
    243 *
    244 * @param options
    245 *        Options for view expansion:
    246 *        - rootVariable: start from the given scope/variable/property.
    247 *        - expandTo: string made up of property names you want to expand.
    248 *        For example: "body.firstChild.nextSibling" given |rootVariable:
    249 *        document|.
    250 * @return object
    251 *         A promise that is resolved only when the last property in |expandTo|
    252 *         is found, and rejected otherwise. Resolution reason is always the
    253 *         last property - |nextSibling| in the example above. Rejection is
    254 *         always the last property that was found.
    255 */
    256 function variablesViewExpandTo(options) {
    257  const root = options.rootVariable;
    258  const expandTo = options.expandTo.split(".");
    259 
    260  return new Promise((resolve, reject) => {
    261    function getNext(prop) {
    262      const name = expandTo.shift();
    263      const newProp = prop.get(name);
    264 
    265      if (expandTo.length) {
    266        ok(newProp, "found property " + name);
    267        if (newProp && newProp.expand) {
    268          newProp.expand();
    269          getNext(newProp);
    270        } else {
    271          reject(prop);
    272        }
    273      } else if (newProp) {
    274        resolve(newProp);
    275      } else {
    276        reject(prop);
    277      }
    278    }
    279 
    280    if (root && root.expand) {
    281      root.expand();
    282      getNext(root);
    283    } else {
    284      resolve(root);
    285    }
    286  });
    287 }
    288 
    289 /**
    290 * Find variables or properties in a VariablesView instance.
    291 *
    292 * @param array ruleArray
    293 *        The array of rules you want to match. Each rule is an object with:
    294 *        - name (string|regexp): property name to match.
    295 *        - value (string|regexp): property value to match.
    296 *        - dontMatch (boolean): make sure the rule doesn't match any property.
    297 * @param boolean parsed
    298 *        true if we want to test the rules in the parse value section of the
    299 *        storage sidebar
    300 * @return object
    301 *         A promise object that is resolved when all the rules complete
    302 *         matching. The resolved callback is given an array of all the rules
    303 *         you wanted to check. Each rule has a new property: |matchedProp|
    304 *         which holds a reference to the Property object instance from the
    305 *         VariablesView. If the rule did not match, then |matchedProp| is
    306 *         undefined.
    307 */
    308 function findVariableViewProperties(ruleArray, parsed) {
    309  // Initialize the search.
    310  function init() {
    311    // If parsed is true, we are checking rules in the parsed value section of
    312    // the storage sidebar. That scope uses a blank variable as a placeholder
    313    // Thus, adding a blank parent to each name
    314    if (parsed) {
    315      ruleArray = ruleArray.map(({ name, value, dontMatch }) => {
    316        return { name: "." + name, value, dontMatch };
    317      });
    318    }
    319    // Separate out the rules that require expanding properties throughout the
    320    // view.
    321    const expandRules = [];
    322    const rules = ruleArray.filter(rule => {
    323      if (typeof rule.name == "string" && rule.name.indexOf(".") > -1) {
    324        expandRules.push(rule);
    325        return false;
    326      }
    327      return true;
    328    });
    329 
    330    // Search through the view those rules that do not require any properties to
    331    // be expanded. Build the array of matchers, outstanding promises to be
    332    // resolved.
    333    const outstanding = [];
    334 
    335    finder(rules, gUI.view, outstanding);
    336 
    337    // Process the rules that need to expand properties.
    338    const lastStep = processExpandRules.bind(null, expandRules);
    339 
    340    // Return the results - a promise resolved to hold the updated ruleArray.
    341    const returnResults = onAllRulesMatched.bind(null, ruleArray);
    342 
    343    return Promise.all(outstanding).then(lastStep).then(returnResults);
    344  }
    345 
    346  function onMatch(prop, rule, matched) {
    347    if (matched && !rule.matchedProp) {
    348      rule.matchedProp = prop;
    349    }
    350  }
    351 
    352  function finder(rules, view, promises) {
    353    for (const scope of view) {
    354      for (const [, prop] of scope) {
    355        for (const rule of rules) {
    356          const matcher = matchVariablesViewProperty(prop, rule);
    357          promises.push(matcher.then(onMatch.bind(null, prop, rule)));
    358        }
    359      }
    360    }
    361  }
    362 
    363  function processExpandRules(rules) {
    364    return new Promise(resolve => {
    365      const rule = rules.shift();
    366      if (!rule) {
    367        resolve(null);
    368      }
    369 
    370      const expandOptions = {
    371        rootVariable: gUI.view.getScopeAtIndex(parsed ? 1 : 0),
    372        expandTo: rule.name,
    373      };
    374 
    375      variablesViewExpandTo(expandOptions)
    376        .then(
    377          function onSuccess(prop) {
    378            const name = rule.name;
    379            const lastName = name.split(".").pop();
    380            rule.name = lastName;
    381 
    382            const matched = matchVariablesViewProperty(prop, rule);
    383            return matched
    384              .then(onMatch.bind(null, prop, rule))
    385              .then(function () {
    386                rule.name = name;
    387              });
    388          },
    389          function onFailure() {
    390            resolve(null);
    391          }
    392        )
    393        .then(processExpandRules.bind(null, rules))
    394        .then(function () {
    395          resolve(null);
    396        });
    397    });
    398  }
    399 
    400  function onAllRulesMatched(rules) {
    401    for (const rule of rules) {
    402      const matched = rule.matchedProp;
    403      if (matched && !rule.dontMatch) {
    404        ok(true, "rule " + rule.name + " matched for property " + matched.name);
    405      } else if (matched && rule.dontMatch) {
    406        ok(
    407          false,
    408          "rule " + rule.name + " should not match property " + matched.name
    409        );
    410      } else {
    411        ok(rule.dontMatch, "rule " + rule.name + " did not match any property");
    412      }
    413    }
    414    return rules;
    415  }
    416 
    417  return init();
    418 }
    419 
    420 /**
    421 * Check if a given Property object from the variables view matches the given
    422 * rule.
    423 *
    424 * @param object prop
    425 *        The variable's view Property instance.
    426 * @param object rule
    427 *        Rules for matching the property. See findVariableViewProperties() for
    428 *        details.
    429 * @return object
    430 *         A promise that is resolved when all the checks complete. Resolution
    431 *         result is a boolean that tells your promise callback the match
    432 *         result: true or false.
    433 */
    434 function matchVariablesViewProperty(prop, rule) {
    435  function resolve(result) {
    436    return Promise.resolve(result);
    437  }
    438 
    439  if (!prop) {
    440    return resolve(false);
    441  }
    442 
    443  // Any kind of string is accepted as name, including empty ones
    444  if (typeof rule.name == "string") {
    445    const match =
    446      rule.name instanceof RegExp
    447        ? rule.name.test(prop.name)
    448        : prop.name == rule.name;
    449    if (!match) {
    450      return resolve(false);
    451    }
    452  }
    453 
    454  if ("value" in rule) {
    455    let displayValue = prop.displayValue;
    456    if (prop.displayValueClassName == "token-string") {
    457      displayValue = displayValue.substring(1, displayValue.length - 1);
    458    }
    459 
    460    const match =
    461      rule.value instanceof RegExp
    462        ? rule.value.test(displayValue)
    463        : displayValue == rule.value;
    464    if (!match) {
    465      info(
    466        "rule " +
    467          rule.name +
    468          " did not match value, expected '" +
    469          rule.value +
    470          "', found '" +
    471          displayValue +
    472          "'"
    473      );
    474      return resolve(false);
    475    }
    476  }
    477 
    478  return resolve(true);
    479 }
    480 
    481 /**
    482 * Click selects a row in the table.
    483 *
    484 * @param {[string]} ids
    485 *        The array id of the item in the tree
    486 */
    487 async function selectTreeItem(ids) {
    488  if (gUI.tree.isSelected(ids)) {
    489    info(`"${ids}" is already selected, returning.`);
    490    return;
    491  }
    492  if (!gUI.tree.exists(ids)) {
    493    info(`"${ids}" does not exist, returning.`);
    494    return;
    495  }
    496 
    497  // The item exists but is not selected... select it.
    498  info(`Selecting "${ids}".`);
    499  if (ids.length > 1) {
    500    const updated = gUI.once("store-objects-updated");
    501    gUI.tree.selectedItem = ids;
    502    await updated;
    503  } else {
    504    // If the length of the IDs array is 1, a storage type
    505    // gets selected and no 'store-objects-updated' event
    506    // will be fired in that case.
    507    gUI.tree.selectedItem = ids;
    508  }
    509 }
    510 
    511 /**
    512 * Click selects a row in the table.
    513 *
    514 * @param {string} id
    515 *        The id of the row in the table widget
    516 */
    517 async function selectTableItem(id) {
    518  const table = gUI.table;
    519  const selector =
    520    ".table-widget-column#" +
    521    table.uniqueId +
    522    " .table-widget-cell[value='" +
    523    id +
    524    "']";
    525  const target = gPanelWindow.document.querySelector(selector);
    526 
    527  ok(target, `row found with id "${id}"`);
    528 
    529  if (!target) {
    530    showAvailableIds();
    531  }
    532 
    533  const updated = gUI.once("sidebar-updated");
    534 
    535  info(`selecting row "${id}"`);
    536  await click(target);
    537  await updated;
    538 }
    539 
    540 /**
    541 * Wait for eventName on target.
    542 *
    543 * @param {object} target An observable object that either supports on/off or
    544 * addEventListener/removeEventListener
    545 * @param {string} eventName
    546 * @param {boolean} useCapture Optional, for addEventListener/removeEventListener
    547 * @return A promise that resolves when the event has been handled
    548 */
    549 function once(target, eventName, useCapture = false) {
    550  info("Waiting for event: '" + eventName + "' on " + target + ".");
    551 
    552  return new Promise(resolve => {
    553    for (const [add, remove] of [
    554      ["addEventListener", "removeEventListener"],
    555      ["addListener", "removeListener"],
    556      ["on", "off"],
    557    ]) {
    558      if (add in target && remove in target) {
    559        target[add](
    560          eventName,
    561          function onEvent(...aArgs) {
    562            info("Got event: '" + eventName + "' on " + target + ".");
    563            target[remove](eventName, onEvent, useCapture);
    564            resolve(...aArgs);
    565          },
    566          useCapture
    567        );
    568        break;
    569      }
    570    }
    571  });
    572 }
    573 
    574 /**
    575 * Get values for a row.
    576 *
    577 * @param  {string}  id
    578 *         The uniqueId of the given row.
    579 * @param  {boolean} includeHidden
    580 *         Include hidden columns.
    581 *
    582 * @return {object}
    583 *         An object of column names to values for the given row.
    584 */
    585 function getRowValues(id, includeHidden = false) {
    586  const cells = getRowCells(id, includeHidden);
    587  const values = {};
    588 
    589  for (const name in cells) {
    590    const cell = cells[name];
    591 
    592    values[name] = cell.value;
    593  }
    594 
    595  return values;
    596 }
    597 
    598 /**
    599 * Get the row element for a given id
    600 *
    601 * @param  {string}  id
    602 *         The uniqueId of the given row.
    603 * @returns {Element|null}
    604 */
    605 function getRowItem(id) {
    606  const doc = gPanelWindow.document;
    607  const table = gUI.table;
    608  return doc.querySelector(
    609    `.table-widget-column#${table.uniqueId} .table-widget-cell[value='${id}']`
    610  );
    611 }
    612 
    613 /**
    614 * Get cells for a row.
    615 *
    616 * @param  {string}  id
    617 *         The uniqueId of the given row.
    618 * @param  {boolean} includeHidden
    619 *         Include hidden columns.
    620 *
    621 * @return {object}
    622 *         An object of column names to cells for the given row.
    623 */
    624 function getRowCells(id, includeHidden = false) {
    625  const table = gUI.table;
    626  const item = getRowItem(id);
    627 
    628  if (!item) {
    629    ok(
    630      false,
    631      `The row id '${id}' that was passed to getRowCells() does not ` +
    632        `exist. ${getAvailableIds()}`
    633    );
    634  }
    635 
    636  const index = table.columns.get(table.uniqueId).cellNodes.indexOf(item);
    637  const cells = {};
    638 
    639  for (const [name, column] of [...table.columns]) {
    640    if (!includeHidden && column.column.parentNode.hidden) {
    641      continue;
    642    }
    643    cells[name] = column.cellNodes[index];
    644  }
    645 
    646  return cells;
    647 }
    648 
    649 /**
    650 * Check for an empty table.
    651 */
    652 function isTableEmpty() {
    653  const doc = gPanelWindow.document;
    654  const table = gUI.table;
    655  const cells = doc.querySelectorAll(
    656    ".table-widget-column#" + table.uniqueId + " .table-widget-cell"
    657  );
    658  return cells.length === 0;
    659 }
    660 
    661 /**
    662 * Get available ids... useful for error reporting.
    663 */
    664 function getAvailableIds() {
    665  const doc = gPanelWindow.document;
    666  const table = gUI.table;
    667 
    668  let out = "Available ids:\n";
    669  const cells = doc.querySelectorAll(
    670    ".table-widget-column#" + table.uniqueId + " .table-widget-cell"
    671  );
    672  for (const cell of cells) {
    673    out += `  - ${cell.getAttribute("value")}\n`;
    674  }
    675 
    676  return out;
    677 }
    678 
    679 /**
    680 * Show available ids.
    681 */
    682 function showAvailableIds() {
    683  info(getAvailableIds());
    684 }
    685 
    686 /**
    687 * Get a cell value.
    688 *
    689 * @param {string} id
    690 *        The uniqueId of the row.
    691 * @param {string} column
    692 *        The id of the column
    693 *
    694 * @yield {string}
    695 *        The cell value.
    696 */
    697 function getCellValue(id, column) {
    698  const row = getRowValues(id, true);
    699 
    700  if (typeof row[column] === "undefined") {
    701    let out = "";
    702    for (const key in row) {
    703      const value = row[key];
    704 
    705      out += `  - ${key} = ${value}\n`;
    706    }
    707 
    708    ok(
    709      false,
    710      `The column name '${column}' that was passed to ` +
    711        `getCellValue() does not exist. Current column names and row ` +
    712        `values are:\n${out}`
    713    );
    714  }
    715 
    716  return row[column];
    717 }
    718 
    719 /**
    720 * Edit a cell value. The cell is assumed to be in edit mode, see startCellEdit.
    721 *
    722 * @param {string} id
    723 *        The uniqueId of the row.
    724 * @param {string} column
    725 *        The id of the column
    726 * @param {string} newValue
    727 *        Replacement value.
    728 * @param {boolean} validate
    729 *        Validate result? Default true.
    730 *
    731 * @yield {string}
    732 *        The uniqueId of the changed row.
    733 */
    734 async function editCell(id, column, newValue, validate = true) {
    735  const row = getRowCells(id, true);
    736  const editableFieldsEngine = gUI.table._editableFieldsEngine;
    737 
    738  editableFieldsEngine.edit(row[column]);
    739 
    740  await typeWithTerminator(newValue, "KEY_Enter", validate);
    741 }
    742 
    743 /**
    744 * Begin edit mode for a cell.
    745 *
    746 * @param {string} id
    747 *        The uniqueId of the row.
    748 * @param {string} column
    749 *        The id of the column
    750 * @param {boolean} selectText
    751 *        Select text? Default true.
    752 */
    753 function startCellEdit(id, column, selectText = true) {
    754  const row = getRowCells(id, true);
    755  const editableFieldsEngine = gUI.table._editableFieldsEngine;
    756  const cell = row[column];
    757 
    758  info("Selecting row " + id);
    759  gUI.table.selectedRow = id;
    760 
    761  info("Starting cell edit (" + id + ", " + column + ")");
    762  editableFieldsEngine.edit(cell);
    763 
    764  if (!selectText) {
    765    const textbox = gUI.table._editableFieldsEngine.textbox;
    766    textbox.selectionEnd = textbox.selectionStart;
    767  }
    768 }
    769 
    770 /**
    771 * Check a cell value.
    772 *
    773 * @param {string} id
    774 *        The uniqueId of the row.
    775 * @param {string} column
    776 *        The id of the column
    777 * @param {string} expected
    778 *        Expected value.
    779 */
    780 function checkCell(id, column, expected) {
    781  is(
    782    getCellValue(id, column),
    783    expected,
    784    column + " column has the right value for " + id
    785  );
    786 }
    787 
    788 /**
    789 * Check that a cell is not in edit mode.
    790 *
    791 * @param {string} id
    792 *        The uniqueId of the row.
    793 * @param {string} column
    794 *        The id of the column
    795 */
    796 function checkCellUneditable(id, column) {
    797  const row = getRowCells(id, true);
    798  const cell = row[column];
    799 
    800  const editableFieldsEngine = gUI.table._editableFieldsEngine;
    801  const textbox = editableFieldsEngine.textbox;
    802 
    803  // When a field is being edited, the cell is hidden, and the textbox is made visible.
    804  ok(
    805    !cell.hidden && textbox.hidden,
    806    `The cell located in column ${column} and row ${id} is not editable.`
    807  );
    808 }
    809 
    810 /**
    811 * Show or hide a column.
    812 *
    813 * @param  {string} id
    814 *         The uniqueId of the given column.
    815 * @param  {boolean} state
    816 *         true = show, false = hide
    817 */
    818 function showColumn(id, state) {
    819  const columns = gUI.table.columns;
    820  const column = columns.get(id);
    821  column.column.hidden = !state;
    822 }
    823 
    824 /**
    825 * Toggle sort direction on a column by clicking on the column header.
    826 *
    827 * @param  {string} id
    828 *         The uniqueId of the given column.
    829 */
    830 function clickColumnHeader(id) {
    831  const columns = gUI.table.columns;
    832  const column = columns.get(id);
    833  const header = column.header;
    834 
    835  header.click();
    836 }
    837 
    838 /**
    839 * Show or hide all columns.
    840 *
    841 * @param  {boolean} state
    842 *         true = show, false = hide
    843 */
    844 function showAllColumns(state) {
    845  const columns = gUI.table.columns;
    846 
    847  for (const [id] of columns) {
    848    showColumn(id, state);
    849  }
    850 }
    851 
    852 /**
    853 * Type a string in the currently selected editor and then wait for the row to
    854 * be updated.
    855 *
    856 * @param  {string} str
    857 *         The string to type.
    858 * @param  {string} terminator
    859 *         The terminating key e.g. KEY_Enter or KEY_Tab
    860 * @param  {boolean} validate
    861 *         Validate result? Default true.
    862 */
    863 async function typeWithTerminator(str, terminator, validate = true) {
    864  const editableFieldsEngine = gUI.table._editableFieldsEngine;
    865  const textbox = editableFieldsEngine.textbox;
    866  const colName = textbox.closest(".table-widget-column").id;
    867 
    868  const changeExpected = str !== textbox.value;
    869 
    870  if (!changeExpected) {
    871    return editableFieldsEngine.currentTarget.getAttribute("data-id");
    872  }
    873 
    874  info("Typing " + str);
    875  EventUtils.sendString(str, gPanelWindow);
    876 
    877  info("Pressing " + terminator);
    878  EventUtils.synthesizeKey(terminator, null, gPanelWindow);
    879 
    880  if (validate) {
    881    info("Validating results... waiting for ROW_EDIT event.");
    882    const uniqueId = await gUI.table.once(TableWidget.EVENTS.ROW_EDIT);
    883 
    884    checkCell(uniqueId, colName, str);
    885    return uniqueId;
    886  }
    887 
    888  return gUI.table.once(TableWidget.EVENTS.ROW_EDIT);
    889 }
    890 
    891 function getCurrentEditorValue() {
    892  const editableFieldsEngine = gUI.table._editableFieldsEngine;
    893  const textbox = editableFieldsEngine.textbox;
    894 
    895  return textbox.value;
    896 }
    897 
    898 /**
    899 * Press a key x times.
    900 *
    901 * @param  {string} key
    902 *         The key to press e.g. VK_RETURN or VK_TAB
    903 * @param {number} x
    904 *         The number of times to press the key.
    905 * @param {object} modifiers
    906 *         The event modifier e.g. {shiftKey: true}
    907 */
    908 function PressKeyXTimes(key, x, modifiers = {}) {
    909  for (let i = 0; i < x; i++) {
    910    EventUtils.synthesizeKey(key, modifiers);
    911  }
    912 }
    913 
    914 /**
    915 * Verify the storage inspector state: check that given type/host exists
    916 * in the tree, and that the table contains rows with specified names.
    917 *
    918 * @param {Array} state Array of state specifications. For example,
    919 *        [["cookies", "example.com"], ["c1", "c2"]] means to select the
    920 *        "example.com" host in cookies and then verify there are "c1" and "c2"
    921 *        cookies (and no other ones).
    922 */
    923 async function checkState(state) {
    924  for (const [store, names] of state) {
    925    const storeName = store.join(" > ");
    926    info(`Selecting tree item ${storeName}`);
    927    await selectTreeItem(store);
    928 
    929    const items = gUI.table.items;
    930 
    931    is(
    932      items.size,
    933      names.length,
    934      `There is correct number of rows in ${storeName}`
    935    );
    936 
    937    if (names.length === 0) {
    938      showAvailableIds();
    939    }
    940 
    941    for (const name of names) {
    942      if (!items.has(name)) {
    943        showAvailableIds();
    944      }
    945      ok(items.has(name), `There is item with name '${name}' in ${storeName}`);
    946    }
    947  }
    948 }
    949 
    950 /**
    951 * Checks if document's active element is within the given element.
    952 *
    953 * @param  {HTMLDocument}  doc document with active element in question
    954 * @param  {DOMNode}       container element tested on focus containment
    955 * @return {boolean}
    956 */
    957 function containsFocus(doc, container) {
    958  let elm = doc.activeElement;
    959  while (elm) {
    960    if (elm === container) {
    961      return true;
    962    }
    963    elm = elm.parentNode;
    964  }
    965  return false;
    966 }
    967 
    968 var focusSearchBoxUsingShortcut = async function (panelWin, callback) {
    969  info("Focusing search box");
    970  const searchBox = panelWin.document.getElementById("storage-searchbox");
    971  const focused = once(searchBox, "focus");
    972 
    973  panelWin.focus();
    974 
    975  const shortcut =
    976    await panelWin.document.l10n.formatValue("storage-filter-key");
    977  synthesizeKeyShortcut(shortcut);
    978 
    979  await focused;
    980 
    981  if (callback) {
    982    callback();
    983  }
    984 };
    985 
    986 function getCookieId(name, domain, path, partitionKey = "") {
    987  return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}${SEPARATOR_GUID}${partitionKey}`;
    988 }
    989 
    990 function setPermission(url, permission) {
    991  const nsIPermissionManager = Ci.nsIPermissionManager;
    992 
    993  const uri = Services.io.newURI(url);
    994  const principal = Services.scriptSecurityManager.createContentPrincipal(
    995    uri,
    996    {}
    997  );
    998 
    999  Cc["@mozilla.org/permissionmanager;1"]
   1000    .getService(nsIPermissionManager)
   1001    .addFromPrincipal(principal, permission, nsIPermissionManager.ALLOW_ACTION);
   1002 }
   1003 
   1004 function toggleSidebar() {
   1005  gUI.sidebarToggleBtn.click();
   1006 }
   1007 
   1008 function sidebarToggleVisible() {
   1009  return !gUI.sidebarToggleBtn.hidden;
   1010 }
   1011 
   1012 /**
   1013 * Check whether the variables view in the sidebar contains a tree.
   1014 *
   1015 * @param  {boolean} state
   1016 *         Should a tree be visible?
   1017 */
   1018 function sidebarParseTreeVisible(state) {
   1019  if (state) {
   1020    Assert.greater(
   1021      gUI.view._testOnlyHierarchy.size,
   1022      2,
   1023      "Parse tree should be visible."
   1024    );
   1025  } else {
   1026    Assert.lessOrEqual(
   1027      gUI.view._testOnlyHierarchy.size,
   1028      2,
   1029      "Parse tree should not be visible."
   1030    );
   1031  }
   1032 }
   1033 
   1034 /**
   1035 * Add an item.
   1036 *
   1037 * @param  {Array} store
   1038 *         An array containing the path to the store to which we wish to add an
   1039 *         item.
   1040 * @return {Promise} A Promise that resolves to the row id of the added item.
   1041 */
   1042 async function performAdd(store) {
   1043  const storeName = store.join(" > ");
   1044  const toolbar = gPanelWindow.document.getElementById("storage-toolbar");
   1045  const type = store[0];
   1046 
   1047  await selectTreeItem(store);
   1048 
   1049  const menuAdd = toolbar.querySelector("#add-button");
   1050 
   1051  if (menuAdd.hidden) {
   1052    is(
   1053      menuAdd.hidden,
   1054      false,
   1055      `performAdd called for ${storeName} but it is not supported`
   1056    );
   1057    return "";
   1058  }
   1059 
   1060  const eventEdit = gUI.table.once("row-edit");
   1061  const eventWait = gUI.once("store-objects-edit");
   1062 
   1063  menuAdd.click();
   1064 
   1065  const rowId = await eventEdit;
   1066  await eventWait;
   1067 
   1068  const key = type === "cookies" ? "uniqueKey" : "name";
   1069  const value = getCellValue(rowId, key);
   1070 
   1071  is(rowId, value, `Row '${rowId}' was successfully added.`);
   1072 
   1073  return rowId;
   1074 }
   1075 
   1076 // Cell css selector that can be used to count or select cells.
   1077 // The selector is restricted to a single column to avoid counting duplicates.
   1078 const CELL_SELECTOR =
   1079  "#storage-table .table-widget-column:first-child .table-widget-cell";
   1080 
   1081 function getCellLength() {
   1082  return gPanelWindow.document.querySelectorAll(CELL_SELECTOR).length;
   1083 }
   1084 
   1085 function checkCellLength(len) {
   1086  is(getCellLength(), len, `Table should contain ${len} items`);
   1087 }
   1088 
   1089 async function scroll() {
   1090  const $ = id => gPanelWindow.document.querySelector(id);
   1091  const table = $("#storage-table .table-widget-body");
   1092  const cell = $(CELL_SELECTOR);
   1093  const cellHeight = cell.getBoundingClientRect().height;
   1094 
   1095  const onStoresUpdate = gUI.once("store-objects-updated");
   1096  table.scrollTop += cellHeight * 50;
   1097  await onStoresUpdate;
   1098 }
   1099 
   1100 /**
   1101 * Asserts that the given tree path exists
   1102 *
   1103 * @param {Document} doc
   1104 * @param {Array} path
   1105 * @param {boolean} isExpected
   1106 */
   1107 function checkTree(doc, path, isExpected = true) {
   1108  const doesExist = isInTree(doc, path);
   1109  ok(
   1110    isExpected ? doesExist : !doesExist,
   1111    `${path.join(" > ")} is ${isExpected ? "" : "not "}in the tree`
   1112  );
   1113 }
   1114 
   1115 /**
   1116 * Returns whether a tree path exists
   1117 *
   1118 * @param {Document} doc
   1119 * @param {Array} path
   1120 */
   1121 function isInTree(doc, path) {
   1122  const treeId = JSON.stringify(path);
   1123  return !!doc.querySelector(`[data-id='${treeId}']`);
   1124 }
   1125 
   1126 /**
   1127 * Returns the label of the node for the provided tree path
   1128 *
   1129 * @param {Document} doc
   1130 * @param {Array} path
   1131 * @returns {string}
   1132 */
   1133 function getTreeNodeLabel(doc, path) {
   1134  const treeId = JSON.stringify(path);
   1135  return doc.querySelector(`[data-id='${treeId}'] .tree-widget-item`)
   1136    .textContent;
   1137 }
   1138 
   1139 /**
   1140 * Checks that the pair <name, value> is displayed at the data table
   1141 *
   1142 * @param {string} name
   1143 * @param {any} value
   1144 */
   1145 function checkStorageData(name, value) {
   1146  ok(
   1147    hasStorageData(name, value),
   1148    `Table row has an entry for: ${name} with value: ${value}`
   1149  );
   1150 }
   1151 
   1152 async function waitForStorageData(name, value) {
   1153  info("Waiting for data to appear in the table");
   1154  await waitFor(() => hasStorageData(name, value));
   1155  ok(true, `Table row has an entry for: ${name} with value: ${value}`);
   1156 }
   1157 
   1158 /**
   1159 * Returns whether the pair <name, value> is displayed at the data table
   1160 *
   1161 * @param {string} name
   1162 * @param {any} value
   1163 */
   1164 function hasStorageData(name, value) {
   1165  return gUI.table.items.get(name)?.value === value;
   1166 }
   1167 
   1168 /**
   1169 * Returns an URL of a page that uses the document-builder to generate its content
   1170 *
   1171 * @param {string} domain
   1172 * @param {string} html
   1173 * @param {string} protocol
   1174 */
   1175 function buildURLWithContent(domain, html, protocol = "https") {
   1176  return `${protocol}://${domain}/document-builder.sjs?html=${encodeURI(html)}`;
   1177 }
   1178 
   1179 /**
   1180 * Asserts that the given cookie holds the provided value in the data table
   1181 *
   1182 * @param {string} name
   1183 * @param {string} value
   1184 */
   1185 function checkCookieData(name, value) {
   1186  ok(
   1187    hasCookieData(name, value),
   1188    `Table row has an entry for: ${name} with value: ${value}`
   1189  );
   1190 }
   1191 
   1192 /**
   1193 * Returns whether the given cookie holds the provided value in the data table
   1194 *
   1195 * @param {string} name
   1196 * @param {string} value
   1197 */
   1198 function hasCookieData(name, value) {
   1199  const rows = Array.from(gUI.table.items);
   1200  const cookie = rows.map(([, data]) => data).find(x => x.name === name);
   1201 
   1202  info(`found ${cookie?.value}`);
   1203  return cookie?.value === value;
   1204 }