summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/dom/events
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/dom/events')
-rw-r--r--testing/web-platform/tests/dom/events/AddEventListenerOptions-once.any.js96
-rw-r--r--testing/web-platform/tests/dom/events/AddEventListenerOptions-passive.any.js134
-rw-r--r--testing/web-platform/tests/dom/events/AddEventListenerOptions-signal.any.js143
-rw-r--r--testing/web-platform/tests/dom/events/Body-FrameSet-Event-Handlers.html123
-rw-r--r--testing/web-platform/tests/dom/events/CustomEvent.html35
-rw-r--r--testing/web-platform/tests/dom/events/Event-cancelBubble.html132
-rw-r--r--testing/web-platform/tests/dom/events/Event-constants.html23
-rw-r--r--testing/web-platform/tests/dom/events/Event-constructors.any.js120
-rw-r--r--testing/web-platform/tests/dom/events/Event-defaultPrevented-after-dispatch.html44
-rw-r--r--testing/web-platform/tests/dom/events/Event-defaultPrevented.html55
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-bubble-canceled.html59
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-bubbles-false.html98
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-bubbles-true.html108
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-click.html369
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-click.tentative.html78
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-detached-click.html20
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-detached-input-and-change.html190
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-handlers-changed.html91
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-listener-order.window.js20
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-multiple-cancelBubble.html51
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-multiple-stopPropagation.html51
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-omitted-capture.html70
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-on-disabled-elements.html251
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-order-at-target.html31
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-order.html26
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-other-document.html23
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-propagation-stopped.html59
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-redispatch.html124
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-reenter.html66
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-target-moved.html73
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-target-removed.html72
-rw-r--r--testing/web-platform/tests/dom/events/Event-dispatch-throwing.html51
-rw-r--r--testing/web-platform/tests/dom/events/Event-init-while-dispatching.html83
-rw-r--r--testing/web-platform/tests/dom/events/Event-initEvent.html136
-rw-r--r--testing/web-platform/tests/dom/events/Event-isTrusted.any.js11
-rw-r--r--testing/web-platform/tests/dom/events/Event-propagation.html48
-rw-r--r--testing/web-platform/tests/dom/events/Event-returnValue.html64
-rw-r--r--testing/web-platform/tests/dom/events/Event-stopImmediatePropagation.html34
-rw-r--r--testing/web-platform/tests/dom/events/Event-stopPropagation-cancel-bubbling.html20
-rw-r--r--testing/web-platform/tests/dom/events/Event-subclasses-constructors.html179
-rw-r--r--testing/web-platform/tests/dom/events/Event-timestamp-cross-realm-getter.html27
-rw-r--r--testing/web-platform/tests/dom/events/Event-timestamp-high-resolution.html16
-rw-r--r--testing/web-platform/tests/dom/events/Event-timestamp-high-resolution.https.html16
-rw-r--r--testing/web-platform/tests/dom/events/Event-timestamp-safe-resolution.html49
-rw-r--r--testing/web-platform/tests/dom/events/Event-type-empty.html35
-rw-r--r--testing/web-platform/tests/dom/events/Event-type.html22
-rw-r--r--testing/web-platform/tests/dom/events/EventListener-addEventListener.sub.window.js9
-rw-r--r--testing/web-platform/tests/dom/events/EventListener-handleEvent-cross-realm.html75
-rw-r--r--testing/web-platform/tests/dom/events/EventListener-handleEvent.html102
-rw-r--r--testing/web-platform/tests/dom/events/EventListener-incumbent-global-1.sub.html20
-rw-r--r--testing/web-platform/tests/dom/events/EventListener-incumbent-global-2.sub.html20
-rw-r--r--testing/web-platform/tests/dom/events/EventListener-incumbent-global-subframe-1.sub.html13
-rw-r--r--testing/web-platform/tests/dom/events/EventListener-incumbent-global-subframe-2.sub.html13
-rw-r--r--testing/web-platform/tests/dom/events/EventListener-incumbent-global-subsubframe.sub.html20
-rw-r--r--testing/web-platform/tests/dom/events/EventListener-invoke-legacy.html66
-rw-r--r--testing/web-platform/tests/dom/events/EventListenerOptions-capture.html98
-rw-r--r--testing/web-platform/tests/dom/events/EventTarget-add-listener-platform-object.html32
-rw-r--r--testing/web-platform/tests/dom/events/EventTarget-add-remove-listener.any.js21
-rw-r--r--testing/web-platform/tests/dom/events/EventTarget-addEventListener.any.js9
-rw-r--r--testing/web-platform/tests/dom/events/EventTarget-constructible.any.js62
-rw-r--r--testing/web-platform/tests/dom/events/EventTarget-dispatchEvent-returnvalue.html71
-rw-r--r--testing/web-platform/tests/dom/events/EventTarget-dispatchEvent.html104
-rw-r--r--testing/web-platform/tests/dom/events/EventTarget-removeEventListener.any.js8
-rw-r--r--testing/web-platform/tests/dom/events/EventTarget-this-of-listener.html182
-rw-r--r--testing/web-platform/tests/dom/events/KeyEvent-initKeyEvent.html23
-rw-r--r--testing/web-platform/tests/dom/events/event-disabled-dynamic.html21
-rw-r--r--testing/web-platform/tests/dom/events/event-global-extra.window.js90
-rw-r--r--testing/web-platform/tests/dom/events/event-global-is-still-set-when-coercing-beforeunload-result.html23
-rw-r--r--testing/web-platform/tests/dom/events/event-global-is-still-set-when-reporting-exception-onerror.html43
-rw-r--r--testing/web-platform/tests/dom/events/event-global-set-before-handleEvent-lookup.window.js19
-rw-r--r--testing/web-platform/tests/dom/events/event-global.html117
-rw-r--r--testing/web-platform/tests/dom/events/event-global.worker.js14
-rw-r--r--testing/web-platform/tests/dom/events/focus-event-document-move.html33
-rw-r--r--testing/web-platform/tests/dom/events/keypress-dispatch-crash.html15
-rw-r--r--testing/web-platform/tests/dom/events/legacy-pre-activation-behavior.window.js10
-rw-r--r--testing/web-platform/tests/dom/events/mouse-event-retarget.html26
-rw-r--r--testing/web-platform/tests/dom/events/no-focus-events-at-clicking-editable-content-in-link.html80
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-body.html19
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-div.html35
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-document.html19
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-root.html19
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-window.html19
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-body.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-div.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-document.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-root.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-window.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-body.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-div.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-document.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-root.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-window.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-body.html18
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-div.html34
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-document.html18
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-root.html18
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-window.html18
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-body.html19
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-div.html35
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-document.html19
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-root.html19
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-window.html19
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-body.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-div.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-document.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-root.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-window.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-body.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-div.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-document.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-root.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-window.html25
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-body.html18
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-div.html34
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-document.html18
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-root.html18
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-window.html18
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/resources/scrolling.js34
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/resources/touching.js34
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/resources/wait-for.js15
-rw-r--r--testing/web-platform/tests/dom/events/non-cancelable-when-passive/synthetic-events-cancelable.html34
-rw-r--r--testing/web-platform/tests/dom/events/passive-by-default.html50
-rw-r--r--testing/web-platform/tests/dom/events/relatedTarget.window.js81
-rw-r--r--testing/web-platform/tests/dom/events/replace-event-listener-null-browsing-context-crash.html16
-rw-r--r--testing/web-platform/tests/dom/events/resources/empty-document.html3
-rw-r--r--testing/web-platform/tests/dom/events/resources/event-global-extra-frame.html9
-rw-r--r--testing/web-platform/tests/dom/events/resources/event-global-is-still-set-when-coercing-beforeunload-result-frame.html6
-rw-r--r--testing/web-platform/tests/dom/events/resources/prefixed-animation-event-tests.js366
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/iframe-chains.html48
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/input-text-scroll-event-when-using-arrow-keys.html71
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/overscroll-deltas.html85
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-document.html62
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-element-with-overscroll-behavior.html92
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-scrolled-element.html65
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-window.html52
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scroll_support.js163
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-sequence-of-scrolls.tentative.html63
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-snap.html87
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-programmatic-scroll.html135
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-scrollIntoView.html124
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-document.html70
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-element-with-overscroll-behavior.html102
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-scrolled-element.html68
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-window.html55
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-for-user-scroll.html199
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-handler-content-attributes.html108
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-not-fired-after-removing-scroller.tentative.html84
-rw-r--r--testing/web-platform/tests/dom/events/shadow-relatedTarget.html30
-rw-r--r--testing/web-platform/tests/dom/events/webkit-animation-end-event.html20
-rw-r--r--testing/web-platform/tests/dom/events/webkit-animation-iteration-event.html23
-rw-r--r--testing/web-platform/tests/dom/events/webkit-animation-start-event.html20
-rw-r--r--testing/web-platform/tests/dom/events/webkit-transition-end-event.html21
152 files changed, 8494 insertions, 0 deletions
diff --git a/testing/web-platform/tests/dom/events/AddEventListenerOptions-once.any.js b/testing/web-platform/tests/dom/events/AddEventListenerOptions-once.any.js
new file mode 100644
index 0000000000..b4edd4345c
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/AddEventListenerOptions-once.any.js
@@ -0,0 +1,96 @@
+// META: title=AddEventListenerOptions.once
+
+"use strict";
+
+test(function() {
+ var invoked_once = false;
+ var invoked_normal = false;
+ function handler_once() {
+ invoked_once = true;
+ }
+ function handler_normal() {
+ invoked_normal = true;
+ }
+
+ const et = new EventTarget();
+ et.addEventListener('test', handler_once, {once: true});
+ et.addEventListener('test', handler_normal);
+ et.dispatchEvent(new Event('test'));
+ assert_equals(invoked_once, true, "Once handler should be invoked");
+ assert_equals(invoked_normal, true, "Normal handler should be invoked");
+
+ invoked_once = false;
+ invoked_normal = false;
+ et.dispatchEvent(new Event('test'));
+ assert_equals(invoked_once, false, "Once handler shouldn't be invoked again");
+ assert_equals(invoked_normal, true, "Normal handler should be invoked again");
+ et.removeEventListener('test', handler_normal);
+}, "Once listener should be invoked only once");
+
+test(function() {
+ const et = new EventTarget();
+ var invoked_count = 0;
+ function handler() {
+ invoked_count++;
+ if (invoked_count == 1)
+ et.dispatchEvent(new Event('test'));
+ }
+ et.addEventListener('test', handler, {once: true});
+ et.dispatchEvent(new Event('test'));
+ assert_equals(invoked_count, 1, "Once handler should only be invoked once");
+
+ invoked_count = 0;
+ function handler2() {
+ invoked_count++;
+ if (invoked_count == 1)
+ et.addEventListener('test', handler2, {once: true});
+ if (invoked_count <= 2)
+ et.dispatchEvent(new Event('test'));
+ }
+ et.addEventListener('test', handler2, {once: true});
+ et.dispatchEvent(new Event('test'));
+ assert_equals(invoked_count, 2, "Once handler should only be invoked once after each adding");
+}, "Once listener should be invoked only once even if the event is nested");
+
+test(function() {
+ var invoked_count = 0;
+ function handler() {
+ invoked_count++;
+ }
+
+ const et = new EventTarget();
+
+ et.addEventListener('test', handler, {once: true});
+ et.addEventListener('test', handler);
+ et.dispatchEvent(new Event('test'));
+ assert_equals(invoked_count, 1, "The handler should only be added once");
+
+ invoked_count = 0;
+ et.dispatchEvent(new Event('test'));
+ assert_equals(invoked_count, 0, "The handler was added as a once listener");
+
+ invoked_count = 0;
+ et.addEventListener('test', handler, {once: true});
+ et.removeEventListener('test', handler);
+ et.dispatchEvent(new Event('test'));
+ assert_equals(invoked_count, 0, "The handler should have been removed");
+}, "Once listener should be added / removed like normal listeners");
+
+test(function() {
+ const et = new EventTarget();
+
+ var invoked_count = 0;
+
+ for (let n = 4; n > 0; n--) {
+ et.addEventListener('test', (e) => {
+ invoked_count++;
+ e.stopImmediatePropagation();
+ }, {once: true});
+ }
+
+ for (let n = 4; n > 0; n--) {
+ et.dispatchEvent(new Event('test'));
+ }
+
+ assert_equals(invoked_count, 4, "The listeners should be invoked");
+}, "Multiple once listeners should be invoked even if the stopImmediatePropagation is set");
diff --git a/testing/web-platform/tests/dom/events/AddEventListenerOptions-passive.any.js b/testing/web-platform/tests/dom/events/AddEventListenerOptions-passive.any.js
new file mode 100644
index 0000000000..8e59cf5b37
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/AddEventListenerOptions-passive.any.js
@@ -0,0 +1,134 @@
+// META: title=AddEventListenerOptions.passive
+
+test(function() {
+ var supportsPassive = false;
+ var query_options = {
+ get passive() {
+ supportsPassive = true;
+ return false;
+ },
+ get dummy() {
+ assert_unreached("dummy value getter invoked");
+ return false;
+ }
+ };
+
+ const et = new EventTarget();
+ et.addEventListener('test_event', null, query_options);
+ assert_true(supportsPassive, "addEventListener doesn't support the passive option");
+
+ supportsPassive = false;
+ et.removeEventListener('test_event', null, query_options);
+ assert_false(supportsPassive, "removeEventListener supports the passive option when it should not");
+}, "Supports passive option on addEventListener only");
+
+function testPassiveValue(optionsValue, expectedDefaultPrevented, existingEventTarget) {
+ var defaultPrevented = undefined;
+ var handler = function handler(e) {
+ assert_false(e.defaultPrevented, "Event prematurely marked defaultPrevented");
+ e.preventDefault();
+ defaultPrevented = e.defaultPrevented;
+ }
+ const et = existingEventTarget || new EventTarget();
+ et.addEventListener('test', handler, optionsValue);
+ var uncanceled = et.dispatchEvent(new Event('test', {bubbles: true, cancelable: true}));
+
+ assert_equals(defaultPrevented, expectedDefaultPrevented, "Incorrect defaultPrevented for options: " + JSON.stringify(optionsValue));
+ assert_equals(uncanceled, !expectedDefaultPrevented, "Incorrect return value from dispatchEvent");
+
+ et.removeEventListener('test', handler, optionsValue);
+}
+
+test(function() {
+ testPassiveValue(undefined, true);
+ testPassiveValue({}, true);
+ testPassiveValue({passive: false}, true);
+ testPassiveValue({passive: true}, false);
+ testPassiveValue({passive: 0}, true);
+ testPassiveValue({passive: 1}, false);
+}, "preventDefault should be ignored if-and-only-if the passive option is true");
+
+function testPassiveValueOnReturnValue(test, optionsValue, expectedDefaultPrevented) {
+ var defaultPrevented = undefined;
+ var handler = test.step_func(e => {
+ assert_false(e.defaultPrevented, "Event prematurely marked defaultPrevented");
+ e.returnValue = false;
+ defaultPrevented = e.defaultPrevented;
+ });
+ const et = new EventTarget();
+ et.addEventListener('test', handler, optionsValue);
+ var uncanceled = et.dispatchEvent(new Event('test', {bubbles: true, cancelable: true}));
+
+ assert_equals(defaultPrevented, expectedDefaultPrevented, "Incorrect defaultPrevented for options: " + JSON.stringify(optionsValue));
+ assert_equals(uncanceled, !expectedDefaultPrevented, "Incorrect return value from dispatchEvent");
+
+ et.removeEventListener('test', handler, optionsValue);
+}
+
+async_test(t => {
+ testPassiveValueOnReturnValue(t, undefined, true);
+ testPassiveValueOnReturnValue(t, {}, true);
+ testPassiveValueOnReturnValue(t, {passive: false}, true);
+ testPassiveValueOnReturnValue(t, {passive: true}, false);
+ testPassiveValueOnReturnValue(t, {passive: 0}, true);
+ testPassiveValueOnReturnValue(t, {passive: 1}, false);
+ t.done();
+}, "returnValue should be ignored if-and-only-if the passive option is true");
+
+function testPassiveWithOtherHandlers(optionsValue, expectedDefaultPrevented) {
+ var handlerInvoked1 = false;
+ var dummyHandler1 = function() {
+ handlerInvoked1 = true;
+ };
+ var handlerInvoked2 = false;
+ var dummyHandler2 = function() {
+ handlerInvoked2 = true;
+ };
+
+ const et = new EventTarget();
+ et.addEventListener('test', dummyHandler1, {passive:true});
+ et.addEventListener('test', dummyHandler2);
+
+ testPassiveValue(optionsValue, expectedDefaultPrevented, et);
+
+ assert_true(handlerInvoked1, "Extra passive handler not invoked");
+ assert_true(handlerInvoked2, "Extra non-passive handler not invoked");
+
+ et.removeEventListener('test', dummyHandler1);
+ et.removeEventListener('test', dummyHandler2);
+}
+
+test(function() {
+ testPassiveWithOtherHandlers({}, true);
+ testPassiveWithOtherHandlers({passive: false}, true);
+ testPassiveWithOtherHandlers({passive: true}, false);
+}, "passive behavior of one listener should be unaffected by the presence of other listeners");
+
+function testOptionEquivalence(optionValue1, optionValue2, expectedEquality) {
+ var invocationCount = 0;
+ var handler = function handler(e) {
+ invocationCount++;
+ }
+ const et = new EventTarget();
+ et.addEventListener('test', handler, optionValue1);
+ et.addEventListener('test', handler, optionValue2);
+ et.dispatchEvent(new Event('test', {bubbles: true}));
+ assert_equals(invocationCount, expectedEquality ? 1 : 2, "equivalence of options " +
+ JSON.stringify(optionValue1) + " and " + JSON.stringify(optionValue2));
+ et.removeEventListener('test', handler, optionValue1);
+ et.removeEventListener('test', handler, optionValue2);
+}
+
+test(function() {
+ // Sanity check options that should be treated as distinct handlers
+ testOptionEquivalence({capture:true}, {capture:false, passive:false}, false);
+ testOptionEquivalence({capture:true}, {passive:true}, false);
+
+ // Option values that should be treated as equivalent
+ testOptionEquivalence({}, {passive:false}, true);
+ testOptionEquivalence({passive:true}, {passive:false}, true);
+ testOptionEquivalence(undefined, {passive:true}, true);
+ testOptionEquivalence({capture: true, passive: false}, {capture: true, passive: true}, true);
+
+}, "Equivalence of option values");
+
diff --git a/testing/web-platform/tests/dom/events/AddEventListenerOptions-signal.any.js b/testing/web-platform/tests/dom/events/AddEventListenerOptions-signal.any.js
new file mode 100644
index 0000000000..e6a3426159
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/AddEventListenerOptions-signal.any.js
@@ -0,0 +1,143 @@
+'use strict';
+
+test(function() {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('test', handler, { signal: controller.signal });
+ et.dispatchEvent(new Event('test'));
+ assert_equals(count, 1, "Adding a signal still adds a listener");
+ et.dispatchEvent(new Event('test'));
+ assert_equals(count, 2, "The listener was not added with the once flag");
+ controller.abort();
+ et.dispatchEvent(new Event('test'));
+ assert_equals(count, 2, "Aborting on the controller removes the listener");
+ et.addEventListener('test', handler, { signal: controller.signal });
+ et.dispatchEvent(new Event('test'));
+ assert_equals(count, 2, "Passing an aborted signal never adds the handler");
+}, "Passing an AbortSignal to addEventListener options should allow removing a listener");
+
+test(function() {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('test', handler, { signal: controller.signal });
+ et.removeEventListener('test', handler);
+ et.dispatchEvent(new Event('test'));
+ assert_equals(count, 0, "The listener was still removed");
+}, "Passing an AbortSignal to addEventListener does not prevent removeEventListener");
+
+test(function() {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('test', handler, { signal: controller.signal, once: true });
+ controller.abort();
+ et.dispatchEvent(new Event('test'));
+ assert_equals(count, 0, "The listener was still removed");
+}, "Passing an AbortSignal to addEventListener works with the once flag");
+
+test(function() {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('test', handler, { signal: controller.signal, once: true });
+ et.removeEventListener('test', handler);
+ et.dispatchEvent(new Event('test'));
+ assert_equals(count, 0, "The listener was still removed");
+}, "Removing a once listener works with a passed signal");
+
+test(function() {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('first', handler, { signal: controller.signal, once: true });
+ et.addEventListener('second', handler, { signal: controller.signal, once: true });
+ controller.abort();
+ et.dispatchEvent(new Event('first'));
+ et.dispatchEvent(new Event('second'));
+ assert_equals(count, 0, "The listener was still removed");
+}, "Passing an AbortSignal to multiple listeners");
+
+test(function() {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('test', handler, { signal: controller.signal, capture: true });
+ controller.abort();
+ et.dispatchEvent(new Event('test'));
+ assert_equals(count, 0, "The listener was still removed");
+}, "Passing an AbortSignal to addEventListener works with the capture flag");
+
+test(function() {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('test', () => {
+ controller.abort();
+ }, { signal: controller.signal });
+ et.addEventListener('test', handler, { signal: controller.signal });
+ et.dispatchEvent(new Event('test'));
+ assert_equals(count, 0, "The listener was still removed");
+}, "Aborting from a listener does not call future listeners");
+
+test(function() {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('test', () => {
+ et.addEventListener('test', handler, { signal: controller.signal });
+ controller.abort();
+ }, { signal: controller.signal });
+ et.dispatchEvent(new Event('test'));
+ assert_equals(count, 0, "The listener was still removed");
+}, "Adding then aborting a listener in another listener does not call it");
+
+test(function() {
+ const et = new EventTarget();
+ const ac = new AbortController();
+ let count = 0;
+ et.addEventListener('foo', () => {
+ et.addEventListener('foo', () => {
+ count++;
+ if (count > 5) ac.abort();
+ et.dispatchEvent(new Event('foo'));
+ }, { signal: ac.signal });
+ et.dispatchEvent(new Event('foo'));
+ }, { once: true });
+ et.dispatchEvent(new Event('foo'));
+}, "Aborting from a nested listener should remove it");
+
+test(function() {
+ const et = new EventTarget();
+ assert_throws_js(TypeError, () => { et.addEventListener("foo", () => {}, { signal: null }); });
+}, "Passing null as the signal should throw");
+
+test(function() {
+ const et = new EventTarget();
+ assert_throws_js(TypeError, () => { et.addEventListener("foo", null, { signal: null }); });
+}, "Passing null as the signal should throw (listener is also null)");
diff --git a/testing/web-platform/tests/dom/events/Body-FrameSet-Event-Handlers.html b/testing/web-platform/tests/dom/events/Body-FrameSet-Event-Handlers.html
new file mode 100644
index 0000000000..3a891158d5
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Body-FrameSet-Event-Handlers.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<html>
+<title>HTMLBodyElement and HTMLFrameSetElement Event Handler Tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+function getObject(interface) {
+ switch(interface) {
+ case "Element":
+ var e = document.createElementNS("http://example.com/", "example");
+ assert_true(e instanceof Element);
+ assert_false(e instanceof HTMLElement);
+ assert_false(e instanceof SVGElement);
+ return e;
+ case "HTMLElement":
+ var e = document.createElement("html");
+ assert_true(e instanceof HTMLElement);
+ return e;
+ case "HTMLBodyElement":
+ var e = document.createElement("body");
+ assert_true(e instanceof HTMLBodyElement);
+ return e;
+ case "HTMLFormElement":
+ var e = document.createElement("form");
+ assert_true(e instanceof HTMLFormElement);
+ return e;
+ case "HTMLFrameSetElement":
+ var e = document.createElement("frameset");
+ assert_true(e instanceof HTMLFrameSetElement);
+ return e;
+ case "SVGElement":
+ var e = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ assert_true(e instanceof SVGElement);
+ return e;
+ case "Document":
+ assert_true(document instanceof Document);
+ return document;
+ case "Window":
+ assert_true(window instanceof Window);
+ return window;
+ }
+ assert_unreached();
+}
+
+function testSet(interface, attribute) {
+ test(function() {
+ var object = getObject(interface);
+ function nop() {}
+ assert_equals(object[attribute], null, "Initially null");
+ object[attribute] = nop;
+ assert_equals(object[attribute], nop, "Return same function");
+ object[attribute] = "";
+ assert_equals(object[attribute], null, "Return null after setting string");
+ object[attribute] = null;
+ assert_equals(object[attribute], null, "Finally null");
+ }, "Set " + interface + "." + attribute);
+}
+
+function testReflect(interface, attribute) {
+ test(function() {
+ var element = getObject(interface);
+ assert_false(element.hasAttribute(attribute), "Initially missing");
+ element.setAttribute(attribute, "return");
+ assert_equals(element.getAttribute(attribute), "return", "Return same string");
+ assert_equals(typeof element[attribute], "function", "Convert to function");
+ element.removeAttribute(attribute);
+ }, "Reflect " + interface + "." + attribute);
+}
+
+function testForwardToWindow(interface, attribute) {
+ test(function() {
+ var element = getObject(interface);
+ window[attribute] = null;
+ element.setAttribute(attribute, "return");
+ assert_equals(typeof window[attribute], "function", "Convert to function");
+ assert_equals(window[attribute], element[attribute], "Forward content attribute");
+ function nop() {}
+ element[attribute] = nop;
+ assert_equals(window[attribute], nop, "Forward IDL attribute");
+ window[attribute] = null;
+ }, "Forward " + interface + "." + attribute + " to Window");
+}
+
+// Object.propertyIsEnumerable cannot be used because it doesn't
+// work with properties inherited through the prototype chain.
+function getEnumerable(interface) {
+ var enumerable = {};
+ for (var attribute in getObject(interface)) {
+ enumerable[attribute] = true;
+ }
+ return enumerable;
+}
+
+var enumerableCache = {};
+function testEnumerate(interface, attribute) {
+ if (!(interface in enumerableCache)) {
+ enumerableCache[interface] = getEnumerable(interface);
+ }
+ test(function() {
+ assert_true(enumerableCache[interface][attribute]);
+ }, "Enumerate " + interface + "." + attribute);
+}
+
+[
+ "onblur",
+ "onerror",
+ "onfocus",
+ "onload",
+ "onscroll",
+ "onresize"
+].forEach(function(attribute) {
+ testSet("HTMLBodyElement", attribute);
+ testEnumerate("HTMLBodyElement", attribute);
+ testReflect("HTMLBodyElement", attribute);
+ testForwardToWindow("HTMLBodyElement", attribute);
+ testSet("HTMLFrameSetElement", attribute);
+ testEnumerate("HTMLFrameSetElement", attribute);
+ testReflect("HTMLFrameSetElement", attribute);
+ testForwardToWindow("HTMLFrameSetElement", attribute);
+});
+</script>
+</html>
diff --git a/testing/web-platform/tests/dom/events/CustomEvent.html b/testing/web-platform/tests/dom/events/CustomEvent.html
new file mode 100644
index 0000000000..87050943f9
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/CustomEvent.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<title>CustomEvent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ var type = "foo";
+
+ var target = document.createElement("div");
+ target.addEventListener(type, this.step_func(function(evt) {
+ assert_equals(evt.type, type);
+ }), true);
+
+ var fooEvent = document.createEvent("CustomEvent");
+ fooEvent.initEvent(type, true, true);
+ target.dispatchEvent(fooEvent);
+}, "CustomEvent dispatching.");
+
+test(function() {
+ var e = document.createEvent("CustomEvent");
+ assert_throws_js(TypeError, function() {
+ e.initCustomEvent();
+ });
+}, "First parameter to initCustomEvent should be mandatory.");
+
+test(function() {
+ var e = document.createEvent("CustomEvent");
+ e.initCustomEvent("foo");
+ assert_equals(e.type, "foo", "type");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+ assert_equals(e.detail, null, "detail");
+}, "initCustomEvent's default parameter values.");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-cancelBubble.html b/testing/web-platform/tests/dom/events/Event-cancelBubble.html
new file mode 100644
index 0000000000..d8d2d7239d
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-cancelBubble.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Event.cancelBubble</title>
+ <link rel="author" title="Chris Rebert" href="http://chrisrebert.com">
+ <link rel="help" href="https://dom.spec.whatwg.org/#dom-event-cancelbubble">
+ <meta name="flags" content="dom">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+ <div id="outer">
+ <div id="middle">
+ <div id="inner"></div>
+ </div>
+ </div>
+ <script>
+test(function () {
+ // See https://dom.spec.whatwg.org/#stop-propagation-flag
+ var e = document.createEvent('Event');
+ assert_false(e.cancelBubble, "cancelBubble must be false after event creation.");
+}, "cancelBubble must be false when an event is initially created.");
+
+test(function () {
+ // See https://dom.spec.whatwg.org/#concept-event-initialize
+
+ // Event which bubbles.
+ var one = document.createEvent('Event');
+ one.cancelBubble = true;
+ one.initEvent('foo', true/*bubbles*/, false/*cancelable*/);
+ assert_false(one.cancelBubble, "initEvent() must set cancelBubble to false. [bubbles=true]");
+ // Re-initialization.
+ one.cancelBubble = true;
+ one.initEvent('foo', true/*bubbles*/, false/*cancelable*/);
+ assert_false(one.cancelBubble, "2nd initEvent() call must set cancelBubble to false. [bubbles=true]");
+
+ // Event which doesn't bubble.
+ var two = document.createEvent('Event');
+ two.cancelBubble = true;
+ two.initEvent('foo', false/*bubbles*/, false/*cancelable*/);
+ assert_false(two.cancelBubble, "initEvent() must set cancelBubble to false. [bubbles=false]");
+ // Re-initialization.
+ two.cancelBubble = true;
+ two.initEvent('foo', false/*bubbles*/, false/*cancelable*/);
+ assert_false(two.cancelBubble, "2nd initEvent() call must set cancelBubble to false. [bubbles=false]");
+}, "Initializing an event must set cancelBubble to false.");
+
+test(function () {
+ // See https://dom.spec.whatwg.org/#dom-event-stoppropagation
+ var e = document.createEvent('Event');
+ e.stopPropagation();
+ assert_true(e.cancelBubble, "stopPropagation() must set cancelBubble to true.");
+}, "stopPropagation() must set cancelBubble to true.");
+
+test(function () {
+ // See https://dom.spec.whatwg.org/#dom-event-stopimmediatepropagation
+ var e = document.createEvent('Event');
+ e.stopImmediatePropagation();
+ assert_true(e.cancelBubble, "stopImmediatePropagation() must set cancelBubble to true.");
+}, "stopImmediatePropagation() must set cancelBubble to true.");
+
+test(function () {
+ var one = document.createEvent('Event');
+ one.stopPropagation();
+ one.cancelBubble = false;
+ assert_true(one.cancelBubble, "cancelBubble must still be true after attempting to set it to false.");
+}, "Event.cancelBubble=false must have no effect.");
+
+test(function (t) {
+ var outer = document.getElementById('outer');
+ var middle = document.getElementById('middle');
+ var inner = document.getElementById('inner');
+
+ outer.addEventListener('barbaz', t.step_func(function () {
+ assert_unreached("Setting Event.cancelBubble=false after setting Event.cancelBubble=true should have no effect.");
+ }), false/*useCapture*/);
+
+ middle.addEventListener('barbaz', function (e) {
+ e.cancelBubble = true;// Stop propagation.
+ e.cancelBubble = false;// Should be a no-op.
+ }, false/*useCapture*/);
+
+ var barbazEvent = document.createEvent('Event');
+ barbazEvent.initEvent('barbaz', true/*bubbles*/, false/*cancelable*/);
+ inner.dispatchEvent(barbazEvent);
+}, "Event.cancelBubble=false must have no effect during event propagation.");
+
+test(function () {
+ // See https://dom.spec.whatwg.org/#concept-event-dispatch
+ // "14. Unset event’s [...] stop propagation flag,"
+ var e = document.createEvent('Event');
+ e.initEvent('foobar', true/*bubbles*/, true/*cancelable*/);
+ document.body.addEventListener('foobar', function listener(e) {
+ e.stopPropagation();
+ });
+ document.body.dispatchEvent(e);
+ assert_false(e.cancelBubble, "cancelBubble must be false after an event has been dispatched.");
+}, "cancelBubble must be false after an event has been dispatched.");
+
+test(function (t) {
+ var outer = document.getElementById('outer');
+ var middle = document.getElementById('middle');
+ var inner = document.getElementById('inner');
+
+ var propagationStopper = function (e) {
+ e.cancelBubble = true;
+ };
+
+ // Bubble phase
+ middle.addEventListener('bar', propagationStopper, false/*useCapture*/);
+ outer.addEventListener('bar', t.step_func(function listenerOne() {
+ assert_unreached("Setting cancelBubble=true should stop the event from bubbling further.");
+ }), false/*useCapture*/);
+
+ var barEvent = document.createEvent('Event');
+ barEvent.initEvent('bar', true/*bubbles*/, false/*cancelable*/);
+ inner.dispatchEvent(barEvent);
+
+ // Capture phase
+ outer.addEventListener('qux', propagationStopper, true/*useCapture*/);
+ middle.addEventListener('qux', t.step_func(function listenerTwo() {
+ assert_unreached("Setting cancelBubble=true should stop the event from propagating further, including during the Capture Phase.");
+ }), true/*useCapture*/);
+
+ var quxEvent = document.createEvent('Event');
+ quxEvent.initEvent('qux', false/*bubbles*/, false/*cancelable*/);
+ inner.dispatchEvent(quxEvent);
+}, "Event.cancelBubble=true must set the stop propagation flag.");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/dom/events/Event-constants.html b/testing/web-platform/tests/dom/events/Event-constants.html
new file mode 100644
index 0000000000..635e9894d9
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-constants.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<title>Event constants</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../constants.js"></script>
+<div id="log"></div>
+<script>
+var objects;
+setup(function() {
+ objects = [
+ [Event, "Event interface object"],
+ [Event.prototype, "Event prototype object"],
+ [document.createEvent("Event"), "Event object"],
+ [document.createEvent("CustomEvent"), "CustomEvent object"]
+ ]
+})
+testConstants(objects, [
+ ["NONE", 0],
+ ["CAPTURING_PHASE", 1],
+ ["AT_TARGET", 2],
+ ["BUBBLING_PHASE", 3]
+], "eventPhase")
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-constructors.any.js b/testing/web-platform/tests/dom/events/Event-constructors.any.js
new file mode 100644
index 0000000000..faa623ea92
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-constructors.any.js
@@ -0,0 +1,120 @@
+// META: title=Event constructors
+
+test(function() {
+ assert_throws_js(
+ TypeError,
+ () => Event(""),
+ "Calling Event constructor without 'new' must throw")
+})
+test(function() {
+ assert_throws_js(TypeError, function() {
+ new Event()
+ })
+})
+test(function() {
+ var test_error = { name: "test" }
+ assert_throws_exactly(test_error, function() {
+ new Event({ toString: function() { throw test_error; } })
+ })
+})
+test(function() {
+ var ev = new Event("")
+ assert_equals(ev.type, "")
+ assert_equals(ev.target, null)
+ assert_equals(ev.srcElement, null)
+ assert_equals(ev.currentTarget, null)
+ assert_equals(ev.eventPhase, Event.NONE)
+ assert_equals(ev.bubbles, false)
+ assert_equals(ev.cancelable, false)
+ assert_equals(ev.defaultPrevented, false)
+ assert_equals(ev.returnValue, true)
+ assert_equals(ev.isTrusted, false)
+ assert_true(ev.timeStamp > 0)
+ assert_true("initEvent" in ev)
+})
+test(function() {
+ var ev = new Event("test")
+ assert_equals(ev.type, "test")
+ assert_equals(ev.target, null)
+ assert_equals(ev.srcElement, null)
+ assert_equals(ev.currentTarget, null)
+ assert_equals(ev.eventPhase, Event.NONE)
+ assert_equals(ev.bubbles, false)
+ assert_equals(ev.cancelable, false)
+ assert_equals(ev.defaultPrevented, false)
+ assert_equals(ev.returnValue, true)
+ assert_equals(ev.isTrusted, false)
+ assert_true(ev.timeStamp > 0)
+ assert_true("initEvent" in ev)
+})
+test(function() {
+ assert_throws_js(TypeError, function() { Event("test") },
+ 'Calling Event constructor without "new" must throw');
+})
+test(function() {
+ var ev = new Event("I am an event", { bubbles: true, cancelable: false})
+ assert_equals(ev.type, "I am an event")
+ assert_equals(ev.bubbles, true)
+ assert_equals(ev.cancelable, false)
+})
+test(function() {
+ var ev = new Event("@", { bubblesIGNORED: true, cancelable: true})
+ assert_equals(ev.type, "@")
+ assert_equals(ev.bubbles, false)
+ assert_equals(ev.cancelable, true)
+})
+test(function() {
+ var ev = new Event("@", { "bubbles\0IGNORED": true, cancelable: true})
+ assert_equals(ev.type, "@")
+ assert_equals(ev.bubbles, false)
+ assert_equals(ev.cancelable, true)
+})
+test(function() {
+ var ev = new Event("Xx", { cancelable: true})
+ assert_equals(ev.type, "Xx")
+ assert_equals(ev.bubbles, false)
+ assert_equals(ev.cancelable, true)
+})
+test(function() {
+ var ev = new Event("Xx", {})
+ assert_equals(ev.type, "Xx")
+ assert_equals(ev.bubbles, false)
+ assert_equals(ev.cancelable, false)
+})
+test(function() {
+ var ev = new Event("Xx", {bubbles: true, cancelable: false, sweet: "x"})
+ assert_equals(ev.type, "Xx")
+ assert_equals(ev.bubbles, true)
+ assert_equals(ev.cancelable, false)
+ assert_equals(ev.sweet, undefined)
+})
+test(function() {
+ var called = []
+ var ev = new Event("Xx", {
+ get cancelable() {
+ called.push("cancelable")
+ return false
+ },
+ get bubbles() {
+ called.push("bubbles")
+ return true;
+ },
+ get sweet() {
+ called.push("sweet")
+ return "x"
+ }
+ })
+ assert_array_equals(called, ["bubbles", "cancelable"])
+ assert_equals(ev.type, "Xx")
+ assert_equals(ev.bubbles, true)
+ assert_equals(ev.cancelable, false)
+ assert_equals(ev.sweet, undefined)
+})
+test(function() {
+ var ev = new CustomEvent("$", {detail: 54, sweet: "x", sweet2: "x", cancelable:true})
+ assert_equals(ev.type, "$")
+ assert_equals(ev.bubbles, false)
+ assert_equals(ev.cancelable, true)
+ assert_equals(ev.sweet, undefined)
+ assert_equals(ev.detail, 54)
+})
diff --git a/testing/web-platform/tests/dom/events/Event-defaultPrevented-after-dispatch.html b/testing/web-platform/tests/dom/events/Event-defaultPrevented-after-dispatch.html
new file mode 100644
index 0000000000..8fef005eb5
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-defaultPrevented-after-dispatch.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Event.defaultPrevented is not reset after dispatchEvent()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id=log></div>
+<input id="target" type="hidden" value=""/>
+<script>
+test(function() {
+ var EVENT = "foo";
+ var TARGET = document.getElementById("target");
+ var evt = document.createEvent("Event");
+ evt.initEvent(EVENT, true, true);
+
+ TARGET.addEventListener(EVENT, this.step_func(function(e) {
+ e.preventDefault();
+ assert_true(e.defaultPrevented, "during dispatch");
+ }), true);
+ TARGET.dispatchEvent(evt);
+
+ assert_true(evt.defaultPrevented, "after dispatch");
+ assert_equals(evt.target, TARGET);
+ assert_equals(evt.srcElement, TARGET);
+}, "Default prevention via preventDefault");
+
+test(function() {
+ var EVENT = "foo";
+ var TARGET = document.getElementById("target");
+ var evt = document.createEvent("Event");
+ evt.initEvent(EVENT, true, true);
+
+ TARGET.addEventListener(EVENT, this.step_func(function(e) {
+ e.returnValue = false;
+ assert_true(e.defaultPrevented, "during dispatch");
+ }), true);
+ TARGET.dispatchEvent(evt);
+
+ assert_true(evt.defaultPrevented, "after dispatch");
+ assert_equals(evt.target, TARGET);
+ assert_equals(evt.srcElement, TARGET);
+}, "Default prevention via returnValue");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-defaultPrevented.html b/testing/web-platform/tests/dom/events/Event-defaultPrevented.html
new file mode 100644
index 0000000000..2548fa3e06
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-defaultPrevented.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<title>Event.defaultPrevented</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var ev;
+test(function() {
+ ev = document.createEvent("Event");
+ assert_equals(ev.defaultPrevented, false, "defaultPrevented");
+}, "When an event is created, defaultPrevented should be initialized to false.");
+test(function() {
+ ev.initEvent("foo", true, false);
+ assert_equals(ev.bubbles, true, "bubbles");
+ assert_equals(ev.cancelable, false, "cancelable");
+ assert_equals(ev.defaultPrevented, false, "defaultPrevented");
+}, "initEvent should work correctly (not cancelable).");
+test(function() {
+ assert_equals(ev.cancelable, false, "cancelable (before)");
+ ev.preventDefault();
+ assert_equals(ev.cancelable, false, "cancelable (after)");
+ assert_equals(ev.defaultPrevented, false, "defaultPrevented");
+}, "preventDefault() should not change defaultPrevented if cancelable is false.");
+test(function() {
+ assert_equals(ev.cancelable, false, "cancelable (before)");
+ ev.returnValue = false;
+ assert_equals(ev.cancelable, false, "cancelable (after)");
+ assert_equals(ev.defaultPrevented, false, "defaultPrevented");
+}, "returnValue should not change defaultPrevented if cancelable is false.");
+test(function() {
+ ev.initEvent("foo", true, true);
+ assert_equals(ev.bubbles, true, "bubbles");
+ assert_equals(ev.cancelable, true, "cancelable");
+ assert_equals(ev.defaultPrevented, false, "defaultPrevented");
+}, "initEvent should work correctly (cancelable).");
+test(function() {
+ assert_equals(ev.cancelable, true, "cancelable (before)");
+ ev.preventDefault();
+ assert_equals(ev.cancelable, true, "cancelable (after)");
+ assert_equals(ev.defaultPrevented, true, "defaultPrevented");
+}, "preventDefault() should change defaultPrevented if cancelable is true.");
+test(function() {
+ ev.initEvent("foo", true, true);
+ assert_equals(ev.cancelable, true, "cancelable (before)");
+ ev.returnValue = false;
+ assert_equals(ev.cancelable, true, "cancelable (after)");
+ assert_equals(ev.defaultPrevented, true, "defaultPrevented");
+}, "returnValue should change defaultPrevented if cancelable is true.");
+test(function() {
+ ev.initEvent("foo", true, true);
+ assert_equals(ev.bubbles, true, "bubbles");
+ assert_equals(ev.cancelable, true, "cancelable");
+ assert_equals(ev.defaultPrevented, false, "defaultPrevented");
+}, "initEvent should unset defaultPrevented.");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-bubble-canceled.html b/testing/web-platform/tests/dom/events/Event-dispatch-bubble-canceled.html
new file mode 100644
index 0000000000..20f398f66f
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-bubble-canceled.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Setting cancelBubble=true prior to dispatchEvent()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<table id="table" border="1" style="display: none">
+ <tbody id="table-body">
+ <tr id="table-row">
+ <td id="table-cell">Shady Grove</td>
+ <td>Aeolian</td>
+ </tr>
+ <tr id="parent">
+ <td id="target">Over the river, Charlie</td>
+ <td>Dorian</td>
+ </tr>
+ </tbody>
+</table>
+
+<script>
+test(function() {
+ var event = "foo";
+ var target = document.getElementById("target");
+ var parent = document.getElementById("parent");
+ var tbody = document.getElementById("table-body");
+ var table = document.getElementById("table");
+ var body = document.body;
+ var html = document.documentElement;
+ var current_targets = [window, document, html, body, table, tbody, parent, target];
+ var expected_targets = [];
+ var actual_targets = [];
+ var expected_phases = [];
+ var actual_phases = [];
+
+ var test_event = function(evt) {
+ actual_targets.push(evt.currentTarget);
+ actual_phases.push(evt.eventPhase);
+ };
+
+ for (var i = 0; i < current_targets.length; ++i) {
+ current_targets[i].addEventListener(event, test_event, true);
+ current_targets[i].addEventListener(event, test_event, false);
+ }
+
+ var evt = document.createEvent("Event");
+ evt.initEvent(event, true, true);
+ evt.cancelBubble = true;
+ target.dispatchEvent(evt);
+
+ assert_array_equals(actual_targets, expected_targets, "actual_targets");
+ assert_array_equals(actual_phases, expected_phases, "actual_phases");
+});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-bubbles-false.html b/testing/web-platform/tests/dom/events/Event-dispatch-bubbles-false.html
new file mode 100644
index 0000000000..0f43cb0275
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-bubbles-false.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title> Event.bubbles attribute is set to false </title>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-event-initevent">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-dispatch">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<table id="table" border="1" style="display: none">
+ <tbody id="table-body">
+ <tr id="table-row">
+ <td id="table-cell">Shady Grove</td>
+ <td>Aeolian</td>
+ </tr>
+ <tr id="parent">
+ <td id="target">Over the river, Charlie</td>
+ <td>Dorian</td>
+ </tr>
+ </tbody>
+</table>
+<script>
+function targetsForDocumentChain(document) {
+ return [
+ document,
+ document.documentElement,
+ document.getElementsByTagName("body")[0],
+ document.getElementById("table"),
+ document.getElementById("table-body"),
+ document.getElementById("parent")
+ ];
+}
+
+function testChain(document, targetParents, phases, event_type) {
+ var target = document.getElementById("target");
+ var targets = targetParents.concat(target);
+ var expected_targets = targets.concat(target);
+
+ var actual_targets = [], actual_phases = [];
+ var test_event = function(evt) {
+ actual_targets.push(evt.currentTarget);
+ actual_phases.push(evt.eventPhase);
+ }
+
+ for (var i = 0; i < targets.length; i++) {
+ targets[i].addEventListener(event_type, test_event, true);
+ targets[i].addEventListener(event_type, test_event, false);
+ }
+
+ var evt = document.createEvent("Event");
+ evt.initEvent(event_type, false, true);
+
+ target.dispatchEvent(evt);
+
+ assert_array_equals(actual_targets, expected_targets, "targets");
+ assert_array_equals(actual_phases, phases, "phases");
+}
+
+var phasesForDocumentChain = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+];
+
+test(function () {
+ var chainWithWindow = [window].concat(targetsForDocumentChain(document));
+ testChain(
+ document, chainWithWindow, [Event.CAPTURING_PHASE].concat(phasesForDocumentChain), "click");
+}, "In window.document with click event");
+
+test(function () {
+ testChain(document, targetsForDocumentChain(document), phasesForDocumentChain, "load");
+}, "In window.document with load event")
+
+test(function () {
+ var documentClone = document.cloneNode(true);
+ testChain(
+ documentClone, targetsForDocumentChain(documentClone), phasesForDocumentChain, "click");
+}, "In window.document.cloneNode(true)");
+
+test(function () {
+ var newDocument = new Document();
+ newDocument.appendChild(document.documentElement.cloneNode(true));
+ testChain(
+ newDocument, targetsForDocumentChain(newDocument), phasesForDocumentChain, "click");
+}, "In new Document()");
+
+test(function () {
+ var HTMLDocument = document.implementation.createHTMLDocument();
+ HTMLDocument.body.appendChild(document.getElementById("table").cloneNode(true));
+ testChain(
+ HTMLDocument, targetsForDocumentChain(HTMLDocument), phasesForDocumentChain, "click");
+}, "In DOMImplementation.createHTMLDocument()");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-bubbles-true.html b/testing/web-platform/tests/dom/events/Event-dispatch-bubbles-true.html
new file mode 100644
index 0000000000..b23605a1eb
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-bubbles-true.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title> Event.bubbles attribute is set to false </title>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-event-initevent">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-dispatch">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<table id="table" border="1" style="display: none">
+ <tbody id="table-body">
+ <tr id="table-row">
+ <td id="table-cell">Shady Grove</td>
+ <td>Aeolian</td>
+ </tr>
+ <tr id="parent">
+ <td id="target">Over the river, Charlie</td>
+ <td>Dorian</td>
+ </tr>
+ </tbody>
+</table>
+<script>
+function concatReverse(a) {
+ return a.concat(a.map(function(x) { return x }).reverse());
+}
+
+function targetsForDocumentChain(document) {
+ return [
+ document,
+ document.documentElement,
+ document.getElementsByTagName("body")[0],
+ document.getElementById("table"),
+ document.getElementById("table-body"),
+ document.getElementById("parent")
+ ];
+}
+
+function testChain(document, targetParents, phases, event_type) {
+ var target = document.getElementById("target");
+ var targets = targetParents.concat(target);
+ var expected_targets = concatReverse(targets);
+
+ var actual_targets = [], actual_phases = [];
+ var test_event = function(evt) {
+ actual_targets.push(evt.currentTarget);
+ actual_phases.push(evt.eventPhase);
+ }
+
+ for (var i = 0; i < targets.length; i++) {
+ targets[i].addEventListener(event_type, test_event, true);
+ targets[i].addEventListener(event_type, test_event, false);
+ }
+
+ var evt = document.createEvent("Event");
+ evt.initEvent(event_type, true, true);
+
+ target.dispatchEvent(evt);
+
+ assert_array_equals(actual_targets, expected_targets, "targets");
+ assert_array_equals(actual_phases, phases, "phases");
+}
+
+var phasesForDocumentChain = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+];
+
+test(function () {
+ var chainWithWindow = [window].concat(targetsForDocumentChain(document));
+ var phases = [Event.CAPTURING_PHASE].concat(phasesForDocumentChain, Event.BUBBLING_PHASE);
+ testChain(document, chainWithWindow, phases, "click");
+}, "In window.document with click event");
+
+test(function () {
+ testChain(document, targetsForDocumentChain(document), phasesForDocumentChain, "load");
+}, "In window.document with load event")
+
+test(function () {
+ var documentClone = document.cloneNode(true);
+ testChain(
+ documentClone, targetsForDocumentChain(documentClone), phasesForDocumentChain, "click");
+}, "In window.document.cloneNode(true)");
+
+test(function () {
+ var newDocument = new Document();
+ newDocument.appendChild(document.documentElement.cloneNode(true));
+ testChain(
+ newDocument, targetsForDocumentChain(newDocument), phasesForDocumentChain, "click");
+}, "In new Document()");
+
+test(function () {
+ var HTMLDocument = document.implementation.createHTMLDocument();
+ HTMLDocument.body.appendChild(document.getElementById("table").cloneNode(true));
+ testChain(
+ HTMLDocument, targetsForDocumentChain(HTMLDocument), phasesForDocumentChain, "click");
+}, "In DOMImplementation.createHTMLDocument()");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-click.html b/testing/web-platform/tests/dom/events/Event-dispatch-click.html
new file mode 100644
index 0000000000..010305775d
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-click.html
@@ -0,0 +1,369 @@
+<!doctype html>
+<title>Synthetic click event "magic"</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<div id=dump style=display:none></div>
+<script>
+var dump = document.getElementById("dump")
+
+async_test(function(t) {
+ var input = document.createElement("input")
+ input.type = "checkbox"
+ dump.appendChild(input)
+ input.onclick = t.step_func_done(function() {
+ assert_true(input.checked)
+ })
+ input.click()
+}, "basic with click()")
+
+async_test(function(t) {
+ var input = document.createElement("input")
+ input.type = "checkbox"
+ dump.appendChild(input)
+ input.onclick = t.step_func_done(function() {
+ assert_true(input.checked)
+ })
+ input.dispatchEvent(new MouseEvent("click", {bubbles:true})) // equivalent to the above
+}, "basic with dispatchEvent()")
+
+async_test(function(t) {
+ var input = document.createElement("input")
+ input.type = "checkbox"
+ dump.appendChild(input)
+ input.onclick = t.step_func_done(function() {
+ assert_false(input.checked)
+ })
+ input.dispatchEvent(new Event("click", {bubbles:true})) // no MouseEvent
+}, "basic with wrong event class")
+
+async_test(function(t) {
+ var input = document.createElement("input")
+ input.type = "checkbox"
+ dump.appendChild(input)
+ var child = input.appendChild(new Text("does not matter"))
+ child.dispatchEvent(new MouseEvent("click")) // does not bubble
+ assert_false(input.checked)
+ t.done()
+}, "look at parents only when event bubbles")
+
+async_test(function(t) {
+ var input = document.createElement("input")
+ input.type = "checkbox"
+ dump.appendChild(input)
+ input.onclick = t.step_func_done(function() {
+ assert_true(input.checked)
+ })
+ var child = input.appendChild(new Text("does not matter"))
+ child.dispatchEvent(new MouseEvent("click", {bubbles:true}))
+}, "look at parents when event bubbles")
+
+async_test(function(t) {
+ var input = document.createElement("input")
+ input.type = "checkbox"
+ dump.appendChild(input)
+ input.onclick = t.step_func(function() {
+ assert_false(input.checked, "input pre-click must not be triggered")
+ })
+ var child = input.appendChild(document.createElement("input"))
+ child.type = "checkbox"
+ child.onclick = t.step_func(function() {
+ assert_true(child.checked, "child pre-click must be triggered")
+ })
+ child.dispatchEvent(new MouseEvent("click", {bubbles:true}))
+ t.done()
+}, "pick the first with activation behavior <input type=checkbox>")
+
+async_test(function(t) { // as above with <a>
+ window.hrefComplete = t.step_func(function(a) {
+ assert_equals(a, 'child');
+ t.done();
+ });
+ var link = document.createElement("a")
+ link.href = "javascript:hrefComplete('link')" // must not be triggered
+ dump.appendChild(link)
+ var child = link.appendChild(document.createElement("a"))
+ child.href = "javascript:hrefComplete('child')"
+ child.dispatchEvent(new MouseEvent("click", {bubbles:true}))
+}, "pick the first with activation behavior <a href>")
+
+async_test(function(t) {
+ var input = document.createElement("input")
+ input.type = "checkbox"
+ dump.appendChild(input)
+ var clickEvent = new MouseEvent("click")
+ input.onchange = t.step_func_done(function() {
+ assert_false(clickEvent.defaultPrevented)
+ assert_true(clickEvent.returnValue)
+ assert_equals(clickEvent.eventPhase, 0)
+ assert_equals(clickEvent.currentTarget, null)
+ assert_equals(clickEvent.target, input)
+ assert_equals(clickEvent.srcElement, input)
+ assert_equals(clickEvent.composedPath().length, 0)
+ })
+ input.dispatchEvent(clickEvent)
+}, "event state during post-click handling")
+
+async_test(function(t) {
+ var input = document.createElement("input")
+ input.type = "checkbox"
+ dump.appendChild(input)
+ var clickEvent = new MouseEvent("click")
+ var finalTarget = document.createElement("doesnotmatter")
+ finalTarget.onclick = t.step_func_done(function() {
+ assert_equals(clickEvent.target, finalTarget)
+ assert_equals(clickEvent.srcElement, finalTarget)
+ })
+ input.onchange = t.step_func(function() {
+ finalTarget.dispatchEvent(clickEvent)
+ })
+ input.dispatchEvent(clickEvent)
+}, "redispatch during post-click handling")
+
+async_test(function(t) {
+ var input = document.createElement("input")
+ input.type = "checkbox"
+ dump.appendChild(input)
+ var child = input.appendChild(document.createElement("input"))
+ child.type = "checkbox"
+ child.disabled = true
+ child.click()
+ assert_false(input.checked)
+ assert_false(child.checked)
+ t.done()
+}, "disabled checkbox still has activation behavior")
+
+async_test(function(t) {
+ var state = "start"
+
+ var form = document.createElement("form")
+ form.onsubmit = t.step_func(() => {
+ if(state == "start" || state == "checkbox") {
+ state = "failure"
+ } else if(state == "form") {
+ state = "done"
+ }
+ return false
+ })
+ dump.appendChild(form)
+ var button = form.appendChild(document.createElement("button"))
+ button.type = "submit"
+ var checkbox = button.appendChild(document.createElement("input"))
+ checkbox.type = "checkbox"
+ checkbox.onclick = t.step_func(() => {
+ if(state == "start") {
+ assert_unreached()
+ } else if(state == "checkbox") {
+ assert_true(checkbox.checked)
+ }
+ })
+ checkbox.disabled = true
+ checkbox.click()
+ assert_equals(state, "start")
+
+ state = "checkbox"
+ checkbox.disabled = false
+ checkbox.click()
+ assert_equals(state, "checkbox")
+
+ state = "form"
+ button.click()
+ assert_equals(state, "done")
+
+ t.done()
+}, "disabled checkbox still has activation behavior, part 2")
+
+async_test(function(t) {
+ var input = document.createElement("input")
+ input.type = "checkbox"
+ input.onclick = t.step_func_done(function() {
+ assert_true(input.checked)
+ })
+ input.click()
+}, "disconnected checkbox should be checked")
+
+async_test(function(t) {
+ var input = document.createElement("input")
+ input.type = "radio"
+ input.onclick = t.step_func_done(function() {
+ assert_true(input.checked)
+ })
+ input.click()
+}, "disconnected radio should be checked")
+
+async_test(t => {
+ const input = document.createElement('input');
+ input.type = 'checkbox';
+ input.onclick = t.step_func_done(() => {
+ assert_true(input.checked);
+ });
+ input.dispatchEvent(new MouseEvent('click'));
+}, `disconnected checkbox should be checked from dispatchEvent(new MouseEvent('click'))`);
+
+async_test(t => {
+ const input = document.createElement('input');
+ input.type = 'radio';
+ input.onclick = t.step_func_done(() => {
+ assert_true(input.checked);
+ });
+ input.dispatchEvent(new MouseEvent('click'));
+}, `disconnected radio should be checked from dispatchEvent(new MouseEvent('click'))`);
+
+test(() => {
+ const input = document.createElement("input");
+ input.type = "checkbox";
+ input.disabled = true;
+ input.dispatchEvent(new MouseEvent("click"));
+ assert_true(input.checked);
+}, `disabled checkbox should be checked from dispatchEvent(new MouseEvent("click"))`);
+
+test(() => {
+ const input = document.createElement("input");
+ input.type = "radio";
+ input.disabled = true;
+ input.dispatchEvent(new MouseEvent("click"));
+ assert_true(input.checked);
+}, `disabled radio should be checked from dispatchEvent(new MouseEvent("click"))`);
+
+async_test(t => {
+ const input = document.createElement("input");
+ input.type = "checkbox";
+ input.disabled = true;
+ input.onclick = t.step_func_done();
+ input.dispatchEvent(new MouseEvent("click"));
+}, `disabled checkbox should fire onclick`);
+
+async_test(t => {
+ const input = document.createElement("input");
+ input.type = "radio";
+ input.disabled = true;
+ input.onclick = t.step_func_done();
+ input.dispatchEvent(new MouseEvent("click"));
+}, `disabled radio should fire onclick`);
+
+async_test(t => {
+ const input = document.createElement("input");
+ input.type = "checkbox";
+ input.disabled = true;
+ input.onclick = t.step_func(ev => {
+ assert_true(input.checked);
+ ev.preventDefault();
+ queueMicrotask(t.step_func_done(() => {
+ assert_false(input.checked);
+ }));
+ });
+ input.dispatchEvent(new MouseEvent("click", { cancelable: true }));
+}, `disabled checkbox should get legacy-canceled-activation behavior`);
+
+async_test(t => {
+ const input = document.createElement("input");
+ input.type = "radio";
+ input.disabled = true;
+ input.onclick = t.step_func(ev => {
+ assert_true(input.checked);
+ ev.preventDefault();
+ queueMicrotask(t.step_func_done(() => {
+ assert_false(input.checked);
+ }));
+ });
+ input.dispatchEvent(new MouseEvent("click", { cancelable: true }));
+}, `disabled radio should get legacy-canceled-activation behavior`);
+
+test(t => {
+ const input = document.createElement("input");
+ input.type = "checkbox";
+ input.disabled = true;
+ const ev = new MouseEvent("click", { cancelable: true });
+ ev.preventDefault();
+ input.dispatchEvent(ev);
+ assert_false(input.checked);
+}, `disabled checkbox should get legacy-canceled-activation behavior 2`);
+
+test(t => {
+ const input = document.createElement("input");
+ input.type = "radio";
+ input.disabled = true;
+ const ev = new MouseEvent("click", { cancelable: true });
+ ev.preventDefault();
+ input.dispatchEvent(ev);
+ assert_false(input.checked);
+}, `disabled radio should get legacy-canceled-activation behavior 2`);
+
+for (const type of ["checkbox", "radio"]) {
+ for (const handler of ["oninput", "onchange"]) {
+ async_test(t => {
+ const input = document.createElement("input");
+ input.type = type;
+ input.onclick = t.step_func(ev => {
+ input.disabled = true;
+ });
+ input[handler] = t.step_func(ev => {
+ assert_equals(input.checked, true);
+ t.done();
+ });
+ dump.append(input);
+ input.click();
+ }, `disabling ${type} in onclick listener shouldn't suppress ${handler}`);
+ }
+}
+
+async_test(function(t) {
+ var form = document.createElement("form")
+ var didSubmit = false
+ form.onsubmit = t.step_func(() => {
+ didSubmit = true
+ return false
+ })
+ var input = form.appendChild(document.createElement("input"))
+ input.type = "submit"
+ input.click()
+ assert_false(didSubmit)
+ t.done()
+}, "disconnected form should not submit")
+
+async_test(t => {
+ const form = document.createElement("form");
+ form.onsubmit = t.step_func(ev => {
+ ev.preventDefault();
+ assert_unreached("The form is unexpectedly submitted.");
+ });
+ dump.append(form);
+ const input = form.appendChild(document.createElement("input"));
+ input.type = "submit"
+ input.disabled = true;
+ input.dispatchEvent(new MouseEvent("click", { cancelable: true }));
+ t.done();
+}, "disabled submit button should not activate");
+
+async_test(t => {
+ const form = document.createElement("form");
+ form.onsubmit = t.step_func(ev => {
+ ev.preventDefault();
+ assert_unreached("The form is unexpectedly submitted.");
+ });
+ dump.append(form);
+ const input = form.appendChild(document.createElement("input"));
+ input.onclick = t.step_func(() => {
+ input.disabled = true;
+ });
+ input.type = "submit"
+ input.dispatchEvent(new MouseEvent("click", { cancelable: true }));
+ t.done();
+}, "submit button should not activate if the event listener disables it");
+
+async_test(t => {
+ const form = document.createElement("form");
+ form.onsubmit = t.step_func(ev => {
+ ev.preventDefault();
+ assert_unreached("The form is unexpectedly submitted.");
+ });
+ dump.append(form);
+ const input = form.appendChild(document.createElement("input"));
+ input.onclick = t.step_func(() => {
+ input.type = "submit"
+ input.disabled = true;
+ });
+ input.click();
+ t.done();
+}, "submit button that morphed from checkbox should not activate");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-click.tentative.html b/testing/web-platform/tests/dom/events/Event-dispatch-click.tentative.html
new file mode 100644
index 0000000000..cfdae55ef2
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-click.tentative.html
@@ -0,0 +1,78 @@
+<!doctype html>
+<title>Clicks on input element</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=dump style=display:none></div>
+<script>
+var dump = document.getElementById("dump")
+
+test(t => {
+ const input = document.createElement("input");
+ input.type = "checkbox";
+ input.disabled = true;
+ const label = document.createElement("label");
+ label.append(input);
+ dump.append(label);
+ label.click();
+ assert_false(input.checked);
+}, "disabled checkbox should not be checked from label click");
+
+test(t => {
+ const input = document.createElement("input");
+ input.type = "radio";
+ input.disabled = true;
+ const label = document.createElement("label");
+ label.append(input);
+ dump.append(label);
+ label.click();
+ assert_false(input.checked);
+}, "disabled radio should not be checked from label click");
+
+test(t => {
+ const input = document.createElement("input");
+ input.type = "checkbox";
+ input.disabled = true;
+ const label = document.createElement("label");
+ label.append(input);
+ dump.append(label);
+ label.dispatchEvent(new MouseEvent("click"));
+ assert_false(input.checked);
+}, "disabled checkbox should not be checked from label click by dispatchEvent");
+
+test(t => {
+ const input = document.createElement("input");
+ input.type = "radio";
+ input.disabled = true;
+ const label = document.createElement("label");
+ label.append(input);
+ dump.append(label);
+ label.dispatchEvent(new MouseEvent("click"));
+ assert_false(input.checked);
+}, "disabled radio should not be checked from label click by dispatchEvent");
+
+test(t => {
+ const checkbox = dump.appendChild(document.createElement("input"));
+ checkbox.type = "checkbox";
+ checkbox.onclick = ev => {
+ checkbox.type = "date";
+ ev.preventDefault();
+ };
+ checkbox.dispatchEvent(new MouseEvent("click", { cancelable: true }));
+ assert_false(checkbox.checked);
+}, "checkbox morphed into another type should not mutate checked state");
+
+test(t => {
+ const radio1 = dump.appendChild(document.createElement("input"));
+ const radio2 = dump.appendChild(radio1.cloneNode());
+ radio1.type = radio2.type = "radio";
+ radio1.name = radio2.name = "foo";
+ radio2.checked = true;
+ radio1.onclick = ev => {
+ radio1.type = "date";
+ ev.preventDefault();
+ };
+ radio1.dispatchEvent(new MouseEvent("click", { cancelable: true }));
+ assert_false(radio1.checked);
+ assert_true(radio2.checked);
+}, "radio morphed into another type should not steal the existing checked state");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-detached-click.html b/testing/web-platform/tests/dom/events/Event-dispatch-detached-click.html
new file mode 100644
index 0000000000..76ea3d78ba
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-detached-click.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>Click event on an element not in the document</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+test(function() {
+ var EVENT = "click";
+ var TARGET = document.createElement("somerandomelement");
+ var t = async_test("Click event can be dispatched to an element that is not in the document.")
+ TARGET.addEventListener(EVENT, t.step_func(function(evt) {
+ assert_equals(evt.target, TARGET);
+ assert_equals(evt.srcElement, TARGET);
+ t.done();
+ }), true);
+ var e = document.createEvent("Event");
+ e.initEvent(EVENT, true, true);
+ TARGET.dispatchEvent(e);
+});
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-detached-input-and-change.html b/testing/web-platform/tests/dom/events/Event-dispatch-detached-input-and-change.html
new file mode 100644
index 0000000000..a53ae71ac2
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-detached-input-and-change.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Joey Arhar" href="mailto:jarhar@chromium.org">
+<title>input and change events for detached checkbox and radio elements</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+
+test(() => {
+ const input = document.createElement('input');
+ input.type = 'checkbox';
+
+ let inputEventFired = false;
+ input.addEventListener('input', () => inputEventFired = true);
+ let changeEventFired = false;
+ input.addEventListener('change', () => changeEventFired = true);
+ input.click();
+ assert_false(inputEventFired);
+ assert_false(changeEventFired);
+}, 'detached checkbox should not emit input or change events on click().');
+
+test(() => {
+ const input = document.createElement('input');
+ input.type = 'radio';
+
+ let inputEventFired = false;
+ input.addEventListener('input', () => inputEventFired = true);
+ let changeEventFired = false;
+ input.addEventListener('change', () => changeEventFired = true);
+ input.click();
+ assert_false(inputEventFired);
+ assert_false(changeEventFired);
+}, 'detached radio should not emit input or change events on click().');
+
+test(() => {
+ const input = document.createElement('input');
+ input.type = 'checkbox';
+
+ let inputEventFired = false;
+ input.addEventListener('input', () => inputEventFired = true);
+ let changeEventFired = false;
+ input.addEventListener('change', () => changeEventFired = true);
+ input.dispatchEvent(new MouseEvent('click'));
+ assert_false(inputEventFired);
+ assert_false(changeEventFired);
+}, `detached checkbox should not emit input or change events on dispatchEvent(new MouseEvent('click')).`);
+
+test(() => {
+ const input = document.createElement('input');
+ input.type = 'radio';
+
+ let inputEventFired = false;
+ input.addEventListener('input', () => inputEventFired = true);
+ let changeEventFired = false;
+ input.addEventListener('change', () => changeEventFired = true);
+ input.dispatchEvent(new MouseEvent('click'));
+ assert_false(inputEventFired);
+ assert_false(changeEventFired);
+}, `detached radio should not emit input or change events on dispatchEvent(new MouseEvent('click')).`);
+
+
+test(() => {
+ const input = document.createElement('input');
+ input.type = 'checkbox';
+ document.body.appendChild(input);
+
+ let inputEventFired = false;
+ input.addEventListener('input', () => inputEventFired = true);
+ let changeEventFired = false;
+ input.addEventListener('change', () => changeEventFired = true);
+ input.click();
+ assert_true(inputEventFired);
+ assert_true(changeEventFired);
+}, 'attached checkbox should emit input and change events on click().');
+
+test(() => {
+ const input = document.createElement('input');
+ input.type = 'radio';
+ document.body.appendChild(input);
+
+ let inputEventFired = false;
+ input.addEventListener('input', () => inputEventFired = true);
+ let changeEventFired = false;
+ input.addEventListener('change', () => changeEventFired = true);
+ input.click();
+ assert_true(inputEventFired);
+ assert_true(changeEventFired);
+}, 'attached radio should emit input and change events on click().');
+
+test(() => {
+ const input = document.createElement('input');
+ input.type = 'checkbox';
+ document.body.appendChild(input);
+
+ let inputEventFired = false;
+ input.addEventListener('input', () => inputEventFired = true);
+ let changeEventFired = false;
+ input.addEventListener('change', () => changeEventFired = true);
+ input.dispatchEvent(new MouseEvent('click'));
+ assert_true(inputEventFired);
+ assert_true(changeEventFired);
+}, `attached checkbox should emit input and change events on dispatchEvent(new MouseEvent('click')).`);
+
+test(() => {
+ const input = document.createElement('input');
+ input.type = 'radio';
+ document.body.appendChild(input);
+
+ let inputEventFired = false;
+ input.addEventListener('input', () => inputEventFired = true);
+ let changeEventFired = false;
+ input.addEventListener('change', () => changeEventFired = true);
+ input.dispatchEvent(new MouseEvent('click'));
+ assert_true(inputEventFired);
+ assert_true(changeEventFired);
+}, `attached radio should emit input and change events on dispatchEvent(new MouseEvent('click')).`);
+
+
+test(() => {
+ const input = document.createElement('input');
+ input.type = 'checkbox';
+ const shadowHost = document.createElement('div');
+ document.body.appendChild(shadowHost);
+ const shadowRoot = shadowHost.attachShadow({mode: 'open'});
+ shadowRoot.appendChild(input);
+
+ let inputEventFired = false;
+ input.addEventListener('input', () => inputEventFired = true);
+ let changeEventFired = false;
+ input.addEventListener('change', () => changeEventFired = true);
+ input.click();
+ assert_true(inputEventFired);
+ assert_true(changeEventFired);
+}, 'attached to shadow dom checkbox should emit input and change events on click().');
+
+test(() => {
+ const input = document.createElement('input');
+ input.type = 'radio';
+ const shadowHost = document.createElement('div');
+ document.body.appendChild(shadowHost);
+ const shadowRoot = shadowHost.attachShadow({mode: 'open'});
+ shadowRoot.appendChild(input);
+
+ let inputEventFired = false;
+ input.addEventListener('input', () => inputEventFired = true);
+ let changeEventFired = false;
+ input.addEventListener('change', () => changeEventFired = true);
+ input.click();
+ assert_true(inputEventFired);
+ assert_true(changeEventFired);
+}, 'attached to shadow dom radio should emit input and change events on click().');
+
+test(() => {
+ const input = document.createElement('input');
+ input.type = 'checkbox';
+ const shadowHost = document.createElement('div');
+ document.body.appendChild(shadowHost);
+ const shadowRoot = shadowHost.attachShadow({mode: 'open'});
+ shadowRoot.appendChild(input);
+
+ let inputEventFired = false;
+ input.addEventListener('input', () => inputEventFired = true);
+ let changeEventFired = false;
+ input.addEventListener('change', () => changeEventFired = true);
+ input.dispatchEvent(new MouseEvent('click'));
+ assert_true(inputEventFired);
+ assert_true(changeEventFired);
+}, `attached to shadow dom checkbox should emit input and change events on dispatchEvent(new MouseEvent('click')).`);
+
+test(() => {
+ const input = document.createElement('input');
+ input.type = 'radio';
+ const shadowHost = document.createElement('div');
+ document.body.appendChild(shadowHost);
+ const shadowRoot = shadowHost.attachShadow({mode: 'open'});
+ shadowRoot.appendChild(input);
+
+ let inputEventFired = false;
+ input.addEventListener('input', () => inputEventFired = true);
+ let changeEventFired = false;
+ input.addEventListener('change', () => changeEventFired = true);
+ input.dispatchEvent(new MouseEvent('click'));
+ assert_true(inputEventFired);
+ assert_true(changeEventFired);
+}, `attached to shadow dom radio should emit input and change events on dispatchEvent(new MouseEvent('click')).`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-handlers-changed.html b/testing/web-platform/tests/dom/events/Event-dispatch-handlers-changed.html
new file mode 100644
index 0000000000..24e6fd70cb
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-handlers-changed.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title> Dispatch additional events inside an event listener </title>
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-dispatch">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+
+<table id="table" border="1" style="display: none">
+ <tbody id="table-body">
+ <tr id="table-row">
+ <td id="table-cell">Shady Grove</td>
+ <td>Aeolian</td>
+ </tr>
+ <tr id="parent">
+ <td id="target">Over the river, Charlie</td>
+ <td>Dorian</td>
+ </tr>
+ </tbody>
+</table>
+
+<script>
+test(function() {
+ var event_type = "bar";
+ var target = document.getElementById("target");
+ var parent = document.getElementById("parent");
+ var tbody = document.getElementById("table-body");
+ var table = document.getElementById("table");
+ var body = document.body;
+ var html = document.documentElement;
+ var targets = [window, document, html, body, table, tbody, parent, target];
+ var expected_targets = [
+ window,
+ document,
+ html,
+ body,
+ table,
+ tbody,
+ parent,
+ target,
+ target,
+ target, // The additional listener for target runs as we copy its listeners twice
+ parent,
+ tbody,
+ table,
+ body,
+ html,
+ document,
+ window
+ ];
+ var expected_listeners = [0,0,0,0,0,0,0,0,1,3,1,1,1,1,1,1,1];
+
+ var actual_targets = [], actual_listeners = [];
+ var test_event_function = function(i) {
+ return this.step_func(function(evt) {
+ actual_targets.push(evt.currentTarget);
+ actual_listeners.push(i);
+
+ if (evt.eventPhase != evt.BUBBLING_PHASE && evt.currentTarget.foo != 1) {
+ evt.currentTarget.removeEventListener(event_type, event_handlers[0], true);
+ evt.currentTarget.addEventListener(event_type, event_handlers[2], true);
+ evt.currentTarget.foo = 1;
+ }
+
+ if (evt.eventPhase != evt.CAPTURING_PHASE && evt.currentTarget.foo != 3) {
+ evt.currentTarget.removeEventListener(event_type, event_handlers[0], false);
+ evt.currentTarget.addEventListener(event_type, event_handlers[3], false);
+ evt.currentTarget.foo = 3;
+ }
+ });
+ }.bind(this);
+ var event_handlers = [
+ test_event_function(0),
+ test_event_function(1),
+ test_event_function(2),
+ test_event_function(3),
+ ];
+
+ for (var i = 0; i < targets.length; ++i) {
+ targets[i].addEventListener(event_type, event_handlers[0], true);
+ targets[i].addEventListener(event_type, event_handlers[1], false);
+ }
+
+ var evt = document.createEvent("Event");
+ evt.initEvent(event_type, true, true);
+ target.dispatchEvent(evt);
+
+ assert_array_equals(actual_targets, expected_targets, "actual_targets");
+ assert_array_equals(actual_listeners, expected_listeners, "actual_listeners");
+});
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-listener-order.window.js b/testing/web-platform/tests/dom/events/Event-dispatch-listener-order.window.js
new file mode 100644
index 0000000000..a01a472872
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-listener-order.window.js
@@ -0,0 +1,20 @@
+test(t => {
+ const hostParent = document.createElement("section"),
+ host = hostParent.appendChild(document.createElement("div")),
+ shadowRoot = host.attachShadow({ mode: "closed" }),
+ targetParent = shadowRoot.appendChild(document.createElement("p")),
+ target = targetParent.appendChild(document.createElement("span")),
+ path = [hostParent, host, shadowRoot, targetParent, target],
+ expected = [],
+ result = [];
+ path.forEach((node, index) => {
+ expected.splice(index, 0, "capturing " + node.nodeName);
+ expected.splice(index + 1, 0, "bubbling " + node.nodeName);
+ });
+ path.forEach(node => {
+ node.addEventListener("test", () => { result.push("bubbling " + node.nodeName) });
+ node.addEventListener("test", () => { result.push("capturing " + node.nodeName) }, true);
+ });
+ target.dispatchEvent(new CustomEvent('test', { detail: {}, bubbles: true, composed: true }));
+ assert_array_equals(result, expected);
+});
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-multiple-cancelBubble.html b/testing/web-platform/tests/dom/events/Event-dispatch-multiple-cancelBubble.html
new file mode 100644
index 0000000000..2873fd7794
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-multiple-cancelBubble.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Multiple dispatchEvent() and cancelBubble</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id=log></div>
+
+<div id="parent" style="display: none">
+ <input id="target" type="hidden" value=""/>
+</div>
+
+<script>
+test(function() {
+ var event_type = "foo";
+ var target = document.getElementById("target");
+ var parent = document.getElementById("parent");
+ var actual_result;
+ var test_event = function(evt) {
+ actual_result.push(evt.currentTarget);
+
+ if (parent == evt.currentTarget) {
+ evt.cancelBubble = true;
+ }
+ };
+
+ var evt = document.createEvent("Event");
+ evt.initEvent(event_type, true, true);
+
+ target.addEventListener(event_type, test_event, false);
+ parent.addEventListener(event_type, test_event, false);
+ document.addEventListener(event_type, test_event, false);
+ window.addEventListener(event_type, test_event, false);
+
+ actual_result = [];
+ target.dispatchEvent(evt);
+ assert_array_equals(actual_result, [target, parent]);
+
+ actual_result = [];
+ parent.dispatchEvent(evt);
+ assert_array_equals(actual_result, [parent]);
+
+ actual_result = [];
+ document.dispatchEvent(evt);
+ assert_array_equals(actual_result, [document, window]);
+});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-multiple-stopPropagation.html b/testing/web-platform/tests/dom/events/Event-dispatch-multiple-stopPropagation.html
new file mode 100644
index 0000000000..72644bd861
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-multiple-stopPropagation.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title> Multiple dispatchEvent() and stopPropagation() </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id=log></div>
+
+<div id="parent" style="display: none">
+ <input id="target" type="hidden" value=""/>
+</div>
+
+<script>
+test(function() {
+ var event_type = "foo";
+ var target = document.getElementById("target");
+ var parent = document.getElementById("parent");
+ var actual_result;
+ var test_event = function(evt) {
+ actual_result.push(evt.currentTarget);
+
+ if (parent == evt.currentTarget) {
+ evt.stopPropagation();
+ }
+ };
+
+ var evt = document.createEvent("Event");
+ evt.initEvent(event_type, true, true);
+
+ target.addEventListener(event_type, test_event, false);
+ parent.addEventListener(event_type, test_event, false);
+ document.addEventListener(event_type, test_event, false);
+ window.addEventListener(event_type, test_event, false);
+
+ actual_result = [];
+ target.dispatchEvent(evt);
+ assert_array_equals(actual_result, [target, parent]);
+
+ actual_result = [];
+ parent.dispatchEvent(evt);
+ assert_array_equals(actual_result, [parent]);
+
+ actual_result = [];
+ document.dispatchEvent(evt);
+ assert_array_equals(actual_result, [document, window]);
+});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-omitted-capture.html b/testing/web-platform/tests/dom/events/Event-dispatch-omitted-capture.html
new file mode 100644
index 0000000000..77074d9a3e
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-omitted-capture.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>EventTarget.addEventListener: capture argument omitted</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-dispatch">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<table id="table" border="1" style="display: none">
+ <tbody id="table-body">
+ <tr id="table-row">
+ <td id="table-cell">Shady Grove</td>
+ <td>Aeolian</td>
+ </tr>
+ <tr id="parent">
+ <td id="target">Over the river, Charlie</td>
+ <td>Dorian</td>
+ </tr>
+ </tbody>
+</table>
+<script>
+test(function() {
+ var event_type = "foo";
+ var target = document.getElementById("target");
+ var targets = [
+ target,
+ document.getElementById("parent"),
+ document.getElementById("table-body"),
+ document.getElementById("table"),
+ document.body,
+ document.documentElement,
+ document,
+ window
+ ];
+ var phases = [
+ Event.AT_TARGET,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE
+ ];
+
+ var actual_targets = [], actual_phases = [];
+ var test_event = function(evt) {
+ actual_targets.push(evt.currentTarget);
+ actual_phases.push(evt.eventPhase);
+ }
+
+ for (var i = 0; i < targets.length; i++) {
+ targets[i].addEventListener(event_type, test_event);
+ }
+
+ var evt = document.createEvent("Event");
+ evt.initEvent(event_type, true, true);
+
+ target.dispatchEvent(evt);
+
+ for (var i = 0; i < targets.length; i++) {
+ targets[i].removeEventListener(event_type, test_event);
+ }
+
+ target.dispatchEvent(evt);
+
+ assert_array_equals(actual_targets, targets, "targets");
+ assert_array_equals(actual_phases, phases, "phases");
+}, "EventTarget.addEventListener with the capture argument omitted");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-on-disabled-elements.html b/testing/web-platform/tests/dom/events/Event-dispatch-on-disabled-elements.html
new file mode 100644
index 0000000000..361006a724
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-on-disabled-elements.html
@@ -0,0 +1,251 @@
+<!doctype html>
+<meta charset="utf8">
+<meta name="timeout" content="long">
+<title>Events must dispatch on disabled elements</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<style>
+ @keyframes fade {
+ 0% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+ }
+</style>
+<body>
+<script>
+// HTML elements that can be disabled
+const formElements = ["button", "fieldset", "input", "select", "textarea"];
+
+test(() => {
+ for (const localName of formElements) {
+ const elem = document.createElement(localName);
+ elem.disabled = true;
+ // pass becomes true if the event is called and it's the right type.
+ let pass = false;
+ const listener = ({ type }) => {
+ pass = type === "click";
+ };
+ elem.addEventListener("click", listener, { once: true });
+ elem.dispatchEvent(new Event("click"));
+ assert_true(
+ pass,
+ `Untrusted "click" Event didn't dispatch on ${elem.constructor.name}.`
+ );
+ }
+}, "Can dispatch untrusted 'click' Events at disabled HTML elements.");
+
+test(() => {
+ for (const localName of formElements) {
+ const elem = document.createElement(localName);
+ elem.disabled = true;
+ // pass becomes true if the event is called and it's the right type.
+ let pass = false;
+ const listener = ({ type }) => {
+ pass = type === "pass";
+ };
+ elem.addEventListener("pass", listener, { once: true });
+ elem.dispatchEvent(new Event("pass"));
+ assert_true(
+ pass,
+ `Untrusted "pass" Event didn't dispatch on ${elem.constructor.name}`
+ );
+ }
+}, "Can dispatch untrusted Events at disabled HTML elements.");
+
+test(() => {
+ for (const localName of formElements) {
+ const elem = document.createElement(localName);
+ elem.disabled = true;
+ // pass becomes true if the event is called and it's the right type.
+ let pass = false;
+ const listener = ({ type }) => {
+ pass = type === "custom-pass";
+ };
+ elem.addEventListener("custom-pass", listener, { once: true });
+ elem.dispatchEvent(new CustomEvent("custom-pass"));
+ assert_true(
+ pass,
+ `CustomEvent "custom-pass" didn't dispatch on ${elem.constructor.name}`
+ );
+ }
+}, "Can dispatch CustomEvents at disabled HTML elements.");
+
+test(() => {
+ for (const localName of formElements) {
+ const elem = document.createElement(localName);
+
+ // Element is disabled... so this click() MUST NOT fire an event.
+ elem.disabled = true;
+ let pass = true;
+ elem.onclick = e => {
+ pass = false;
+ };
+ elem.click();
+ assert_true(
+ pass,
+ `.click() must not dispatch "click" event on disabled ${
+ elem.constructor.name
+ }.`
+ );
+
+ // Element is (re)enabled... so this click() fires an event.
+ elem.disabled = false;
+ pass = false;
+ elem.onclick = e => {
+ pass = true;
+ };
+ elem.click();
+ assert_true(
+ pass,
+ `.click() must dispatch "click" event on enabled ${
+ elem.constructor.name
+ }.`
+ );
+ }
+}, "Calling click() on disabled elements must not dispatch events.");
+
+promise_test(async () => {
+ // For each form element type, set up transition event handlers.
+ for (const localName of formElements) {
+ const elem = document.createElement(localName);
+ elem.disabled = true;
+ document.body.appendChild(elem);
+ const eventPromises = [
+ "transitionrun",
+ "transitionstart",
+ "transitionend",
+ ].map(eventType => {
+ return new Promise(r => {
+ elem.addEventListener(eventType, r);
+ });
+ });
+ // Flushing style triggers transition.
+ getComputedStyle(elem).opacity;
+ elem.style.transition = "opacity .1s";
+ elem.style.opacity = 0;
+ getComputedStyle(elem).opacity;
+ // All the events fire...
+ await Promise.all(eventPromises);
+ elem.remove();
+ }
+}, "CSS Transitions transitionrun, transitionstart, transitionend events fire on disabled form elements");
+
+promise_test(async () => {
+ // For each form element type, set up transition event handlers.
+ for (const localName of formElements) {
+ const elem = document.createElement(localName);
+ elem.disabled = true;
+ document.body.appendChild(elem);
+ getComputedStyle(elem).opacity;
+ elem.style.transition = "opacity 100s";
+ // We use ontransitionstart to cancel the event.
+ elem.ontransitionstart = () => {
+ elem.style.display = "none";
+ };
+ const promiseToCancel = new Promise(r => {
+ elem.ontransitioncancel = r;
+ });
+ // Flushing style triggers the transition.
+ elem.style.opacity = 0;
+ getComputedStyle(elem).opacity;
+ await promiseToCancel;
+ // And we are done with this element.
+ elem.remove();
+ }
+}, "CSS Transitions transitioncancel event fires on disabled form elements");
+
+promise_test(async () => {
+ // For each form element type, set up transition event handlers.
+ for (const localName of formElements) {
+ const elem = document.createElement(localName);
+ document.body.appendChild(elem);
+ elem.disabled = true;
+ const animationStartPromise = new Promise(r => {
+ elem.addEventListener("animationstart", () => {
+ // Seek to the second iteration to trigger the animationiteration event
+ elem.style.animationDelay = "-100s"
+ r();
+ });
+ });
+ const animationIterationPromise = new Promise(r => {
+ elem.addEventListener("animationiteration", ()=>{
+ elem.style.animationDelay = "-200s"
+ r();
+ });
+ });
+ const animationEndPromise = new Promise(r => {
+ elem.addEventListener("animationend", r);
+ });
+ elem.style.animation = "fade 100s 2";
+ elem.classList.add("animate");
+ // All the events fire...
+ await Promise.all([
+ animationStartPromise,
+ animationIterationPromise,
+ animationEndPromise,
+ ]);
+ elem.remove();
+ }
+}, "CSS Animation animationstart, animationiteration, animationend fire on disabled form elements");
+
+promise_test(async () => {
+ // For each form element type, set up transition event handlers.
+ for (const localName of formElements) {
+ const elem = document.createElement(localName);
+ document.body.appendChild(elem);
+ elem.disabled = true;
+
+ const promiseToCancel = new Promise(r => {
+ elem.addEventListener("animationcancel", r);
+ });
+
+ elem.addEventListener("animationstart", () => {
+ // Cancel the animation by hiding it.
+ elem.style.display = "none";
+ });
+
+ // Trigger the animation
+ elem.style.animation = "fade 100s";
+ elem.classList.add("animate");
+ await promiseToCancel;
+ // And we are done with this element.
+ elem.remove();
+ }
+}, "CSS Animation's animationcancel event fires on disabled form elements");
+
+promise_test(async () => {
+ for (const localName of formElements) {
+ const elem = document.createElement(localName);
+ elem.disabled = true;
+ document.body.appendChild(elem);
+ // Element is disabled, so clicking must not fire events
+ let pass = true;
+ elem.onclick = e => {
+ pass = false;
+ };
+ // Disabled elements are not clickable.
+ await test_driver.click(elem);
+ assert_true(
+ pass,
+ `${elem.constructor.name} is disabled, so onclick must not fire.`
+ );
+ // Element is (re)enabled... so this click() will fire an event.
+ pass = false;
+ elem.disabled = false;
+ elem.onclick = () => {
+ pass = true;
+ };
+ await test_driver.click(elem);
+ assert_true(
+ pass,
+ `${elem.constructor.name} is enabled, so onclick must fire.`
+ );
+ elem.remove();
+ }
+}, "Real clicks on disabled elements must not dispatch events.");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-order-at-target.html b/testing/web-platform/tests/dom/events/Event-dispatch-order-at-target.html
new file mode 100644
index 0000000000..79673c3256
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-order-at-target.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Listeners are invoked in correct order (AT_TARGET phase)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+"use strict";
+
+test(() => {
+ const el = document.createElement("div");
+ const expectedOrder = ["capturing", "bubbling"];
+
+ let actualOrder = [];
+ el.addEventListener("click", evt => {
+ assert_equals(evt.eventPhase, Event.AT_TARGET);
+ actualOrder.push("bubbling");
+ }, false);
+ el.addEventListener("click", evt => {
+ assert_equals(evt.eventPhase, Event.AT_TARGET);
+ actualOrder.push("capturing");
+ }, true);
+
+ el.dispatchEvent(new Event("click", {bubbles: true}));
+ assert_array_equals(actualOrder, expectedOrder, "bubbles: true");
+
+ actualOrder = [];
+ el.dispatchEvent(new Event("click", {bubbles: false}));
+ assert_array_equals(actualOrder, expectedOrder, "bubbles: false");
+}, "Listeners are invoked in correct order (AT_TARGET phase)");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-order.html b/testing/web-platform/tests/dom/events/Event-dispatch-order.html
new file mode 100644
index 0000000000..ca94434595
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-order.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<title>Event phases order</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(function() {
+ document.addEventListener('DOMContentLoaded', this.step_func_done(function() {
+ var parent = document.getElementById('parent');
+ var child = document.getElementById('child');
+
+ var order = [];
+
+ parent.addEventListener('click', this.step_func(function(){ order.push(1) }), true);
+ child.addEventListener('click', this.step_func(function(){ order.push(2) }), false);
+ parent.addEventListener('click', this.step_func(function(){ order.push(3) }), false);
+
+ child.dispatchEvent(new Event('click', {bubbles: true}));
+
+ assert_array_equals(order, [1, 2, 3]);
+ }));
+}, "Event phases order");
+</script>
+<div id="parent">
+ <div id="child"></div>
+</div>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-other-document.html b/testing/web-platform/tests/dom/events/Event-dispatch-other-document.html
new file mode 100644
index 0000000000..689b48087a
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-other-document.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<title>Custom event on an element in another document</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+test(function() {
+ var doc = document.implementation.createHTMLDocument("Demo");
+ var element = doc.createElement("div");
+ var called = false;
+ element.addEventListener("foo", this.step_func(function(ev) {
+ assert_false(called);
+ called = true;
+ assert_equals(ev.target, element);
+ assert_equals(ev.srcElement, element);
+ }));
+ doc.body.appendChild(element);
+
+ var event = new Event("foo");
+ element.dispatchEvent(event);
+ assert_true(called);
+});
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-propagation-stopped.html b/testing/web-platform/tests/dom/events/Event-dispatch-propagation-stopped.html
new file mode 100644
index 0000000000..889f8cfe11
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-propagation-stopped.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title> Calling stopPropagation() prior to dispatchEvent() </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id=log></div>
+
+<table id="table" border="1" style="display: none">
+ <tbody id="table-body">
+ <tr id="table-row">
+ <td id="table-cell">Shady Grove</td>
+ <td>Aeolian</td>
+ </tr>
+ <tr id="parent">
+ <td id="target">Over the river, Charlie</td>
+ <td>Dorian</td>
+ </tr>
+ </tbody>
+</table>
+
+<script>
+test(function() {
+ var event = "foo";
+ var target = document.getElementById("target");
+ var parent = document.getElementById("parent");
+ var tbody = document.getElementById("table-body");
+ var table = document.getElementById("table");
+ var body = document.body;
+ var html = document.documentElement;
+ var current_targets = [window, document, html, body, table, tbody, parent, target];
+ var expected_targets = [];
+ var actual_targets = [];
+ var expected_phases = [];
+ var actual_phases = [];
+
+ var test_event = function(evt) {
+ actual_targets.push(evt.currentTarget);
+ actual_phases.push(evt.eventPhase);
+ };
+
+ for (var i = 0; i < current_targets.length; ++i) {
+ current_targets[i].addEventListener(event, test_event, true);
+ current_targets[i].addEventListener(event, test_event, false);
+ }
+
+ var evt = document.createEvent("Event");
+ evt.initEvent(event, true, true);
+ evt.stopPropagation();
+ target.dispatchEvent(evt);
+
+ assert_array_equals(actual_targets, expected_targets, "actual_targets");
+ assert_array_equals(actual_phases, expected_phases, "actual_phases");
+});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-redispatch.html b/testing/web-platform/tests/dom/events/Event-dispatch-redispatch.html
new file mode 100644
index 0000000000..cf861ca177
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-redispatch.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<meta charset=urf-8>
+<title>EventTarget#dispatchEvent(): redispatching a native event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<button>click me!</button>
+<div id=log></div>
+<script>
+var test_contentLoaded_redispatching = async_test("Redispatching DOMContentLoaded event after being dispatched");
+var test_mouseup_redispatching = async_test("Redispatching mouseup event whose default action dispatches a click event");
+var test_redispatching_of_dispatching_event = async_test("Redispatching event which is being dispatched");
+
+var buttonElement = document.querySelector("button");
+var contentLoadedEvent;
+
+var waitForLoad = new Promise(resolve => {
+ window.addEventListener("load", () => { requestAnimationFrame(resolve); }, {capture: false, once: true});
+});
+
+document.addEventListener("DOMContentLoaded", event => {
+ contentLoadedEvent = event;
+ test_redispatching_of_dispatching_event.step(() => {
+ assert_throws_dom("InvalidStateError", () => {
+ document.dispatchEvent(contentLoadedEvent);
+ }, "Trusted DOMContentLoaded event");
+ });
+}, {capture: true, once: true});
+
+window.addEventListener("load", loadEvent => {
+ let untrustedContentLoadedEvent;
+ buttonElement.addEventListener("DOMContentLoaded", event => {
+ untrustedContentLoadedEvent = event;
+ test_contentLoaded_redispatching.step(() => {
+ assert_false(untrustedContentLoadedEvent.isTrusted, "Redispatched DOMContentLoaded event shouldn't be trusted");
+ });
+ test_redispatching_of_dispatching_event.step(() => {
+ assert_throws_dom("InvalidStateError", () => {
+ document.dispatchEvent(untrustedContentLoadedEvent);
+ }, "Untrusted DOMContentLoaded event");
+ });
+ });
+
+ test_contentLoaded_redispatching.step(() => {
+ assert_true(contentLoadedEvent.isTrusted, "Received DOMContentLoaded event should be trusted before redispatching");
+ buttonElement.dispatchEvent(contentLoadedEvent);
+ assert_false(contentLoadedEvent.isTrusted, "Received DOMContentLoaded event shouldn't be trusted after redispatching");
+ assert_not_equals(untrustedContentLoadedEvent, undefined, "Untrusted DOMContentLoaded event should've been fired");
+ test_contentLoaded_redispatching.done();
+ });
+}, {capture: true, once: true});
+
+async function testMouseUpAndClickEvent() {
+ let mouseupEvent;
+ buttonElement.addEventListener("mouseup", event => {
+ mouseupEvent = event;
+ test_mouseup_redispatching.step(() => {
+ assert_true(mouseupEvent.isTrusted, "First mouseup event should be trusted");
+ });
+ test_redispatching_of_dispatching_event.step(() => {
+ assert_throws_dom("InvalidStateError", () => {
+ buttonElement.dispatchEvent(mouseupEvent);
+ }, "Trusted mouseup event");
+ });
+ }, {once: true});
+
+ let clickEvent;
+ buttonElement.addEventListener("click", event => {
+ clickEvent = event;
+ test_mouseup_redispatching.step(() => {
+ assert_true(clickEvent.isTrusted, "First click event should be trusted");
+ });
+ test_redispatching_of_dispatching_event.step(() => {
+ assert_throws_dom("InvalidStateError", function() {
+ buttonElement.dispatchEvent(event);
+ }, "Trusted click event");
+ });
+ buttonElement.addEventListener("mouseup", event => {
+ test_mouseup_redispatching.step(() => {
+ assert_false(event.isTrusted, "Redispatched mouseup event shouldn't be trusted");
+ });
+ test_redispatching_of_dispatching_event.step(() => {
+ assert_throws_dom("InvalidStateError", function() {
+ buttonElement.dispatchEvent(event);
+ }, "Untrusted mouseup event");
+ });
+ }, {once: true});
+ function onClick() {
+ test_mouseup_redispatching.step(() => {
+ assert_true(false, "click event shouldn't be fired for dispatched mouseup event");
+ });
+ }
+ test_mouseup_redispatching.step(() => {
+ assert_true(mouseupEvent.isTrusted, "Received mouseup event should be trusted before redispatching from click event listener");
+ buttonElement.addEventListener("click", onClick);
+ buttonElement.dispatchEvent(mouseupEvent);
+ buttonElement.removeEventListener("click", onClick);
+ assert_false(mouseupEvent.isTrusted, "Received mouseup event shouldn't be trusted after redispatching");
+ assert_true(clickEvent.isTrusted, "First click event should still be trusted even after redispatching mouseup event");
+ });
+ }, {once: true});
+
+ await waitForLoad;
+ let bounds = buttonElement.getBoundingClientRect();
+ test(() => { assert_true(true); }, `Synthesizing click on button...`);
+ new test_driver.click(buttonElement)
+ .then(() => {
+ test_mouseup_redispatching.step(() => {
+ assert_not_equals(clickEvent, undefined, "mouseup and click events should've been fired");
+ });
+ test_mouseup_redispatching.done();
+ test_redispatching_of_dispatching_event.done();
+ }, (reason) => {
+ test_mouseup_redispatching.step(() => {
+ assert_true(false, `Failed to send mouse click due to ${reason}`);
+ });
+ test_mouseup_redispatching.done();
+ test_redispatching_of_dispatching_event.done();
+ });
+}
+testMouseUpAndClickEvent();
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-reenter.html b/testing/web-platform/tests/dom/events/Event-dispatch-reenter.html
new file mode 100644
index 0000000000..71f8517bdd
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-reenter.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title> Dispatch additional events inside an event listener </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<table id="table" border="1" style="display: none">
+ <tbody id="table-body">
+ <tr id="table-row">
+ <td id="table-cell">Shady Grove</td>
+ <td>Aeolian</td>
+ </tr>
+ <tr id="parent">
+ <td id="target">Over the river, Charlie</td>
+ <td>Dorian</td>
+ </tr>
+ </tbody>
+</table>
+<script>
+test(function() {
+ var event_type = "foo";
+ var target = document.getElementById("target");
+ var parent = document.getElementById("parent");
+ var tbody = document.getElementById("table-body");
+ var table = document.getElementById("table");
+ var body = document.body;
+ var html = document.documentElement;
+ var targets = [window, document, html, body, table, tbody, parent, target];
+ var expected_targets = [
+ window, document, html, body, table,
+ target, parent, tbody,
+ table, body, html, document, window,
+ tbody, parent, target];
+ var actual_targets = [];
+ var expected_types = [
+ "foo", "foo", "foo", "foo", "foo",
+ "bar", "bar", "bar",
+ "bar", "bar", "bar", "bar", "bar",
+ "foo", "foo", "foo"
+ ];
+
+ var actual_targets = [], actual_types = [];
+ var test_event = this.step_func(function(evt) {
+ actual_targets.push(evt.currentTarget);
+ actual_types.push(evt.type);
+
+ if (table == evt.currentTarget && event_type == evt.type) {
+ var e = document.createEvent("Event");
+ e.initEvent("bar", true, true);
+ target.dispatchEvent(e);
+ }
+ });
+
+ for (var i = 0; i < targets.length; ++i) {
+ targets[i].addEventListener(event_type, test_event, true);
+ targets[i].addEventListener("bar", test_event, false);
+ }
+
+ var evt = document.createEvent("Event");
+ evt.initEvent(event_type, false, true);
+ target.dispatchEvent(evt);
+
+ assert_array_equals(actual_targets, expected_targets, "actual_targets");
+ assert_array_equals(actual_types, expected_types, "actual_types");
+});
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-target-moved.html b/testing/web-platform/tests/dom/events/Event-dispatch-target-moved.html
new file mode 100644
index 0000000000..facb2c7b95
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-target-moved.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title> Determined event propagation path - target moved </title>
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-dispatch">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<table id="table" border="1" style="display: none">
+ <tbody id="table-body">
+ <tr id="table-row">
+ <td id="table-cell">Shady Grove</td>
+ <td>Aeolian</td>
+ </tr>
+ <tr id="parent">
+ <td id="target">Over the river, Charlie</td>
+ <td>Dorian</td>
+ </tr>
+ </tbody>
+</table>
+<script>
+test(function() {
+ var event_type = "foo";
+ var target = document.getElementById("target");
+ var parent = document.getElementById("parent");
+ var tbody = document.getElementById("table-body");
+ var table = document.getElementById("table");
+ var body = document.body;
+ var html = document.documentElement;
+ var targets = [window, document, html, body, table, tbody, parent, target];
+ var expected_targets = targets.concat([target, parent, tbody, table, body, html, document, window]);
+ var phases = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ ];
+
+ var actual_targets = [], actual_phases = [];
+ var test_event = this.step_func(function(evt) {
+ if (parent === target.parentNode) {
+ var table_row = document.getElementById("table-row");
+ table_row.appendChild(parent.removeChild(target));
+ }
+
+ actual_targets.push(evt.currentTarget);
+ actual_phases.push(evt.eventPhase);
+ });
+
+ for (var i = 0; i < targets.length; i++) {
+ targets[i].addEventListener(event_type, test_event, true);
+ targets[i].addEventListener(event_type, test_event, false);
+ }
+
+ var evt = document.createEvent("Event");
+ evt.initEvent(event_type, true, true);
+ target.dispatchEvent(evt);
+
+ assert_array_equals(actual_targets, expected_targets, "targets");
+ assert_array_equals(actual_phases, phases, "phases");
+}, "Event propagation path when an element in it is moved within the DOM");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-target-removed.html b/testing/web-platform/tests/dom/events/Event-dispatch-target-removed.html
new file mode 100644
index 0000000000..531799c3ad
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-target-removed.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Determined event propagation path - target removed</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-dispatch">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<table id="table" border="1" style="display: none">
+ <tbody id="table-body">
+ <tr id="table-row">
+ <td id="table-cell">Shady Grove</td>
+ <td>Aeolian</td>
+ </tr>
+ <tr id="parent">
+ <td id="target">Over the river, Charlie</td>
+ <td>Dorian</td>
+ </tr>
+ </tbody>
+</table>
+<script>
+test(function() {
+ var event_type = "foo";
+ var target = document.getElementById("target");
+ var parent = document.getElementById("parent");
+ var tbody = document.getElementById("table-body");
+ var table = document.getElementById("table");
+ var body = document.body;
+ var html = document.documentElement;
+ var targets = [window, document, html, body, table, tbody, parent, target];
+ var expected_targets = targets.concat([target, parent, tbody, table, body, html, document, window]);
+ var phases = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ ];
+
+ var actual_targets = [], actual_phases = [];
+ var test_event = this.step_func(function(evt) {
+ if (parent === target.parentNode) {
+ parent.removeChild(target);
+ }
+
+ actual_targets.push(evt.currentTarget);
+ actual_phases.push(evt.eventPhase);
+ });
+
+ for (var i = 0; i < targets.length; i++) {
+ targets[i].addEventListener(event_type, test_event, true);
+ targets[i].addEventListener(event_type, test_event, false);
+ }
+
+ var evt = document.createEvent("Event");
+ evt.initEvent(event_type, true, true);
+ target.dispatchEvent(evt);
+
+ assert_array_equals(actual_targets, expected_targets, "targets");
+ assert_array_equals(actual_phases, phases, "phases");
+}, "Event propagation path when an element in it is removed from the DOM");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-throwing.html b/testing/web-platform/tests/dom/events/Event-dispatch-throwing.html
new file mode 100644
index 0000000000..7d1c0d94a0
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-dispatch-throwing.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>Throwing in event listeners</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+setup({allow_uncaught_exception:true})
+
+test(function() {
+ var errorEvents = 0;
+ window.onerror = this.step_func(function(e) {
+ assert_equals(typeof e, 'string');
+ ++errorEvents;
+ });
+
+ var element = document.createElement('div');
+
+ element.addEventListener('click', function() {
+ throw new Error('Error from only listener');
+ });
+
+ element.dispatchEvent(new Event('click'));
+
+ assert_equals(errorEvents, 1);
+}, "Throwing in event listener with a single listeners");
+
+test(function() {
+ var errorEvents = 0;
+ window.onerror = this.step_func(function(e) {
+ assert_equals(typeof e, 'string');
+ ++errorEvents;
+ });
+
+ var element = document.createElement('div');
+
+ var secondCalled = false;
+
+ element.addEventListener('click', function() {
+ throw new Error('Error from first listener');
+ });
+ element.addEventListener('click', this.step_func(function() {
+ secondCalled = true;
+ }), false);
+
+ element.dispatchEvent(new Event('click'));
+
+ assert_equals(errorEvents, 1);
+ assert_true(secondCalled);
+}, "Throwing in event listener with multiple listeners");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-init-while-dispatching.html b/testing/web-platform/tests/dom/events/Event-init-while-dispatching.html
new file mode 100644
index 0000000000..2aa1f6701c
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-init-while-dispatching.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Re-initializing events while dispatching them</title>
+<link rel="author" title="Josh Matthews" href="mailto:josh@joshmatthews.net">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var events = {
+ 'KeyboardEvent': {
+ 'constructor': function() { return new KeyboardEvent("type", {key: "A"}); },
+ 'init': function(ev) { ev.initKeyboardEvent("type2", true, true, null, "a", 1, "", true, "") },
+ 'check': function(ev) {
+ assert_equals(ev.key, "A", "initKeyboardEvent key setter should short-circuit");
+ assert_false(ev.repeat, "initKeyboardEvent repeat setter should short-circuit");
+ assert_equals(ev.location, 0, "initKeyboardEvent location setter should short-circuit");
+ }
+ },
+ 'MouseEvent': {
+ 'constructor': function() { return new MouseEvent("type"); },
+ 'init': function(ev) { ev.initMouseEvent("type2", true, true, null, 0, 1, 1, 1, 1, true, true, true, true, 1, null) },
+ 'check': function(ev) {
+ assert_equals(ev.screenX, 0, "initMouseEvent screenX setter should short-circuit");
+ assert_equals(ev.screenY, 0, "initMouseEvent screenY setter should short-circuit");
+ assert_equals(ev.clientX, 0, "initMouseEvent clientX setter should short-circuit");
+ assert_equals(ev.clientY, 0, "initMouseEvent clientY setter should short-circuit");
+ assert_false(ev.ctrlKey, "initMouseEvent ctrlKey setter should short-circuit");
+ assert_false(ev.altKey, "initMouseEvent altKey setter should short-circuit");
+ assert_false(ev.shiftKey, "initMouseEvent shiftKey setter should short-circuit");
+ assert_false(ev.metaKey, "initMouseEvent metaKey setter should short-circuit");
+ assert_equals(ev.button, 0, "initMouseEvent button setter should short-circuit");
+ }
+ },
+ 'CustomEvent': {
+ 'constructor': function() { return new CustomEvent("type") },
+ 'init': function(ev) { ev.initCustomEvent("type2", true, true, 1) },
+ 'check': function(ev) {
+ assert_equals(ev.detail, null, "initCustomEvent detail setter should short-circuit");
+ }
+ },
+ 'UIEvent': {
+ 'constructor': function() { return new UIEvent("type") },
+ 'init': function(ev) { ev.initUIEvent("type2", true, true, window, 1) },
+ 'check': function(ev) {
+ assert_equals(ev.view, null, "initUIEvent view setter should short-circuit");
+ assert_equals(ev.detail, 0, "initUIEvent detail setter should short-circuit");
+ }
+ },
+ 'Event': {
+ 'constructor': function() { return new Event("type") },
+ 'init': function(ev) { ev.initEvent("type2", true, true) },
+ 'check': function(ev) {
+ assert_equals(ev.bubbles, false, "initEvent bubbles setter should short-circuit");
+ assert_equals(ev.cancelable, false, "initEvent cancelable setter should short-circuit");
+ assert_equals(ev.type, "type", "initEvent type setter should short-circuit");
+ }
+ }
+};
+
+var names = Object.keys(events);
+for (var i = 0; i < names.length; i++) {
+ var t = async_test("Calling init" + names[i] + " while dispatching.");
+ t.step(function() {
+ var e = events[names[i]].constructor();
+
+ var target = document.createElement("div")
+ target.addEventListener("type", t.step_func(function() {
+ events[names[i]].init(e);
+
+ var o = e;
+ while ((o = Object.getPrototypeOf(o))) {
+ if (!(o.constructor.name in events)) {
+ break;
+ }
+ events[o.constructor.name].check(e);
+ }
+ }), false);
+
+ assert_equals(target.dispatchEvent(e), true, "dispatchEvent must return true")
+ });
+ t.done();
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-initEvent.html b/testing/web-platform/tests/dom/events/Event-initEvent.html
new file mode 100644
index 0000000000..ad1018d4da
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-initEvent.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<title>Event.initEvent</title>
+<link rel="author" title="Ms2ger" href="mailto:Ms2ger@gmail.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var booleans = [true, false];
+booleans.forEach(function(bubbles) {
+ booleans.forEach(function(cancelable) {
+ test(function() {
+ var e = document.createEvent("Event")
+ e.initEvent("type", bubbles, cancelable)
+
+ // Step 2.
+ // Stop (immediate) propagation flag is tested later
+ assert_equals(e.defaultPrevented, false, "defaultPrevented")
+ assert_equals(e.returnValue, true, "returnValue")
+ // Step 3.
+ assert_equals(e.isTrusted, false, "isTrusted")
+ // Step 4.
+ assert_equals(e.target, null, "target")
+ assert_equals(e.srcElement, null, "srcElement")
+ // Step 5.
+ assert_equals(e.type, "type", "type")
+ // Step 6.
+ assert_equals(e.bubbles, bubbles, "bubbles")
+ // Step 7.
+ assert_equals(e.cancelable, cancelable, "cancelable")
+ }, "Properties of initEvent(type, " + bubbles + ", " + cancelable + ")")
+ })
+})
+
+test(function() {
+ var e = document.createEvent("Event")
+ e.initEvent("type 1", true, false)
+ assert_equals(e.type, "type 1", "type (first init)")
+ assert_equals(e.bubbles, true, "bubbles (first init)")
+ assert_equals(e.cancelable, false, "cancelable (first init)")
+
+ e.initEvent("type 2", false, true)
+ assert_equals(e.type, "type 2", "type (second init)")
+ assert_equals(e.bubbles, false, "bubbles (second init)")
+ assert_equals(e.cancelable, true, "cancelable (second init)")
+}, "Calling initEvent multiple times (getting type).")
+
+test(function() {
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=998809
+ var e = document.createEvent("Event")
+ e.initEvent("type 1", true, false)
+ assert_equals(e.bubbles, true, "bubbles (first init)")
+ assert_equals(e.cancelable, false, "cancelable (first init)")
+
+ e.initEvent("type 2", false, true)
+ assert_equals(e.type, "type 2", "type (second init)")
+ assert_equals(e.bubbles, false, "bubbles (second init)")
+ assert_equals(e.cancelable, true, "cancelable (second init)")
+}, "Calling initEvent multiple times (not getting type).")
+
+// Step 2.
+async_test(function() {
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=17715
+
+ var e = document.createEvent("Event")
+ e.initEvent("type", false, false)
+ assert_equals(e.type, "type", "type (first init)")
+ assert_equals(e.bubbles, false, "bubbles (first init)")
+ assert_equals(e.cancelable, false, "cancelable (first init)")
+
+ var target = document.createElement("div")
+ target.addEventListener("type", this.step_func(function() {
+ e.initEvent("fail", true, true)
+ assert_equals(e.type, "type", "type (second init)")
+ assert_equals(e.bubbles, false, "bubbles (second init)")
+ assert_equals(e.cancelable, false, "cancelable (second init)")
+ }), false)
+
+ assert_equals(target.dispatchEvent(e), true, "dispatchEvent must return true")
+
+ this.done()
+}, "Calling initEvent must not have an effect during dispatching.")
+
+test(function() {
+ var e = document.createEvent("Event")
+ e.stopPropagation()
+ e.initEvent("type", false, false)
+ var target = document.createElement("div")
+ var called = false
+ target.addEventListener("type", function() { called = true }, false)
+ assert_false(e.cancelBubble, "cancelBubble must be false")
+ assert_true(target.dispatchEvent(e), "dispatchEvent must return true")
+ assert_true(called, "Listener must be called")
+}, "Calling initEvent must unset the stop propagation flag.")
+
+test(function() {
+ var e = document.createEvent("Event")
+ e.stopImmediatePropagation()
+ e.initEvent("type", false, false)
+ var target = document.createElement("div")
+ var called = false
+ target.addEventListener("type", function() { called = true }, false)
+ assert_true(target.dispatchEvent(e), "dispatchEvent must return true")
+ assert_true(called, "Listener must be called")
+}, "Calling initEvent must unset the stop immediate propagation flag.")
+
+async_test(function() {
+ var e = document.createEvent("Event")
+ e.initEvent("type", false, false)
+
+ var target = document.createElement("div")
+ target.addEventListener("type", this.step_func(function() {
+ e.initEvent("type2", true, true);
+ assert_equals(e.type, "type", "initEvent type setter should short-circuit");
+ assert_false(e.bubbles, "initEvent bubbles setter should short-circuit");
+ assert_false(e.cancelable, "initEvent cancelable setter should short-circuit");
+ }), false)
+ assert_equals(target.dispatchEvent(e), true, "dispatchEvent must return true")
+
+ this.done()
+}, "Calling initEvent during propagation.")
+
+test(function() {
+ var e = document.createEvent("Event")
+ assert_throws_js(TypeError, function() {
+ e.initEvent()
+ })
+}, "First parameter to initEvent should be mandatory.")
+
+test(function() {
+ var e = document.createEvent("Event")
+ e.initEvent("type")
+ assert_equals(e.type, "type", "type")
+ assert_false(e.bubbles, "bubbles")
+ assert_false(e.cancelable, "cancelable")
+}, "Tests initEvent's default parameter values.")
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-isTrusted.any.js b/testing/web-platform/tests/dom/events/Event-isTrusted.any.js
new file mode 100644
index 0000000000..00bcecd0ed
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-isTrusted.any.js
@@ -0,0 +1,11 @@
+test(function() {
+ var desc1 = Object.getOwnPropertyDescriptor(new Event("x"), "isTrusted");
+ assert_not_equals(desc1, undefined);
+ assert_equals(typeof desc1.get, "function");
+
+ var desc2 = Object.getOwnPropertyDescriptor(new Event("x"), "isTrusted");
+ assert_not_equals(desc2, undefined);
+ assert_equals(typeof desc2.get, "function");
+
+ assert_equals(desc1.get, desc2.get);
+});
diff --git a/testing/web-platform/tests/dom/events/Event-propagation.html b/testing/web-platform/tests/dom/events/Event-propagation.html
new file mode 100644
index 0000000000..33989eb4bf
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-propagation.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<title>Event propagation tests</title>
+<link rel=author title="Aryeh Gregor" href=ayg@aryeh.name>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+"use strict";
+
+function testPropagationFlag(ev, expected, desc) {
+ test(function() {
+ var called = false;
+ var callback = function() { called = true };
+ this.add_cleanup(function() {
+ document.head.removeEventListener("foo", callback)
+ });
+ document.head.addEventListener("foo", callback);
+ document.head.dispatchEvent(ev);
+ assert_equals(called, expected, "Propagation flag");
+ // dispatchEvent resets the propagation flags so it will happily dispatch
+ // the event the second time around.
+ document.head.dispatchEvent(ev);
+ assert_equals(called, true, "Propagation flag after first dispatch");
+ }, desc);
+}
+
+var ev = document.createEvent("Event");
+ev.initEvent("foo", true, false);
+testPropagationFlag(ev, true, "Newly-created Event");
+ev.stopPropagation();
+testPropagationFlag(ev, false, "After stopPropagation()");
+ev.initEvent("foo", true, false);
+testPropagationFlag(ev, true, "Reinitialized after stopPropagation()");
+
+var ev = document.createEvent("Event");
+ev.initEvent("foo", true, false);
+ev.stopImmediatePropagation();
+testPropagationFlag(ev, false, "After stopImmediatePropagation()");
+ev.initEvent("foo", true, false);
+testPropagationFlag(ev, true, "Reinitialized after stopImmediatePropagation()");
+
+var ev = document.createEvent("Event");
+ev.initEvent("foo", true, false);
+ev.cancelBubble = true;
+testPropagationFlag(ev, false, "After cancelBubble=true");
+ev.initEvent("foo", true, false);
+testPropagationFlag(ev, true, "Reinitialized after cancelBubble=true");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-returnValue.html b/testing/web-platform/tests/dom/events/Event-returnValue.html
new file mode 100644
index 0000000000..08df2d4141
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-returnValue.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Event.returnValue</title>
+ <link rel="author" title="Chris Rebert" href="http://chrisrebert.com">
+ <link rel="help" href="https://dom.spec.whatwg.org/#dom-event-returnvalue">
+ <meta name="flags" content="dom">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+ <div id="log"></div>
+ <script>
+test(function() {
+ var ev = new Event("foo");
+ assert_true(ev.returnValue, "returnValue");
+}, "When an event is created, returnValue should be initialized to true.");
+test(function() {
+ var ev = new Event("foo", {"cancelable": false});
+ assert_false(ev.cancelable, "cancelable (before)");
+ ev.preventDefault();
+ assert_false(ev.cancelable, "cancelable (after)");
+ assert_true(ev.returnValue, "returnValue");
+}, "preventDefault() should not change returnValue if cancelable is false.");
+test(function() {
+ var ev = new Event("foo", {"cancelable": false});
+ assert_false(ev.cancelable, "cancelable (before)");
+ ev.returnValue = false;
+ assert_false(ev.cancelable, "cancelable (after)");
+ assert_true(ev.returnValue, "returnValue");
+}, "returnValue=false should have no effect if cancelable is false.");
+test(function() {
+ var ev = new Event("foo", {"cancelable": true});
+ assert_true(ev.cancelable, "cancelable (before)");
+ ev.preventDefault();
+ assert_true(ev.cancelable, "cancelable (after)");
+ assert_false(ev.returnValue, "returnValue");
+}, "preventDefault() should change returnValue if cancelable is true.");
+test(function() {
+ var ev = new Event("foo", {"cancelable": true});
+ assert_true(ev.cancelable, "cancelable (before)");
+ ev.returnValue = false;
+ assert_true(ev.cancelable, "cancelable (after)");
+ assert_false(ev.returnValue, "returnValue");
+}, "returnValue should change returnValue if cancelable is true.");
+test(function() {
+ var ev = document.createEvent("Event");
+ ev.returnValue = false;
+ ev.initEvent("foo", true, true);
+ assert_true(ev.bubbles, "bubbles");
+ assert_true(ev.cancelable, "cancelable");
+ assert_true(ev.returnValue, "returnValue");
+}, "initEvent should unset returnValue.");
+test(function() {
+ var ev = new Event("foo", {"cancelable": true});
+ ev.preventDefault();
+ ev.returnValue = true;// no-op
+ assert_true(ev.defaultPrevented);
+ assert_false(ev.returnValue);
+}, "returnValue=true should have no effect once the canceled flag was set.");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/dom/events/Event-stopImmediatePropagation.html b/testing/web-platform/tests/dom/events/Event-stopImmediatePropagation.html
new file mode 100644
index 0000000000..b75732257a
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-stopImmediatePropagation.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Event's stopImmediatePropagation</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-event-stopimmediatepropagation">
+<link rel="author" href="mailto:d@domenic.me" title="Domenic Denicola">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="target"></div>
+
+<script>
+"use strict";
+
+setup({ single_test: true });
+
+const target = document.querySelector("#target");
+
+let timesCalled = 0;
+target.addEventListener("test", e => {
+ ++timesCalled;
+ e.stopImmediatePropagation();
+ assert_equals(e.cancelBubble, true, "The stop propagation flag must have been set");
+});
+target.addEventListener("test", () => {
+ ++timesCalled;
+});
+
+const e = new Event("test");
+target.dispatchEvent(e);
+assert_equals(timesCalled, 1, "The second listener must not have been called");
+
+done();
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-stopPropagation-cancel-bubbling.html b/testing/web-platform/tests/dom/events/Event-stopPropagation-cancel-bubbling.html
new file mode 100644
index 0000000000..5c2c49f338
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-stopPropagation-cancel-bubbling.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Joey Arhar" href="mailto:jarhar@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+test(t => {
+ const element = document.createElement('div');
+
+ element.addEventListener('click', () => {
+ event.stopPropagation();
+ }, { capture: true });
+
+ element.addEventListener('click',
+ t.unreached_func('stopPropagation in the capture handler should have canceled this bubble handler.'));
+
+ element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
+});
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-subclasses-constructors.html b/testing/web-platform/tests/dom/events/Event-subclasses-constructors.html
new file mode 100644
index 0000000000..08a5ded011
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-subclasses-constructors.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Event constructors</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+function assert_props(iface, event, defaults) {
+ assert_true(event instanceof self[iface]);
+ expected[iface].properties.forEach(function(p) {
+ var property = p[0], value = p[defaults ? 1 : 2];
+ assert_true(property in event,
+ "Event " + format_value(event) + " should have a " +
+ property + " property");
+ assert_equals(event[property], value,
+ "The value of the " + property + " property should be " +
+ format_value(value));
+ });
+ if ("parent" in expected[iface]) {
+ assert_props(expected[iface].parent, event, defaults);
+ }
+}
+
+// Class declarations don't go on the global by default, so put it there ourselves:
+
+self.SubclassedEvent = class SubclassedEvent extends Event {
+ constructor(name, props) {
+ super(name, props);
+ if (props && typeof(props) == "object" && "customProp" in props) {
+ this.customProp = props.customProp;
+ } else {
+ this.customProp = 5;
+ }
+ }
+
+ get fixedProp() {
+ return 17;
+ }
+}
+
+var EventModifierInit = [
+ ["ctrlKey", false, true],
+ ["shiftKey", false, true],
+ ["altKey", false, true],
+ ["metaKey", false, true],
+];
+var expected = {
+ "Event": {
+ "properties": [
+ ["bubbles", false, true],
+ ["cancelable", false, true],
+ ["isTrusted", false, false],
+ ],
+ },
+
+ "UIEvent": {
+ "parent": "Event",
+ "properties": [
+ ["view", null, window],
+ ["detail", 0, 7],
+ ],
+ },
+
+ "FocusEvent": {
+ "parent": "UIEvent",
+ "properties": [
+ ["relatedTarget", null, document],
+ ],
+ },
+
+ "MouseEvent": {
+ "parent": "UIEvent",
+ "properties": EventModifierInit.concat([
+ ["screenX", 0, 40],
+ ["screenY", 0, 40],
+ ["clientX", 0, 40],
+ ["clientY", 0, 40],
+ ["button", 0, 40],
+ ["buttons", 0, 40],
+ ["relatedTarget", null, document],
+ ]),
+ },
+
+ "WheelEvent": {
+ "parent": "MouseEvent",
+ "properties": [
+ ["deltaX", 0.0, 3.1],
+ ["deltaY", 0.0, 3.1],
+ ["deltaZ", 0.0, 3.1],
+ ["deltaMode", 0, 40],
+ ],
+ },
+
+ "KeyboardEvent": {
+ "parent": "UIEvent",
+ "properties": EventModifierInit.concat([
+ ["key", "", "string"],
+ ["code", "", "string"],
+ ["location", 0, 7],
+ ["repeat", false, true],
+ ["isComposing", false, true],
+ ["charCode", 0, 7],
+ ["keyCode", 0, 7],
+ ["which", 0, 7],
+ ]),
+ },
+
+ "CompositionEvent": {
+ "parent": "UIEvent",
+ "properties": [
+ ["data", "", "string"],
+ ],
+ },
+
+ "SubclassedEvent": {
+ "parent": "Event",
+ "properties": [
+ ["customProp", 5, 8],
+ ["fixedProp", 17, 17],
+ ],
+ },
+};
+
+Object.keys(expected).forEach(function(iface) {
+ test(function() {
+ var event = new self[iface]("type");
+ assert_props(iface, event, true);
+ }, iface + " constructor (no argument)");
+
+ test(function() {
+ var event = new self[iface]("type", undefined);
+ assert_props(iface, event, true);
+ }, iface + " constructor (undefined argument)");
+
+ test(function() {
+ var event = new self[iface]("type", null);
+ assert_props(iface, event, true);
+ }, iface + " constructor (null argument)");
+
+ test(function() {
+ var event = new self[iface]("type", {});
+ assert_props(iface, event, true);
+ }, iface + " constructor (empty argument)");
+
+ test(function() {
+ var dictionary = {};
+ expected[iface].properties.forEach(function(p) {
+ var property = p[0], value = p[1];
+ dictionary[property] = value;
+ });
+ var event = new self[iface]("type", dictionary);
+ assert_props(iface, event, true);
+ }, iface + " constructor (argument with default values)");
+
+ test(function() {
+ function fill_in(iface, dictionary) {
+ if ("parent" in expected[iface]) {
+ fill_in(expected[iface].parent, dictionary)
+ }
+ expected[iface].properties.forEach(function(p) {
+ var property = p[0], value = p[2];
+ dictionary[property] = value;
+ });
+ }
+
+ var dictionary = {};
+ fill_in(iface, dictionary);
+
+ var event = new self[iface]("type", dictionary);
+ assert_props(iface, event, false);
+ }, iface + " constructor (argument with non-default values)");
+});
+
+test(function () {
+ assert_throws_js(TypeError, function() {
+ new UIEvent("x", { view: 7 })
+ });
+}, "UIEvent constructor (view argument with wrong type)")
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-timestamp-cross-realm-getter.html b/testing/web-platform/tests/dom/events/Event-timestamp-cross-realm-getter.html
new file mode 100644
index 0000000000..45823de26b
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-timestamp-cross-realm-getter.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>event.timeStamp is initialized using event's relevant global object</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#ref-for-dom-event-timestamp%E2%91%A1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+const t = async_test();
+t.step_timeout(() => {
+ const iframeDelayed = document.createElement("iframe");
+ iframeDelayed.onload = t.step_func_done(() => {
+ // Use eval() to eliminate false-positive test result for WebKit builds before r280256,
+ // which invoked WebIDL accessors in context of lexical (caller) global object.
+ const timeStampExpected = iframeDelayed.contentWindow.eval(`new Event("foo").timeStamp`);
+ const eventDelayed = new iframeDelayed.contentWindow.Event("foo");
+
+ const {get} = Object.getOwnPropertyDescriptor(Event.prototype, "timeStamp");
+ assert_approx_equals(get.call(eventDelayed), timeStampExpected, 5, "via Object.getOwnPropertyDescriptor");
+
+ Object.setPrototypeOf(eventDelayed, Event.prototype);
+ assert_approx_equals(eventDelayed.timeStamp, timeStampExpected, 5, "via Object.setPrototypeOf");
+ });
+ document.body.append(iframeDelayed);
+}, 1000);
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-timestamp-high-resolution.html b/testing/web-platform/tests/dom/events/Event-timestamp-high-resolution.html
new file mode 100644
index 0000000000..a049fef64b
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-timestamp-high-resolution.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script type="text/javascript">
+'use strict';
+for (let eventType of ["MouseEvent", "KeyboardEvent", "WheelEvent", "FocusEvent"]) {
+ test(function() {
+ let before = performance.now();
+ let e = new window[eventType]('test');
+ let after = performance.now();
+ assert_greater_than_equal(e.timeStamp, before, "Event timestamp should be greater than performance.now() timestamp taken before its creation");
+ assert_less_than_equal(e.timeStamp, after, "Event timestamp should be less than performance.now() timestamp taken after its creation");
+ }, `Constructed ${eventType} timestamp should be high resolution and have the same time origin as performance.now()`);
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-timestamp-high-resolution.https.html b/testing/web-platform/tests/dom/events/Event-timestamp-high-resolution.https.html
new file mode 100644
index 0000000000..70f9742947
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-timestamp-high-resolution.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script type="text/javascript">
+'use strict';
+for (let eventType of ["GamepadEvent"]) {
+ test(function() {
+ let before = performance.now();
+ let e = new window[eventType]('test');
+ let after = performance.now();
+ assert_greater_than_equal(e.timeStamp, before, "Event timestamp should be greater than performance.now() timestamp taken before its creation");
+ assert_less_than_equal(e.timeStamp, after, "Event timestamp should be less than performance.now() timestamp taken after its creation");
+ }, `Constructed ${eventType} timestamp should be high resolution and have the same time origin as performance.now()`);
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-timestamp-safe-resolution.html b/testing/web-platform/tests/dom/events/Event-timestamp-safe-resolution.html
new file mode 100644
index 0000000000..24f2dec93c
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-timestamp-safe-resolution.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script type="text/javascript">
+'use strict';
+
+// Computes greatest common divisor of a and b using Euclid's algorithm
+function computeGCD(a, b) {
+ if (!Number.isInteger(a) || !Number.isInteger(b)) {
+ throw new Error('Parameters must be integer numbers');
+ }
+
+ var r;
+ while (b != 0) {
+ r = a % b;
+ a = b;
+ b = r;
+ }
+ return (a < 0) ? -a : a;
+}
+
+// Finds minimum resolution Δ given a set of samples which are known to be in the form of N*Δ.
+// We use GCD of all samples as a simple estimator.
+function estimateMinimumResolution(samples) {
+ var gcd;
+ for (const sample of samples) {
+ gcd = gcd ? computeGCD(gcd, sample) : sample;
+ }
+
+ return gcd;
+}
+
+test(function() {
+ const samples = [];
+ for (var i = 0; i < 1e3; i++) {
+ var deltaInMicroSeconds = 0;
+ const e1 = new MouseEvent('test1');
+ do {
+ const e2 = new MouseEvent('test2');
+ deltaInMicroSeconds = Math.round((e2.timeStamp - e1.timeStamp) * 1000);
+ } while (deltaInMicroSeconds == 0) // only collect non-zero samples
+
+ samples.push(deltaInMicroSeconds);
+ }
+
+ const minResolution = estimateMinimumResolution(samples);
+ assert_greater_than_equal(minResolution, 5);
+}, 'Event timestamp should not have a resolution better than 5 microseconds');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/dom/events/Event-type-empty.html b/testing/web-platform/tests/dom/events/Event-type-empty.html
new file mode 100644
index 0000000000..225b85a613
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-type-empty.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>Event.type set to the empty string</title>
+<link rel="author" title="Ms2ger" href="mailto:Ms2ger@gmail.com">
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-event-type">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+function do_test(t, e) {
+ assert_equals(e.type, "", "type");
+ assert_equals(e.bubbles, false, "bubbles");
+ assert_equals(e.cancelable, false, "cancelable");
+
+ var target = document.createElement("div");
+ var handled = false;
+ target.addEventListener("", t.step_func(function(e) {
+ handled = true;
+ }));
+ assert_true(target.dispatchEvent(e));
+ assert_true(handled);
+}
+
+async_test(function() {
+ var e = document.createEvent("Event");
+ e.initEvent("", false, false);
+ do_test(this, e);
+ this.done();
+}, "initEvent");
+
+async_test(function() {
+ var e = new Event("");
+ do_test(this, e);
+ this.done();
+}, "Constructor");
+</script>
diff --git a/testing/web-platform/tests/dom/events/Event-type.html b/testing/web-platform/tests/dom/events/Event-type.html
new file mode 100644
index 0000000000..22792f5c6c
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/Event-type.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<title>Event.type</title>
+<link rel="author" title="Ms2ger" href="mailto:Ms2ger@gmail.com">
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-event-type">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ var e = document.createEvent("Event")
+ assert_equals(e.type, "");
+}, "Event.type should initially be the empty string");
+test(function() {
+ var e = document.createEvent("Event")
+ e.initEvent("foo", false, false)
+ assert_equals(e.type, "foo")
+}, "Event.type should be initialized by initEvent");
+test(function() {
+ var e = new Event("bar")
+ assert_equals(e.type, "bar")
+}, "Event.type should be initialized by the constructor");
+</script>
diff --git a/testing/web-platform/tests/dom/events/EventListener-addEventListener.sub.window.js b/testing/web-platform/tests/dom/events/EventListener-addEventListener.sub.window.js
new file mode 100644
index 0000000000..b44bc33285
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventListener-addEventListener.sub.window.js
@@ -0,0 +1,9 @@
+async_test(function(t) {
+ let crossOriginFrame = document.createElement('iframe');
+ crossOriginFrame.src = 'https://{{hosts[alt][]}}:{{ports[https][0]}}/common/blank.html';
+ document.body.appendChild(crossOriginFrame);
+ crossOriginFrame.addEventListener('load', t.step_func_done(function() {
+ let crossOriginWindow = crossOriginFrame.contentWindow;
+ window.addEventListener('click', crossOriginWindow);
+ }));
+}, "EventListener.addEventListener doesn't throw when a cross origin object is passed in.");
diff --git a/testing/web-platform/tests/dom/events/EventListener-handleEvent-cross-realm.html b/testing/web-platform/tests/dom/events/EventListener-handleEvent-cross-realm.html
new file mode 100644
index 0000000000..663d04213f
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventListener-handleEvent-cross-realm.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-realm EventListener throws TypeError of its associated Realm</title>
+<link rel="help" href="https://webidl.spec.whatwg.org/#ref-for-prepare-to-run-script">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe name="eventListenerGlobalObject" src="resources/empty-document.html"></iframe>
+
+<script>
+setup({ allow_uncaught_exception: true });
+
+test_onload(() => {
+ const eventTarget = new EventTarget;
+ const eventListener = new eventListenerGlobalObject.Object;
+
+ eventTarget.addEventListener("foo", eventListener);
+ assert_reports_exception(eventListenerGlobalObject.TypeError, () => { eventTarget.dispatchEvent(new Event("foo")); });
+}, "EventListener is cross-realm plain object without 'handleEvent' property");
+
+test_onload(() => {
+ const eventTarget = new EventTarget;
+ const eventListener = new eventListenerGlobalObject.Object;
+ eventListener.handleEvent = {};
+
+ eventTarget.addEventListener("foo", eventListener);
+ assert_reports_exception(eventListenerGlobalObject.TypeError, () => { eventTarget.dispatchEvent(new Event("foo")); });
+}, "EventListener is cross-realm plain object with non-callable 'handleEvent' property");
+
+test_onload(() => {
+ const eventTarget = new EventTarget;
+ const { proxy, revoke } = Proxy.revocable(() => {}, {});
+ revoke();
+
+ const eventListener = new eventListenerGlobalObject.Object;
+ eventListener.handleEvent = proxy;
+
+ eventTarget.addEventListener("foo", eventListener);
+ assert_reports_exception(eventListenerGlobalObject.TypeError, () => { eventTarget.dispatchEvent(new Event("foo")); });
+}, "EventListener is cross-realm plain object with revoked Proxy as 'handleEvent' property");
+
+test_onload(() => {
+ const eventTarget = new EventTarget;
+ const { proxy, revoke } = eventListenerGlobalObject.Proxy.revocable({}, {});
+ revoke();
+
+ eventTarget.addEventListener("foo", proxy);
+ assert_reports_exception(eventListenerGlobalObject.TypeError, () => { eventTarget.dispatchEvent(new Event("foo")); });
+}, "EventListener is cross-realm non-callable revoked Proxy");
+
+test_onload(() => {
+ const eventTarget = new EventTarget;
+ const { proxy, revoke } = eventListenerGlobalObject.Proxy.revocable(() => {}, {});
+ revoke();
+
+ eventTarget.addEventListener("foo", proxy);
+ assert_reports_exception(eventListenerGlobalObject.TypeError, () => { eventTarget.dispatchEvent(new Event("foo")); });
+}, "EventListener is cross-realm callable revoked Proxy");
+
+function test_onload(fn, desc) {
+ async_test(t => { window.addEventListener("load", t.step_func_done(fn)); }, desc);
+}
+
+function assert_reports_exception(expectedConstructor, fn) {
+ let error;
+ const onErrorHandler = event => { error = event.error; };
+
+ eventListenerGlobalObject.addEventListener("error", onErrorHandler);
+ fn();
+ eventListenerGlobalObject.removeEventListener("error", onErrorHandler);
+
+ assert_equals(typeof error, "object");
+ assert_equals(error.constructor, expectedConstructor);
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/EventListener-handleEvent.html b/testing/web-platform/tests/dom/events/EventListener-handleEvent.html
new file mode 100644
index 0000000000..06bc1f6e2a
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventListener-handleEvent.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>EventListener::handleEvent()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://dom.spec.whatwg.org/#callbackdef-eventlistener">
+<div id=log></div>
+<script>
+setup({ allow_uncaught_exception: true });
+
+test(function(t) {
+ var type = "foo";
+ var target = document.createElement("div");
+ var eventListener = {
+ handleEvent: function(evt) {
+ var that = this;
+ t.step(function() {
+ assert_equals(evt.type, type);
+ assert_equals(evt.target, target);
+ assert_equals(evt.srcElement, target);
+ assert_equals(that, eventListener);
+ });
+ },
+ };
+
+ target.addEventListener(type, eventListener);
+ target.dispatchEvent(new Event(type));
+}, "calls `handleEvent` method of `EventListener`");
+
+test(function(t) {
+ var type = "foo";
+ var target = document.createElement("div");
+ var calls = 0;
+
+ target.addEventListener(type, {
+ get handleEvent() {
+ calls++;
+ return function() {};
+ },
+ });
+
+ assert_equals(calls, 0);
+ target.dispatchEvent(new Event(type));
+ target.dispatchEvent(new Event(type));
+ assert_equals(calls, 2);
+}, "performs `Get` every time event is dispatched");
+
+test(function(t) {
+ var type = "foo";
+ var target = document.createElement("div");
+ var calls = 0;
+ var eventListener = function() { calls++; };
+ eventListener.handleEvent = t.unreached_func("`handleEvent` method should not be called on functions");
+
+ target.addEventListener(type, eventListener);
+ target.dispatchEvent(new Event(type));
+ assert_equals(calls, 1);
+}, "doesn't call `handleEvent` method on callable `EventListener`");
+
+const uncaught_error_test = async (t, getHandleEvent) => {
+ const type = "foo";
+ const target = document.createElement("div");
+
+ let calls = 0;
+ target.addEventListener(type, {
+ get handleEvent() {
+ calls++;
+ return getHandleEvent();
+ },
+ });
+
+ const timeout = () => {
+ return new Promise(resolve => {
+ t.step_timeout(resolve, 0);
+ });
+ };
+
+ const eventWatcher = new EventWatcher(t, window, "error", timeout);
+ const errorPromise = eventWatcher.wait_for("error");
+
+ target.dispatchEvent(new Event(type));
+
+ const event = await errorPromise;
+ assert_equals(calls, 1, "handleEvent property was not looked up");
+ throw event.error;
+};
+
+promise_test(t => {
+ const error = { name: "test" };
+
+ return promise_rejects_exactly(t, error,
+ uncaught_error_test(t, () => { throw error; }));
+}, "rethrows errors when getting `handleEvent`");
+
+promise_test(t => {
+ return promise_rejects_js(t, TypeError, uncaught_error_test(t, () => null));
+}, "throws if `handleEvent` is falsy and not callable");
+
+promise_test(t => {
+ return promise_rejects_js(t, TypeError, uncaught_error_test(t, () => 42));
+}, "throws if `handleEvent` is thruthy and not callable");
+</script>
diff --git a/testing/web-platform/tests/dom/events/EventListener-incumbent-global-1.sub.html b/testing/web-platform/tests/dom/events/EventListener-incumbent-global-1.sub.html
new file mode 100644
index 0000000000..9d941385cb
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventListener-incumbent-global-1.sub.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<iframe src="{{location[scheme]}}://{{domains[www1]}}:{{ports[http][0]}}{{location[path]}}/../EventListener-incumbent-global-subframe-1.sub.html"></iframe>
+<script>
+
+var t = async_test("Check the incumbent global EventListeners are called with");
+
+onload = t.step_func(function() {
+ onmessage = t.step_func_done(function(e) {
+ var d = e.data;
+ assert_equals(d.actual, d.expected, d.reason);
+ });
+
+ frames[0].postMessage("start", "*");
+});
+
+</script>
diff --git a/testing/web-platform/tests/dom/events/EventListener-incumbent-global-2.sub.html b/testing/web-platform/tests/dom/events/EventListener-incumbent-global-2.sub.html
new file mode 100644
index 0000000000..4433c098d7
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventListener-incumbent-global-2.sub.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<iframe src="{{location[scheme]}}://{{domains[www1]}}:{{ports[http][0]}}{{location[path]}}/../EventListener-incumbent-global-subframe-2.sub.html"></iframe>
+<script>
+
+var t = async_test("Check the incumbent global EventListeners are called with");
+
+onload = t.step_func(function() {
+ onmessage = t.step_func_done(function(e) {
+ var d = e.data;
+ assert_equals(d.actual, d.expected, d.reason);
+ });
+
+ frames[0].postMessage("start", "*");
+});
+
+</script>
diff --git a/testing/web-platform/tests/dom/events/EventListener-incumbent-global-subframe-1.sub.html b/testing/web-platform/tests/dom/events/EventListener-incumbent-global-subframe-1.sub.html
new file mode 100644
index 0000000000..25487cc5e0
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventListener-incumbent-global-subframe-1.sub.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<iframe src="{{location[scheme]}}://{{domains[www2]}}:{{ports[http][0]}}{{location[path]}}/../EventListener-incumbent-global-subsubframe.sub.html"></iframe>
+<script>
+ document.domain = "{{host}}";
+ onmessage = function(e) {
+ if (e.data == "start") {
+ frames[0].document.body.addEventListener("click", frames[0].postMessage.bind(frames[0], "respond", "*", undefined));
+ frames[0].postMessage("sendclick", "*");
+ } else {
+ parent.postMessage(e.data, "*");
+ }
+ }
+</script>
diff --git a/testing/web-platform/tests/dom/events/EventListener-incumbent-global-subframe-2.sub.html b/testing/web-platform/tests/dom/events/EventListener-incumbent-global-subframe-2.sub.html
new file mode 100644
index 0000000000..9c7235e2ad
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventListener-incumbent-global-subframe-2.sub.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<iframe src="{{location[scheme]}}://{{domains[www2]}}:{{ports[http][0]}}{{location[path]}}/../EventListener-incumbent-global-subsubframe.sub.html"></iframe>
+<script>
+ document.domain = "{{host}}";
+ onmessage = function(e) {
+ if (e.data == "start") {
+ frames[0].document.body.addEventListener("click", frames[0].getTheListener());
+ frames[0].postMessage("sendclick", "*");
+ } else {
+ parent.postMessage(e.data, "*");
+ }
+ }
+</script>
diff --git a/testing/web-platform/tests/dom/events/EventListener-incumbent-global-subsubframe.sub.html b/testing/web-platform/tests/dom/events/EventListener-incumbent-global-subsubframe.sub.html
new file mode 100644
index 0000000000..dd683f6f65
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventListener-incumbent-global-subsubframe.sub.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<script>
+ function getTheListener() {
+ return postMessage.bind(this, "respond", "*", undefined)
+ }
+ document.domain = "{{host}}";
+ onmessage = function (e) {
+ if (e.data == "sendclick") {
+ document.body.click();
+ } else {
+ parent.postMessage(
+ {
+ actual: e.origin,
+ expected: parent.location.origin,
+ reason: "Incumbent should have been the caller of addEventListener()"
+ },
+ "*")
+ };
+ }
+</script>
diff --git a/testing/web-platform/tests/dom/events/EventListener-invoke-legacy.html b/testing/web-platform/tests/dom/events/EventListener-invoke-legacy.html
new file mode 100644
index 0000000000..a01afcd8d1
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventListener-invoke-legacy.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Invoke legacy event listener</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @keyframes test {
+ 0% { color: red; }
+ 100% { color: green; }
+ }
+</style>
+<div id="log"></div>
+<script>
+function runLegacyEventTest(type, legacyType, ctor, setup) {
+ function createTestElem(t) {
+ var elem = document.createElement('div');
+ document.body.appendChild(elem);
+ t.add_cleanup(function() {
+ document.body.removeChild(elem);
+ });
+ return elem;
+ }
+
+ async_test(function(t) {
+ var elem = createTestElem(t);
+ var gotEvent = false;
+ elem.addEventListener(legacyType,
+ t.unreached_func("listener of " + legacyType + " should not be invoked"));
+ elem.addEventListener(type, t.step_func(function() {
+ assert_false(gotEvent, "unexpected " + type + " event");
+ gotEvent = true;
+ t.step_timeout(function() { t.done(); }, 100);
+ }));
+ setup(elem);
+ }, "Listener of " + type);
+
+ async_test(function(t) {
+ var elem = createTestElem(t);
+ var count = 0;
+ elem.addEventListener(legacyType, t.step_func(function() {
+ ++count;
+ if (count > 1) {
+ assert_unreached("listener of " + legacyType + " should not be invoked again");
+ return;
+ }
+ elem.dispatchEvent(new window[ctor](type));
+ t.done();
+ }));
+ setup(elem);
+ }, "Legacy listener of " + type);
+}
+
+function setupTransition(elem) {
+ getComputedStyle(elem).color;
+ elem.style.color = 'green';
+ elem.style.transition = 'color 30ms';
+}
+
+function setupAnimation(elem) {
+ elem.style.animation = 'test 30ms';
+}
+
+runLegacyEventTest('transitionend', 'webkitTransitionEnd', "TransitionEvent", setupTransition);
+runLegacyEventTest('animationend', 'webkitAnimationEnd', "AnimationEvent", setupAnimation);
+runLegacyEventTest('animationstart', 'webkitAnimationStart', "AnimationEvent", setupAnimation);
+</script>
diff --git a/testing/web-platform/tests/dom/events/EventListenerOptions-capture.html b/testing/web-platform/tests/dom/events/EventListenerOptions-capture.html
new file mode 100644
index 0000000000..f72cf3ca54
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventListenerOptions-capture.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<title>EventListenerOptions.capture</title>
+<link rel="author" title="Rick Byers" href="mailto:rbyers@chromium.org">
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-eventlisteneroptions-capture">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+
+<script>
+
+function testCaptureValue(captureValue, expectedValue) {
+ var handlerPhase = undefined;
+ var handler = function handler(e) {
+ assert_equals(handlerPhase, undefined, "Handler invoked after remove");
+ handlerPhase = e.eventPhase;
+ }
+ document.addEventListener('test', handler, captureValue);
+ document.body.dispatchEvent(new Event('test', {bubbles: true}));
+ document.removeEventListener('test', handler, captureValue);
+ document.body.dispatchEvent(new Event('test', {bubbles: true}));
+ assert_equals(handlerPhase, expectedValue, "Incorrect event phase for value: " + JSON.stringify(captureValue));
+}
+
+test(function() {
+ testCaptureValue(true, Event.CAPTURING_PHASE);
+ testCaptureValue(false, Event.BUBBLING_PHASE);
+ testCaptureValue(null, Event.BUBBLING_PHASE);
+ testCaptureValue(undefined, Event.BUBBLING_PHASE);
+ testCaptureValue(2.3, Event.CAPTURING_PHASE);
+ testCaptureValue(-1000.3, Event.CAPTURING_PHASE);
+ testCaptureValue(NaN, Event.BUBBLING_PHASE);
+ testCaptureValue(+0.0, Event.BUBBLING_PHASE);
+ testCaptureValue(-0.0, Event.BUBBLING_PHASE);
+ testCaptureValue("", Event.BUBBLING_PHASE);
+ testCaptureValue("AAAA", Event.CAPTURING_PHASE);
+}, "Capture boolean should be honored correctly");
+
+test(function() {
+ testCaptureValue({}, Event.BUBBLING_PHASE);
+ testCaptureValue({capture:true}, Event.CAPTURING_PHASE);
+ testCaptureValue({capture:false}, Event.BUBBLING_PHASE);
+ testCaptureValue({capture:2}, Event.CAPTURING_PHASE);
+ testCaptureValue({capture:0}, Event.BUBBLING_PHASE);
+}, "Capture option should be honored correctly");
+
+test(function() {
+ var supportsCapture = false;
+ var query_options = {
+ get capture() {
+ supportsCapture = true;
+ return false;
+ },
+ get dummy() {
+ assert_unreached("dummy value getter invoked");
+ return false;
+ }
+ };
+
+ document.addEventListener('test_event', null, query_options);
+ assert_true(supportsCapture, "addEventListener doesn't support the capture option");
+ supportsCapture = false;
+ document.removeEventListener('test_event', null, query_options);
+ assert_true(supportsCapture, "removeEventListener doesn't support the capture option");
+}, "Supports capture option");
+
+function testOptionEquality(addOptionValue, removeOptionValue, expectedEquality) {
+ var handlerInvoked = false;
+ var handler = function handler(e) {
+ assert_equals(handlerInvoked, false, "Handler invoked multiple times");
+ handlerInvoked = true;
+ }
+ document.addEventListener('test', handler, addOptionValue);
+ document.removeEventListener('test', handler, removeOptionValue);
+ document.body.dispatchEvent(new Event('test', {bubbles: true}));
+ assert_equals(!handlerInvoked, expectedEquality, "equivalence of options " +
+ JSON.stringify(addOptionValue) + " and " + JSON.stringify(removeOptionValue));
+ if (handlerInvoked)
+ document.removeEventListener('test', handler, addOptionValue);
+}
+
+test(function() {
+ // Option values that should be treated as equivalent
+ testOptionEquality({}, false, true);
+ testOptionEquality({capture: false}, false, true);
+ testOptionEquality(true, {capture: true}, true);
+ testOptionEquality({capture: null}, undefined, true);
+ testOptionEquality({capture: true}, {dummy: false, capture: 1}, true);
+ testOptionEquality({dummy: true}, false, true);
+
+ // Option values that should be treated as distinct
+ testOptionEquality(true, false, false);
+ testOptionEquality(true, {capture:false}, false);
+ testOptionEquality({}, true, false);
+
+}, "Equivalence of option values");
+
+</script>
diff --git a/testing/web-platform/tests/dom/events/EventTarget-add-listener-platform-object.html b/testing/web-platform/tests/dom/events/EventTarget-add-listener-platform-object.html
new file mode 100644
index 0000000000..d5565c22b3
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventTarget-add-listener-platform-object.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>addEventListener with a platform object</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+</script>
+<my-custom-click id=click>Click me!</my-custom-click>
+<script>
+"use strict";
+setup({ single_test: true });
+
+class MyCustomClick extends HTMLElement {
+ connectedCallback() {
+ this.addEventListener("click", this);
+ }
+
+ handleEvent(event) {
+ if (event.target === this) {
+ this.dataset.yay = "It worked!";
+ }
+ }
+}
+window.customElements.define("my-custom-click", MyCustomClick);
+
+const customElement = document.getElementById("click");
+customElement.click();
+
+assert_equals(customElement.dataset.yay, "It worked!");
+
+done();
+</script>
diff --git a/testing/web-platform/tests/dom/events/EventTarget-add-remove-listener.any.js b/testing/web-platform/tests/dom/events/EventTarget-add-remove-listener.any.js
new file mode 100644
index 0000000000..b1d7ffb3e0
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventTarget-add-remove-listener.any.js
@@ -0,0 +1,21 @@
+// META: title=EventTarget's addEventListener + removeEventListener
+
+"use strict";
+
+function listener(evt) {
+ evt.preventDefault();
+ return false;
+}
+
+test(() => {
+ const et = new EventTarget();
+ et.addEventListener("x", listener, false);
+ let event = new Event("x", { cancelable: true });
+ let ret = et.dispatchEvent(event);
+ assert_false(ret);
+
+ et.removeEventListener("x", listener);
+ event = new Event("x", { cancelable: true });
+ ret = et.dispatchEvent(event);
+ assert_true(ret);
+}, "Removing an event listener without explicit capture arg should succeed");
diff --git a/testing/web-platform/tests/dom/events/EventTarget-addEventListener.any.js b/testing/web-platform/tests/dom/events/EventTarget-addEventListener.any.js
new file mode 100644
index 0000000000..e22da4aff8
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventTarget-addEventListener.any.js
@@ -0,0 +1,9 @@
+// META: title=EventTarget.addEventListener
+
+// Step 1.
+test(function() {
+ const et = new EventTarget();
+ assert_equals(et.addEventListener("x", null, false), undefined);
+ assert_equals(et.addEventListener("x", null, true), undefined);
+ assert_equals(et.addEventListener("x", null), undefined);
+}, "Adding a null event listener should succeed");
diff --git a/testing/web-platform/tests/dom/events/EventTarget-constructible.any.js b/testing/web-platform/tests/dom/events/EventTarget-constructible.any.js
new file mode 100644
index 0000000000..b0e7614e62
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventTarget-constructible.any.js
@@ -0,0 +1,62 @@
+"use strict";
+
+test(() => {
+ const target = new EventTarget();
+ const event = new Event("foo", { bubbles: true, cancelable: false });
+ let callCount = 0;
+
+ function listener(e) {
+ assert_equals(e, event);
+ ++callCount;
+ }
+
+ target.addEventListener("foo", listener);
+
+ target.dispatchEvent(event);
+ assert_equals(callCount, 1);
+
+ target.dispatchEvent(event);
+ assert_equals(callCount, 2);
+
+ target.removeEventListener("foo", listener);
+ target.dispatchEvent(event);
+ assert_equals(callCount, 2);
+}, "A constructed EventTarget can be used as expected");
+
+test(() => {
+ class NicerEventTarget extends EventTarget {
+ on(...args) {
+ this.addEventListener(...args);
+ }
+
+ off(...args) {
+ this.removeEventListener(...args);
+ }
+
+ dispatch(type, detail) {
+ this.dispatchEvent(new CustomEvent(type, { detail }));
+ }
+ }
+
+ const target = new NicerEventTarget();
+ const event = new Event("foo", { bubbles: true, cancelable: false });
+ const detail = "some data";
+ let callCount = 0;
+
+ function listener(e) {
+ assert_equals(e.detail, detail);
+ ++callCount;
+ }
+
+ target.on("foo", listener);
+
+ target.dispatch("foo", detail);
+ assert_equals(callCount, 1);
+
+ target.dispatch("foo", detail);
+ assert_equals(callCount, 2);
+
+ target.off("foo", listener);
+ target.dispatch("foo", detail);
+ assert_equals(callCount, 2);
+}, "EventTarget can be subclassed");
diff --git a/testing/web-platform/tests/dom/events/EventTarget-dispatchEvent-returnvalue.html b/testing/web-platform/tests/dom/events/EventTarget-dispatchEvent-returnvalue.html
new file mode 100644
index 0000000000..c4466e0d6c
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventTarget-dispatchEvent-returnvalue.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>EventTarget.dispatchEvent: return value</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-dispatch">
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-event-preventdefault">
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-event-returnvalue">
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-event-defaultprevented">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<table id="table" border="1" style="display: none">
+ <tbody id="table-body">
+ <tr id="table-row">
+ <td id="table-cell">Shady Grove</td>
+ <td>Aeolian</td>
+ </tr>
+ <tr id="parent">
+ <td id="target">Over the river, Charlie</td>
+ <td>Dorian</td>
+ </tr>
+ </tbody>
+</table>
+<script>
+test(function() {
+ var event_type = "foo";
+ var target = document.getElementById("target");
+ var parent = document.getElementById("parent");
+ var default_prevented;
+ var return_value;
+
+ parent.addEventListener(event_type, function(e) {}, true);
+ target.addEventListener(event_type, function(e) {
+ evt.preventDefault();
+ default_prevented = evt.defaultPrevented;
+ return_value = evt.returnValue;
+ }, true);
+ target.addEventListener(event_type, function(e) {}, true);
+
+ var evt = document.createEvent("Event");
+ evt.initEvent(event_type, true, true);
+
+ assert_true(parent.dispatchEvent(evt));
+ assert_false(target.dispatchEvent(evt));
+ assert_true(default_prevented);
+ assert_false(return_value);
+}, "Return value of EventTarget.dispatchEvent() affected by preventDefault().");
+
+test(function() {
+ var event_type = "foo";
+ var target = document.getElementById("target");
+ var parent = document.getElementById("parent");
+ var default_prevented;
+ var return_value;
+
+ parent.addEventListener(event_type, function(e) {}, true);
+ target.addEventListener(event_type, function(e) {
+ evt.returnValue = false;
+ default_prevented = evt.defaultPrevented;
+ return_value = evt.returnValue;
+ }, true);
+ target.addEventListener(event_type, function(e) {}, true);
+
+ var evt = document.createEvent("Event");
+ evt.initEvent(event_type, true, true);
+
+ assert_true(parent.dispatchEvent(evt));
+ assert_false(target.dispatchEvent(evt));
+ assert_true(default_prevented);
+ assert_false(return_value);
+}, "Return value of EventTarget.dispatchEvent() affected by returnValue.");
+</script>
diff --git a/testing/web-platform/tests/dom/events/EventTarget-dispatchEvent.html b/testing/web-platform/tests/dom/events/EventTarget-dispatchEvent.html
new file mode 100644
index 0000000000..783561f5fb
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventTarget-dispatchEvent.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>EventTarget.dispatchEvent</title>
+<link rel="author" title="Olli Pettay" href="mailto:Olli.Pettay@gmail.com">
+<link rel="author" title="Ms2ger" href="mailto:Ms2ger@gmail.com">
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-eventtarget-dispatchevent">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/dom/nodes/Document-createEvent.js"></script>
+<div id="log"></div>
+<script>
+setup({
+ "allow_uncaught_exception": true,
+})
+
+test(function() {
+ assert_throws_js(TypeError, function() { document.dispatchEvent(null) })
+}, "Calling dispatchEvent(null).")
+
+for (var alias in aliases) {
+ test(function() {
+ var e = document.createEvent(alias)
+ assert_equals(e.type, "", "Event type should be empty string before initialization")
+ assert_throws_dom("InvalidStateError", function() { document.dispatchEvent(e) })
+ }, "If the event's initialized flag is not set, an InvalidStateError must be thrown (" + alias + ").")
+}
+
+var dispatch_dispatch = async_test("If the event's dispatch flag is set, an InvalidStateError must be thrown.")
+dispatch_dispatch.step(function() {
+ var e = document.createEvent("Event")
+ e.initEvent("type", false, false)
+
+ var target = document.createElement("div")
+ target.addEventListener("type", dispatch_dispatch.step_func(function() {
+ assert_throws_dom("InvalidStateError", function() {
+ target.dispatchEvent(e)
+ })
+ assert_throws_dom("InvalidStateError", function() {
+ document.dispatchEvent(e)
+ })
+ }), false)
+
+ assert_equals(target.dispatchEvent(e), true, "dispatchEvent must return true")
+
+ dispatch_dispatch.done()
+})
+
+test(function() {
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=17713
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=17714
+
+ var e = document.createEvent("Event")
+ e.initEvent("type", false, false)
+
+ var called = []
+
+ var target = document.createElement("div")
+ target.addEventListener("type", function() {
+ called.push("First")
+ throw new Error()
+ }, false)
+
+ target.addEventListener("type", function() {
+ called.push("Second")
+ }, false)
+
+ assert_equals(target.dispatchEvent(e), true, "dispatchEvent must return true")
+ assert_array_equals(called, ["First", "Second"],
+ "Should have continued to call other event listeners")
+}, "Exceptions from event listeners must not be propagated.")
+
+async_test(function() {
+ var results = []
+ var outerb = document.createElement("b")
+ var middleb = outerb.appendChild(document.createElement("b"))
+ var innerb = middleb.appendChild(document.createElement("b"))
+ outerb.addEventListener("x", this.step_func(function() {
+ middleb.addEventListener("x", this.step_func(function() {
+ results.push("middle")
+ }), true)
+ results.push("outer")
+ }), true)
+ innerb.dispatchEvent(new Event("x"))
+ assert_array_equals(results, ["outer", "middle"])
+ this.done()
+}, "Event listeners added during dispatch should be called");
+
+async_test(function() {
+ var results = []
+ var b = document.createElement("b")
+ b.addEventListener("x", this.step_func(function() {
+ results.push(1)
+ }), true)
+ b.addEventListener("x", this.step_func(function() {
+ results.push(2)
+ }), false)
+ b.addEventListener("x", this.step_func(function() {
+ results.push(3)
+ }), true)
+ b.dispatchEvent(new Event("x"))
+ assert_array_equals(results, [1, 3, 2])
+ this.done()
+}, "Capturing event listeners should be called before non-capturing ones")
+</script>
diff --git a/testing/web-platform/tests/dom/events/EventTarget-removeEventListener.any.js b/testing/web-platform/tests/dom/events/EventTarget-removeEventListener.any.js
new file mode 100644
index 0000000000..289dfcfbab
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventTarget-removeEventListener.any.js
@@ -0,0 +1,8 @@
+// META: title=EventTarget.removeEventListener
+
+// Step 1.
+test(function() {
+ assert_equals(globalThis.removeEventListener("x", null, false), undefined);
+ assert_equals(globalThis.removeEventListener("x", null, true), undefined);
+ assert_equals(globalThis.removeEventListener("x", null), undefined);
+}, "removing a null event listener should succeed");
diff --git a/testing/web-platform/tests/dom/events/EventTarget-this-of-listener.html b/testing/web-platform/tests/dom/events/EventTarget-this-of-listener.html
new file mode 100644
index 0000000000..506564c413
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/EventTarget-this-of-listener.html
@@ -0,0 +1,182 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>EventTarget listeners this value</title>
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-listener-invoke">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+
+ const nodes = [
+ document.createElement("p"),
+ document.createTextNode("some text"),
+ document.createDocumentFragment(),
+ document.createComment("a comment"),
+ document.createProcessingInstruction("target", "data")
+ ];
+
+ let callCount = 0;
+ for (const node of nodes) {
+ node.addEventListener("someevent", function () {
+ ++callCount;
+ assert_equals(this, node);
+ });
+
+ node.dispatchEvent(new CustomEvent("someevent"));
+ }
+
+ assert_equals(callCount, nodes.length);
+
+}, "the this value inside the event listener callback should be the node");
+
+test(() => {
+
+ const nodes = [
+ document.createElement("p"),
+ document.createTextNode("some text"),
+ document.createDocumentFragment(),
+ document.createComment("a comment"),
+ document.createProcessingInstruction("target", "data")
+ ];
+
+ let callCount = 0;
+ for (const node of nodes) {
+ const handler = {
+ handleEvent() {
+ ++callCount;
+ assert_equals(this, handler);
+ }
+ };
+
+ node.addEventListener("someevent", handler);
+
+ node.dispatchEvent(new CustomEvent("someevent"));
+ }
+
+ assert_equals(callCount, nodes.length);
+
+}, "the this value inside the event listener object handleEvent should be the object");
+
+test(() => {
+
+ const nodes = [
+ document.createElement("p"),
+ document.createTextNode("some text"),
+ document.createDocumentFragment(),
+ document.createComment("a comment"),
+ document.createProcessingInstruction("target", "data")
+ ];
+
+ let callCount = 0;
+ for (const node of nodes) {
+ const handler = {
+ handleEvent() {
+ assert_unreached("should not call the old handleEvent method");
+ }
+ };
+
+ node.addEventListener("someevent", handler);
+ handler.handleEvent = function () {
+ ++callCount;
+ assert_equals(this, handler);
+ };
+
+ node.dispatchEvent(new CustomEvent("someevent"));
+ }
+
+ assert_equals(callCount, nodes.length);
+
+}, "dispatchEvent should invoke the current handleEvent method of the object");
+
+test(() => {
+
+ const nodes = [
+ document.createElement("p"),
+ document.createTextNode("some text"),
+ document.createDocumentFragment(),
+ document.createComment("a comment"),
+ document.createProcessingInstruction("target", "data")
+ ];
+
+ let callCount = 0;
+ for (const node of nodes) {
+ const handler = {};
+
+ node.addEventListener("someevent", handler);
+ handler.handleEvent = function () {
+ ++callCount;
+ assert_equals(this, handler);
+ };
+
+ node.dispatchEvent(new CustomEvent("someevent"));
+ }
+
+ assert_equals(callCount, nodes.length);
+
+}, "addEventListener should not require handleEvent to be defined on object listeners");
+
+test(() => {
+
+ const nodes = [
+ document.createElement("p"),
+ document.createTextNode("some text"),
+ document.createDocumentFragment(),
+ document.createComment("a comment"),
+ document.createProcessingInstruction("target", "data")
+ ];
+
+ let callCount = 0;
+ for (const node of nodes) {
+ function handler() {
+ ++callCount;
+ assert_equals(this, node);
+ }
+
+ handler.handleEvent = () => {
+ assert_unreached("should not call the handleEvent method on a function");
+ };
+
+ node.addEventListener("someevent", handler);
+
+ node.dispatchEvent(new CustomEvent("someevent"));
+ }
+
+ assert_equals(callCount, nodes.length);
+
+}, "handleEvent properties added to a function before addEventListener are not reached");
+
+test(() => {
+
+ const nodes = [
+ document.createElement("p"),
+ document.createTextNode("some text"),
+ document.createDocumentFragment(),
+ document.createComment("a comment"),
+ document.createProcessingInstruction("target", "data")
+ ];
+
+ let callCount = 0;
+ for (const node of nodes) {
+ function handler() {
+ ++callCount;
+ assert_equals(this, node);
+ }
+
+ node.addEventListener("someevent", handler);
+
+ handler.handleEvent = () => {
+ assert_unreached("should not call the handleEvent method on a function");
+ };
+
+ node.dispatchEvent(new CustomEvent("someevent"));
+ }
+
+ assert_equals(callCount, nodes.length);
+
+}, "handleEvent properties added to a function after addEventListener are not reached");
+
+</script>
diff --git a/testing/web-platform/tests/dom/events/KeyEvent-initKeyEvent.html b/testing/web-platform/tests/dom/events/KeyEvent-initKeyEvent.html
new file mode 100644
index 0000000000..3fffaba014
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/KeyEvent-initKeyEvent.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>KeyEvent.initKeyEvent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+// The legacy KeyEvent.initKeyEvent shouldn't be defined in the wild anymore.
+// https://www.w3.org/TR/1999/WD-DOM-Level-2-19990923/events.html#Events-Event-initKeyEvent
+test(function() {
+ const event = document.createEvent("KeyboardEvent");
+ assert_true(event?.initKeyEvent === undefined);
+}, "KeyboardEvent.initKeyEvent shouldn't be defined (created by createEvent(\"KeyboardEvent\")");
+
+test(function() {
+ const event = new KeyboardEvent("keypress");
+ assert_true(event?.initKeyEvent === undefined);
+}, "KeyboardEvent.initKeyEvent shouldn't be defined (created by constructor)");
+
+test(function() {
+ assert_true(KeyboardEvent.prototype.initKeyEvent === undefined);
+}, "KeyboardEvent.prototype.initKeyEvent shouldn't be defined");
+</script>
diff --git a/testing/web-platform/tests/dom/events/event-disabled-dynamic.html b/testing/web-platform/tests/dom/events/event-disabled-dynamic.html
new file mode 100644
index 0000000000..3f995b02f1
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/event-disabled-dynamic.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test that disabled is honored immediately in presence of dynamic changes</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Andreas Farre" href="mailto:afarre@mozilla.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#enabling-and-disabling-form-controls:-the-disabled-attribute">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1405087">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<input type="button" value="Click" disabled>
+<script>
+async_test(t => {
+ // NOTE: This test will timeout if it fails.
+ window.addEventListener('load', t.step_func(() => {
+ let e = document.querySelector('input');
+ e.disabled = false;
+ e.onclick = t.step_func_done(() => {});
+ e.click();
+ }));
+}, "disabled is honored properly in presence of dynamic changes");
+</script>
diff --git a/testing/web-platform/tests/dom/events/event-global-extra.window.js b/testing/web-platform/tests/dom/events/event-global-extra.window.js
new file mode 100644
index 0000000000..0f14961c40
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/event-global-extra.window.js
@@ -0,0 +1,90 @@
+const otherWindow = document.body.appendChild(document.createElement("iframe")).contentWindow;
+
+["EventTarget", "XMLHttpRequest"].forEach(constructorName => {
+ async_test(t => {
+ const eventTarget = new otherWindow[constructorName]();
+ eventTarget.addEventListener("hi", t.step_func_done(e => {
+ assert_equals(otherWindow.event, undefined);
+ assert_equals(e, window.event);
+ }));
+ eventTarget.dispatchEvent(new Event("hi"));
+ }, "window.event for constructors from another global: " + constructorName);
+});
+
+// XXX: It would be good to test a subclass of EventTarget once we sort out
+// https://github.com/heycam/webidl/issues/540
+
+async_test(t => {
+ const element = document.body.appendChild(otherWindow.document.createElement("meh"));
+ element.addEventListener("yo", t.step_func_done(e => {
+ assert_equals(e, window.event);
+ }));
+ element.dispatchEvent(new Event("yo"));
+}, "window.event and element from another document");
+
+async_test(t => {
+ const doc = otherWindow.document,
+ element = doc.body.appendChild(doc.createElement("meh")),
+ child = element.appendChild(doc.createElement("bleh"));
+ element.addEventListener("yoyo", t.step_func(e => {
+ document.body.appendChild(element);
+ assert_equals(element.ownerDocument, document);
+ assert_equals(window.event, e);
+ assert_equals(otherWindow.event, undefined);
+ }), true);
+ element.addEventListener("yoyo", t.step_func(e => {
+ assert_equals(element.ownerDocument, document);
+ assert_equals(window.event, e);
+ assert_equals(otherWindow.event, undefined);
+ }), true);
+ child.addEventListener("yoyo", t.step_func_done(e => {
+ assert_equals(child.ownerDocument, document);
+ assert_equals(window.event, e);
+ assert_equals(otherWindow.event, undefined);
+ }));
+ child.dispatchEvent(new Event("yoyo"));
+}, "window.event and moving an element post-dispatch");
+
+test(t => {
+ const host = document.createElement("div"),
+ shadow = host.attachShadow({ mode: "open" }),
+ child = shadow.appendChild(document.createElement("trala")),
+ furtherChild = child.appendChild(document.createElement("waddup"));
+ let counter = 0;
+ host.addEventListener("hi", t.step_func(e => {
+ assert_equals(window.event, e);
+ assert_equals(counter++, 3);
+ }));
+ child.addEventListener("hi", t.step_func(e => {
+ assert_equals(window.event, undefined);
+ assert_equals(counter++, 2);
+ }));
+ furtherChild.addEventListener("hi", t.step_func(e => {
+ host.appendChild(child);
+ assert_equals(window.event, undefined);
+ assert_equals(counter++, 0);
+ }));
+ furtherChild.addEventListener("hi", t.step_func(e => {
+ assert_equals(window.event, undefined);
+ assert_equals(counter++, 1);
+ }));
+ furtherChild.dispatchEvent(new Event("hi", { composed: true, bubbles: true }));
+ assert_equals(counter, 4);
+}, "window.event should not be affected by nodes moving post-dispatch");
+
+async_test(t => {
+ const frame = document.body.appendChild(document.createElement("iframe"));
+ frame.src = "resources/event-global-extra-frame.html";
+ frame.onload = t.step_func_done((load_event) => {
+ const event = new Event("hi");
+ document.addEventListener("hi", frame.contentWindow.listener); // listener intentionally not wrapped in t.step_func
+ document.addEventListener("hi", t.step_func(e => {
+ assert_equals(event, e);
+ assert_equals(window.event, e);
+ }));
+ document.dispatchEvent(event);
+ assert_equals(frameState.event, event);
+ assert_equals(frameState.windowEvent, event);
+ assert_equals(frameState.parentEvent, load_event);
+ });
+}, "Listener from a different global");
diff --git a/testing/web-platform/tests/dom/events/event-global-is-still-set-when-coercing-beforeunload-result.html b/testing/web-platform/tests/dom/events/event-global-is-still-set-when-coercing-beforeunload-result.html
new file mode 100644
index 0000000000..a64c8b6b8b
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/event-global-is-still-set-when-coercing-beforeunload-result.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>window.event is still set when 'beforeunload' result is coerced to string</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#ref-for-window-current-event%E2%91%A1">
+<link rel="help" href="https://webidl.spec.whatwg.org/#call-a-user-objects-operation">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="iframe" src="resources/event-global-is-still-set-when-coercing-beforeunload-result-frame.html"></iframe>
+<body>
+<script>
+window.onload = () => {
+ async_test(t => {
+ iframe.onload = t.step_func_done(() => {
+ assert_equals(typeof window.currentEventInToString, "object");
+ assert_equals(window.currentEventInToString.type, "beforeunload");
+ });
+
+ iframe.contentWindow.location.href = "about:blank";
+ });
+};
+</script>
diff --git a/testing/web-platform/tests/dom/events/event-global-is-still-set-when-reporting-exception-onerror.html b/testing/web-platform/tests/dom/events/event-global-is-still-set-when-reporting-exception-onerror.html
new file mode 100644
index 0000000000..ceaac4fe2b
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/event-global-is-still-set-when-reporting-exception-onerror.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.onerror handler restores window.event after it reports an exception</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<iframe src="resources/empty-document.html"></iframe>
+<iframe src="resources/empty-document.html"></iframe>
+
+<script>
+setup({ allow_uncaught_exception: true });
+
+async_test(t => {
+ window.onload = t.step_func_done(onLoadEvent => {
+ frames[0].onerror = new frames[1].Function(`
+ top.eventDuringSecondOnError = top.window.event;
+ top.frames[0].eventDuringSecondOnError = top.frames[0].event;
+ top.frames[1].eventDuringSecondOnError = top.frames[1].event;
+ `);
+
+ window.onerror = new frames[0].Function(`
+ top.eventDuringFirstOnError = top.window.event;
+ top.frames[0].eventDuringFirstOnError = top.frames[0].event;
+ top.frames[1].eventDuringFirstOnError = top.frames[1].event;
+
+ foo; // cause second onerror
+ `);
+
+ const myEvent = new ErrorEvent("error", { error: new Error("myError") });
+ window.dispatchEvent(myEvent);
+
+ assert_equals(top.eventDuringFirstOnError, onLoadEvent);
+ assert_equals(frames[0].eventDuringFirstOnError, myEvent);
+ assert_equals(frames[1].eventDuringFirstOnError, undefined);
+
+ assert_equals(top.eventDuringSecondOnError, onLoadEvent);
+ assert_equals(frames[0].eventDuringSecondOnError, myEvent);
+ assert_equals(frames[1].eventDuringSecondOnError.error.name, "ReferenceError");
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/dom/events/event-global-set-before-handleEvent-lookup.window.js b/testing/web-platform/tests/dom/events/event-global-set-before-handleEvent-lookup.window.js
new file mode 100644
index 0000000000..8f934bcea9
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/event-global-set-before-handleEvent-lookup.window.js
@@ -0,0 +1,19 @@
+// https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke (steps 8.2 - 12)
+// https://webidl.spec.whatwg.org/#call-a-user-objects-operation (step 10.1)
+
+test(() => {
+ const eventTarget = new EventTarget;
+
+ let currentEvent;
+ eventTarget.addEventListener("foo", {
+ get handleEvent() {
+ currentEvent = window.event;
+ return () => {};
+ }
+ });
+
+ const event = new Event("foo");
+ eventTarget.dispatchEvent(event);
+
+ assert_equals(currentEvent, event);
+}, "window.event is set before 'handleEvent' lookup");
diff --git a/testing/web-platform/tests/dom/events/event-global.html b/testing/web-platform/tests/dom/events/event-global.html
new file mode 100644
index 0000000000..3e8d25ecb5
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/event-global.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<title>window.event tests</title>
+<link rel="author" title="Mike Taylor" href="mailto:miketaylr@gmail.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+setup({allow_uncaught_exception: true});
+
+test(t => {
+ assert_own_property(window, "event");
+ assert_equals(window.event, undefined);
+}, "event exists on window, which is initially set to undefined");
+
+async_test(t => {
+ let target = document.createElement("div");
+ assert_equals(window.event, undefined, "undefined before dispatch");
+
+ let clickEvent = new Event("click");
+ target.addEventListener("click", t.step_func_done(e => {
+ assert_equals(window.event, clickEvent, "window.event set to current event during dispatch");
+ }));
+
+ target.dispatchEvent(clickEvent);
+ assert_equals(window.event, undefined, "undefined after dispatch");
+}, "window.event is only defined during dispatch");
+
+async_test(t => {
+ let parent = document.createElement("div");
+ let root = parent.attachShadow({mode: "closed"});
+ let span = document.createElement("span");
+ root.appendChild(span);
+
+ span.addEventListener("test", t.step_func(e => {
+ assert_equals(window.event, undefined);
+ assert_not_equals(window.event, e);
+ }));
+
+ parent.addEventListener("test", t.step_func_done(e => {
+ assert_equals(window.event, e);
+ assert_not_equals(window.event, undefined);
+ }));
+
+ parent.dispatchEvent(new Event("test", {composed: true}));
+}, "window.event is undefined if the target is in a shadow tree (event dispatched outside shadow tree)");
+
+async_test(t => {
+ let parent = document.createElement("div");
+ let root = parent.attachShadow({mode: "closed"});
+ let span = document.createElement("span");
+ root.appendChild(span);
+ let shadowNode = root.firstElementChild;
+
+ shadowNode.addEventListener("test", t.step_func((e) => {
+ assert_not_equals(window.event, e);
+ assert_equals(window.event, undefined);
+ }));
+
+ parent.addEventListener("test", t.step_func_done(e => {
+ assert_equals(window.event, e);
+ assert_not_equals(window.event, undefined);
+ }));
+
+ shadowNode.dispatchEvent(new Event("test", {composed: true, bubbles: true}));
+}, "window.event is undefined if the target is in a shadow tree (event dispatched inside shadow tree)");
+
+async_test(t => {
+ let parent = document.createElement("div");
+ let root = parent.attachShadow({mode: "open"});
+ document.body.append(parent)
+ let span = document.createElement("span");
+ root.append(span);
+ let shadowNode = root.firstElementChild;
+
+ shadowNode.addEventListener("error", t.step_func(e => {
+ assert_not_equals(window.event, e);
+ assert_equals(window.event, undefined);
+ }));
+
+ let windowOnErrorCalled = false;
+ window.onerror = t.step_func_done(() => {
+ windowOnErrorCalled = true;
+ assert_equals(typeof window.event, "object");
+ assert_equals(window.event.type, "error");
+ });
+
+ shadowNode.dispatchEvent(new ErrorEvent("error", {composed: true, bubbles: true}));
+ assert_true(windowOnErrorCalled);
+}, "window.event is undefined inside window.onerror if the target is in a shadow tree (ErrorEvent dispatched inside shadow tree)");
+
+async_test(t => {
+ let target1 = document.createElement("div");
+ let target2 = document.createElement("div");
+
+ target2.addEventListener("dude", t.step_func(() => {
+ assert_equals(window.event.type, "dude");
+ }));
+
+ target1.addEventListener("cool", t.step_func_done(() => {
+ assert_equals(window.event.type, "cool", "got expected event from global event during dispatch");
+ target2.dispatchEvent(new Event("dude"));
+ assert_equals(window.event.type, "cool", "got expected event from global event after handling a different event handler callback");
+ }));
+
+ target1.dispatchEvent(new Event("cool"));
+}, "window.event is set to the current event during dispatch");
+
+async_test(t => {
+ let target = document.createElement("div");
+
+ target.addEventListener("click", t.step_func_done(e => {
+ assert_equals(e, window.event);
+ }));
+
+ target.dispatchEvent(new Event("click"));
+}, "window.event is set to the current event, which is the event passed to dispatch");
+</script>
diff --git a/testing/web-platform/tests/dom/events/event-global.worker.js b/testing/web-platform/tests/dom/events/event-global.worker.js
new file mode 100644
index 0000000000..116cf32932
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/event-global.worker.js
@@ -0,0 +1,14 @@
+importScripts("/resources/testharness.js");
+test(t => {
+ let seen = false;
+ const event = new Event("hi");
+ assert_equals(self.event, undefined);
+ self.addEventListener("hi", t.step_func(e => {
+ seen = true;
+ assert_equals(self.event, undefined);
+ assert_equals(e, event);
+ }));
+ self.dispatchEvent(event);
+ assert_true(seen);
+}, "There's no self.event (that's why we call it window.event) in workers");
+done();
diff --git a/testing/web-platform/tests/dom/events/focus-event-document-move.html b/testing/web-platform/tests/dom/events/focus-event-document-move.html
new file mode 100644
index 0000000000..2943761ce1
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/focus-event-document-move.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel="help" href="https://crbug.com/747207">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<script>
+ function handleDown(node) {
+ var d2 = new Document();
+ d2.appendChild(node);
+ }
+</script>
+
+<!-- No crash should occur if a node is moved during mousedown. -->
+<div id='click' onmousedown='handleDown(this)'>Click me</div>
+
+<script>
+ const target = document.getElementById('click');
+ async_test(t => {
+ let actions = new test_driver.Actions()
+ .pointerMove(0, 0, {origin: target})
+ .pointerDown()
+ .pointerUp()
+ .send()
+ .then(t.step_func_done(() => {
+ assert_equals(null,document.getElementById('click'));
+ }))
+ .catch(e => t.step_func(() => assert_unreached('Error')));
+ },'Moving a node during mousedown should not crash');
+</script>
diff --git a/testing/web-platform/tests/dom/events/keypress-dispatch-crash.html b/testing/web-platform/tests/dom/events/keypress-dispatch-crash.html
new file mode 100644
index 0000000000..3207adbd8c
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/keypress-dispatch-crash.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<link rel="author" title="Robert Flack" href="mailto:flackr@chromium.org">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1209098">
+
+<!-- No crash should occur if a keypress is dispatched to a constructed document. -->
+
+<script>
+var newDoc = document.implementation.createDocument( "", null);
+var testNode = newDoc.createElement('div');
+newDoc.append(testNode);
+
+var syntheticEvent = document.createEvent('KeyboardEvents');
+syntheticEvent.initKeyboardEvent("keypress");
+testNode.dispatchEvent(syntheticEvent)
+</script>
diff --git a/testing/web-platform/tests/dom/events/legacy-pre-activation-behavior.window.js b/testing/web-platform/tests/dom/events/legacy-pre-activation-behavior.window.js
new file mode 100644
index 0000000000..e9e84bfad1
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/legacy-pre-activation-behavior.window.js
@@ -0,0 +1,10 @@
+test(t => {
+ const input = document.body.appendChild(document.createElement('input'));
+ input.type = "radio";
+ t.add_cleanup(() => input.remove());
+ const clickEvent = new MouseEvent('click', { button: 0, which: 1 });
+ input.addEventListener('change', t.step_func(() => {
+ assert_equals(clickEvent.eventPhase, Event.NONE);
+ }));
+ input.dispatchEvent(clickEvent);
+}, "Use NONE phase during legacy-pre-activation behavior");
diff --git a/testing/web-platform/tests/dom/events/mouse-event-retarget.html b/testing/web-platform/tests/dom/events/mouse-event-retarget.html
new file mode 100644
index 0000000000..c9ce6240d4
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/mouse-event-retarget.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<title>Script created MouseEvent properly retargets and adjusts offsetX</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<style>
+body {
+ margin: 8px;
+ padding: 0;
+}
+</style>
+
+<div id="target">Hello</div>
+
+<script>
+async_test(t => {
+ target.addEventListener('click', ev => {
+ t.step(() => assert_equals(ev.offsetX, 42));
+ t.done();
+ });
+
+ const ev = new MouseEvent('click', { clientX: 50 });
+ target.dispatchEvent(ev);
+}, "offsetX is correctly adjusted");
+</script>
diff --git a/testing/web-platform/tests/dom/events/no-focus-events-at-clicking-editable-content-in-link.html b/testing/web-platform/tests/dom/events/no-focus-events-at-clicking-editable-content-in-link.html
new file mode 100644
index 0000000000..dc08636c46
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/no-focus-events-at-clicking-editable-content-in-link.html
@@ -0,0 +1,80 @@
+<!doctype html>
+<html>
+<head>
+<meta chareset="utf-8">
+<title>Clicking editable content in link shouldn't cause redundant focus related events</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+</head>
+<body>
+<a href="#"><span contenteditable>Hello</span></a>
+<a href="#" contenteditable><span>Hello</span></a>
+<script>
+function promiseTicks() {
+ return new Promise(resolve => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(resolve);
+ });
+ });
+}
+
+async function clickElementAndCollectFocusEvents(x, y, options) {
+ await promiseTicks();
+ let events = [];
+ for (const eventType of ["focus", "blur", "focusin", "focusout"]) {
+ document.addEventListener(eventType, event => {
+ events.push(`type: ${event.type}, target: ${event.target.nodeName}`);
+ }, {capture: true});
+ }
+
+ const waitForClickEvent = new Promise(resolve => {
+ addEventListener("click", resolve, {capture: true, once: true});
+ });
+
+ await new test_driver
+ .Actions()
+ .pointerMove(x, y, options)
+ .pointerDown()
+ .pointerUp()
+ .send();
+
+ await waitForClickEvent;
+ await promiseTicks();
+ return events;
+}
+
+promise_test(async t => {
+ document.activeElement?.blur();
+ const editingHost = document.querySelector("span[contenteditable]");
+ editingHost.blur();
+ const focusEvents =
+ await clickElementAndCollectFocusEvents(5, 5, {origin: editingHost});
+ assert_array_equals(
+ focusEvents,
+ [
+ "type: focus, target: SPAN",
+ "type: focusin, target: SPAN",
+ ],
+ "Click event shouldn't cause redundant focus events");
+}, "Click editable element in link");
+
+promise_test(async t => {
+ document.activeElement?.blur();
+ const editingHost = document.querySelector("a[contenteditable]");
+ editingHost.blur();
+ const focusEvents =
+ await clickElementAndCollectFocusEvents(5, 5, {origin: editingHost});
+ assert_array_equals(
+ focusEvents,
+ [
+ "type: focus, target: A",
+ "type: focusin, target: A",
+ ],
+ "Click event shouldn't cause redundant focus events");
+}, "Click editable link");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-body.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-body.html
new file mode 100644
index 0000000000..5574fe0acb
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-body.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>non-passive mousewheel event listener on body</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<link rel="help" href="https://github.com/w3c/uievents/issues/331">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.body,
+ eventName: 'mousewheel',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-div.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-div.html
new file mode 100644
index 0000000000..6fbf692cd7
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-div.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>non-passive mousewheel event listener on div</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<link rel="help" href="https://github.com/w3c/uievents/issues/331">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<style>
+ html, body {
+ overflow: hidden;
+ margin: 0;
+ }
+ #div {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ overflow: scroll;
+ }
+</style>
+<div class=remove-on-cleanup id=div>
+ <div style="height: 200vh"></div>
+</div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.getElementById('div'),
+ eventName: 'mousewheel',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-document.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-document.html
new file mode 100644
index 0000000000..7d07393c69
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-document.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>non-passive mousewheel event listener on document</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<link rel="help" href="https://github.com/w3c/uievents/issues/331">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document,
+ eventName: 'mousewheel',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-root.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-root.html
new file mode 100644
index 0000000000..e85fbacaba
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-root.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>non-passive mousewheel event listener on root</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<link rel="help" href="https://github.com/w3c/uievents/issues/331">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.documentElement,
+ eventName: 'mousewheel',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-window.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-window.html
new file mode 100644
index 0000000000..29b09f8561
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-mousewheel-event-listener-on-window.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>non-passive mousewheel event listener on window</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<link rel="help" href="https://github.com/w3c/uievents/issues/331">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: window,
+ eventName: 'mousewheel',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-body.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-body.html
new file mode 100644
index 0000000000..f417bdd0a6
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-body.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>non-passive touchmove event listener on body</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.body,
+ eventName: 'touchmove',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-div.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-div.html
new file mode 100644
index 0000000000..11c9345407
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-div.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>non-passive touchmove event listener on div</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.getElementById('touchDiv'),
+ eventName: 'touchmove',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-document.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-document.html
new file mode 100644
index 0000000000..8b95a8d492
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-document.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>non-passive touchmove event listener on document</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document,
+ eventName: 'touchmove',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-root.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-root.html
new file mode 100644
index 0000000000..c41ab72bd8
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-root.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>non-passive touchmove event listener on root</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.documentElement,
+ eventName: 'touchmove',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-window.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-window.html
new file mode 100644
index 0000000000..3d6675c566
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchmove-event-listener-on-window.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<link rel="help" href="https://github.com/WICG/interventions/issues/35">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: window,
+ eventName: 'touchstart',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-body.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-body.html
new file mode 100644
index 0000000000..f6e6ecb06d
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-body.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>non-passive touchstart event listener on body</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.body,
+ eventName: 'touchstart',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-div.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-div.html
new file mode 100644
index 0000000000..2e7c6e6b3b
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-div.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>non-passive touchstart event listener on div</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.getElementById('touchDiv'),
+ eventName: 'touchstart',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-document.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-document.html
new file mode 100644
index 0000000000..22fcbdc322
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-document.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>non-passive touchstart event listener on document</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document,
+ eventName: 'touchstart',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-root.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-root.html
new file mode 100644
index 0000000000..56c51349a0
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-root.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>non-passive touchstart event listener on root</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.documentElement,
+ eventName: 'touchstart',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-window.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-window.html
new file mode 100644
index 0000000000..4e9d424a9d
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-touchstart-event-listener-on-window.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>non-passive touchstart event listener on window</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: window,
+ eventName: 'touchstart',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-body.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-body.html
new file mode 100644
index 0000000000..070cadc291
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-body.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>non-passive wheel event listener on body</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.body,
+ eventName: 'wheel',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-div.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-div.html
new file mode 100644
index 0000000000..c49d18ac13
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-div.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<title>non-passive wheel event listener on div</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<style>
+ html, body {
+ overflow: hidden;
+ margin: 0;
+ }
+ #div {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ overflow: scroll;
+ }
+</style>
+<div class=remove-on-cleanup id=div>
+ <div style="height: 200vh"></div>
+</div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.getElementById('div'),
+ eventName: 'wheel',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-document.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-document.html
new file mode 100644
index 0000000000..31a55cad43
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-document.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>non-passive wheel event listener on document</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document,
+ eventName: 'wheel',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-root.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-root.html
new file mode 100644
index 0000000000..b7bacbfc7c
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-root.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>non-passive wheel event listener on root</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.documentElement,
+ eventName: 'wheel',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-window.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-window.html
new file mode 100644
index 0000000000..c236059df4
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/non-passive-wheel-event-listener-on-window.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>non-passive wheel event listener on window</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: window,
+ eventName: 'wheel',
+ passive: false,
+ expectCancelable: true,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-body.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-body.html
new file mode 100644
index 0000000000..9db12cfbdc
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-body.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>passive mousewheel event listener on body</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<link rel="help" href="https://github.com/w3c/uievents/issues/331">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.body,
+ eventName: 'mousewheel',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-div.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-div.html
new file mode 100644
index 0000000000..373670856b
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-div.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>passive mousewheel event listener on div</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<link rel="help" href="https://github.com/w3c/uievents/issues/331">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<style>
+ html, body {
+ overflow: hidden;
+ margin: 0;
+ }
+ #div {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ overflow: scroll;
+ }
+</style>
+<div class=remove-on-cleanup id=div>
+ <div style="height: 200vh"></div>
+</div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.getElementById('div'),
+ eventName: 'mousewheel',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-document.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-document.html
new file mode 100644
index 0000000000..71262280b6
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-document.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>passive mousewheel event listener on document</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<link rel="help" href="https://github.com/w3c/uievents/issues/331">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document,
+ eventName: 'mousewheel',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-root.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-root.html
new file mode 100644
index 0000000000..fc641d172e
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-root.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>passive mousewheel event listener on root</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<link rel="help" href="https://github.com/w3c/uievents/issues/331">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.documentElement,
+ eventName: 'mousewheel',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-window.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-window.html
new file mode 100644
index 0000000000..f60955c7c4
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-mousewheel-event-listener-on-window.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>passive mousewheel event listener on window</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<link rel="help" href="https://github.com/w3c/uievents/issues/331">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: window,
+ eventName: 'mousewheel',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-body.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-body.html
new file mode 100644
index 0000000000..2349bad258
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-body.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>passive touchmove event listener on body</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.body,
+ eventName: 'touchmove',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-div.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-div.html
new file mode 100644
index 0000000000..a61b34851e
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-div.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>passive touchmove event listener on div</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.getElementById('touchDiv'),
+ eventName: 'touchmove',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-document.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-document.html
new file mode 100644
index 0000000000..b49971b5b0
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-document.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>passive touchmove event listener on document</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document,
+ eventName: 'touchmove',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-root.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-root.html
new file mode 100644
index 0000000000..b851704590
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-root.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>passive touchmove event listener on root</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.documentElement,
+ eventName: 'touchmove',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-window.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-window.html
new file mode 100644
index 0000000000..351d6ace84
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchmove-event-listener-on-window.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>passive touchmove event listener on window</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: window,
+ eventName: 'touchstart',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-body.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-body.html
new file mode 100644
index 0000000000..c3d2b577fd
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-body.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>passive touchstart event listener on body</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.body,
+ eventName: 'touchstart',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-div.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-div.html
new file mode 100644
index 0000000000..103e7f0d23
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-div.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>passive touchstart event listener on div</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.getElementById('touchDiv'),
+ eventName: 'touchstart',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-document.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-document.html
new file mode 100644
index 0000000000..2e4de2405f
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-document.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>passive touchstart event listener on document</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document,
+ eventName: 'touchstart',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-root.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-root.html
new file mode 100644
index 0000000000..0f52e9a16f
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-root.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>passive touchstart event listener on root</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.documentElement,
+ eventName: 'touchstart',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-window.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-window.html
new file mode 100644
index 0000000000..c47af8101f
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-touchstart-event-listener-on-window.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>passive touchstart event listener on window</title>
+<link rel="help" href="https://w3c.github.io/touch-events/#cancelability">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/wait-for.js"></script>
+<script src="resources/touching.js"></script>
+<style>
+#touchDiv {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<div id="touchDiv"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: window,
+ eventName: 'touchstart',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-body.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-body.html
new file mode 100644
index 0000000000..fe0869b022
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-body.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>passive wheel event listener on body</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.body,
+ eventName: 'wheel',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-div.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-div.html
new file mode 100644
index 0000000000..e2ca6e795a
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-div.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<title>passive wheel event listener on div</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<style>
+ html, body {
+ overflow: hidden;
+ margin: 0;
+ }
+ #div {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ overflow: scroll;
+ }
+</style>
+<div class=remove-on-cleanup id=div>
+ <div style="height: 200vh"></div>
+</div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.getElementById('div'),
+ eventName: 'wheel',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-document.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-document.html
new file mode 100644
index 0000000000..61b716f7bb
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-document.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>passive wheel event listener on document</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document,
+ eventName: 'wheel',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-root.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-root.html
new file mode 100644
index 0000000000..6b383bc871
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-root.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>passive wheel event listener on root</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: document.documentElement,
+ eventName: 'wheel',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-window.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-window.html
new file mode 100644
index 0000000000..a1e901f552
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/passive-wheel-event-listener-on-window.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>passive wheel event listener on window</title>
+<link rel="help" href="https://w3c.github.io/uievents/#cancelability-of-wheel-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/scrolling.js"></script>
+<div class=remove-on-cleanup style="height: 200vh"></div>
+<script>
+ document.body.onload = () => runTest({
+ target: window,
+ eventName: 'wheel',
+ passive: true,
+ expectCancelable: false,
+ });
+</script>
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/resources/scrolling.js b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/resources/scrolling.js
new file mode 100644
index 0000000000..88e10f5efd
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/resources/scrolling.js
@@ -0,0 +1,34 @@
+function raf() {
+ return new Promise((resolve) => {
+ // rAF twice.
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(resolve);
+ });
+ });
+}
+
+async function runTest({target, eventName, passive, expectCancelable}) {
+ await raf();
+
+ let cancelable = null;
+ let arrived = false;
+ target.addEventListener(eventName, function (event) {
+ cancelable = event.cancelable;
+ arrived = true;
+ }, {passive:passive, once:true});
+
+ promise_test(async (t) => {
+ t.add_cleanup(() => {
+ document.querySelector('.remove-on-cleanup')?.remove();
+ });
+ const pos_x = Math.floor(window.innerWidth / 2);
+ const pos_y = Math.floor(window.innerHeight / 2);
+ const delta_x = 0;
+ const delta_y = 100;
+
+ await new test_driver.Actions()
+ .scroll(pos_x, pos_y, delta_x, delta_y).send();
+ await t.step_wait(() => arrived, `Didn't get event ${eventName} on ${target.localName}`);
+ assert_equals(cancelable, expectCancelable);
+ });
+}
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/resources/touching.js b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/resources/touching.js
new file mode 100644
index 0000000000..620d26804b
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/resources/touching.js
@@ -0,0 +1,34 @@
+function waitForCompositorCommit() {
+ return new Promise((resolve) => {
+ // rAF twice.
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(resolve);
+ });
+ });
+}
+
+function injectInput(touchDiv) {
+ return new test_driver.Actions()
+ .addPointer("touch_pointer", "touch")
+ .pointerMove(0, 0, {origin: touchDiv})
+ .pointerDown()
+ .pointerMove(30, 30)
+ .pointerUp()
+ .send();
+}
+
+function runTest({target, eventName, passive, expectCancelable}) {
+ let touchDiv = document.getElementById("touchDiv");
+ let cancelable = null;
+ let arrived = false;
+ target.addEventListener(eventName, function (event) {
+ cancelable = event.cancelable;
+ arrived = true;
+ }, {passive});
+ promise_test(async () => {
+ await waitForCompositorCommit();
+ await injectInput(touchDiv);
+ await waitFor(() => arrived);
+ assert_equals(cancelable, expectCancelable);
+ });
+}
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/resources/wait-for.js b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/resources/wait-for.js
new file mode 100644
index 0000000000..0bf3e55834
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/resources/wait-for.js
@@ -0,0 +1,15 @@
+function waitFor(condition, MAX_FRAME = 500) {
+ return new Promise((resolve, reject) => {
+ function tick(frames) {
+ // We requestAnimationFrame either for MAX_FRAME frames or until condition is
+ // met.
+ if (frames >= MAX_FRAME)
+ reject(new Error(`Condition did not become true after ${MAX_FRAME} frames`));
+ else if (condition())
+ resolve();
+ else
+ requestAnimationFrame(() => tick(frames + 1));
+ }
+ tick(0);
+ });
+}
diff --git a/testing/web-platform/tests/dom/events/non-cancelable-when-passive/synthetic-events-cancelable.html b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/synthetic-events-cancelable.html
new file mode 100644
index 0000000000..4287770b8d
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/non-cancelable-when-passive/synthetic-events-cancelable.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<title>Synthetic events are always cancelable by default</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#dictdef-eventinit">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const eventsMap = {
+ wheel: 'WheelEvent',
+ mousewheel: 'WheelEvent',
+ touchstart: 'TouchEvent',
+ touchmove: 'TouchEvent',
+ touchend: 'TouchEvent',
+ touchcancel: 'TouchEvent',
+}
+function isCancelable(eventName, interfaceName) {
+ test(() => {
+ assert_implements(interfaceName in self, `${interfaceName} should be supported`);
+ let defaultPrevented = null;
+ addEventListener(eventName, event => {
+ event.preventDefault();
+ defaultPrevented = event.defaultPrevented;
+ });
+ const event = new self[interfaceName](eventName);
+ assert_false(event.cancelable, 'cancelable');
+ const dispatchEventReturnValue = dispatchEvent(event);
+ assert_false(defaultPrevented, 'defaultPrevented');
+ assert_true(dispatchEventReturnValue, 'dispatchEvent() return value');
+ }, `Synthetic ${eventName} event with interface ${interfaceName} is not cancelable`);
+}
+for (const eventName in eventsMap) {
+ isCancelable(eventName, eventsMap[eventName]);
+ isCancelable(eventName, 'Event');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/passive-by-default.html b/testing/web-platform/tests/dom/events/passive-by-default.html
new file mode 100644
index 0000000000..02029f4dac
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/passive-by-default.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Default passive event listeners on window, document, document element, body</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#default-passive-value">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+ <div id="div"></div>
+<script>
+ function isListenerPassive(eventName, eventTarget, passive, expectPassive) {
+ test(() => {
+ let defaultPrevented = null;
+ let handler = event => {
+ event.preventDefault();
+ defaultPrevented = event.defaultPrevented;
+ eventTarget.removeEventListener(eventName, handler);
+ };
+ if (passive === 'omitted') {
+ eventTarget.addEventListener(eventName, handler);
+ } else {
+ eventTarget.addEventListener(eventName, handler, {passive});
+ }
+ let dispatchEventReturnValue = eventTarget.dispatchEvent(new Event(eventName, {cancelable: true}));
+ assert_equals(defaultPrevented, !expectPassive, 'defaultPrevented');
+ assert_equals(dispatchEventReturnValue, expectPassive, 'dispatchEvent() return value');
+ }, `${eventName} listener is ${expectPassive ? '' : 'non-'}passive ${passive === 'omitted' ? 'by default' : `with {passive:${passive}}`} for ${eventTarget.constructor.name}`);
+ }
+
+ const eventNames = {
+ touchstart: true,
+ touchmove: true,
+ wheel: true,
+ mousewheel: true,
+ touchend: false
+ };
+ const passiveEventTargets = [window, document, document.documentElement, document.body];
+ const div = document.getElementById('div');
+
+ for (const eventName in eventNames) {
+ for (const eventTarget of passiveEventTargets) {
+ isListenerPassive(eventName, eventTarget, 'omitted', eventNames[eventName]);
+ isListenerPassive(eventName, eventTarget, undefined, eventNames[eventName]);
+ isListenerPassive(eventName, eventTarget, false, false);
+ isListenerPassive(eventName, eventTarget, true, true);
+ }
+ isListenerPassive(eventName, div, 'omitted', false);
+ isListenerPassive(eventName, div, undefined, false);
+ isListenerPassive(eventName, div, false, false);
+ isListenerPassive(eventName, div, true, true);
+ }
+</script>
diff --git a/testing/web-platform/tests/dom/events/relatedTarget.window.js b/testing/web-platform/tests/dom/events/relatedTarget.window.js
new file mode 100644
index 0000000000..ebc83ceb20
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/relatedTarget.window.js
@@ -0,0 +1,81 @@
+// https://dom.spec.whatwg.org/#concept-event-dispatch
+
+const host = document.createElement("div"),
+ child = host.appendChild(document.createElement("p")),
+ shadow = host.attachShadow({ mode: "closed" }),
+ slot = shadow.appendChild(document.createElement("slot"));
+
+test(() => {
+ for (target of [shadow, slot]) {
+ for (relatedTarget of [new XMLHttpRequest(), self, host]) {
+ const event = new FocusEvent("demo", { relatedTarget: relatedTarget });
+ target.dispatchEvent(event);
+ assert_equals(event.target, null);
+ assert_equals(event.relatedTarget, null);
+ }
+ }
+}, "Reset if target pointed to a shadow tree");
+
+test(() => {
+ for (relatedTarget of [shadow, slot]) {
+ for (target of [new XMLHttpRequest(), self, host]) {
+ const event = new FocusEvent("demo", { relatedTarget: relatedTarget });
+ target.dispatchEvent(event);
+ assert_equals(event.target, target);
+ assert_equals(event.relatedTarget, host);
+ }
+ }
+}, "Retarget a shadow-tree relatedTarget");
+
+test(t => {
+ const shadowChild = shadow.appendChild(document.createElement("div"));
+ shadowChild.addEventListener("demo", t.step_func(() => document.body.appendChild(shadowChild)));
+ const event = new FocusEvent("demo", { relatedTarget: new XMLHttpRequest() });
+ shadowChild.dispatchEvent(event);
+ assert_equals(shadowChild.parentNode, document.body);
+ assert_equals(event.target, null);
+ assert_equals(event.relatedTarget, null);
+ shadowChild.remove();
+}, "Reset if target pointed to a shadow tree pre-dispatch");
+
+test(t => {
+ const shadowChild = shadow.appendChild(document.createElement("div"));
+ document.body.addEventListener("demo", t.step_func(() => document.body.appendChild(shadowChild)));
+ const event = new FocusEvent("demo", { relatedTarget: shadowChild });
+ document.body.dispatchEvent(event);
+ assert_equals(shadowChild.parentNode, document.body);
+ assert_equals(event.target, document.body);
+ assert_equals(event.relatedTarget, host);
+ shadowChild.remove();
+}, "Retarget a shadow-tree relatedTarget, part 2");
+
+test(t => {
+ const event = new FocusEvent("heya", { relatedTarget: shadow, cancelable: true }),
+ callback = t.unreached_func();
+ host.addEventListener("heya", callback);
+ t.add_cleanup(() => host.removeEventListener("heya", callback));
+ event.preventDefault();
+ assert_true(event.defaultPrevented);
+ assert_false(host.dispatchEvent(event));
+ assert_equals(event.target, null);
+ assert_equals(event.relatedTarget, null);
+ // Check that the dispatch flag is cleared
+ event.initEvent("x");
+ assert_equals(event.type, "x");
+}, "Reset targets on early return");
+
+test(t => {
+ const input = document.body.appendChild(document.createElement("input")),
+ event = new MouseEvent("click", { relatedTarget: shadow });
+ let seen = false;
+ t.add_cleanup(() => input.remove());
+ input.type = "checkbox";
+ input.oninput = t.step_func(() => {
+ assert_equals(event.target, null);
+ assert_equals(event.relatedTarget, null);
+ assert_equals(event.composedPath().length, 0);
+ seen = true;
+ });
+ assert_true(input.dispatchEvent(event));
+ assert_true(seen);
+}, "Reset targets before activation behavior");
diff --git a/testing/web-platform/tests/dom/events/replace-event-listener-null-browsing-context-crash.html b/testing/web-platform/tests/dom/events/replace-event-listener-null-browsing-context-crash.html
new file mode 100644
index 0000000000..f41955eedd
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/replace-event-listener-null-browsing-context-crash.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>Event listeners: replace listener after moving between documents</title>
+<link rel="author" title="Nate Chapin" href="mailto:japhet@chromium.org">
+<link rel="help" href="https://w3c.github.io/touch-events/#dom-globaleventhandlers-ontouchcancel">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1083793">
+<meta name="assert" content="Overwriting an attribute event listener after adopting the owning node to a different document should not crash"/>
+<progress id="p">
+<iframe id="i"></iframe>
+<script>
+var p = document.getElementById("p");
+i.contentDocument.adoptNode(p);
+p.setAttribute("ontouchcancel", "");
+document.body.appendChild(p);
+p.setAttribute("ontouchcancel", "");
+</script>
+
diff --git a/testing/web-platform/tests/dom/events/resources/empty-document.html b/testing/web-platform/tests/dom/events/resources/empty-document.html
new file mode 100644
index 0000000000..b9cd130a07
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/resources/empty-document.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body>
diff --git a/testing/web-platform/tests/dom/events/resources/event-global-extra-frame.html b/testing/web-platform/tests/dom/events/resources/event-global-extra-frame.html
new file mode 100644
index 0000000000..241dda8b66
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/resources/event-global-extra-frame.html
@@ -0,0 +1,9 @@
+<script>
+function listener(e) {
+ parent.frameState = {
+ event: e,
+ windowEvent: window.event,
+ parentEvent: parent.event
+ }
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/resources/event-global-is-still-set-when-coercing-beforeunload-result-frame.html b/testing/web-platform/tests/dom/events/resources/event-global-is-still-set-when-coercing-beforeunload-result-frame.html
new file mode 100644
index 0000000000..5df4fa2793
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/resources/event-global-is-still-set-when-coercing-beforeunload-result-frame.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<meta charset="utf-8">
+<body>
+<script>
+window.onbeforeunload = () => ({ toString: () => { parent.currentEventInToString = window.event; } });
+</script>
diff --git a/testing/web-platform/tests/dom/events/resources/prefixed-animation-event-tests.js b/testing/web-platform/tests/dom/events/resources/prefixed-animation-event-tests.js
new file mode 100644
index 0000000000..021b6bb9df
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/resources/prefixed-animation-event-tests.js
@@ -0,0 +1,366 @@
+'use strict'
+
+// Runs a set of tests for a given prefixed/unprefixed animation event (e.g.
+// animationstart/webkitAnimationStart).
+//
+// The eventDetails object must have the following form:
+// {
+// isTransition: false, <-- can be omitted, default false
+// unprefixedType: 'animationstart',
+// prefixedType: 'webkitAnimationStart',
+// animationCssStyle: '1ms', <-- must NOT include animation name or
+// transition property
+// }
+function runAnimationEventTests(eventDetails) {
+ const {
+ isTransition,
+ unprefixedType,
+ prefixedType,
+ animationCssStyle
+ } = eventDetails;
+
+ // Derive the DOM event handler names, e.g. onanimationstart.
+ const unprefixedHandler = `on${unprefixedType}`;
+ const prefixedHandler = `on${prefixedType.toLowerCase()}`;
+
+ const style = document.createElement('style');
+ document.head.appendChild(style);
+ if (isTransition) {
+ style.sheet.insertRule(
+ `.baseStyle { width: 100px; transition: width ${animationCssStyle}; }`);
+ style.sheet.insertRule('.transition { width: 200px !important; }');
+ } else {
+ style.sheet.insertRule('@keyframes anim {}');
+ }
+
+ function triggerAnimation(div) {
+ if (isTransition) {
+ div.classList.add('transition');
+ } else {
+ div.style.animation = `anim ${animationCssStyle}`;
+ }
+ }
+
+ test(t => {
+ const div = createDiv(t);
+
+ assert_equals(div[unprefixedHandler], null,
+ `${unprefixedHandler} should initially be null`);
+ assert_equals(div[prefixedHandler], null,
+ `${prefixedHandler} should initially be null`);
+
+ // Setting one should not affect the other.
+ div[unprefixedHandler] = () => { };
+
+ assert_not_equals(div[unprefixedHandler], null,
+ `setting ${unprefixedHandler} should make it non-null`);
+ assert_equals(div[prefixedHandler], null,
+ `setting ${unprefixedHandler} should not affect ${prefixedHandler}`);
+
+ div[prefixedHandler] = () => { };
+
+ assert_not_equals(div[prefixedHandler], null,
+ `setting ${prefixedHandler} should make it non-null`);
+ assert_not_equals(div[unprefixedHandler], div[prefixedHandler],
+ 'the setters should be different');
+ }, `${unprefixedHandler} and ${prefixedHandler} are not aliases`);
+
+ // The below tests primarily test the interactions of prefixed animation
+ // events in the algorithm for invoking events:
+ // https://dom.spec.whatwg.org/#concept-event-listener-invoke
+
+ promise_test(async t => {
+ const div = createDiv(t);
+
+ let receivedEventCount = 0;
+ addTestScopedEventHandler(t, div, prefixedHandler, () => {
+ receivedEventCount++;
+ });
+ addTestScopedEventListener(t, div, prefixedType, () => {
+ receivedEventCount++;
+ });
+
+ // The HTML spec[0] specifies that the prefixed event handlers have an
+ // 'Event handler event type' of the appropriate prefixed event type. E.g.
+ // onwebkitanimationend creates a listener for the event type
+ // 'webkitAnimationEnd'.
+ //
+ // [0]: https://html.spec.whatwg.org/multipage/webappapis.html#event-handlers-on-elements,-document-objects,-and-window-objects
+ div.dispatchEvent(new AnimationEvent(prefixedType));
+ assert_equals(receivedEventCount, 2,
+ 'prefixed listener and handler received event');
+ }, `dispatchEvent of a ${prefixedType} event does trigger a ` +
+ `prefixed event handler or listener`);
+
+ promise_test(async t => {
+ const div = createDiv(t);
+
+ let receivedEvent = false;
+ addTestScopedEventHandler(t, div, unprefixedHandler, () => {
+ receivedEvent = true;
+ });
+ addTestScopedEventListener(t, div, unprefixedType, () => {
+ receivedEvent = true;
+ });
+
+ div.dispatchEvent(new AnimationEvent(prefixedType));
+ assert_false(receivedEvent,
+ 'prefixed listener or handler received event');
+ }, `dispatchEvent of a ${prefixedType} event does not trigger an ` +
+ `unprefixed event handler or listener`);
+
+
+ promise_test(async t => {
+ const div = createDiv(t);
+
+ let receivedEvent = false;
+ addTestScopedEventHandler(t, div, prefixedHandler, () => {
+ receivedEvent = true;
+ });
+ addTestScopedEventListener(t, div, prefixedType, () => {
+ receivedEvent = true;
+ });
+
+ // The rewrite rules from
+ // https://dom.spec.whatwg.org/#concept-event-listener-invoke step 8 do not
+ // apply because isTrusted will be false.
+ div.dispatchEvent(new AnimationEvent(unprefixedType));
+ assert_false(receivedEvent, 'prefixed listener or handler received event');
+ }, `dispatchEvent of an ${unprefixedType} event does not trigger a ` +
+ `prefixed event handler or listener`);
+
+ promise_test(async t => {
+ const div = createDiv(t);
+
+ let receivedEvent = false;
+ addTestScopedEventHandler(t, div, prefixedHandler, () => {
+ receivedEvent = true;
+ });
+
+ triggerAnimation(div);
+ await waitForEventThenAnimationFrame(t, unprefixedType);
+ assert_true(receivedEvent, `received ${prefixedHandler} event`);
+ }, `${prefixedHandler} event handler should trigger for an animation`);
+
+ promise_test(async t => {
+ const div = createDiv(t);
+
+ let receivedPrefixedEvent = false;
+ addTestScopedEventHandler(t, div, prefixedHandler, () => {
+ receivedPrefixedEvent = true;
+ });
+ let receivedUnprefixedEvent = false;
+ addTestScopedEventHandler(t, div, unprefixedHandler, () => {
+ receivedUnprefixedEvent = true;
+ });
+
+ triggerAnimation(div);
+ await waitForEventThenAnimationFrame(t, unprefixedType);
+ assert_true(receivedUnprefixedEvent, `received ${unprefixedHandler} event`);
+ assert_false(receivedPrefixedEvent, `received ${prefixedHandler} event`);
+ }, `${prefixedHandler} event handler should not trigger if an unprefixed ` +
+ `event handler also exists`);
+
+ promise_test(async t => {
+ const div = createDiv(t);
+
+ let receivedPrefixedEvent = false;
+ addTestScopedEventHandler(t, div, prefixedHandler, () => {
+ receivedPrefixedEvent = true;
+ });
+ let receivedUnprefixedEvent = false;
+ addTestScopedEventListener(t, div, unprefixedType, () => {
+ receivedUnprefixedEvent = true;
+ });
+
+ triggerAnimation(div);
+ await waitForEventThenAnimationFrame(t, unprefixedHandler);
+ assert_true(receivedUnprefixedEvent, `received ${unprefixedHandler} event`);
+ assert_false(receivedPrefixedEvent, `received ${prefixedHandler} event`);
+ }, `${prefixedHandler} event handler should not trigger if an unprefixed ` +
+ `listener also exists`);
+
+ promise_test(async t => {
+ // We use a parent/child relationship to be able to register both prefixed
+ // and unprefixed event handlers without the deduplication logic kicking in.
+ const parent = createDiv(t);
+ const child = createDiv(t);
+ parent.appendChild(child);
+ // After moving the child, we have to clean style again.
+ getComputedStyle(child).transition;
+ getComputedStyle(child).width;
+
+ let observedUnprefixedType;
+ addTestScopedEventHandler(t, parent, unprefixedHandler, e => {
+ observedUnprefixedType = e.type;
+ });
+ let observedPrefixedType;
+ addTestScopedEventHandler(t, child, prefixedHandler, e => {
+ observedPrefixedType = e.type;
+ });
+
+ triggerAnimation(child);
+ await waitForEventThenAnimationFrame(t, unprefixedType);
+
+ assert_equals(observedUnprefixedType, unprefixedType);
+ assert_equals(observedPrefixedType, prefixedType);
+ }, `event types for prefixed and unprefixed ${unprefixedType} event ` +
+ `handlers should be named appropriately`);
+
+ promise_test(async t => {
+ const div = createDiv(t);
+
+ let receivedEvent = false;
+ addTestScopedEventListener(t, div, prefixedType, () => {
+ receivedEvent = true;
+ });
+
+ triggerAnimation(div);
+ await waitForEventThenAnimationFrame(t, unprefixedHandler);
+ assert_true(receivedEvent, `received ${prefixedType} event`);
+ }, `${prefixedType} event listener should trigger for an animation`);
+
+ promise_test(async t => {
+ const div = createDiv(t);
+
+ let receivedPrefixedEvent = false;
+ addTestScopedEventListener(t, div, prefixedType, () => {
+ receivedPrefixedEvent = true;
+ });
+ let receivedUnprefixedEvent = false;
+ addTestScopedEventListener(t, div, unprefixedType, () => {
+ receivedUnprefixedEvent = true;
+ });
+
+ triggerAnimation(div);
+ await waitForEventThenAnimationFrame(t, unprefixedHandler);
+ assert_true(receivedUnprefixedEvent, `received ${unprefixedType} event`);
+ assert_false(receivedPrefixedEvent, `received ${prefixedType} event`);
+ }, `${prefixedType} event listener should not trigger if an unprefixed ` +
+ `listener also exists`);
+
+ promise_test(async t => {
+ const div = createDiv(t);
+
+ let receivedPrefixedEvent = false;
+ addTestScopedEventListener(t, div, prefixedType, () => {
+ receivedPrefixedEvent = true;
+ });
+ let receivedUnprefixedEvent = false;
+ addTestScopedEventHandler(t, div, unprefixedHandler, () => {
+ receivedUnprefixedEvent = true;
+ });
+
+ triggerAnimation(div);
+ await waitForEventThenAnimationFrame(t, unprefixedHandler);
+ assert_true(receivedUnprefixedEvent, `received ${unprefixedType} event`);
+ assert_false(receivedPrefixedEvent, `received ${prefixedType} event`);
+ }, `${prefixedType} event listener should not trigger if an unprefixed ` +
+ `event handler also exists`);
+
+ promise_test(async t => {
+ // We use a parent/child relationship to be able to register both prefixed
+ // and unprefixed event listeners without the deduplication logic kicking in.
+ const parent = createDiv(t);
+ const child = createDiv(t);
+ parent.appendChild(child);
+ // After moving the child, we have to clean style again.
+ getComputedStyle(child).transition;
+ getComputedStyle(child).width;
+
+ let observedUnprefixedType;
+ addTestScopedEventListener(t, parent, unprefixedType, e => {
+ observedUnprefixedType = e.type;
+ });
+ let observedPrefixedType;
+ addTestScopedEventListener(t, child, prefixedType, e => {
+ observedPrefixedType = e.type;
+ });
+
+ triggerAnimation(child);
+ await waitForEventThenAnimationFrame(t, unprefixedHandler);
+
+ assert_equals(observedUnprefixedType, unprefixedType);
+ assert_equals(observedPrefixedType, prefixedType);
+ }, `event types for prefixed and unprefixed ${unprefixedType} event ` +
+ `listeners should be named appropriately`);
+
+ promise_test(async t => {
+ const div = createDiv(t);
+
+ let receivedEvent = false;
+ addTestScopedEventListener(t, div, prefixedType.toLowerCase(), () => {
+ receivedEvent = true;
+ });
+ addTestScopedEventListener(t, div, prefixedType.toUpperCase(), () => {
+ receivedEvent = true;
+ });
+
+ triggerAnimation(div);
+ await waitForEventThenAnimationFrame(t, unprefixedHandler);
+ assert_false(receivedEvent, `received ${prefixedType} event`);
+ }, `${prefixedType} event listener is case sensitive`);
+}
+
+// Below are utility functions.
+
+// Creates a div element, appends it to the document body and removes the
+// created element during test cleanup.
+function createDiv(test) {
+ const element = document.createElement('div');
+ element.classList.add('baseStyle');
+ document.body.appendChild(element);
+ test.add_cleanup(() => {
+ element.remove();
+ });
+
+ // Flush style before returning. Some browsers only do partial style re-calc,
+ // so ask for all important properties to make sure they are applied.
+ getComputedStyle(element).transition;
+ getComputedStyle(element).width;
+
+ return element;
+}
+
+// Adds an event handler for |handlerName| (calling |callback|) to the given
+// |target|, that will automatically be cleaned up at the end of the test.
+function addTestScopedEventHandler(test, target, handlerName, callback) {
+ assert_regexp_match(
+ handlerName, /^on/, 'Event handler names must start with "on"');
+ assert_equals(target[handlerName], null,
+ `${handlerName} must be supported and not previously set`);
+ target[handlerName] = callback;
+ // We need this cleaned up even if the event handler doesn't run.
+ test.add_cleanup(() => {
+ if (target[handlerName])
+ target[handlerName] = null;
+ });
+}
+
+// Adds an event listener for |type| (calling |callback|) to the given
+// |target|, that will automatically be cleaned up at the end of the test.
+function addTestScopedEventListener(test, target, type, callback) {
+ target.addEventListener(type, callback);
+ // We need this cleaned up even if the event handler doesn't run.
+ test.add_cleanup(() => {
+ target.removeEventListener(type, callback);
+ });
+}
+
+// Returns a promise that will resolve once the passed event (|eventName|) has
+// triggered and one more animation frame has happened. Automatically chooses
+// between an event handler or event listener based on whether |eventName|
+// begins with 'on'.
+//
+// We always listen on window as we don't want to interfere with the test via
+// triggering the prefixed event deduplication logic.
+function waitForEventThenAnimationFrame(test, eventName) {
+ return new Promise((resolve, _) => {
+ const eventFunc = eventName.startsWith('on')
+ ? addTestScopedEventHandler : addTestScopedEventListener;
+ eventFunc(test, window, eventName, () => {
+ // rAF once to give the event under test time to come through.
+ requestAnimationFrame(resolve);
+ });
+ });
+}
diff --git a/testing/web-platform/tests/dom/events/scrolling/iframe-chains.html b/testing/web-platform/tests/dom/events/scrolling/iframe-chains.html
new file mode 100644
index 0000000000..fb7d674aae
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/iframe-chains.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<style>
+
+body { margin: 0; padding: 10px; }
+.space { height: 2000px; }
+
+#scroller {
+ border: 3px solid green;
+ position: absolute;
+ z-index: 0;
+ overflow: auto;
+ padding: 10px;
+ width: 250px;
+ height: 150px;
+}
+
+.ifr {
+ border: 3px solid blue;
+ width: 200px;
+ height: 100px;
+}
+
+</style>
+</head>
+<body>
+<div id=scroller>
+ <iframe srcdoc="SCROLL ME" class=ifr></iframe>
+ <div class=space></div>
+</div>
+<div class=space></div>
+<script>
+
+promise_test(async t => {
+ await new test_driver.Actions().scroll(50, 50, 0, 50).send();
+ // Allow the possibility the scroll is not fully synchronous
+ await t.step_wait(() => scroller.scrollTop === 50);
+}, "Wheel scroll in iframe chains to containing element.");
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/dom/events/scrolling/input-text-scroll-event-when-using-arrow-keys.html b/testing/web-platform/tests/dom/events/scrolling/input-text-scroll-event-when-using-arrow-keys.html
new file mode 100644
index 0000000000..f84e446527
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/input-text-scroll-event-when-using-arrow-keys.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+</head>
+<body onload=runTest()>
+ <p>Moving the cursor using the arrow keys into an
+ input element fires scroll events when text has to scroll into view.
+ Uses arrow keys to move forward and backwards in the input
+ element.</p>
+ <input type="text" style='width: 50px'
+ value="Fooooooooooooooooooooooooooooooooooooooooooooooooo"/>
+ <textarea rows="4" cols="4">
+ Fooooooooooooooooooooooooooooooooooooooooooooooooo
+ </textarea>
+
+ <script>
+ async function moveCursorRightInsideElement(element, value){
+ var arrowRight = '\uE014';
+ for(var i=0;i<value;i++){
+ await test_driver.send_keys(element, arrowRight);
+ }
+ }
+
+ function runTest(){
+ promise_test(async(t) => { return new Promise(async (resolve, reject) => {
+ var input = document.getElementsByTagName('input')[0];
+ function handleScroll(){
+ resolve("Scroll Event successfully fired!");
+ }
+ input.addEventListener('scroll', handleScroll, false);
+ // move cursor to the right until the text scrolls
+ while(input.scrollLeft === 0){
+ await moveCursorRightInsideElement(input, 1);
+ }
+ // if there is no scroll event fired then test will fail by timeout
+ })},
+ /*
+ Moving the cursor using the arrow keys into an input element
+ fires scroll events when text has to scroll into view.
+ Uses arrow keys to move right in the input element.
+ */
+ "Scroll event fired for <input> element.");
+
+ promise_test(async(t) => { return new Promise(async (resolve, reject) => {
+ var textarea = document.getElementsByTagName('textarea')[0];
+ function handleScroll(){
+ resolve("Scroll Event successfully fired!");
+ }
+ textarea.addEventListener('scroll', handleScroll, false);
+ // move cursor to the right until the text scrolls
+ while(textarea.scrollLeft === 0){
+ await moveCursorRightInsideElement(textarea, 1);
+ }
+ // if there is no scroll event fired then test will fail by timeout
+ })},
+ /*
+ Moving the cursor using the arrow keys into a textarea element
+ fires scroll events when text has to scroll into view.
+ Uses arrow keys to move right in the textarea element.
+ */
+ "Scroll event fired for <textarea> element.");
+ }
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/dom/events/scrolling/overscroll-deltas.html b/testing/web-platform/tests/dom/events/scrolling/overscroll-deltas.html
new file mode 100644
index 0000000000..6f0b77f22e
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/overscroll-deltas.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#targetDiv {
+ width: 500px;
+ height: 500px;
+ background: red;
+}
+html, body {
+ /* Prevent any built-in browser overscroll features from consuming the scroll
+ * deltas */
+ overscroll-behavior: none;
+}
+
+</style>
+
+<body style="margin:0;" onload=runTest()>
+<div id="targetDiv">
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var overscrolled_x_deltas = [];
+var overscrolled_y_deltas = [];
+var scrollend_received = false;
+
+function onOverscroll(event) {
+ overscrolled_x_deltas.push(event.deltaX);
+ overscrolled_y_deltas.push(event.deltaY);
+}
+
+function onScrollend(event) {
+ scrollend_received = true;
+}
+
+document.addEventListener("overscroll", onOverscroll);
+document.addEventListener("scrollend", onScrollend);
+
+function runTest() {
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+
+ // Scroll up on target div and wait for the doc to get overscroll event.
+ await touchScrollInTarget(300, target_div, 'up');
+ await waitFor(() => { return scrollend_received; },
+ 'Document did not receive scrollend event.');
+
+ // Even though we request 300 pixels of scroll, the API above doesn't
+ // guarantee how much scroll delta will be generated - different browsers
+ // can consume different amounts for "touch slop" (for example). Ensure the
+ // overscroll reaches at least 100 pixels which is a fairly conservative
+ // value.
+ assert_greater_than(overscrolled_y_deltas.length, 0, "There should be at least one overscroll events when overscrolling.");
+ assert_equals(overscrolled_x_deltas.filter(function(x){ return x!=0; }).length, 0, "The deltaX attribute must be 0 when there is no scrolling in x direction.");
+ assert_less_than_equal(Math.max(...overscrolled_y_deltas), 0, "The deltaY attribute must be <= 0 when there is overscrolling in up direction.");
+ assert_less_than_equal(Math.min(...overscrolled_y_deltas),-100, "The deltaY attribute must be the number of pixels overscrolled.");
+
+ await waitForCompositorCommit();
+ overscrolled_x_deltas = [];
+ overscrolled_y_deltas = [];
+ scrollend_received = false;
+
+ // Scroll left on target div and wait for the doc to get overscroll event.
+ await touchScrollInTarget(300, target_div, 'left');
+ await waitFor(() => { return scrollend_received; },
+ 'Document did not receive scrollend event.');
+
+ // TODO(bokan): It looks like Chrome inappropriately filters some scroll
+ // events despite |overscroll-behavior| being set to none. The overscroll
+ // amount here has been loosened but this should be fixed in Chrome.
+ // https://crbug.com/1112183.
+ assert_greater_than(overscrolled_y_deltas.length, 0, "There should be at least one overscroll events when overscrolling.");
+ assert_equals(overscrolled_y_deltas.filter(function(x){ return x!=0; }).length, 0, "The deltaY attribute must be 0 when there is no scrolling in y direction.");
+ assert_less_than_equal(Math.max(...overscrolled_x_deltas), 0, "The deltaX attribute must be <= 0 when there is overscrolling in left direction.");
+ assert_less_than_equal(Math.min(...overscrolled_x_deltas),-50, "The deltaX attribute must be the number of pixels overscrolled.");
+
+ }, 'Tests that the document gets overscroll event with right deltaX/Y attributes.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-document.html b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-document.html
new file mode 100644
index 0000000000..c054ffca9c
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-document.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var overscrolled_x_delta = 0;
+var overscrolled_y_delta = 0;
+function onOverscroll(event) {
+ assert_false(event.cancelable);
+ // overscroll events are bubbled when the target node is document.
+ assert_true(event.bubbles);
+ overscrolled_x_delta = event.deltaX;
+ overscrolled_y_delta = event.deltaY;
+}
+document.addEventListener("overscroll", onOverscroll);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no overscroll event is sent to target_div.
+ target_div.addEventListener("overscroll",
+ t.unreached_func("target_div got unexpected overscroll event."));
+ await waitForCompositorCommit();
+
+ // Scroll up on target div and wait for the doc to get overscroll event.
+ await touchScrollInTarget(300, target_div, 'up');
+ await waitFor(() => { return overscrolled_y_delta < 0; },
+ 'Document did not receive overscroll event after scroll up on target.');
+ assert_equals(target_div.scrollTop, 0);
+
+ // Scroll left on target div and wait for the doc to get overscroll event.
+ await touchScrollInTarget(300, target_div, 'left');
+ await waitFor(() => { return overscrolled_x_delta < 0; },
+ 'Document did not receive overscroll event after scroll left on target.');
+ assert_equals(target_div.scrollLeft, 0);
+ }, 'Tests that the document gets overscroll event when no element scrolls ' +
+ 'after touch scrolling.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-element-with-overscroll-behavior.html b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-element-with-overscroll-behavior.html
new file mode 100644
index 0000000000..750080e656
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-element-with-overscroll-behavior.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#overscrollXDiv {
+ width: 600px;
+ height: 600px;
+ overflow: scroll;
+ overscroll-behavior-x: contain;
+}
+#overscrollYDiv {
+ width: 500px;
+ height: 500px;
+ overflow: scroll;
+ overscroll-behavior-y: none;
+}
+#targetDiv {
+ width: 400px;
+ height: 400px;
+ overflow: scroll;
+}
+.content {
+ width:800px;
+ height:800px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="overscrollXDiv">
+ <div class=content>
+ <div id="overscrollYDiv">
+ <div class=content>
+ <div id="targetDiv">
+ <div class="content">
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var overscrolled_x_delta = 0;
+var overscrolled_y_delta = 0;
+function onOverscrollX(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ overscrolled_x_delta = event.deltaX;
+}
+function onOverscrollY(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ overscrolled_y_delta = event.deltaY;
+}
+// Test that both "onoverscroll" and addEventListener("overscroll"... work.
+document.getElementById('overscrollXDiv').onoverscroll = onOverscrollX;
+document.getElementById('overscrollYDiv').
+ addEventListener("overscroll", onOverscrollY);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no overscroll event is sent to document or target_div.
+ document.addEventListener("overscroll",
+ t.unreached_func("Document got unexpected overscroll event."));
+ target_div.addEventListener("overscroll",
+ t.unreached_func("target_div got unexpected overscroll event."));
+ await waitForCompositorCommit();
+ // Scroll up on target div and wait for the element with overscroll-y to get
+ // overscroll event.
+ await touchScrollInTarget(300, target_div, 'up');
+ await waitFor(() => { return overscrolled_y_delta < 0; },
+ 'Expected element did not receive overscroll event after scroll up on ' +
+ 'target.');
+ assert_equals(target_div.scrollTop, 0);
+
+ // Scroll left on target div and wait for the element with overscroll-x to
+ // get overscroll event.
+ await touchScrollInTarget(300, target_div, 'left');
+ await waitFor(() => { return overscrolled_x_delta < 0; },
+ 'Expected element did not receive overscroll event after scroll left ' +
+ 'on target.');
+ assert_equals(target_div.scrollLeft, 0);
+ }, 'Tests that the last element in the cut scroll chain gets overscroll ' +
+ 'event when no element scrolls by touch.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-scrolled-element.html b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-scrolled-element.html
new file mode 100644
index 0000000000..cfc782a809
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-scrolled-element.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#scrollableDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="scrollableDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+var scrolling_div = document.getElementById('scrollableDiv');
+var overscrolled_x_delta = 0;
+var overscrolled_y_delta = 0;
+function onOverscroll(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ overscrolled_x_delta = event.deltaX;
+ overscrolled_y_delta = event.deltaY;
+}
+scrolling_div.addEventListener("overscroll", onOverscroll);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no overscroll event is sent to document.
+ document.addEventListener("overscroll",
+ t.unreached_func("Document got unexpected overscroll event."));
+ await waitForCompositorCommit();
+
+ // Do a horizontal scroll and wait for overscroll event.
+ await touchScrollInTarget(300, scrolling_div , 'right');
+ await waitFor(() => { return overscrolled_x_delta > 0; },
+ 'Scroller did not receive overscroll event after horizontal scroll.');
+ assert_equals(scrolling_div.scrollWidth - scrolling_div.scrollLeft,
+ scrolling_div.clientWidth);
+
+ overscrolled_x_delta = 0;
+ overscrolled_y_delta = 0;
+
+ // Do a vertical scroll and wait for overscroll event.
+ await touchScrollInTarget(300, scrolling_div, 'down');
+ await waitFor(() => { return overscrolled_y_delta > 0; },
+ 'Scroller did not receive overscroll event after vertical scroll.');
+ assert_equals(scrolling_div.scrollHeight - scrolling_div.scrollTop,
+ scrolling_div.clientHeight);
+ }, 'Tests that the scrolled element gets overscroll event after fully scrolling by touch.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-window.html b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-window.html
new file mode 100644
index 0000000000..ef5ae3daef
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-window.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var window_received_overscroll = false;
+
+function onOverscroll(event) {
+ assert_false(event.cancelable);
+ // overscroll events targetting document are bubbled to the window.
+ assert_true(event.bubbles);
+ window_received_overscroll = true;
+}
+window.addEventListener("overscroll", onOverscroll);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no overscroll event is sent to target_div.
+ target_div.addEventListener("overscroll",
+ t.unreached_func("target_div got unexpected overscroll event."));
+ // Scroll up on target div and wait for the window to get overscroll event.
+ await touchScrollInTarget(300, target_div, 'up');
+ await waitFor(() => { return window_received_overscroll; },
+ 'Window did not receive overscroll event after scroll up on target.');
+ }, 'Tests that the window gets overscroll event when no element scrolls' +
+ 'after touch scrolling.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scroll_support.js b/testing/web-platform/tests/dom/events/scrolling/scroll_support.js
new file mode 100644
index 0000000000..169393e4c3
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scroll_support.js
@@ -0,0 +1,163 @@
+async function waitForScrollendEvent(test, target, timeoutMs = 500) {
+ return new Promise((resolve, reject) => {
+ const timeoutCallback = test.step_timeout(() => {
+ reject(`No Scrollend event received for target ${target}`);
+ }, timeoutMs);
+ target.addEventListener('scrollend', (evt) => {
+ clearTimeout(timeoutCallback);
+ resolve(evt);
+ }, { once: true });
+ });
+}
+
+const MAX_FRAME = 700;
+const MAX_UNCHANGED_FRAMES = 20;
+
+// Returns a promise that resolves when the given condition is met or rejects
+// after MAX_FRAME animation frames.
+// TODO(crbug.com/1400399): deprecate. We should not use frame based waits in
+// WPT as frame rates may vary greatly in different testing environments.
+function waitFor(condition, error_message = 'Reaches the maximum frames.') {
+ return new Promise((resolve, reject) => {
+ function tick(frames) {
+ // We requestAnimationFrame either for MAX_FRAM frames or until condition
+ // is met.
+ if (frames >= MAX_FRAME)
+ reject(error_message);
+ else if (condition())
+ resolve();
+ else
+ requestAnimationFrame(tick.bind(this, frames + 1));
+ }
+ tick(0);
+ });
+}
+
+// TODO(crbug.com/1400446): Test driver should defer sending events until the
+// browser is ready. Also the term compositor-commit is misleading as not all
+// user-agents use a compositor process.
+function waitForCompositorCommit() {
+ return new Promise((resolve) => {
+ // rAF twice.
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(resolve);
+ });
+ });
+}
+
+// TODO(crbug.com/1400399): Deprecate as frame rates may vary greatly in
+// different test environments.
+function waitForAnimationEnd(getValue) {
+ var last_changed_frame = 0;
+ var last_position = getValue();
+ return new Promise((resolve, reject) => {
+ function tick(frames) {
+ // We requestAnimationFrame either for MAX_FRAME or until
+ // MAX_UNCHANGED_FRAMES with no change have been observed.
+ if (frames >= MAX_FRAME || frames - last_changed_frame > MAX_UNCHANGED_FRAMES) {
+ resolve();
+ } else {
+ current_value = getValue();
+ if (last_position != current_value) {
+ last_changed_frame = frames;
+ last_position = current_value;
+ }
+ requestAnimationFrame(tick.bind(this, frames + 1));
+ }
+ }
+ tick(0);
+ })
+}
+
+// Scrolls in target according to move_path with pauses in between
+function touchScrollInTargetSequentiallyWithPause(target, move_path, pause_time_in_ms = 100) {
+ const test_driver_actions = new test_driver.Actions()
+ .addPointer("pointer1", "touch")
+ .pointerMove(0, 0, {origin: target})
+ .pointerDown();
+
+ const substeps = 5;
+ let x = 0;
+ let y = 0;
+ // Do each move in 5 steps
+ for(let move of move_path) {
+ let step_x = (move.x - x) / substeps;
+ let step_y = (move.y - y) / substeps;
+ for(let step = 0; step < substeps; step++) {
+ x += step_x;
+ y += step_y;
+ test_driver_actions.pointerMove(x, y, {origin: target});
+ }
+ test_driver_actions.pause(pause_time_in_ms);
+ }
+
+ return test_driver_actions.pointerUp().send();
+}
+
+function touchScrollInTarget(pixels_to_scroll, target, direction, pause_time_in_ms = 100) {
+ var x_delta = 0;
+ var y_delta = 0;
+ const num_movs = 5;
+ if (direction == "down") {
+ y_delta = -1 * pixels_to_scroll / num_movs;
+ } else if (direction == "up") {
+ y_delta = pixels_to_scroll / num_movs;
+ } else if (direction == "right") {
+ x_delta = -1 * pixels_to_scroll / num_movs;
+ } else if (direction == "left") {
+ x_delta = pixels_to_scroll / num_movs;
+ } else {
+ throw("scroll direction '" + direction + "' is not expected, direction should be 'down', 'up', 'left' or 'right'");
+ }
+ return new test_driver.Actions()
+ .addPointer("pointer1", "touch")
+ .pointerMove(0, 0, {origin: target})
+ .pointerDown()
+ .pointerMove(x_delta, y_delta, {origin: target})
+ .pointerMove(2 * x_delta, 2 * y_delta, {origin: target})
+ .pointerMove(3 * x_delta, 3 * y_delta, {origin: target})
+ .pointerMove(4 * x_delta, 4 * y_delta, {origin: target})
+ .pointerMove(5 * x_delta, 5 * y_delta, {origin: target})
+ .pause(pause_time_in_ms)
+ .pointerUp()
+ .send();
+}
+
+// Trigger fling by doing pointerUp right after pointerMoves.
+function touchFlingInTarget(pixels_to_scroll, target, direction) {
+ touchScrollInTarget(pixels_to_scroll, target, direction, 0 /* pause_time */);
+}
+
+function mouseActionsInTarget(target, origin, delta, pause_time_in_ms = 100) {
+ return new test_driver.Actions()
+ .addPointer("pointer1", "mouse")
+ .pointerMove(origin.x, origin.y, { origin: target })
+ .pointerDown()
+ .pointerMove(origin.x + delta.x, origin.y + delta.y, { origin: target })
+ .pointerMove(origin.x + delta.x * 2, origin.y + delta.y * 2, { origin: target })
+ .pause(pause_time_in_ms)
+ .pointerUp()
+ .send();
+}
+
+// Returns a promise that resolves when the given condition holds for 10
+// animation frames or rejects if the condition changes to false within 10
+// animation frames.
+// TODO(crbug.com/1400399): Deprecate as frame rates may very greatly in
+// different test environments.
+function conditionHolds(condition, error_message = 'Condition is not true anymore.') {
+ const MAX_FRAME = 10;
+ return new Promise((resolve, reject) => {
+ function tick(frames) {
+ // We requestAnimationFrame either for 10 frames or until condition is
+ // violated.
+ if (frames >= MAX_FRAME)
+ resolve();
+ else if (!condition())
+ reject(error_message);
+ else
+ requestAnimationFrame(tick.bind(this, frames + 1));
+ }
+ tick(0);
+ });
+}
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-sequence-of-scrolls.tentative.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-sequence-of-scrolls.tentative.html
new file mode 100644
index 0000000000..77bf029ced
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-sequence-of-scrolls.tentative.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 500px;
+ height: 4000px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+const target_div = document.getElementById('targetDiv');
+let scrollend_arrived = false;
+let scrollend_event_count = 0;
+
+function onScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ scrollend_arrived = true;
+ scrollend_event_count += 1;
+}
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no scrollend event is sent to document.
+ document.addEventListener("scrollend",
+ t.unreached_func("document got unexpected scrollend event."));
+ await waitForCompositorCommit();
+
+ // Scroll down & up & down on target div and wait for the target_div to get scrollend event.
+ target_div.addEventListener("scrollend", onScrollEnd);
+ const move_path = [
+ { x: 0, y: -300}, // down
+ { x: 0, y: -100}, // up
+ { x: 0, y: -400}, // down
+ { x: 0, y: -200}, // up
+ ];
+ await touchScrollInTargetSequentiallyWithPause(target_div, move_path, 150);
+
+ await waitFor(() => {return scrollend_arrived;},
+ 'target_div did not receive scrollend event after sequence of scrolls on target.');
+ assert_equals(scrollend_event_count, 1);
+ }, "Move down, up and down again, receive scrollend event only once");
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-snap.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-snap.html
new file mode 100644
index 0000000000..03079ddc6c
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-snap.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+div {
+ position: absolute;
+}
+#scroller {
+ width: 500px;
+ height: 500px;
+ overflow: scroll;
+ scroll-snap-type: both mandatory;
+ border: solid black 5px;
+}
+#space {
+ width: 2000px;
+ height: 2000px;
+}
+.target {
+ width: 200px;
+ height: 200px;
+ scroll-snap-align: start;
+ background-color: blue;
+}
+</style>
+
+<body style="margin:0" onload=runTests()>
+ <div id="scroller">
+ <div id="space"></div>
+ <div class="target" style="left: 0px; top: 0px;"></div>
+ <div class="target" style="left: 80px; top: 80px;"></div>
+ <div class="target" style="left: 200px; top: 200px;"></div>
+ </div>
+</body>
+
+<script>
+var scroller = document.getElementById("scroller");
+var space = document.getElementById("space");
+const MAX_FRAME_COUNT = 700;
+const MAX_UNCHANGED_FRAME = 20;
+
+function scrollTop() {
+ return scroller.scrollTop;
+}
+
+var scroll_arrived_after_scroll_end = false;
+var scroll_end_arrived = false;
+scroller.addEventListener("scroll", () => {
+ if (scroll_end_arrived)
+ scroll_arrived_after_scroll_end = true;
+});
+scroller.addEventListener("scrollend", () => {
+ scroll_end_arrived = true;
+});
+
+function runTests() {
+ promise_test (async () => {
+ await waitForCompositorCommit();
+ await touchScrollInTarget(100, scroller, 'down');
+ // Wait for the scroll snap animation to finish.
+ await waitForAnimationEnd(scrollTop);
+ await waitFor(() => { return scroll_end_arrived; });
+ // Verify that scroll snap animation has finished before firing scrollend event.
+ assert_false(scroll_arrived_after_scroll_end);
+ }, "Tests that scrollend is fired after scroll snap animation completion.");
+
+ promise_test (async () => {
+ // Reset scroll state.
+ scroller.scrollTo(0, 0);
+ await waitForCompositorCommit();
+ scroll_end_arrived = false;
+ scroll_arrived_after_scroll_end = false;
+
+ await touchFlingInTarget(50, scroller, 'down');
+ // Wait for the scroll snap animation to finish.
+ await waitForAnimationEnd(scrollTop);
+ await waitFor(() => { return scroll_end_arrived; });
+ // Verify that scroll snap animation has finished before firing scrollend event.
+ assert_false(scroll_arrived_after_scroll_end);
+ }, "Tests that scrollend is fired after fling snap animation completion.");
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-programmatic-scroll.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-programmatic-scroll.html
new file mode 100644
index 0000000000..c6569e0beb
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-programmatic-scroll.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<meta name="timeout" content="long">
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+html {
+ height: 3000px;
+ width: 3000px;
+}
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+<script>
+var element_scrollend_arrived = false;
+var document_scrollend_arrived = false;
+
+function onElementScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ element_scrollend_arrived = true;
+}
+
+function onDocumentScrollEnd(event) {
+ assert_false(event.cancelable);
+ // scrollend events are bubbled when the target node is document.
+ assert_true(event.bubbles);
+ document_scrollend_arrived = true;
+}
+
+function callScrollFunction([scrollTarget, scrollFunction, args]) {
+ scrollTarget[scrollFunction](args);
+}
+
+function runTest() {
+ let root_element = document.scrollingElement;
+ let target_div = document.getElementById("targetDiv");
+
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+ target_div.addEventListener("scrollend", onElementScrollEnd);
+ document.addEventListener("scrollend", onDocumentScrollEnd);
+
+ let test_cases = [
+ [target_div, 200, 200, [target_div, "scrollTo", { top: 200, left: 200, behavior: "auto" }]],
+ [target_div, 0, 0, [target_div, "scrollTo", { top: 0, left: 0, behavior: "smooth" }]],
+ [root_element, 200, 200, [root_element, "scrollTo", { top: 200, left: 200, behavior: "auto" }]],
+ [root_element, 0, 0, [root_element, "scrollTo", { top: 0, left: 0, behavior: "smooth" }]],
+ [target_div, 200, 200, [target_div, "scrollBy", { top: 200, left: 200, behavior: "auto" }]],
+ [target_div, 0, 0, [target_div, "scrollBy", { top: -200, left: -200, behavior: "smooth" }]],
+ [root_element, 200, 200, [root_element, "scrollBy", { top: 200, left: 200, behavior: "auto" }]],
+ [root_element, 0, 0, [root_element, "scrollBy", { top: -200, left: -200, behavior: "smooth" }]]
+ ];
+
+ for(i = 0; i < test_cases.length; i++) {
+ let t = test_cases[i];
+ let target = t[0];
+ let expected_x = t[1];
+ let expected_y = t[2];
+ let scroll_datas = t[3];
+
+ callScrollFunction(scroll_datas);
+ await waitFor(() => { return element_scrollend_arrived || document_scrollend_arrived; }, target.tagName + "." + scroll_datas[1] + " did not receive scrollend event.");
+ if (target == root_element)
+ assert_false(element_scrollend_arrived);
+ else
+ assert_false(document_scrollend_arrived);
+ assert_equals(target.scrollLeft, expected_x, target.tagName + "." + scroll_datas[1] + " scrollLeft");
+ assert_equals(target.scrollTop, expected_y, target.tagName + "." + scroll_datas[1] + " scrollTop");
+
+ element_scrollend_arrived = false;
+ document_scrollend_arrived = false;
+ }
+ }, "Tests scrollend event for calling scroll functions.");
+
+ promise_test(async (t) => {
+ await waitForCompositorCommit();
+
+ let test_cases = [
+ [target_div, "scrollTop"],
+ [target_div, "scrollLeft"],
+ [root_element, "scrollTop"],
+ [root_element, "scrollLeft"]
+ ];
+ for (i = 0; i < test_cases.length; i++) {
+ let t = test_cases[i];
+ let target = t[0];
+ let attribute = t[1];
+ let position = 200;
+
+ target.style.scrollBehavior = "smooth";
+ target[attribute] = position;
+ await waitFor(() => { return element_scrollend_arrived || document_scrollend_arrived; }, target.tagName + "." + attribute + " did not receive scrollend event.");
+ if (target == root_element)
+ assert_false(element_scrollend_arrived);
+ else
+ assert_false(document_scrollend_arrived);
+ assert_equals(target[attribute], position, target.tagName + "." + attribute + " ");
+ element_scrollend_arrived = false;
+ document_scrollend_arrived = false;
+
+ await waitForCompositorCommit();
+ target.style.scrollBehavior = "auto";
+ target[attribute] = 0;
+ await waitFor(() => { return element_scrollend_arrived || document_scrollend_arrived; }, target.tagName + "." + attribute + " did not receive scrollend event.");
+ if (target == root_element)
+ assert_false(element_scrollend_arrived);
+ else
+ assert_false(document_scrollend_arrived);
+ assert_equals(target[attribute], 0, target.tagName + "." + attribute + " ");
+ element_scrollend_arrived = false;
+ document_scrollend_arrived = false;
+ }
+ }, "Tests scrollend event for changing scroll attributes.");
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-scrollIntoView.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-scrollIntoView.html
new file mode 100644
index 0000000000..8782b1dfee
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-scrollIntoView.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<meta name="timeout" content="long">
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+html {
+ height: 3000px;
+ width: 3000px;
+}
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+<script>
+var element_scrollend_arrived = false;
+var document_scrollend_arrived = false;
+
+function onElementScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ element_scrollend_arrived = true;
+}
+
+function onDocumentScrollEnd(event) {
+ assert_false(event.cancelable);
+ // scrollend events are bubbled when the target node is document.
+ assert_true(event.bubbles);
+ document_scrollend_arrived = true;
+}
+
+function callScrollFunction([scrollTarget, scrollFunction, args]) {
+ scrollTarget[scrollFunction](args);
+}
+
+function runTest() {
+ let root_element = document.scrollingElement;
+ let target_div = document.getElementById("targetDiv");
+ let inner_div = document.getElementById("innerDiv");
+
+ // Get expected position for root_element scrollIntoView.
+ root_element.scrollTo(10000, 10000);
+ let max_root_x = root_element.scrollLeft;
+ let max_root_y = root_element.scrollTop;
+ root_element.scrollTo(0, 0);
+
+ target_div.scrollTo(10000, 10000);
+ let max_element_x = target_div.scrollLeft;
+ let max_element_y = target_div.scrollTop;
+ target_div.scrollTo(0, 0);
+
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+ target_div.addEventListener("scrollend", onElementScrollEnd);
+ document.addEventListener("scrollend", onDocumentScrollEnd);
+
+ let test_cases = [
+ [target_div, max_element_x, max_element_y, [inner_div, "scrollIntoView", { inline: "end", block: "end", behavior: "auto" }]],
+ [target_div, 0, 0, [inner_div, "scrollIntoView", { inline: "start", block: "start", behavior: "smooth" }]],
+ [root_element, max_root_x, max_root_y, [root_element, "scrollIntoView", { inline: "end", block: "end", behavior: "smooth" }]],
+ [root_element, 0, 0, [root_element, "scrollIntoView", { inline: "start", block: "start", behavior: "smooth" }]]
+ ];
+
+ for(i = 0; i < test_cases.length; i++) {
+ let t = test_cases[i];
+ let target = t[0];
+ let expected_x = t[1];
+ let expected_y = t[2];
+ let scroll_datas = t[3];
+
+ callScrollFunction(scroll_datas);
+ await waitFor(() => { return element_scrollend_arrived || document_scrollend_arrived; }, target.tagName + ".scrollIntoView did not receive scrollend event.");
+ if (target == root_element)
+ assert_false(element_scrollend_arrived);
+ else
+ assert_false(document_scrollend_arrived);
+ assert_equals(target.scrollLeft, expected_x, target.tagName + ".scrollIntoView scrollLeft");
+ assert_equals(target.scrollTop, expected_y, target.tagName + ".scrollIntoView scrollTop");
+
+ element_scrollend_arrived = false;
+ document_scrollend_arrived = false;
+ }
+ }, "Tests scrollend event for scrollIntoView.");
+
+ promise_test(async (t) => {
+ document.body.removeChild(target_div);
+ let out_div = document.createElement("div");
+ out_div.style = "width: 100px; height:100px; overflow:scroll; scroll-behavior:smooth;";
+ out_div.appendChild(target_div);
+ document.body.appendChild(out_div);
+ await waitForCompositorCommit();
+
+ element_scrollend_arrived = false;
+ document_scrollend_arrived = false;
+ inner_div.scrollIntoView({ inline: "end", block: "end", behavior: "auto" });
+ await waitFor(() => { return element_scrollend_arrived || document_scrollend_arrived; }, "Nested scrollIntoView did not receive scrollend event.");
+ assert_equals(root_element.scrollLeft, 0, "Nested scrollIntoView root_element scrollLeft");
+ assert_equals(root_element.scrollTop, 0, "Nested scrollIntoView root_element scrollTop");
+ assert_equals(out_div.scrollLeft, 100, "Nested scrollIntoView out_div scrollLeft");
+ assert_equals(out_div.scrollTop, 100, "Nested scrollIntoView out_div scrollTop");
+ assert_equals(target_div.scrollLeft, max_element_x, "Nested scrollIntoView target_div scrollLeft");
+ assert_equals(target_div.scrollTop, max_element_y, "Nested scrollIntoView target_div scrollTop");
+ assert_false(document_scrollend_arrived);
+ }, "Tests scrollend event for nested scrollIntoView.");
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-document.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-document.html
new file mode 100644
index 0000000000..3090455388
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-document.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var horizontal_scrollend_arrived = false;
+var vertical_scrollend_arrived = false;
+function onHorizontalScrollEnd(event) {
+ assert_false(event.cancelable);
+ // scrollend events are bubbled when the target node is document.
+ assert_true(event.bubbles);
+ horizontal_scrollend_arrived = true;
+}
+function onVerticalScrollEnd(event) {
+ assert_false(event.cancelable);
+ // scrollend events are bubbled when the target node is document.
+ assert_true(event.bubbles);
+ vertical_scrollend_arrived = true;
+}
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no scrollend event is sent to target_div.
+ target_div.addEventListener("scrollend",
+ t.unreached_func("target_div got unexpected scrollend event."));
+ await waitForCompositorCommit();
+
+ // Scroll left on target div and wait for the doc to get scrollend event.
+ document.addEventListener("scrollend", onHorizontalScrollEnd);
+ await touchScrollInTarget(300, target_div, 'left');
+ await waitFor(() => { return horizontal_scrollend_arrived; },
+ 'Document did not receive scrollend event after scroll left on target.');
+ assert_equals(target_div.scrollLeft, 0);
+ document.removeEventListener("scrollend", onHorizontalScrollEnd);
+
+ // Scroll up on target div and wait for the doc to get scrollend event.
+ document.addEventListener("scrollend", onVerticalScrollEnd);
+ await touchScrollInTarget(300, target_div, 'up');
+ await waitFor(() => { return vertical_scrollend_arrived; },
+ 'Document did not receive scrollend event after scroll up on target.');
+ assert_equals(target_div.scrollTop, 0);
+ }, 'Tests that the document gets scrollend event when no element scrolls by ' +
+ 'touch.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-element-with-overscroll-behavior.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-element-with-overscroll-behavior.html
new file mode 100644
index 0000000000..acad168e56
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-element-with-overscroll-behavior.html
@@ -0,0 +1,102 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#overscrollXDiv {
+ width: 600px;
+ height: 600px;
+ overflow: scroll;
+ overscroll-behavior-x: contain;
+}
+#overscrollYDiv {
+ width: 500px;
+ height: 500px;
+ overflow: scroll;
+ overscroll-behavior-y: none;
+}
+#targetDiv {
+ width: 400px;
+ height: 400px;
+ overflow: scroll;
+}
+.content {
+ width:800px;
+ height:800px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="overscrollXDiv">
+ <div class=content>
+ <div id="overscrollYDiv">
+ <div class=content>
+ <div id="targetDiv">
+ <div class="content">
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var horizontal_scrollend_arrived = false;
+var vertical_scrollend_arrived = false;
+function onHorizontalScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ horizontal_scrollend_arrived = true;
+}
+function onVerticalScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ vertical_scrollend_arrived = true;
+}
+// Test that both "onscrollend" and addEventListener("scrollend"... work.
+document.getElementById('overscrollXDiv').onscrollend = onHorizontalScrollEnd;
+document.getElementById('overscrollYDiv').
+ addEventListener("scrollend", onVerticalScrollEnd);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no scrollend event is sent to document or target_div.
+ document.addEventListener("scrollend",
+ t.unreached_func("Document got unexpected scrollend event."));
+ target_div.addEventListener("scrollend",
+ t.unreached_func("target_div got unexpected scrollend event."));
+ await waitForCompositorCommit();
+
+ // Scroll left on target div and wait for the element with overscroll-x to
+ // get scrollend event.
+ await touchScrollInTarget(300, target_div, 'left');
+ await waitFor(() => { return horizontal_scrollend_arrived; },
+ 'Expected element did not receive scrollend event after scroll left ' +
+ 'on target.');
+ assert_equals(target_div.scrollLeft, 0);
+
+ let touchEndPromise = new Promise((resolve) => {
+ target_div.addEventListener("touchend", resolve);
+ });
+ await touchScrollInTarget(300, target_div, 'up');
+
+ // The scrollend event should never be fired before the gesture has completed.
+ await touchEndPromise;
+
+ // Ensure we wait at least a tick after the touch end.
+ await waitForCompositorCommit();
+
+ // We should not trigger a scrollend event for a scroll that did not change
+ // the scroll position.
+ assert_equals(vertical_scrollend_arrived, false);
+ assert_equals(target_div.scrollTop, 0);
+ }, 'Tests that the last element in the cut scroll chain gets scrollend ' +
+ 'event when no element scrolls by touch.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-scrolled-element.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-scrolled-element.html
new file mode 100644
index 0000000000..7343396942
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-scrolled-element.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#scrollableDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="scrollableDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+var scrolling_div = document.getElementById('scrollableDiv');
+var horizontal_scrollend_arrived = false;
+var vertical_scrollend_arrived = false;
+function onHorizontalScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ horizontal_scrollend_arrived = true;
+}
+function onVerticalScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ vertical_scrollend_arrived = true;
+}
+scrolling_div.addEventListener("scrollend", onHorizontalScrollEnd);
+scrolling_div.addEventListener("scrollend", onVerticalScrollEnd);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no scrollend event is sent to document.
+ document.addEventListener("scrollend",
+ t.unreached_func("Document got unexpected scrollend event."));
+ await waitForCompositorCommit();
+
+ // Do a horizontal scroll and wait for scrollend event.
+ await touchScrollInTarget(300, scrolling_div, 'right');
+ await waitFor(() => { return horizontal_scrollend_arrived; },
+ 'Scroller did not receive scrollend event after horizontal scroll.');
+ assert_equals(scrolling_div.scrollWidth - scrolling_div.scrollLeft,
+ scrolling_div.clientWidth);
+
+ // Do a vertical scroll and wait for scrollend event.
+ await touchScrollInTarget(300, scrolling_div, 'down');
+ await waitFor(() => { return vertical_scrollend_arrived; },
+ 'Scroller did not receive scrollend event after vertical scroll.');
+ assert_equals(scrolling_div.scrollHeight - scrolling_div.scrollTop,
+ scrolling_div.clientHeight);
+ }, 'Tests that the scrolled element gets scrollend event at the end of touch scrolling.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-window.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-window.html
new file mode 100644
index 0000000000..ef72f56d2b
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-window.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var scrollend_arrived = false;
+function onScrollEnd(event) {
+ assert_false(event.cancelable);
+ // scrollend events targetting document are bubbled to the window.
+ assert_true(event.bubbles);
+ scrollend_arrived = true;
+}
+window.addEventListener("scrollend", onScrollEnd);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no scrollend event is sent to target_div.
+ target_div.addEventListener("scrollend",
+ t.unreached_func("target_div got unexpected scrollend event."));
+ await waitForCompositorCommit();
+
+ // Scroll up on target div and wait for the doc to get scrollend event.
+ await touchScrollInTarget(300, target_div, 'up');
+ await waitFor(() => { return scrollend_arrived; },
+ 'Window did not receive scrollend event after scroll up on target.');
+ assert_equals(target_div.scrollTop, 0);
+ }, 'Tests that the window gets scrollend event when no element scrolls ' +
+ 'after touch scrolling.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-for-user-scroll.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-for-user-scroll.html
new file mode 100644
index 0000000000..5146c5f719
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-for-user-scroll.html
@@ -0,0 +1,199 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+ #targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ }
+
+ #innerDiv {
+ width: 400px;
+ height: 400px;
+ }
+</style>
+</head>
+<body style="margin:0" onload=runTest()>
+ <div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+ </div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+
+async function resetTargetScrollState(test) {
+ if (target_div.scrollTop != 0 || target_div.scrollLeft != 0) {
+ target_div.scrollTop = 0;
+ target_div.scrollLeft = 0;
+ return waitForScrollendEvent(test, target_div);
+ }
+}
+
+async function verifyScrollStopped(test) {
+ const unscaled_pause_time_in_ms = 100;
+ const x = target_div.scrollLeft;
+ const y = target_div.scrollTop;
+ return new Promise(resolve => {
+ test.step_timeout(() => {
+ assert_equals(x, target_div.scrollLeft);
+ assert_equals(y, target_div.scrollTop);
+ resolve();
+ }, unscaled_pause_time_in_ms);
+ });
+}
+
+async function verifyNoScrollendOnDocument(test) {
+ const callback =
+ test.unreached_func("window got unexpected scrollend event.");
+ window.addEventListener('scrollend', callback);
+}
+
+async function createScrollendPromise(test) {
+ return waitForScrollendEvent(test, target_div).then(evt => {
+ assert_false(evt.cancelable, 'Event is not cancelable');
+ assert_false(evt.bubbles, 'Event targeting element does not bubble');
+ });
+}
+
+function runTest() {
+ promise_test(async (t) => {
+ // Skip the test on a Mac as they do not support touch screens.
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC')>=0;
+ if (isMac)
+ return;
+
+ await resetTargetScrollState(t);
+ await waitForCompositorCommit();
+
+ const targetScrollendPromise = createScrollendPromise(t);
+ verifyNoScrollendOnDocument(t);
+
+ // Perform a touch drag on target div and wait for target_div to get
+ // a scrollend event.
+ await new test_driver.Actions()
+ .addPointer('TestPointer', 'touch')
+ .pointerMove(0, 0, {origin: target_div}) // 0, 0 is center of element.
+ .pointerDown()
+ .addTick()
+ .pointerMove(0, -40, {origin: target_div}) // Drag up to move down.
+ .addTick()
+ .pause(200) // Prevent inertial scroll.
+ .pointerUp()
+ .send();
+
+ await targetScrollendPromise;
+
+ assert_true(target_div.scrollTop > 0);
+ await verifyScrollStopped(t);
+ }, 'Tests that the target_div gets scrollend event when touch dragging.');
+
+ promise_test(async (t) => {
+ // Skip test on platforms that do not have a visible scrollbar (e.g.
+ // overlay scrollbar).
+ const scrollbar_width = target_div.offsetWidth - target_div.clientWidth;
+ if (scrollbar_width == 0)
+ return;
+
+ await resetTargetScrollState(t);
+ await waitForCompositorCommit();
+
+ const targetScrollendPromise = createScrollendPromise(t);
+ verifyNoScrollendOnDocument(t);
+
+ const bounds = target_div.getBoundingClientRect();
+ const x = bounds.right - scrollbar_width / 2;
+ const y = bounds.bottom - 20;
+ await new test_driver.Actions()
+ .addPointer('TestPointer', 'mouse')
+ .pointerMove(x, y, {origin: 'viewport'})
+ .pointerDown()
+ .addTick()
+ .pointerUp()
+ .send();
+
+ await targetScrollendPromise;
+ assert_true(target_div.scrollTop > 0);
+ await verifyScrollStopped(t);
+ }, 'Tests that the target_div gets scrollend event when clicking ' +
+ 'scrollbar.');
+
+ // Same issue as previous test.
+ promise_test(async (t) => {
+ // Skip test on platforms that do not have a visible scrollbar (e.g.
+ // overlay scrollbar).
+ const scrollbar_width = target_div.offsetWidth - target_div.clientWidth;
+ if (scrollbar_width == 0)
+ return;
+
+ resetTargetScrollState(t);
+ const targetScrollendPromise = createScrollendPromise(t);
+ verifyNoScrollendOnDocument(t);
+
+ const bounds = target_div.getBoundingClientRect();
+ const x = bounds.right - scrollbar_width / 2;
+ const y = bounds.top + 30;
+ const dy = 30;
+ await new test_driver.Actions()
+ .addPointer('TestPointer', 'mouse')
+ .pointerMove(x, y, { origin: 'viewport' })
+ .pointerDown()
+ .pointerMove(x, y + dy, { origin: 'viewport' })
+ .addTick()
+ .pointerUp()
+ .send();
+
+ await targetScrollendPromise;
+ assert_true(target_div.scrollTop > 0);
+ await verifyScrollStopped(t);
+ }, 'Tests that the target_div gets scrollend event when dragging the ' +
+ 'scrollbar thumb.');
+
+ promise_test(async (t) => {
+ resetTargetScrollState(t);
+ const targetScrollendPromise = createScrollendPromise(t);
+ verifyNoScrollendOnDocument(t);
+
+ const x = 0;
+ const y = 0;
+ const dx = 0;
+ const dy = 40;
+ const duration_ms = 10;
+ await new test_driver.Actions()
+ .scroll(x, y, dx, dy, { origin: target_div }, duration_ms)
+ .send();
+
+ await targetScrollendPromise;
+ assert_true(target_div.scrollTop > 0);
+ await verifyScrollStopped(t);
+ }, 'Tests that the target_div gets scrollend event when mouse wheel ' +
+ 'scrolling.');
+
+ promise_test(async (t) => {
+ await resetTargetScrollState(t);
+ await waitForCompositorCommit();
+
+ verifyNoScrollendOnDocument(t);
+ const targetScrollendPromise = createScrollendPromise(t);
+
+ target_div.focus();
+ window.test_driver.send_keys(target_div, '\ue015');
+
+ await targetScrollendPromise;
+ assert_true(target_div.scrollTop > 0);
+ await verifyScrollStopped(t);
+ }, 'Tests that the target_div gets scrollend event when sending DOWN key ' +
+ 'to the target.');
+}
+
+</script>
+</html>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-handler-content-attributes.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-handler-content-attributes.html
new file mode 100644
index 0000000000..47f563c39b
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-handler-content-attributes.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+html, body {
+ margin: 0
+}
+
+body {
+ height: 3000px;
+ width: 3000px;
+}
+
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body onload=runTest() onscrollend="failOnScrollEnd(event)">
+<div id="targetDiv" onscrollend="onElementScrollEnd(event)">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+<script>
+let element_scrollend_arrived = false;
+
+function onElementScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ element_scrollend_arrived = true;
+}
+
+function failOnScrollEnd(event) {
+ assert_true(false, "Scrollend should not be called on: " + event.target);
+}
+
+function runTest() {
+ let target_div = document.getElementById("targetDiv");
+
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+
+ target_div.scrollTo({top: 200, left: 200});
+ await waitFor(() => { return element_scrollend_arrived; },
+ target_div.tagName + " did not receive scrollend event.");
+ assert_equals(target_div.scrollLeft, 200, target_div.tagName + " scrollLeft");
+ assert_equals(target_div.scrollTop, 200, target_div.tagName + " scrollTop");
+ }, "Tests scrollend event is handled by event handler content attribute.");
+
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+
+ document.scrollingElement.scrollTo({top: 200, left: 200});
+ // The document body onscrollend event handler content attribute will fail
+ // here, if it is fired.
+ await waitForCompositorCommit();
+ assert_equals(document.scrollingElement.scrollLeft, 200,
+ "Document scrolled on horizontal axis");
+ assert_equals(document.scrollingElement.scrollTop, 200,
+ "Document scrolled on vertical axis");
+ }, "Tests scrollend event is not fired to document body event handler content attribute.");
+
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+
+ // Reset the scroll position.
+ document.scrollingElement.scrollTo({top: 0, left: 0});
+
+ let scrollend_event = new Promise(resolve => document.onscrollend = resolve);
+ document.scrollingElement.scrollTo({top: 200, left: 200});
+ await scrollend_event;
+
+ assert_equals(document.scrollingElement.scrollTop, 200,
+ "Document scrolled on horizontal axis");
+ assert_equals(document.scrollingElement.scrollLeft, 200,
+ "Document scrolled on vertical axis");
+ }, "Tests scrollend event is fired to document event handler property");
+
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+
+ // Reset the scroll position.
+ target_div.scrollTo({top: 0, left: 0});
+
+ let scrollend_event = new Promise(resolve => target_div.onscrollend = resolve);
+ target_div.scrollTo({top: 200, left: 200});
+ await scrollend_event;
+
+ assert_equals(target_div.scrollLeft, 200,
+ target_div.tagName + " scrolled on horizontal axis");
+ assert_equals(target_div.scrollLeft, 200,
+ target_div.tagName + " scrolled on vertical axis");
+ }, "Tests scrollend event is fired to element event handler property");
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-not-fired-after-removing-scroller.tentative.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-not-fired-after-removing-scroller.tentative.html
new file mode 100644
index 0000000000..95447fbd12
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-not-fired-after-removing-scroller.tentative.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#rootDiv {
+ width: 500px;
+ height: 500px;
+}
+
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 500px;
+ height: 4000px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+</body>
+
+<script>
+let scrollend_arrived = false;
+
+async function setupHtmlAndScrollAndRemoveElement(element_to_remove_id) {
+ document.body.innerHTML=`
+ <div id="rootDiv">
+ <div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+ </div>
+ </div>
+ `;
+ await waitForCompositorCommit();
+
+ const target_div = document.getElementById('targetDiv');
+ const element_to_remove = document.getElementById(element_to_remove_id);
+ let reached_half_scroll = false;
+ scrollend_arrived = false;
+
+ target_div.addEventListener("scrollend", () => {
+ scrollend_arrived = true;
+ });
+
+ target_div.onscroll = () => {
+ // Remove the element after reached half of the scroll offset,
+ if(target_div.scrollTop >= 1000) {
+ reached_half_scroll = true;
+ element_to_remove.remove();
+ }
+ };
+
+ target_div.scrollTo({top:2000, left:0, behavior:"smooth"});
+ await waitFor(() => {return reached_half_scroll; },
+ "target_div never reached scroll offset of 1000");
+ await waitForCompositorCommit();
+}
+
+function runTest() {
+ promise_test (async (t) => {
+ await setupHtmlAndScrollAndRemoveElement("rootDiv");
+ await conditionHolds(() => { return !scrollend_arrived; });
+ }, "No scrollend is received after removing parent div");
+
+ promise_test (async (t) => {
+ await setupHtmlAndScrollAndRemoveElement("targetDiv");
+ await conditionHolds(() => { return !scrollend_arrived; });
+ }, "No scrollend is received after removing scrolling element");
+
+ promise_test (async (t) => {
+ await setupHtmlAndScrollAndRemoveElement("innerDiv");
+ await waitFor(() => { return scrollend_arrived; },
+ 'target_div did not receive scrollend event after vertical scroll.');
+ }, "scrollend is received after removing descendant div");
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/shadow-relatedTarget.html b/testing/web-platform/tests/dom/events/shadow-relatedTarget.html
new file mode 100644
index 0000000000..713555b7d8
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/shadow-relatedTarget.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!--
+ This test is adopted from Olli Pettay's test case at
+ http://mozilla.pettay.fi/shadow_focus.html
+-->
+<div id="host"></div>
+<input id="lightInput">
+<script>
+const root = host.attachShadow({ mode: "closed" });
+root.innerHTML = "<input id='shadowInput'>";
+
+async_test((test) => {
+ root.getElementById("shadowInput").focus();
+ window.addEventListener("focus", test.step_func_done((e) => {
+ assert_equals(e.relatedTarget, host);
+ }, "relatedTarget should be pointing to shadow host."), true);
+ lightInput.focus();
+}, "relatedTarget should not leak at capturing phase, at window object.");
+
+async_test((test) => {
+ root.getElementById("shadowInput").focus();
+ lightInput.addEventListener("focus", test.step_func_done((e) => {
+ assert_equals(e.relatedTarget, host);
+ }, "relatedTarget should be pointing to shadow host."), true);
+ lightInput.focus();
+}, "relatedTarget should not leak at target.");
+
+</script>
diff --git a/testing/web-platform/tests/dom/events/webkit-animation-end-event.html b/testing/web-platform/tests/dom/events/webkit-animation-end-event.html
new file mode 100644
index 0000000000..4186f6b7a9
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/webkit-animation-end-event.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Prefixed CSS Animation end events</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-listener-invoke">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script src="resources/prefixed-animation-event-tests.js"></script>
+<script>
+'use strict';
+
+runAnimationEventTests({
+ unprefixedType: 'animationend',
+ prefixedType: 'webkitAnimationEnd',
+ animationCssStyle: '1ms',
+});
+</script>
diff --git a/testing/web-platform/tests/dom/events/webkit-animation-iteration-event.html b/testing/web-platform/tests/dom/events/webkit-animation-iteration-event.html
new file mode 100644
index 0000000000..fb251972a3
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/webkit-animation-iteration-event.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Prefixed CSS Animation iteration events</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-listener-invoke">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script src="resources/prefixed-animation-event-tests.js"></script>
+<script>
+'use strict';
+
+runAnimationEventTests({
+ unprefixedType: 'animationiteration',
+ prefixedType: 'webkitAnimationIteration',
+ // Use a long duration to avoid missing the animation due to slow machines,
+ // but set a negative delay so that the iteration boundary happens shortly
+ // after the animation starts.
+ animationCssStyle: '100s -99.9s 2',
+});
+</script>
diff --git a/testing/web-platform/tests/dom/events/webkit-animation-start-event.html b/testing/web-platform/tests/dom/events/webkit-animation-start-event.html
new file mode 100644
index 0000000000..ad1036644a
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/webkit-animation-start-event.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Prefixed CSS Animation start events</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-listener-invoke">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script src="resources/prefixed-animation-event-tests.js"></script>
+<script>
+'use strict';
+
+runAnimationEventTests({
+ unprefixedType: 'animationstart',
+ prefixedType: 'webkitAnimationStart',
+ animationCssStyle: '1ms',
+});
+</script>
diff --git a/testing/web-platform/tests/dom/events/webkit-transition-end-event.html b/testing/web-platform/tests/dom/events/webkit-transition-end-event.html
new file mode 100644
index 0000000000..2741824e30
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/webkit-transition-end-event.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Prefixed CSS Transition End event</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-listener-invoke">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script src="resources/prefixed-animation-event-tests.js"></script>
+<script>
+'use strict';
+
+runAnimationEventTests({
+ isTransition: true,
+ unprefixedType: 'transitionend',
+ prefixedType: 'webkitTransitionEnd',
+ animationCssStyle: '1ms',
+});
+</script>