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