1'use strict' 2 3// Runs a set of tests for a given prefixed/unprefixed animation event (e.g. 4// animationstart/webkitAnimationStart). 5// 6// The eventDetails object must have the following form: 7// { 8// isTransition: false, <-- can be omitted, default false 9// unprefixedType: 'animationstart', 10// prefixedType: 'webkitAnimationStart', 11// animationCssStyle: '1ms', <-- must NOT include animation name or 12// transition property 13// } 14function runAnimationEventTests(eventDetails) { 15 const { 16 isTransition, 17 unprefixedType, 18 prefixedType, 19 animationCssStyle 20 } = eventDetails; 21 22 // Derive the DOM event handler names, e.g. onanimationstart. 23 const unprefixedHandler = `on${unprefixedType}`; 24 const prefixedHandler = `on${prefixedType.toLowerCase()}`; 25 26 const style = document.createElement('style'); 27 document.head.appendChild(style); 28 if (isTransition) { 29 style.sheet.insertRule( 30 `.baseStyle { width: 100px; transition: width ${animationCssStyle}; }`); 31 style.sheet.insertRule('.transition { width: 200px !important; }'); 32 } else { 33 style.sheet.insertRule('@keyframes anim {}'); 34 } 35 36 function triggerAnimation(div) { 37 if (isTransition) { 38 div.classList.add('transition'); 39 } else { 40 div.style.animation = `anim ${animationCssStyle}`; 41 } 42 } 43 44 test(t => { 45 const div = createDiv(t); 46 47 assert_equals(div[unprefixedHandler], null, 48 `${unprefixedHandler} should initially be null`); 49 assert_equals(div[prefixedHandler], null, 50 `${prefixedHandler} should initially be null`); 51 52 // Setting one should not affect the other. 53 div[unprefixedHandler] = () => { }; 54 55 assert_not_equals(div[unprefixedHandler], null, 56 `setting ${unprefixedHandler} should make it non-null`); 57 assert_equals(div[prefixedHandler], null, 58 `setting ${unprefixedHandler} should not affect ${prefixedHandler}`); 59 60 div[prefixedHandler] = () => { }; 61 62 assert_not_equals(div[prefixedHandler], null, 63 `setting ${prefixedHandler} should make it non-null`); 64 assert_not_equals(div[unprefixedHandler], div[prefixedHandler], 65 'the setters should be different'); 66 }, `${unprefixedHandler} and ${prefixedHandler} are not aliases`); 67 68 // The below tests primarily test the interactions of prefixed animation 69 // events in the algorithm for invoking events: 70 // https://dom.spec.whatwg.org/#concept-event-listener-invoke 71 72 promise_test(async t => { 73 const div = createDiv(t); 74 75 let receivedEventCount = 0; 76 addTestScopedEventHandler(t, div, prefixedHandler, () => { 77 receivedEventCount++; 78 }); 79 addTestScopedEventListener(t, div, prefixedType, () => { 80 receivedEventCount++; 81 }); 82 83 // The HTML spec[0] specifies that the prefixed event handlers have an 84 // 'Event handler event type' of the appropriate prefixed event type. E.g. 85 // onwebkitanimationend creates a listener for the event type 86 // 'webkitAnimationEnd'. 87 // 88 // [0]: https://html.spec.whatwg.org/multipage/webappapis.html#event-handlers-on-elements,-document-objects,-and-window-objects 89 div.dispatchEvent(new AnimationEvent(prefixedType)); 90 assert_equals(receivedEventCount, 2, 91 'prefixed listener and handler received event'); 92 }, `dispatchEvent of a ${prefixedType} event does trigger a ` + 93 `prefixed event handler or listener`); 94 95 promise_test(async t => { 96 const div = createDiv(t); 97 98 let receivedEvent = false; 99 addTestScopedEventHandler(t, div, unprefixedHandler, () => { 100 receivedEvent = true; 101 }); 102 addTestScopedEventListener(t, div, unprefixedType, () => { 103 receivedEvent = true; 104 }); 105 106 div.dispatchEvent(new AnimationEvent(prefixedType)); 107 assert_false(receivedEvent, 108 'prefixed listener or handler received event'); 109 }, `dispatchEvent of a ${prefixedType} event does not trigger an ` + 110 `unprefixed event handler or listener`); 111 112 113 promise_test(async t => { 114 const div = createDiv(t); 115 116 let receivedEvent = false; 117 addTestScopedEventHandler(t, div, prefixedHandler, () => { 118 receivedEvent = true; 119 }); 120 addTestScopedEventListener(t, div, prefixedType, () => { 121 receivedEvent = true; 122 }); 123 124 // The rewrite rules from 125 // https://dom.spec.whatwg.org/#concept-event-listener-invoke step 8 do not 126 // apply because isTrusted will be false. 127 div.dispatchEvent(new AnimationEvent(unprefixedType)); 128 assert_false(receivedEvent, 'prefixed listener or handler received event'); 129 }, `dispatchEvent of an ${unprefixedType} event does not trigger a ` + 130 `prefixed event handler or listener`); 131 132 promise_test(async t => { 133 const div = createDiv(t); 134 135 let receivedEvent = false; 136 addTestScopedEventHandler(t, div, prefixedHandler, () => { 137 receivedEvent = true; 138 }); 139 140 triggerAnimation(div); 141 await waitForEventThenAnimationFrame(t, unprefixedType); 142 assert_true(receivedEvent, `received ${prefixedHandler} event`); 143 }, `${prefixedHandler} event handler should trigger for an animation`); 144 145 promise_test(async t => { 146 const div = createDiv(t); 147 148 let receivedPrefixedEvent = false; 149 addTestScopedEventHandler(t, div, prefixedHandler, () => { 150 receivedPrefixedEvent = true; 151 }); 152 let receivedUnprefixedEvent = false; 153 addTestScopedEventHandler(t, div, unprefixedHandler, () => { 154 receivedUnprefixedEvent = true; 155 }); 156 157 triggerAnimation(div); 158 await waitForEventThenAnimationFrame(t, unprefixedType); 159 assert_true(receivedUnprefixedEvent, `received ${unprefixedHandler} event`); 160 assert_false(receivedPrefixedEvent, `received ${prefixedHandler} event`); 161 }, `${prefixedHandler} event handler should not trigger if an unprefixed ` + 162 `event handler also exists`); 163 164 promise_test(async t => { 165 const div = createDiv(t); 166 167 let receivedPrefixedEvent = false; 168 addTestScopedEventHandler(t, div, prefixedHandler, () => { 169 receivedPrefixedEvent = true; 170 }); 171 let receivedUnprefixedEvent = false; 172 addTestScopedEventListener(t, div, unprefixedType, () => { 173 receivedUnprefixedEvent = true; 174 }); 175 176 triggerAnimation(div); 177 await waitForEventThenAnimationFrame(t, unprefixedHandler); 178 assert_true(receivedUnprefixedEvent, `received ${unprefixedHandler} event`); 179 assert_false(receivedPrefixedEvent, `received ${prefixedHandler} event`); 180 }, `${prefixedHandler} event handler should not trigger if an unprefixed ` + 181 `listener also exists`); 182 183 promise_test(async t => { 184 // We use a parent/child relationship to be able to register both prefixed 185 // and unprefixed event handlers without the deduplication logic kicking in. 186 const parent = createDiv(t); 187 const child = createDiv(t); 188 parent.appendChild(child); 189 // After moving the child, we have to clean style again. 190 getComputedStyle(child).transition; 191 getComputedStyle(child).width; 192 193 let observedUnprefixedType; 194 addTestScopedEventHandler(t, parent, unprefixedHandler, e => { 195 observedUnprefixedType = e.type; 196 }); 197 let observedPrefixedType; 198 addTestScopedEventHandler(t, child, prefixedHandler, e => { 199 observedPrefixedType = e.type; 200 }); 201 202 triggerAnimation(child); 203 await waitForEventThenAnimationFrame(t, unprefixedType); 204 205 assert_equals(observedUnprefixedType, unprefixedType); 206 assert_equals(observedPrefixedType, prefixedType); 207 }, `event types for prefixed and unprefixed ${unprefixedType} event ` + 208 `handlers should be named appropriately`); 209 210 promise_test(async t => { 211 const div = createDiv(t); 212 213 let receivedEvent = false; 214 addTestScopedEventListener(t, div, prefixedType, () => { 215 receivedEvent = true; 216 }); 217 218 triggerAnimation(div); 219 await waitForEventThenAnimationFrame(t, unprefixedHandler); 220 assert_true(receivedEvent, `received ${prefixedType} event`); 221 }, `${prefixedType} event listener should trigger for an animation`); 222 223 promise_test(async t => { 224 const div = createDiv(t); 225 226 let receivedPrefixedEvent = false; 227 addTestScopedEventListener(t, div, prefixedType, () => { 228 receivedPrefixedEvent = true; 229 }); 230 let receivedUnprefixedEvent = false; 231 addTestScopedEventListener(t, div, unprefixedType, () => { 232 receivedUnprefixedEvent = true; 233 }); 234 235 triggerAnimation(div); 236 await waitForEventThenAnimationFrame(t, unprefixedHandler); 237 assert_true(receivedUnprefixedEvent, `received ${unprefixedType} event`); 238 assert_false(receivedPrefixedEvent, `received ${prefixedType} event`); 239 }, `${prefixedType} event listener should not trigger if an unprefixed ` + 240 `listener also exists`); 241 242 promise_test(async t => { 243 const div = createDiv(t); 244 245 let receivedPrefixedEvent = false; 246 addTestScopedEventListener(t, div, prefixedType, () => { 247 receivedPrefixedEvent = true; 248 }); 249 let receivedUnprefixedEvent = false; 250 addTestScopedEventHandler(t, div, unprefixedHandler, () => { 251 receivedUnprefixedEvent = true; 252 }); 253 254 triggerAnimation(div); 255 await waitForEventThenAnimationFrame(t, unprefixedHandler); 256 assert_true(receivedUnprefixedEvent, `received ${unprefixedType} event`); 257 assert_false(receivedPrefixedEvent, `received ${prefixedType} event`); 258 }, `${prefixedType} event listener should not trigger if an unprefixed ` + 259 `event handler also exists`); 260 261 promise_test(async t => { 262 // We use a parent/child relationship to be able to register both prefixed 263 // and unprefixed event listeners without the deduplication logic kicking in. 264 const parent = createDiv(t); 265 const child = createDiv(t); 266 parent.appendChild(child); 267 // After moving the child, we have to clean style again. 268 getComputedStyle(child).transition; 269 getComputedStyle(child).width; 270 271 let observedUnprefixedType; 272 addTestScopedEventListener(t, parent, unprefixedType, e => { 273 observedUnprefixedType = e.type; 274 }); 275 let observedPrefixedType; 276 addTestScopedEventListener(t, child, prefixedType, e => { 277 observedPrefixedType = e.type; 278 }); 279 280 triggerAnimation(child); 281 await waitForEventThenAnimationFrame(t, unprefixedHandler); 282 283 assert_equals(observedUnprefixedType, unprefixedType); 284 assert_equals(observedPrefixedType, prefixedType); 285 }, `event types for prefixed and unprefixed ${unprefixedType} event ` + 286 `listeners should be named appropriately`); 287 288 promise_test(async t => { 289 const div = createDiv(t); 290 291 let receivedEvent = false; 292 addTestScopedEventListener(t, div, prefixedType.toLowerCase(), () => { 293 receivedEvent = true; 294 }); 295 addTestScopedEventListener(t, div, prefixedType.toUpperCase(), () => { 296 receivedEvent = true; 297 }); 298 299 triggerAnimation(div); 300 await waitForEventThenAnimationFrame(t, unprefixedHandler); 301 assert_false(receivedEvent, `received ${prefixedType} event`); 302 }, `${prefixedType} event listener is case sensitive`); 303} 304 305// Below are utility functions. 306 307// Creates a div element, appends it to the document body and removes the 308// created element during test cleanup. 309function createDiv(test) { 310 const element = document.createElement('div'); 311 element.classList.add('baseStyle'); 312 document.body.appendChild(element); 313 test.add_cleanup(() => { 314 element.remove(); 315 }); 316 317 // Flush style before returning. Some browsers only do partial style re-calc, 318 // so ask for all important properties to make sure they are applied. 319 getComputedStyle(element).transition; 320 getComputedStyle(element).width; 321 322 return element; 323} 324 325// Adds an event handler for |handlerName| (calling |callback|) to the given 326// |target|, that will automatically be cleaned up at the end of the test. 327function addTestScopedEventHandler(test, target, handlerName, callback) { 328 assert_regexp_match( 329 handlerName, /^on/, 'Event handler names must start with "on"'); 330 assert_equals(target[handlerName], null, 331 `${handlerName} must be supported and not previously set`); 332 target[handlerName] = callback; 333 // We need this cleaned up even if the event handler doesn't run. 334 test.add_cleanup(() => { 335 if (target[handlerName]) 336 target[handlerName] = null; 337 }); 338} 339 340// Adds an event listener for |type| (calling |callback|) to the given 341// |target|, that will automatically be cleaned up at the end of the test. 342function addTestScopedEventListener(test, target, type, callback) { 343 target.addEventListener(type, callback); 344 // We need this cleaned up even if the event handler doesn't run. 345 test.add_cleanup(() => { 346 target.removeEventListener(type, callback); 347 }); 348} 349 350// Returns a promise that will resolve once the passed event (|eventName|) has 351// triggered and one more animation frame has happened. Automatically chooses 352// between an event handler or event listener based on whether |eventName| 353// begins with 'on'. 354// 355// We always listen on window as we don't want to interfere with the test via 356// triggering the prefixed event deduplication logic. 357function waitForEventThenAnimationFrame(test, eventName) { 358 return new Promise((resolve, _) => { 359 const eventFunc = eventName.startsWith('on') 360 ? addTestScopedEventHandler : addTestScopedEventListener; 361 eventFunc(test, window, eventName, () => { 362 // rAF once to give the event under test time to come through. 363 requestAnimationFrame(resolve); 364 }); 365 }); 366} 367