event-timing-test-utils.js (22810B)
1 function mainThreadBusy(ms) { 2 const target = performance.now() + ms; 3 while (performance.now() < target); 4 } 5 6 async function wait() { 7 return new Promise(resolve => step_timeout(resolve, 0)); 8 } 9 10 async function raf() { 11 return new Promise(resolve => requestAnimationFrame(resolve)); 12 } 13 14 async function afterNextPaint() { 15 await raf(); 16 await wait(); 17 } 18 19 async function blockNextEventListener(target, eventType, duration = 120) { 20 return new Promise(resolve => { 21 target.addEventListener(eventType, () => { 22 mainThreadBusy(duration); 23 resolve(); 24 }, { once: true }); 25 }); 26 } 27 28 async function clickAndBlockMain(id, options = {}) { 29 options = { 30 eventType: "pointerdown", 31 duration: 120, 32 ...options 33 }; 34 const element = document.getElementById(id); 35 36 await Promise.all([ 37 blockNextEventListener(element, options.eventType, options.duration), 38 click(element), 39 ]); 40 } 41 42 43 // This method should receive an entry of type 'event'. |isFirst| is true only when we want 44 // to check that the event also happens to correspond to the first event. In this case, the 45 // timings of the 'first-input' entry should be equal to those of this entry. |minDuration| 46 // is used to compared against entry.duration. 47 function verifyEvent(entry, eventType, targetId, isFirst=false, minDuration=104, notCancelable=false) { 48 assert_equals(entry.cancelable, !notCancelable, 'cancelable property'); 49 assert_equals(entry.name, eventType); 50 assert_equals(entry.entryType, 'event'); 51 assert_greater_than_equal(entry.duration, minDuration, 52 "The entry's duration should be greater than or equal to " + minDuration + " ms."); 53 assert_greater_than_equal(entry.processingStart, entry.startTime, 54 "The entry's processingStart should be greater than or equal to startTime."); 55 assert_greater_than_equal(entry.processingEnd, entry.processingStart, 56 "The entry's processingEnd must be at least as large as processingStart."); 57 // |duration| is a number rounded to the nearest 8 ms, so add 4 to get a lower bound 58 // on the actual duration. 59 assert_greater_than_equal(entry.duration + 4, entry.processingEnd - entry.startTime, 60 "The entry's duration must be at least as large as processingEnd - startTime."); 61 if (isFirst) { 62 let firstInputs = performance.getEntriesByType('first-input'); 63 assert_equals(firstInputs.length, 1, 'There should be a single first-input entry'); 64 let firstInput = firstInputs[0]; 65 assert_equals(firstInput.name, entry.name); 66 assert_equals(firstInput.entryType, 'first-input'); 67 assert_equals(firstInput.startTime, entry.startTime); 68 assert_equals(firstInput.duration, entry.duration); 69 assert_equals(firstInput.processingStart, entry.processingStart); 70 assert_equals(firstInput.processingEnd, entry.processingEnd); 71 assert_equals(firstInput.cancelable, entry.cancelable); 72 } 73 if (targetId) { 74 const target = document.getElementById(targetId); 75 assert_equals(entry.target, target); 76 } 77 } 78 79 function verifyClickEvent(entry, targetId, isFirst=false, minDuration=104, event='pointerdown') { 80 verifyEvent(entry, event, targetId, isFirst, minDuration); 81 } 82 83 84 // Add a PerformanceObserver and observe with a durationThreshold of |dur|. This test will 85 // attempt to check that the duration is appropriately checked by: 86 // * Asserting that entries received have a duration which is the smallest multiple of 8 87 // that is greater than or equal to |dur|. 88 // * Issuing |numEntries| entries that has duration greater than |slowDur|. 89 // * Asserting that exactly |numEntries| entries are received. 90 // Parameters: 91 // |t| - the test harness. 92 // |dur| - the durationThreshold for the PerformanceObserver. 93 // |id| - the ID of the element to be clicked. 94 // |numEntries| - the number of entries. 95 // |slowDur| - the min duration of a slow entry. 96 async function testDuration(t, id, numEntries, dur, slowDur) { 97 assert_implements(window.PerformanceEventTiming, 'Event Timing is not supported.'); 98 const observerPromise = new Promise(async resolve => { 99 let minDuration = Math.ceil(dur / 8) * 8; 100 // Exposed events must always have a minimum duration of 16. 101 minDuration = Math.max(minDuration, 16); 102 let numEntriesReceived = 0; 103 new PerformanceObserver(list => { 104 const pointerDowns = list.getEntriesByName('pointerdown'); 105 pointerDowns.forEach(e => { 106 t.step(() => { 107 verifyClickEvent(e, id, false /* isFirst */, minDuration); 108 }); 109 }); 110 numEntriesReceived += pointerDowns.length; 111 // All the entries should be received since the slowDur is higher 112 // than the duration threshold. 113 if (numEntriesReceived === numEntries) 114 resolve(); 115 }).observe({type: "event", durationThreshold: dur}); 116 }); 117 const clicksPromise = new Promise(async resolve => { 118 for (let index = 0; index < numEntries; index++) { 119 // Add some click events that has at least slowDur for duration. 120 await clickAndBlockMain(id, { duration: slowDur }); 121 } 122 resolve(); 123 }); 124 return Promise.all([observerPromise, clicksPromise]); 125 } 126 127 // Add a PerformanceObserver and observe with a durationThreshold of |durThreshold|. This test will 128 // attempt to check that the duration is appropriately checked by: 129 // * Asserting that entries received have a duration which is the smallest multiple of 8 130 // that is greater than or equal to |durThreshold|. 131 // * Issuing |numEntries| entries that have at least |processingDelay| as duration. 132 // * Asserting that the entries we receive has duration greater than or equals to the 133 // duration threshold we setup 134 // Parameters: 135 // |t| - the test harness. 136 // |id| - the ID of the element to be clicked. 137 // |durThreshold| - the durationThreshold for the PerformanceObserver. 138 // |numEntries| - the number of slow and number of fast entries. 139 // |processingDelay| - the event duration we add on each event. 140 async function testDurationWithDurationThreshold(t, id, numEntries, durThreshold, processingDelay) { 141 assert_implements(window.PerformanceEventTiming, 'Event Timing is not supported.'); 142 const observerPromise = new Promise(async resolve => { 143 let minDuration = Math.ceil(durThreshold / 8) * 8; 144 // Exposed events must always have a minimum duration of 16. 145 minDuration = Math.max(minDuration, 16); 146 new PerformanceObserver(t.step_func(list => { 147 const pointerDowns = list.getEntriesByName('pointerdown'); 148 pointerDowns.forEach(p => { 149 assert_greater_than_equal(p.duration, minDuration, 150 "The entry's duration should be greater than or equal to " + minDuration + " ms."); 151 }); 152 resolve(); 153 })).observe({type: "event", durationThreshold: durThreshold}); 154 }); 155 for (let index = 0; index < numEntries; index++) { 156 // These clicks are expected to be ignored, unless the test has some extra delays. 157 // In that case, the test will verify the event duration to ensure the event duration is 158 // greater than the duration threshold 159 await clickAndBlockMain(id, { duration: processingDelay }); 160 } 161 // Send click with event duration equals to or greater than |durThreshold|, so the 162 // observer promise can be resolved 163 await clickAndBlockMain(id, { duration: durThreshold }); 164 return observerPromise; 165 } 166 167 // Apply events that trigger an event of the given |eventType| to be dispatched to the 168 // |target|. Some of these assume that the target is not on the top left corner of the 169 // screen, which means that (0, 0) of the viewport is outside of the |target|. 170 function applyAction(eventType, target) { 171 const actions = new test_driver.Actions(); 172 if (eventType === 'auxclick') { 173 actions.pointerMove(0, 0, {origin: target}) 174 .pointerDown({button: actions.ButtonType.MIDDLE}) 175 .pointerUp({button: actions.ButtonType.MIDDLE}); 176 } else if (eventType === 'click' || eventType === 'mousedown' || eventType === 'mouseup' 177 || eventType === 'pointerdown' || eventType === 'pointerup' 178 || eventType === 'touchstart' || eventType === 'touchend') { 179 actions.pointerMove(0, 0, {origin: target}) 180 .pointerDown() 181 .pointerUp(); 182 } else if (eventType === 'contextmenu') { 183 actions.pointerMove(0, 0, {origin: target}) 184 .pointerDown({button: actions.ButtonType.RIGHT}) 185 .pointerUp({button: actions.ButtonType.RIGHT}); 186 } else if (eventType === 'dblclick') { 187 actions.pointerMove(0, 0, {origin: target}) 188 .pointerDown() 189 .pointerUp() 190 .pointerDown() 191 .pointerUp() 192 // Reset by clicking outside of the target. 193 .pointerMove(0, 0) 194 .pointerDown() 195 } else if (eventType === 'mouseenter' || eventType === 'mouseover' 196 || eventType === 'pointerenter' || eventType === 'pointerover') { 197 // Move outside of the target and then back inside. 198 // Moving it to 0, 1 because 0, 0 doesn't cause the pointer to 199 // move in Firefox. See https://github.com/w3c/webdriver/issues/1545 200 actions.pointerMove(0, 1) 201 .pointerMove(0, 0, {origin: target}); 202 } else if (eventType === 'mouseleave' || eventType === 'mouseout' 203 || eventType === 'pointerleave' || eventType === 'pointerout') { 204 actions.pointerMove(0, 0, {origin: target}) 205 .pointerMove(0, 0); 206 } else if (eventType === 'keyup' || eventType === 'keydown') { 207 // Any key here as an input should work. 208 // TODO: Switch this to use test_driver.Actions.key{up,down} 209 // when test driver supports it. 210 // Please check crbug.com/893480. 211 const key = 'k'; 212 return test_driver.send_keys(target, key); 213 } else { 214 assert_unreached('The event type ' + eventType + ' is not supported.'); 215 } 216 return actions.send(); 217 } 218 219 function requiresListener(eventType) { 220 return ['mouseenter', 221 'mouseleave', 222 'pointerdown', 223 'pointerenter', 224 'pointerleave', 225 'pointerout', 226 'pointerover', 227 'pointerup', 228 'keyup', 229 'keydown' 230 ].includes(eventType); 231 } 232 233 function notCancelable(eventType) { 234 return ['mouseenter', 'mouseleave', 'pointerenter', 'pointerleave'].includes(eventType); 235 } 236 237 // Tests the given |eventType|'s performance.eventCounts value. Since this is populated only when 238 // the event is processed, we check every 10 ms until we've found the |expectedCount|. 239 function testCounts(t, resolve, looseCount, eventType, expectedCount) { 240 const counts = performance.eventCounts.get(eventType); 241 if (counts < expectedCount) { 242 t.step_timeout(() => { 243 testCounts(t, resolve, looseCount, eventType, expectedCount); 244 }, 10); 245 return; 246 } 247 if (looseCount) { 248 assert_greater_than_equal(performance.eventCounts.get(eventType), expectedCount, 249 `Should have at least ${expectedCount} ${eventType} events`) 250 } else { 251 assert_equals(performance.eventCounts.get(eventType), expectedCount, 252 `Should have ${expectedCount} ${eventType} events`); 253 } 254 resolve(); 255 } 256 257 // Tests the given |eventType| by creating events whose target are the element with id 258 // 'target'. The test assumes that such element already exists. |looseCount| is set for 259 // eventTypes for which events would occur for other interactions other than the ones being 260 // specified for the target, so the counts could be larger. 261 async function testEventType(t, eventType, looseCount=false, targetId='target') { 262 assert_implements(window.EventCounts, "Event Counts isn't supported"); 263 const target = document.getElementById(targetId); 264 if (requiresListener(eventType)) { 265 target.addEventListener(eventType, () =>{}); 266 } 267 const initialCount = performance.eventCounts.get(eventType); 268 if (!looseCount) { 269 assert_equals(initialCount, 0, 'No events yet.'); 270 } 271 // Trigger two 'fast' events of the type. 272 await applyAction(eventType, target); 273 await applyAction(eventType, target); 274 await afterNextPaint(); 275 await new Promise(t.step_func(resolve => { 276 testCounts(t, resolve, looseCount, eventType, initialCount + 2); 277 })); 278 // The durationThreshold used by the observer. A slow events needs to be slower than that. 279 const durationThreshold = 16; 280 // Now add an event handler to cause a slow event. 281 target.addEventListener(eventType, () => { 282 mainThreadBusy(durationThreshold + 4); 283 }); 284 const observerPromise = new Promise(async resolve => { 285 new PerformanceObserver(t.step_func(entryList => { 286 let eventTypeEntries = entryList.getEntriesByName(eventType); 287 if (eventTypeEntries.length === 0) 288 return; 289 290 let entry = null; 291 if (!looseCount) { 292 entry = eventTypeEntries[0]; 293 assert_equals(eventTypeEntries.length, 1); 294 } else { 295 // The other events could also be considered slow. Find the one with the correct 296 // target. 297 eventTypeEntries.forEach(e => { 298 if (e.target === document.getElementById(targetId)) 299 entry = e; 300 }); 301 if (!entry) 302 return; 303 } 304 verifyEvent(entry, 305 eventType, 306 targetId, 307 false /* isFirst */, 308 durationThreshold, 309 notCancelable(eventType)); 310 // Shouldn't need async testing here since we already got the observer entry, but might as 311 // well reuse the method. 312 testCounts(t, resolve, looseCount, eventType, initialCount + 3); 313 })).observe({type: 'event', durationThreshold: durationThreshold}); 314 }); 315 // Cause a slow event. 316 await applyAction(eventType, target); 317 318 await afterNextPaint(); 319 320 await observerPromise; 321 } 322 323 function addListeners(target, events) { 324 const eventListener = (e) => { 325 mainThreadBusy(200); 326 }; 327 events.forEach(e => { target.addEventListener(e, eventListener); }); 328 } 329 330 // The testdriver.js, testdriver-vendor.js and testdriver-actions.js need to be 331 // included to use this function. 332 async function tap(target) { 333 return new test_driver.Actions() 334 .addPointer("touchPointer", "touch") 335 .pointerMove(0, 0, { origin: target }) 336 .pointerDown() 337 .pointerUp() 338 .send(); 339 } 340 341 async function click(target) { 342 return test_driver.click(target); 343 } 344 345 async function auxClick(target) { 346 const actions = new test_driver.Actions(); 347 return actions.addPointer("mousePointer", "mouse") 348 .pointerMove(0, 0, { origin: target }) 349 .pointerDown({ button: actions.ButtonType.RIGHT }) 350 .pointerUp({ button: actions.ButtonType.RIGHT }) 351 .send(); 352 } 353 354 async function pointerdown(target) { 355 const actions = new test_driver.Actions(); 356 return actions.addPointer("mousePointer", "mouse") 357 .pointerMove(0, 0, { origin: target }) 358 .pointerDown() 359 .send(); 360 } 361 362 async function orphanPointerup(target) { 363 const actions = new test_driver.Actions(); 364 await actions.addPointer("mousePointer", "mouse") 365 .pointerMove(0, 0, { origin: target }) 366 .pointerUp() 367 .send(); 368 369 // Orphan pointerup doesn't get triggered in some browsers. Sending a 370 // non-pointer related event to make sure that at least an event gets handled. 371 // If a browsers sends an orphan pointerup, it will always be before the 372 // keydown, so the test will correctly handle it. 373 await pressKey(target, 'a'); 374 } 375 376 async function auxPointerdown(target) { 377 const actions = new test_driver.Actions(); 378 return actions.addPointer("mousePointer", "mouse") 379 .pointerMove(0, 0, { origin: target }) 380 .pointerDown({ button: actions.ButtonType.RIGHT }) 381 .send(); 382 } 383 384 async function orphanAuxPointerup(target) { 385 const actions = new test_driver.Actions(); 386 await actions.addPointer("mousePointer", "mouse") 387 .pointerMove(0, 0, { origin: target }) 388 .pointerUp({ button: actions.ButtonType.RIGHT }) 389 .send(); 390 391 // Orphan pointerup doesn't get triggered in some browsers. Sending a 392 // non-pointer related event to make sure that at least an event gets handled. 393 // If a browsers sends an orphan pointerup, it will always be before the 394 // keydown, so the test will correctly handle it. 395 await pressKey(target, 'a'); 396 } 397 398 // The testdriver.js, testdriver-vendor.js need to be included to use this 399 // function. 400 async function pressKey(target, key) { 401 await test_driver.send_keys(target, key); 402 } 403 404 async function flingAndTapInTarget(target) { 405 const actions = new test_driver.Actions(); 406 return actions.addPointer("pointer1", "touch") 407 .pointerMove(0, 0, {origin: target}) 408 .pointerDown() 409 .pointerMove(0, -50, {origin: target}) 410 .pointerMove(0, -50, {origin: target}) 411 .pointerUp() 412 .pause(60) 413 .pointerMove(0, 0, {origin: target}) 414 .pointerDown() 415 .pointerUp() 416 .send(); 417 } 418 419 async function textSelectionInTarget(target) { 420 const actions = new test_driver.Actions(); 421 return actions.addPointer("pointer1", "mouse") 422 .pointerMove(0, 0, {origin: target}) 423 .pointerDown({button: actions.ButtonType.LEFT}) 424 .pointerMove(20, 60, {origin: target}) 425 .pointerMove(20, 120, {origin: target}) 426 .pointerUp() 427 .send(); 428 } 429 430 // The testdriver.js, testdriver-vendor.js need to be included to use this 431 // function. 432 async function addListenersAndPress(target, key, events) { 433 addListeners(target, events); 434 return pressKey(target, key); 435 } 436 437 // The testdriver.js, testdriver-vendor.js need to be included to use this 438 // function. 439 async function addListenersAndClick(target) { 440 addListeners(target, 441 ['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'click']); 442 return click(target); 443 } 444 445 function filterAndAddToMap(events, map) { 446 return function (entry) { 447 if (events.includes(entry.name)) { 448 map.set(entry.name, entry.interactionId); 449 return true; 450 } 451 return false; 452 } 453 } 454 455 async function createPerformanceObserverPromise(observeTypes, callback, readyToResolve 456 ) { 457 return new Promise(resolve => { 458 new PerformanceObserver(entryList => { 459 callback(entryList); 460 461 if (readyToResolve()) { 462 resolve(); 463 } 464 }).observe({ entryTypes: observeTypes }); 465 }); 466 } 467 468 const ENTER_KEY = '\uE007'; 469 const SPACE_KEY = '\uE00D'; 470 471 async function blockPointerDownEventListener(target, duration, count) { 472 return new Promise(resolve => { 473 target.addEventListener("pointerdown", () => { 474 event_count++; 475 mainThreadBusy(duration); 476 if (event_count == count) 477 resolve(); 478 }); 479 }); 480 } 481 482 async function blockCapturePointerDownEventListener(target, duration) { 483 return new Promise(resolve => { 484 target.addEventListener("pointerdown", (e) => { 485 mainThreadBusy(duration); 486 target.setPointerCapture(e.pointerId); 487 resolve(); 488 }); 489 }); 490 } 491 492 async function flingTapAndBlockMain(target, duration) { 493 return Promise.all([ 494 blockPointerDownEventListener(target, duration, 2), 495 blockNextEventListener(target, "pointercancel", duration), 496 blockNextEventListener(target, "scroll", duration), 497 flingAndTapInTarget(target), 498 ]); 499 } 500 501 async function textSelectionAndBlockMain(target, duration) { 502 return Promise.all([ 503 blockCapturePointerDownEventListener(target, "pointerdown", duration), 504 blockNextEventListener(target, "pointermove", duration), 505 blockNextEventListener(target, "scroll", duration), 506 blockNextEventListener(target, "pointerup", 10), 507 textSelectionInTarget(target), 508 // afterNextPaint(), 509 ]); 510 } 511 512 // The testdriver.js, testdriver-vendor.js need to be included to use this 513 // function. 514 async function interactAndObserve(interactionType, target, observerPromise, key = '') { 515 let interactionPromise; 516 switch (interactionType) { 517 case 'key': { 518 addListeners(target, ['keydown', 'keyup']); 519 interactionPromise = pressKey(target, key); 520 } 521 case 'tap': { 522 addListeners(target, ['pointerdown', 'pointerup']); 523 interactionPromise = tap(target); 524 break; 525 } 526 case 'click': { 527 addListeners(target, 528 ['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'click']); 529 interactionPromise = click(target); 530 break; 531 } 532 case 'auxclick': { 533 addListeners(target, 534 ['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'contextmenu', 'auxclick']); 535 interactionPromise = auxClick(target); 536 break; 537 } 538 case 'aux-pointerdown': { 539 addListeners(target, 540 ['mousedown', 'pointerdown', 'contextmenu']); 541 interactionPromise = auxPointerdown(target); 542 break; 543 } 544 case 'aux-pointerdown-and-pointerdown': { 545 addListeners(target, 546 ['mousedown', 'pointerdown', 'contextmenu']); 547 interactionPromise = Promise.all([auxPointerdown(target), pointerdown(target)]); 548 break; 549 } 550 case 'orphan-pointerup': { 551 addListeners(target, ['pointerup', 'keydown']); 552 interactionPromise = orphanPointerup(target); 553 break; 554 } 555 case 'space-key-simulated-click': { 556 addListeners(target, ['keydown', 'click']); 557 interactionPromise = interact('key', target, SPACE_KEY); 558 break; 559 } 560 case 'enter-key-simulated-click': { 561 addListeners(target, ['keydown', 'click']); 562 interactionPromise = interact('key', target, ENTER_KEY); 563 break; 564 } 565 case 'fling-tap': { 566 interactionPromise = flingTapAndBlockMain(target, 30); 567 break; 568 } 569 case 'selection-scroll': { 570 interactionPromise = textSelectionAndBlockMain(target, 30); 571 break; 572 } 573 case 'orphan-keydown': { 574 addListeners(target, ['keydown']); 575 interactionPromise = new test_driver.Actions() 576 .pointerMove(0, 0, {origin: target}) 577 .pointerDown() 578 .pointerUp() 579 .addTick() 580 .keyDown('a') 581 .send(); 582 break; 583 } 584 } 585 return Promise.all([interactionPromise, observerPromise]); 586 } 587 588 async function interact(interactionType, element, key = '') { 589 switch (interactionType) { 590 case 'click': { 591 return click(element); 592 } 593 case 'tap': { 594 return tap(element); 595 } 596 case 'key': { 597 return test_driver.send_keys(element, key); 598 } 599 } 600 } 601 602 async function verifyInteractionCount(t, expectedCount) { 603 await t.step_wait(() => { 604 return performance.interactionCount >= expectedCount; 605 }, 'interactionCount did not increase enough', 10000, 5); 606 assert_equals(performance.interactionCount, expectedCount, 607 'interactionCount increased more than expected'); 608 } 609 610 function interactionCount_test(interactionType, elements, key = '') { 611 return promise_test(async t => { 612 assert_implements(window.PerformanceEventTiming, 613 'Event Timing is not supported'); 614 assert_equals(performance.interactionCount, 0, 'Initial count is not 0'); 615 616 let expectedCount = 1; 617 for (let element of elements) { 618 await interact(interactionType, element, key); 619 await verifyInteractionCount(t, expectedCount++); 620 } 621 }, `EventTiming: verify interactionCount for ${interactionType} interaction`); 622 }