tor-browser

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

event-propagate-disabled.tentative.html (8694B)


      1 <!DOCTYPE html>
      2 <meta charset="utf8">
      3 <meta name="timeout" content="long">
      4 <meta name="viewport" content="width=device-width, initial-scale=1.0">
      5 <title>Event propagation on disabled form elements</title>
      6 <link rel="author" href="mailto:krosylight@mozilla.com">
      7 <link rel="help" href="https://github.com/whatwg/html/issues/2368">
      8 <link rel="help" href="https://github.com/whatwg/html/issues/5886">
      9 <script src="/resources/testharness.js"></script>
     10 <script src="/resources/testharnessreport.js"></script>
     11 <script src="/resources/testdriver.js"></script>
     12 <script src="/resources/testdriver-vendor.js"></script>
     13 <script src="/resources/testdriver-actions.js"></script>
     14 
     15 <div id="cases">
     16  <input> <!-- Sanity check with non-disabled control -->
     17  <select disabled></select>
     18  <select disabled>
     19    <!-- <option> can't be clicked as it doesn't have its own painting area -->
     20    <option>foo</option>
     21  </select>
     22  <fieldset disabled>Text</fieldset>
     23  <fieldset disabled><span class="target">Span</span></fieldset>
     24  <button disabled>Text</button>
     25  <button disabled><span class="target">Span</span></button>
     26  <textarea disabled></textarea>
     27  <input disabled type="button">
     28  <input disabled type="checkbox">
     29  <input disabled type="color" value="#000000">
     30  <input disabled type="date">
     31  <input disabled type="datetime-local">
     32  <input disabled type="email">
     33  <input disabled type="file">
     34  <input disabled type="image">
     35  <input disabled type="month">
     36  <input disabled type="number">
     37  <input disabled type="password">
     38  <input disabled type="radio">
     39  <!-- Native click will click the bar -->
     40  <input disabled type="range" value="0">
     41  <!-- Native click will click the slider -->
     42  <input disabled type="range" value="50">
     43  <input disabled type="reset">
     44  <input disabled type="search">
     45  <input disabled type="submit">
     46  <input disabled type="tel">
     47  <input disabled type="text">
     48  <input disabled type="time">
     49  <input disabled type="url">
     50  <input disabled type="week">
     51  <my-control disabled>Text</my-control>
     52 </div>
     53 
     54 <script>
     55  customElements.define('my-control', class extends HTMLElement {
     56    static get formAssociated() { return true; }
     57    get disabled() { return this.hasAttribute("disabled"); }
     58  });
     59 
     60  /**
     61   * @param {Element} element
     62   */
     63  function getEventFiringTarget(element) {
     64    return element.querySelector(".target") || element;
     65  }
     66 
     67  const allEvents = ["pointermove", "mousemove", "pointerdown", "mousedown", "pointerup", "mouseup", "click"];
     68 
     69  /**
     70   * @param {*} t
     71   * @param {Element} element
     72   * @param {Element} observingElement
     73   */
     74  function setupTest(t, element, observingElement) {
     75    /** @type {{type: string, composedPath: Node[]}[]} */
     76    const observedEvents = [];
     77    const controller = new AbortController();
     78    const { signal } = controller;
     79    const listenerFn = t.step_func(event => {
     80      observedEvents.push({
     81        type: event.type,
     82        target: event.target,
     83        isTrusted: event.isTrusted,
     84        composedPath: event.composedPath().map(n => n.constructor.name),
     85      });
     86    });
     87    for (const event of allEvents) {
     88      observingElement.addEventListener(event, listenerFn, { signal });
     89    }
     90    t.add_cleanup(() => controller.abort());
     91 
     92    const target = getEventFiringTarget(element);
     93    return { target, observedEvents };
     94  }
     95 
     96  /**
     97   * @param {Element} element
     98   * @returns {boolean}
     99   */
    100  function isFormControl(element) {
    101    if (["button", "input", "select", "textarea"].includes(element.localName)) {
    102      return true;
    103    }
    104    return element.constructor.formAssociated;
    105  }
    106 
    107  function isDisabledFormControl(element) {
    108    return isFormControl(element) && element.disabled;
    109  }
    110 
    111  /**
    112   * @param {Element} target
    113   * @param {*} observedEvent
    114   */
    115  function shouldNotBubble(target, observedEvent) {
    116    return (
    117      isDisabledFormControl(target) &&
    118      observedEvent.isTrusted &&
    119      ["mousedown", "mouseup", "click"].includes(observedEvent.type)
    120    );
    121  }
    122 
    123  /**
    124   * @param {Event} event
    125   */
    126  function getExpectedComposedPath(event) {
    127    let target = event.target;
    128    const result = [];
    129    while (target) {
    130      if (shouldNotBubble(target, event)) {
    131        return result;
    132      }
    133      result.push(target.constructor.name);
    134      target = target.parentNode;
    135    }
    136    result.push("Window");
    137    return result;
    138  }
    139 
    140  /**
    141  * @param {object} options
    142  * @param {Element & { disabled: boolean }} options.element
    143  * @param {Element} options.observingElement
    144  * @param {string[]} options.expectedEvents
    145  * @param {(target: Element) => (Promise<void> | void)} options.clickerFn
    146  * @param {string} options.title
    147  */
    148  function promise_event_test({ element, observingElement, expectedEvents, nonDisabledExpectedEvents, clickerFn, title }) {
    149    promise_test(async t => {
    150      const { target, observedEvents } = setupTest(t, element, observingElement);
    151 
    152      await t.step_func(clickerFn)(target);
    153      await new Promise(resolve => t.step_timeout(resolve, 0));
    154 
    155      const expected = isDisabledFormControl(element) ? expectedEvents : nonDisabledExpectedEvents;
    156 
    157      t.step_wait_func_done(() => observedEvents.length > 0,
    158        () => assert_array_equals(observedEvents.map(e => e.type), expected, "Observed events"),
    159        undefined, 1000, 10);
    160      ;
    161 
    162      for (const observed of observedEvents) {
    163        assert_equals(observed.target, target, `${observed.type}.target`)
    164        assert_array_equals(
    165          observed.composedPath,
    166          getExpectedComposedPath(observed),
    167          `${observed.type}.composedPath`
    168        );
    169      }
    170 
    171    }, `${title} on ${element.outerHTML}, observed from <${observingElement.localName}>`);
    172  }
    173 
    174  /**
    175   * @param {object} options
    176   * @param {Element & { disabled: boolean }} options.element
    177   * @param {string[]} options.expectedEvents
    178   * @param {(target: Element) => (Promise<void> | void)} options.clickerFn
    179   * @param {string} options.title
    180   */
    181  function promise_event_test_hierarchy({ element, expectedEvents, nonDisabledExpectedEvents, clickerFn, title }) {
    182    const targets = [element, document.body];
    183    if (element.querySelector(".target")) {
    184      targets.unshift(element.querySelector(".target"));
    185    }
    186    for (const observingElement of targets) {
    187      promise_event_test({ element, observingElement, expectedEvents, nonDisabledExpectedEvents, clickerFn, title });
    188    }
    189  }
    190 
    191  function trusted_click(target) {
    192    // To workaround type=file clicking issue
    193    // https://github.com/w3c/webdriver/issues/1666
    194    return new test_driver.Actions()
    195      .pointerMove(0, 0, { origin: target })
    196      .pointerDown()
    197      .pointerUp()
    198      .send();
    199  }
    200 
    201  const mouseEvents = ["mousemove", "mousedown", "mouseup", "click"];
    202  const pointerEvents = ["pointermove", "pointerdown", "pointerup"];
    203 
    204  // Events except mousedown/up/click
    205  const allowedEvents = ["pointermove", "mousemove", "pointerdown", "pointerup"];
    206 
    207  const elements = document.getElementById("cases").children;
    208  for (const element of elements) {
    209    // Observe on a child element of the control, if exists
    210    const target = element.querySelector(".target");
    211    if (target) {
    212      promise_event_test({
    213        element,
    214        observingElement: target,
    215        expectedEvents: allEvents,
    216        nonDisabledExpectedEvents: allEvents,
    217        clickerFn: trusted_click,
    218        title: "Trusted click"
    219      });
    220    }
    221 
    222    // Observe on the control itself
    223    promise_event_test({
    224      element,
    225      observingElement: element,
    226      expectedEvents: allowedEvents,
    227      nonDisabledExpectedEvents: allEvents,
    228      clickerFn: trusted_click,
    229      title: "Trusted click"
    230    });
    231 
    232    // Observe on document.body
    233    promise_event_test({
    234      element,
    235      observingElement: document.body,
    236      expectedEvents: allowedEvents,
    237      nonDisabledExpectedEvents: allEvents,
    238      clickerFn: trusted_click,
    239      title: "Trusted click"
    240    });
    241 
    242    const eventFirePair = [
    243      [MouseEvent, mouseEvents],
    244      [PointerEvent, pointerEvents]
    245    ];
    246 
    247    for (const [eventInterface, events] of eventFirePair) {
    248      promise_event_test_hierarchy({
    249        element,
    250        expectedEvents: events,
    251        nonDisabledExpectedEvents: events,
    252        clickerFn: target => {
    253          for (const event of events) {
    254            target.dispatchEvent(new eventInterface(event, { bubbles: true }))
    255          }
    256        },
    257        title: `Dispatch new ${eventInterface.name}()`
    258      })
    259    }
    260 
    261    promise_event_test_hierarchy({
    262      element,
    263      expectedEvents: getEventFiringTarget(element) === element ? [] : ["click"],
    264      nonDisabledExpectedEvents: ["click"],
    265      clickerFn: target => target.click(),
    266      title: `click()`
    267    })
    268  }
    269 </script>