diff options
Diffstat (limited to 'testing/web-platform/tests/dom/events')
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> |