tor-browser

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

head.js (21629B)


      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 /* eslint no-unused-vars: [2, {"vars": "local"}] */
      5 
      6 "use strict";
      7 
      8 // Import the inspector's head.js first (which itself imports shared-head.js).
      9 Services.scriptloader.loadSubScript(
     10  "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
     11  this
     12 );
     13 
     14 var {
     15  getInplaceEditorForSpan: inplaceEditor,
     16 } = require("resource://devtools/client/shared/inplace-editor.js");
     17 var clipboard = require("resource://devtools/shared/platform/clipboard.js");
     18 
     19 // If a test times out we want to see the complete log and not just the last few
     20 // lines.
     21 SimpleTest.requestCompleteLog();
     22 
     23 // Toggle this pref on to see all DevTools event communication. This is hugely
     24 // useful for fixing race conditions.
     25 // Services.prefs.setBoolPref("devtools.dump.emit", true);
     26 
     27 /**
     28 * Some tests may need to import one or more of the test helper scripts.
     29 * A test helper script is simply a js file that contains common test code that
     30 * is either not common-enough to be in head.js, or that is located in a
     31 * separate directory.
     32 * The script will be loaded synchronously and in the test's scope.
     33 *
     34 * @param {string} filePath The file path, relative to the current directory.
     35 *                 Examples:
     36 *                 - "helper_attributes_test_runner.js"
     37 */
     38 function loadHelperScript(filePath) {
     39  const testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
     40  Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
     41 }
     42 
     43 /**
     44 * Get the MarkupContainer object instance that corresponds to the given
     45 * NodeFront
     46 *
     47 * @param {NodeFront} nodeFront
     48 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
     49 * loaded in the toolbox
     50 * @return {MarkupContainer}
     51 */
     52 function getContainerForNodeFront(nodeFront, { markup }) {
     53  return markup.getContainer(nodeFront);
     54 }
     55 
     56 /**
     57 * Get the MarkupContainer object instance that corresponds to the given
     58 * selector
     59 *
     60 * @param {string | NodeFront} selector
     61 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
     62 * loaded in the toolbox
     63 * @param {boolean} Set to true in the event that the node shouldn't be found.
     64 * @return {MarkupContainer}
     65 */
     66 var getContainerForSelector = async function (
     67  selector,
     68  inspector,
     69  expectFailure = false
     70 ) {
     71  info("Getting the markup-container for node " + selector);
     72  const nodeFront = await getNodeFront(selector, inspector);
     73  const container = getContainerForNodeFront(nodeFront, inspector);
     74 
     75  if (expectFailure) {
     76    ok(!container, "Shouldn't find markup-container for selector: " + selector);
     77  } else {
     78    ok(container, "Found markup-container for selector: " + selector);
     79  }
     80 
     81  return container;
     82 };
     83 
     84 /**
     85 * Retrieve the nodeValue for the firstChild of a provided selector on the content page.
     86 *
     87 * @param {string} selector
     88 * @return {string} the nodeValue of the first
     89 */
     90 function getFirstChildNodeValue(selector) {
     91  return SpecialPowers.spawn(
     92    gBrowser.selectedBrowser,
     93    [selector],
     94    _selector => {
     95      return content.document.querySelector(_selector).firstChild.nodeValue;
     96    }
     97  );
     98 }
     99 
    100 /**
    101 * Using the markupview's _waitForChildren function, wait for all queued
    102 * children updates to be handled.
    103 *
    104 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    105 * loaded in the toolbox
    106 * @return a promise that resolves when all queued children updates have been
    107 * handled
    108 */
    109 function waitForChildrenUpdated({ markup }) {
    110  info("Waiting for queued children updates to be handled");
    111  return new Promise(resolve => {
    112    markup._waitForChildren().then(() => {
    113      executeSoon(resolve);
    114    });
    115  });
    116 }
    117 
    118 /**
    119 * Simulate a click on the markup-container (a line in the markup-view)
    120 * that corresponds to the selector passed.
    121 *
    122 * @param {string | NodeFront} selector
    123 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    124 * loaded in the toolbox
    125 * @return {Promise} Resolves when the node has been selected.
    126 */
    127 var clickContainer = async function (selector, inspector) {
    128  info("Clicking on the markup-container for node " + selector);
    129 
    130  const nodeFront = await getNodeFront(selector, inspector);
    131  const container = getContainerForNodeFront(nodeFront, inspector);
    132 
    133  const updated = container.selected
    134    ? Promise.resolve()
    135    : inspector.once("inspector-updated");
    136  EventUtils.synthesizeMouseAtCenter(
    137    container.tagLine,
    138    { type: "mousedown" },
    139    inspector.markup.doc.defaultView
    140  );
    141  EventUtils.synthesizeMouseAtCenter(
    142    container.tagLine,
    143    { type: "mouseup" },
    144    inspector.markup.doc.defaultView
    145  );
    146  return updated;
    147 };
    148 
    149 /**
    150 * Focus a given editable element, enter edit mode, set value, and commit
    151 *
    152 * @param {DOMNode} field The element that gets editable after receiving focus
    153 * and <ENTER> keypress
    154 * @param {string} value The string value to be set into the edited field
    155 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    156 * loaded in the toolbox
    157 */
    158 function setEditableFieldValue(field, value, inspector) {
    159  field.focus();
    160  EventUtils.sendKey("return", inspector.panelWin);
    161  const input = inplaceEditor(field).input;
    162  ok(input, "Found editable field for setting value: " + value);
    163  input.value = value;
    164  EventUtils.sendKey("return", inspector.panelWin);
    165 }
    166 
    167 /**
    168 * Focus the new-attribute inplace-editor field of a node's markup container
    169 * and enters the given text, then wait for it to be applied and the for the
    170 * node to mutates (when new attribute(s) is(are) created)
    171 *
    172 * @param {string} selector The selector for the node to edit.
    173 * @param {string} text The new attribute text to be entered (e.g. "id='test'")
    174 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    175 * loaded in the toolbox
    176 * @return a promise that resolves when the node has mutated
    177 */
    178 var addNewAttributes = async function (selector, text, inspector) {
    179  info(`Entering text "${text}" in new attribute field for node ${selector}`);
    180 
    181  const container = await focusNode(selector, inspector);
    182  ok(container, "The container for '" + selector + "' was found");
    183 
    184  info("Listening for the markupmutation event");
    185  const nodeMutated = inspector.once("markupmutation");
    186  setEditableFieldValue(container.editor.newAttr, text, inspector);
    187  await nodeMutated;
    188 };
    189 
    190 /**
    191 * Checks that a node has the given attributes.
    192 *
    193 * @param {string} selector The selector for the node to check.
    194 * @param {object} expected An object containing the attributes to check.
    195 *        e.g. {id: "id1", class: "someclass"}
    196 *
    197 * Note that node.getAttribute() returns attribute values provided by the HTML
    198 * parser. The parser only provides unescaped entities so &amp; will return &.
    199 */
    200 var assertAttributes = async function (selector, expected) {
    201  const actualAttributes = await getContentPageElementAttributes(selector);
    202  is(
    203    actualAttributes.length,
    204    Object.keys(expected).length,
    205    "The node " + selector + " has the expected number of attributes."
    206  );
    207  for (const attr in expected) {
    208    const foundAttr = actualAttributes.find(({ name }) => name === attr);
    209    const foundValue = foundAttr ? foundAttr.value : undefined;
    210    ok(foundAttr, "The node " + selector + " has the attribute " + attr);
    211    is(
    212      foundValue,
    213      expected[attr],
    214      "The node " + selector + " has the correct " + attr + " attribute value"
    215    );
    216  }
    217 };
    218 
    219 /**
    220 * Undo the last markup-view action and wait for the corresponding mutation to
    221 * occur
    222 *
    223 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    224 * loaded in the toolbox
    225 * @return a promise that resolves when the markup-mutation has been treated or
    226 * rejects if no undo action is possible
    227 */
    228 function undoChange(inspector) {
    229  const canUndo = inspector.markup.undo.canUndo();
    230  ok(canUndo, "The last change in the markup-view can be undone");
    231  if (!canUndo) {
    232    return Promise.reject();
    233  }
    234 
    235  const mutated = inspector.once("markupmutation");
    236  inspector.markup.undo.undo();
    237  return mutated;
    238 }
    239 
    240 /**
    241 * Redo the last markup-view action and wait for the corresponding mutation to
    242 * occur
    243 *
    244 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    245 * loaded in the toolbox
    246 * @return a promise that resolves when the markup-mutation has been treated or
    247 * rejects if no redo action is possible
    248 */
    249 function redoChange(inspector) {
    250  const canRedo = inspector.markup.undo.canRedo();
    251  ok(canRedo, "The last change in the markup-view can be redone");
    252  if (!canRedo) {
    253    return Promise.reject();
    254  }
    255 
    256  const mutated = inspector.once("markupmutation");
    257  inspector.markup.undo.redo();
    258  return mutated;
    259 }
    260 
    261 /**
    262 * Check to see if the inspector menu items for editing are disabled.
    263 * Things like Edit As HTML, Delete Node, etc.
    264 *
    265 * @param {NodeFront} nodeFront
    266 * @param {InspectorPanel} inspector
    267 * @param {boolean} assert Should this function run assertions inline.
    268 * @return A promise that resolves with a boolean indicating whether
    269 *         the menu items are disabled once the menu has been checked.
    270 */
    271 var isEditingMenuDisabled = async function (
    272  nodeFront,
    273  inspector,
    274  assert = true
    275 ) {
    276  // To ensure clipboard contains something to paste.
    277  clipboard.copyString("<p>test</p>");
    278 
    279  await selectNode(nodeFront, inspector);
    280  const allMenuItems = openContextMenuAndGetAllItems(inspector);
    281 
    282  const deleteMenuItem = allMenuItems.find(i => i.id === "node-menu-delete");
    283  const editHTMLMenuItem = allMenuItems.find(
    284    i => i.id === "node-menu-edithtml"
    285  );
    286  const pasteHTMLMenuItem = allMenuItems.find(
    287    i => i.id === "node-menu-pasteouterhtml"
    288  );
    289 
    290  if (assert) {
    291    ok(deleteMenuItem.disabled, "Delete menu item is disabled");
    292    ok(editHTMLMenuItem.disabled, "Edit HTML menu item is disabled");
    293    ok(pasteHTMLMenuItem.disabled, "Paste HTML menu item is disabled");
    294  }
    295 
    296  return (
    297    deleteMenuItem.disabled &&
    298    editHTMLMenuItem.disabled &&
    299    pasteHTMLMenuItem.disabled
    300  );
    301 };
    302 
    303 /**
    304 * Check to see if the inspector menu items for editing are enabled.
    305 * Things like Edit As HTML, Delete Node, etc.
    306 *
    307 * @param {NodeFront} nodeFront
    308 * @param {InspectorPanel} inspector
    309 * @param {boolean} assert Should this function run assertions inline.
    310 * @return A promise that resolves with a boolean indicating whether
    311 *         the menu items are enabled once the menu has been checked.
    312 */
    313 var isEditingMenuEnabled = async function (
    314  nodeFront,
    315  inspector,
    316  assert = true
    317 ) {
    318  // To ensure clipboard contains something to paste.
    319  clipboard.copyString("<p>test</p>");
    320 
    321  await selectNode(nodeFront, inspector);
    322  const allMenuItems = openContextMenuAndGetAllItems(inspector);
    323 
    324  const deleteMenuItem = allMenuItems.find(i => i.id === "node-menu-delete");
    325  const editHTMLMenuItem = allMenuItems.find(
    326    i => i.id === "node-menu-edithtml"
    327  );
    328  const pasteHTMLMenuItem = allMenuItems.find(
    329    i => i.id === "node-menu-pasteouterhtml"
    330  );
    331 
    332  if (assert) {
    333    ok(!deleteMenuItem.disabled, "Delete menu item is enabled");
    334    ok(!editHTMLMenuItem.disabled, "Edit HTML menu item is enabled");
    335    ok(!pasteHTMLMenuItem.disabled, "Paste HTML menu item is enabled");
    336  }
    337 
    338  return (
    339    !deleteMenuItem.disabled &&
    340    !editHTMLMenuItem.disabled &&
    341    !pasteHTMLMenuItem.disabled
    342  );
    343 };
    344 
    345 /**
    346 * Wait for all current promises to be resolved. See this as executeSoon that
    347 * can be used with yield.
    348 */
    349 function promiseNextTick() {
    350  return new Promise(resolve => {
    351    executeSoon(resolve);
    352  });
    353 }
    354 
    355 /**
    356 * `await` with timeout.
    357 *
    358 * Usage:
    359 *   const badgeEventAdded = inspector.markup.once("badge-added-event");
    360 *   ...
    361 *   const result = await awaitWithTimeout(badgeEventAdded, 3000);
    362 *   is(result, "timeout", "Ensure that no event badges were added");
    363 *
    364 * @param  {Promise} promise
    365 *         Promise to resolve
    366 * @param  {number} ms
    367 *         Milliseconds to wait.
    368 * @return "timeout" on timeout, otherwise the result of the fulfilled promise.
    369 */
    370 async function awaitWithTimeout(promise, ms) {
    371  const timeout = new Promise(resolve => {
    372    // eslint-disable-next-line
    373    const wait = setTimeout(() => {
    374      clearTimeout(wait);
    375      resolve("timeout");
    376    }, ms);
    377  });
    378 
    379  return Promise.race([promise, timeout]);
    380 }
    381 
    382 /**
    383 * Collapses the current text selection in an input field and tabs to the next
    384 * field.
    385 */
    386 function collapseSelectionAndTab(inspector) {
    387  // collapse selection and move caret to end
    388  EventUtils.sendKey("tab", inspector.panelWin);
    389  // next element
    390  EventUtils.sendKey("tab", inspector.panelWin);
    391 }
    392 
    393 /**
    394 * Collapses the current text selection in an input field and tabs to the
    395 * previous field.
    396 */
    397 function collapseSelectionAndShiftTab(inspector) {
    398  // collapse selection and move caret to end
    399  EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, inspector.panelWin);
    400  // previous element
    401  EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, inspector.panelWin);
    402 }
    403 
    404 /**
    405 * Check that the current focused element is an attribute element in the markup
    406 * view.
    407 *
    408 * @param {string} attrName The attribute name expected to be found
    409 * @param {boolean} editMode Whether or not the attribute should be in edit mode
    410 */
    411 function checkFocusedAttribute(attrName, editMode) {
    412  const focusedAttr = Services.focus.focusedElement;
    413  ok(focusedAttr, "Has a focused element");
    414 
    415  const dataAttr = focusedAttr.parentNode.dataset.attr;
    416  is(dataAttr, attrName, attrName + " attribute editor is currently focused.");
    417  if (editMode) {
    418    // Using a multiline editor for attributes, the focused element should be a textarea.
    419    is(focusedAttr.tagName, "textarea", attrName + "is in edit mode");
    420  } else {
    421    is(focusedAttr.tagName, "span", attrName + "is not in edit mode");
    422  }
    423 }
    424 
    425 /**
    426 * Get attributes for node as how they are represented in editor.
    427 *
    428 * @param  {string} selector
    429 * @param  {InspectorPanel} inspector
    430 * @return {Promise}
    431 *         A promise that resolves with an array of attribute names
    432 *         (e.g. ["id", "class", "href"])
    433 */
    434 var getAttributesFromEditor = async function (selector, inspector) {
    435  const nodeList = (
    436    await getContainerForSelector(selector, inspector)
    437  ).tagLine.querySelectorAll("[data-attr]");
    438 
    439  return [...nodeList].map(node => node.getAttribute("data-attr"));
    440 };
    441 
    442 /**
    443 * Simulate dragging a MarkupContainer by calling its mousedown and mousemove
    444 * handlers.
    445 *
    446 * @param {InspectorPanel} inspector The current inspector-panel instance.
    447 * @param {string | MarkupContainer} selector The selector to identify the node or
    448 * the MarkupContainer for this node.
    449 * @param {number} xOffset Optional x offset to drag by.
    450 * @param {number} yOffset Optional y offset to drag by.
    451 */
    452 async function simulateNodeDrag(
    453  inspector,
    454  selector,
    455  xOffset = 10,
    456  yOffset = 10
    457 ) {
    458  const container =
    459    typeof selector === "string"
    460      ? await getContainerForSelector(selector, inspector)
    461      : selector;
    462  container.elt.scrollIntoView(true);
    463  const rect = container.tagLine.getBoundingClientRect();
    464  const scrollX = inspector.markup.doc.documentElement.scrollLeft;
    465  const scrollY = inspector.markup.doc.documentElement.scrollTop;
    466 
    467  info("Simulate mouseDown on element " + selector);
    468  container._onMouseDown({
    469    target: container.tagLine,
    470    button: 0,
    471    pageX: scrollX + rect.x,
    472    pageY: scrollY + rect.y,
    473    stopPropagation: () => {},
    474    preventDefault: () => {},
    475  });
    476 
    477  // _onMouseDown selects the node, so make sure to wait for the
    478  // inspector-updated event if the current selection was different.
    479  if (inspector.selection.nodeFront !== container.node) {
    480    await inspector.once("inspector-updated");
    481  }
    482 
    483  info("Simulate mouseMove on element " + selector);
    484  container.onMouseMove({
    485    pageX: scrollX + rect.x + xOffset,
    486    pageY: scrollY + rect.y + yOffset,
    487  });
    488 }
    489 
    490 /**
    491 * Simulate dropping a MarkupContainer by calling its mouseup handler. This is
    492 * meant to be called after simulateNodeDrag has been called.
    493 *
    494 * @param {InspectorPanel} inspector The current inspector-panel instance.
    495 * @param {string | MarkupContainer} selector The selector to identify the node or
    496 * the MarkupContainer for this node.
    497 */
    498 async function simulateNodeDrop(inspector, selector) {
    499  info("Simulate mouseUp on element " + selector);
    500  const container =
    501    typeof selector === "string"
    502      ? await getContainerForSelector(selector, inspector)
    503      : selector;
    504  container.onMouseUp();
    505  inspector.markup._onMouseUp();
    506 }
    507 
    508 /**
    509 * Simulate drag'n'dropping a MarkupContainer by calling its mousedown,
    510 * mousemove and mouseup handlers.
    511 *
    512 * @param {InspectorPanel} inspector The current inspector-panel instance.
    513 * @param {string | MarkupContainer} selector The selector to identify the node or
    514 * the MarkupContainer for this node.
    515 * @param {number} xOffset Optional x offset to drag by.
    516 * @param {number} yOffset Optional y offset to drag by.
    517 */
    518 async function simulateNodeDragAndDrop(inspector, selector, xOffset, yOffset) {
    519  await simulateNodeDrag(inspector, selector, xOffset, yOffset);
    520  await simulateNodeDrop(inspector, selector);
    521 }
    522 
    523 /**
    524 * Waits until the element has not scrolled for 30 consecutive frames.
    525 */
    526 async function waitForScrollStop(doc) {
    527  const el = doc.documentElement;
    528  const win = doc.defaultView;
    529  let lastScrollTop = el.scrollTop;
    530  let stopFrameCount = 0;
    531  while (stopFrameCount < 30) {
    532    // Wait for a frame.
    533    await new Promise(resolve => win.requestAnimationFrame(resolve));
    534 
    535    // Check if the element has scrolled.
    536    if (lastScrollTop == el.scrollTop) {
    537      // No scrolling since the last frame.
    538      stopFrameCount++;
    539    } else {
    540      // The element has scrolled. Reset the frame counter.
    541      stopFrameCount = 0;
    542      lastScrollTop = el.scrollTop;
    543    }
    544  }
    545 
    546  return lastScrollTop;
    547 }
    548 
    549 /**
    550 * Select a node in the inspector and try to delete it using the provided key. After that,
    551 * check that the expected element is focused.
    552 *
    553 * @param {InspectorPanel} inspector
    554 *        The current inspector-panel instance.
    555 * @param {string} key
    556 *        The key to simulate to delete the node
    557 * @param {object}
    558 *        - {String} selector: selector of the element to delete.
    559 *        - {String} focusedSelector: selector of the element that should be selected
    560 *        after deleting the node.
    561 *        - {String} pseudo: optional, "before" or "after" if the element focused after
    562 *        deleting the node is supposed to be a before/after pseudo-element.
    563 */
    564 async function checkDeleteAndSelection(
    565  inspector,
    566  key,
    567  { selector, focusedSelector, pseudo }
    568 ) {
    569  info(
    570    "Test deleting node " +
    571      selector +
    572      " with " +
    573      key +
    574      ", " +
    575      "expecting " +
    576      focusedSelector +
    577      " to be focused"
    578  );
    579 
    580  info("Select node " + selector + " and make sure it is focused");
    581  await selectNode(selector, inspector);
    582  await clickContainer(selector, inspector);
    583 
    584  info("Delete the node with: " + key);
    585  const mutated = inspector.once("markupmutation");
    586  EventUtils.sendKey(key, inspector.panelWin);
    587  await Promise.all([mutated, inspector.once("inspector-updated")]);
    588 
    589  let nodeFront = await getNodeFront(focusedSelector, inspector);
    590  if (pseudo) {
    591    // Update the selector for logging in case of failure.
    592    focusedSelector = focusedSelector + "::" + pseudo;
    593    // Retrieve the :before or :after pseudo element of the nodeFront.
    594    const { nodes } = await inspector.walker.children(nodeFront);
    595    nodeFront = pseudo === "before" ? nodes[0] : nodes[nodes.length - 1];
    596  }
    597 
    598  is(
    599    inspector.selection.nodeFront,
    600    nodeFront,
    601    focusedSelector + " is selected after deletion"
    602  );
    603 
    604  info("Check that the node was really removed");
    605  let node = await getNodeFront(selector, inspector);
    606  ok(!node, "The node can't be found in the page anymore");
    607 
    608  info("Undo the deletion to restore the original markup");
    609  await undoChange(inspector);
    610  node = await getNodeFront(selector, inspector);
    611  ok(node, "The node is back");
    612 }
    613 
    614 /**
    615 * Click on the reveal link the provided slotted container.
    616 * Will resolve when selection emits "new-node-front".
    617 */
    618 async function clickOnRevealLink(inspector, container) {
    619  const onSelection = inspector.selection.once("new-node-front");
    620  const revealLink = container.elt.querySelector(".reveal-link");
    621  const tagline = revealLink.closest(".tag-line");
    622  const win = inspector.markup.doc.defaultView;
    623 
    624  // First send a mouseover on the tagline to force the link to be displayed.
    625  EventUtils.synthesizeMouseAtCenter(tagline, { type: "mouseover" }, win);
    626  EventUtils.synthesizeMouseAtCenter(revealLink, {}, win);
    627 
    628  await onSelection;
    629 }
    630 
    631 /**
    632 * Hit `key` on the reveal link in the provided slotted container.
    633 * Will resolve when selection emits "new-node-front".
    634 */
    635 async function keydownOnRevealLink(key, inspector, container) {
    636  const revealLink = container.elt.querySelector(".reveal-link");
    637  const win = inspector.markup.doc.defaultView;
    638 
    639  const root = inspector.markup.getContainer(inspector.markup._rootNode);
    640  root.elt.focus();
    641 
    642  // we need to go through a ENTER + TAB  key sequence to focus on
    643  // the .reveal-link element with the keyboard
    644  const revealFocused = once(revealLink, "focus");
    645  EventUtils.synthesizeKey("KEY_Enter", {}, win);
    646  EventUtils.synthesizeKey("KEY_Tab", {}, win);
    647  info("Waiting for .reveal-link to be focused");
    648  await revealFocused;
    649 
    650  // hit `key` on the .reveal-link
    651  const onSelection = inspector.selection.once("new-node-front");
    652  EventUtils.synthesizeKey(key, {}, win);
    653  await onSelection;
    654 }