tor-browser

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

pointerevent_click_during_parent_capture.html (9471B)


      1 <!DOCTYPE html>
      2 <html>
      3 <head>
      4 <meta charset="utf-8">
      5 <meta name="variant" content="?pointerType=mouse&preventDefault=">
      6 <meta name="variant" content="?pointerType=mouse&preventDefault=pointerdown">
      7 <meta name="variant" content="?pointerType=touch&preventDefault=">
      8 <meta name="variant" content="?pointerType=touch&preventDefault=pointerdown">
      9 <meta name="variant" content="?pointerType=touch&preventDefault=touchstart">
     10 <title>Test `click` event target when a parent element captures the pointer</title>
     11 <style>
     12  #parent {
     13    background: green;
     14    border: 1px solid black;
     15    width: 40px;
     16    height: 40px;
     17  }
     18 
     19  #target {
     20    background: blue;
     21    border: 1px solid black;
     22    width: 20px;
     23    height: 20px;
     24    margin: 10px;
     25  }
     26 </style>
     27 <script src="/resources/testharness.js"></script>
     28 <script src="/resources/testharnessreport.js"></script>
     29 <script src="/resources/testdriver.js"></script>
     30 <script src="/resources/testdriver-vendor.js"></script>
     31 <script src="/resources/testdriver-actions.js"></script>
     32 <script>
     33 "use strict";
     34 
     35 const searchParams = new URLSearchParams(document.location.search);
     36 const pointerType = searchParams.get("pointerType");
     37 const preventDefaultType = searchParams.get("preventDefault");
     38 
     39 addEventListener(
     40  "load",
     41  () => {
     42    const iframe = document.querySelector("iframe");
     43    iframe.contentDocument.head.innerHTML = `<style>${
     44      document.querySelector("style").textContent
     45    }</style>`;
     46 
     47    async function runTest(win, doc) {
     48      let pointerId;
     49      const parent = doc.getElementById("parent");
     50      const target = doc.getElementById("target");
     51      const body = doc.body;
     52      const html = doc.documentElement;
     53      let eventTypes = [];
     54      let composedPaths = [];
     55      function stringifyIfElement(eventTarget) {
     56        if (!(eventTarget instanceof win.Node)) {
     57          return eventTarget;
     58        }
     59        switch (eventTarget.nodeType) {
     60          case win.Node.ELEMENT_NODE:
     61            return `<${eventTarget.localName}${
     62              eventTarget.id ? ` id="${eventTarget.id}"` : ""
     63            }>`;
     64          default:
     65            return eventTarget;
     66        }
     67      }
     68      function stringifyElements(eventTargets) {
     69        return eventTargets.map(stringifyIfElement);
     70      }
     71      function captureEvent(e) {
     72        eventTypes.push(e.type);
     73        composedPaths.push(e.composedPath());
     74      }
     75      const expectedEvents = (() => {
     76        const pathToTarget = [target, parent, body, html, doc, win];
     77        const pathToParent = [parent, body, html, doc, win];
     78        if (pointerType == "mouse") {
     79          if (preventDefaultType == "pointerdown") {
     80            return {
     81              types: ["pointerdown", "pointerup", "click"],
     82              composedPaths: [
     83                pathToTarget, // pointerdown
     84                pathToParent, // pointerup
     85                // Captured by the parent element, `click` should be fired on it.
     86                pathToParent, // click
     87              ],
     88            };
     89          }
     90          return {
     91            types: [
     92              "pointerdown",
     93              "mousedown",
     94              "pointerup",
     95              "mouseup",
     96              "click",
     97            ],
     98            composedPaths: [
     99              pathToTarget, // pointerdown
    100              // `mousedown` target should be considered without the capturing
    101              // element.
    102              pathToTarget, // mousedown
    103              pathToParent, // pointerup
    104              // However, `mouseup` target should be considered with the capturing
    105              // element.
    106              pathToParent, // mouseup
    107              // Captured by the parent element, `click` should be fired on it.
    108              pathToParent, // click
    109            ],
    110          };
    111        }
    112        if (preventDefaultType == "pointerdown") {
    113          return {
    114            types: [
    115              "pointerdown",
    116              "touchstart",
    117              "pointerup",
    118              "touchend",
    119              "click",
    120            ],
    121            composedPaths: [
    122              pathToTarget, // pointerdown
    123              // `touchstart` target should be considered without the capturing
    124              // element.
    125              pathToTarget, // touchstart
    126              pathToParent, // pointerup
    127              // Different from `mouseup`, `touchend` should always be fired on
    128              // same target as `touchstart`.
    129              pathToTarget, // touchend
    130              // `click` event is NOT a compatibility mouse event of Touch
    131              // Events because canceling `pointerdown` should cancel them.
    132              // So, the event target should be considered with `userEvent`
    133              // which caused this `click` event.  In this case, it's the
    134              // preceding `pointerup`.  Therefore, this should be considered
    135              // with the capturing element.
    136              pathToParent, // click
    137            ],
    138          };
    139        }
    140        if (preventDefaultType == "touchstart") {
    141          return {
    142            types: ["pointerdown", "touchstart", "pointerup", "touchend"],
    143            composedPaths: [
    144              pathToTarget, // pointerdown
    145              // `touchstart` target should be considered without the capturing
    146              // element.
    147              pathToTarget, // touchstart
    148              pathToParent, // pointerup
    149              // Different from `mouseup`, `touchend` should always be fired on
    150              // same target as `touchstart`.
    151              pathToTarget, // touchend
    152              // `click` shouldn't be fired if `touchstart` is canceled especially
    153              // for the backward compatibility.
    154            ],
    155          };
    156        }
    157        return {
    158          types: [
    159            "pointerdown",
    160            "touchstart",
    161            "pointerup",
    162            "touchend",
    163            "mousedown",
    164            "mouseup",
    165            "click",
    166          ],
    167          composedPaths: [
    168            pathToTarget, // pointerdown
    169            // `touchstart` target should be considered without the capturing
    170            // element.
    171            pathToTarget, // touchstart
    172            pathToParent, // touchup
    173            // Different from `mouseup`, `touchend` should always be fired on
    174            // same target as `touchstart`.
    175            pathToTarget, // touchend
    176            // Compatibility mouse events should be fired on the element at the
    177            // touch point.
    178            pathToTarget, // mousedown
    179            pathToTarget, // mouseup
    180            // `click` should NOT be a compatibility mouse event of the Touch
    181            // Events since touchstart was not consumed.  So, captured by the
    182            // parent element, `click` should be fired on it.
    183            pathToParent, //click
    184          ],
    185        };
    186      })();
    187 
    188      win.addEventListener(
    189        "pointerdown",
    190        e => {
    191          captureEvent(e);
    192          pointerId = e.pointerId;
    193          parent.setPointerCapture(pointerId);
    194          if (preventDefaultType == e.type) {
    195            e.preventDefault();
    196          }
    197        },
    198        { once: true, passive: false }
    199      );
    200      win.addEventListener(
    201        "pointerup",
    202        e => {
    203          captureEvent(e);
    204          parent.releasePointerCapture(pointerId);
    205        },
    206        { once: true }
    207      );
    208      win.addEventListener(
    209        "touchstart",
    210        e => {
    211          captureEvent(e);
    212          if (preventDefaultType == e.type) {
    213            e.preventDefault();
    214          }
    215        },
    216        { once: true, passive: false }
    217      );
    218      win.addEventListener(
    219        "touchend",
    220        captureEvent,
    221        { once: true }
    222      );
    223      win.addEventListener("mousedown", captureEvent, { once: true });
    224      win.addEventListener("mouseup", captureEvent, { once: true });
    225      win.addEventListener("click", captureEvent, { once: true });
    226 
    227      // Unfortunately, async synthesizing of the touch event will be handled
    228      // in some event loops until dispatching the last `click`.  THerefore,
    229      // we need to wait it with a promise and check no redundant events with
    230      // waiting some more ticks.
    231      const promisePointerUp = new Promise(resolve => {
    232        win.addEventListener(
    233          expectedEvents.types[expectedEvents.types.length - 1],
    234          () => requestAnimationFrame(
    235            () => requestAnimationFrame(resolve)
    236          ),
    237          { once: true }
    238        );
    239      });
    240      await new test_driver.Actions()
    241        .addPointer("TestPointer", pointerType)
    242        .pointerMove(0, 0, { origin: target })
    243        .pointerDown()
    244        .pointerUp()
    245        .send();
    246      await promisePointerUp;
    247 
    248      assert_array_equals(
    249        eventTypes,
    250        expectedEvents.types,
    251        "all expected events should be fired"
    252      );
    253      for (let i = 0; i < expectedEvents.types.length; i++) {
    254        assert_array_equals(
    255          stringifyElements(composedPaths[i]),
    256          stringifyElements(expectedEvents.composedPaths[i]),
    257          `"${expectedEvents.types[i]}" event should be fired on expected target`
    258        );
    259      }
    260    }
    261 
    262    promise_test(async () => {
    263      await runTest(window, document);
    264    }, "Test in the topmost document");
    265    promise_test(async () => {
    266      await runTest(iframe.contentWindow, iframe.contentDocument);
    267    }, "Test in the iframe");
    268  },
    269  { once: true }
    270 );
    271 </script>
    272 </head>
    273 <body>
    274  <div id="parent">
    275    <div id="target"></div>
    276  </div>
    277  <iframe srcdoc="<div id='parent'><div id='target'></div></div>"></iframe>
    278 </body>
    279 </html>