[Scheduler] Add tests for isInputPending (#22140)
This feature was already implemented but we didn't have any tests. So I wrote some.
Andrew Clark committed
Aug 20, 2021 at 09:57 UTC
986d0e61d7085d4b8eea0386235d1b5ca9f47349
1 file changed
+217
-1
packages/scheduler/src/__tests__/Scheduler-test.js
+217
-1
@@ -17,6 +17,7 @@ let runtime;
17
let performance;
18
let cancelCallback;
19
let scheduleCallback;
20
+let requestPaint;
21
let NormalPriority;
22
23
// The Scheduler implementation uses browser APIs like `MessageChannel` and
@@ -40,6 +41,7 @@ describe('SchedulerBrowser', () => {
41
cancelCallback = Scheduler.unstable_cancelCallback;
42
scheduleCallback = Scheduler.unstable_scheduleCallback;
43
NormalPriority = Scheduler.unstable_NormalPriority;
44
+ requestPaint = Scheduler.unstable_requestPaint;
45
});
46
47
afterEach(() => {
@@ -52,6 +54,9 @@ describe('SchedulerBrowser', () => {
54
55
function installMockBrowserRuntime() {
56
let hasPendingMessageEvent = false;
57
+ let isFiringMessageEvent = false;
58
+ let hasPendingDiscreteEvent = false;
59
+ let hasPendingContinuousEvent = false;
60
61
let timerIDCounter = 0;
62
// let timerIDs = new Map();
@@ -94,6 +99,23 @@ describe('SchedulerBrowser', () => {
99
this.port2 = port2;
100
};
101
102
+ const scheduling = {
103
+ isInputPending(options) {
104
+ if (this !== scheduling) {
105
+ throw new Error(
106
+ 'isInputPending called with incorrect `this` context',
107
+ );
108
+ }
109
+
110
+ return (
111
+ hasPendingDiscreteEvent ||
112
+ (options && options.includeContinuous && hasPendingContinuousEvent)
113
+ );
114
+ },
115
+ };
116
+
117
+ global.navigator = {scheduling};
118
+
119
function ensureLogIsEmpty() {
120
if (eventLog.length !== 0) {
121
throw Error('Log is not empty. Call assertLog before continuing.');
@@ -102,6 +124,9 @@ describe('SchedulerBrowser', () => {
124
function advanceTime(ms) {
125
currentTime += ms;
126
}
127
+ function resetTime() {
128
+ currentTime = 0;
129
+ }
130
function fireMessageEvent() {
131
ensureLogIsEmpty();
132
if (!hasPendingMessageEvent) {
@@ -110,7 +135,35 @@ describe('SchedulerBrowser', () => {
135
hasPendingMessageEvent = false;
136
const onMessage = port1.onmessage;
137
log('Message Event');
113
- onMessage();
138
+
139
+ isFiringMessageEvent = true;
140
+ try {
141
+ onMessage();
142
+ } finally {
143
+ isFiringMessageEvent = false;
144
+ if (hasPendingDiscreteEvent) {
145
+ log('Discrete Event');
146
+ hasPendingDiscreteEvent = false;
147
+ }
148
+ if (hasPendingContinuousEvent) {
149
+ log('Continuous Event');
150
+ hasPendingContinuousEvent = false;
151
+ }
152
+ }
153
+ }
154
+ function scheduleDiscreteEvent() {
155
+ if (isFiringMessageEvent) {
156
+ hasPendingDiscreteEvent = true;
157
+ } else {
158
+ log('Discrete Event');
159
+ }
160
+ }
161
+ function scheduleContinuousEvent() {
162
+ if (isFiringMessageEvent) {
163
+ hasPendingContinuousEvent = true;
164
+ } else {
165
+ log('Continuous Event');
166
+ }
167
}
168
function log(val) {
169
eventLog.push(val);
@@ -125,10 +178,13 @@ describe('SchedulerBrowser', () => {
178
}
179
return {
180
advanceTime,
181
+ resetTime,
182
fireMessageEvent,
183
log,
184
isLogEmpty,
185
assertLog,
186
+ scheduleDiscreteEvent,
187
+ scheduleContinuousEvent,
188
};
189
}
190
@@ -144,6 +200,8 @@ describe('SchedulerBrowser', () => {
200
it('task with continuation', () => {
201
scheduleCallback(NormalPriority, () => {
202
runtime.log('Task');
203
+ // Request paint so that we yield at the end of the frame interval
204
+ requestPaint();
205
while (!Scheduler.unstable_shouldYield()) {
206
runtime.advanceTime(1);
207
}
@@ -259,4 +317,162 @@ describe('SchedulerBrowser', () => {
317
runtime.fireMessageEvent();
318
runtime.assertLog(['Message Event', 'B']);
319
});
320
+
321
+ it('when isInputPending is available, we can wait longer before yielding', () => {
322
+ function blockUntilSchedulerAsksToYield() {
323
+ while (!Scheduler.unstable_shouldYield()) {
324
+ runtime.advanceTime(1);
325
+ }
326
+ runtime.log(`Yield at ${performance.now()}ms`);
327
+ }
328
+
329
+ // First show what happens when we don't request a paint
330
+ scheduleCallback(NormalPriority, () => {
331
+ runtime.log('Task with no pending input');
332
+ blockUntilSchedulerAsksToYield();
333
+ });
334
+ runtime.assertLog(['Post Message']);
335
+
336
+ runtime.fireMessageEvent();
337
+ runtime.assertLog([
338
+ 'Message Event',
339
+ 'Task with no pending input',
340
+ // Even though there's no input, eventually Scheduler will yield
341
+ // regardless in case there's a pending main thread task we don't know
342
+ // about, like a network event.
343
+ gate(flags =>
344
+ flags.enableIsInputPending
345
+ ? 'Yield at 300ms'
346
+ : // When isInputPending is disabled, we always yield quickly
347
+ 'Yield at 5ms',
348
+ ),
349
+ ]);
350
+
351
+ runtime.resetTime();
352
+
353
+ // Now do the same thing, but while the task is running, simulate an
354
+ // input event.
355
+ scheduleCallback(NormalPriority, () => {
356
+ runtime.log('Task with pending input');
357
+ runtime.scheduleDiscreteEvent();
358
+ blockUntilSchedulerAsksToYield();
359
+ });
360
+ runtime.assertLog(['Post Message']);
361
+
362
+ runtime.fireMessageEvent();
363
+ runtime.assertLog([
364
+ 'Message Event',
365
+ 'Task with pending input',
366
+ // This time we yielded quickly to unblock the discrete event.
367
+ 'Yield at 5ms',
368
+ 'Discrete Event',
369
+ ]);
370
+ });
371
+
372
+ it(
373
+ 'isInputPending will also check for continuous inputs, but after a ' +
374
+ 'slightly larger threshold',
375
+ () => {
376
+ function blockUntilSchedulerAsksToYield() {
377
+ while (!Scheduler.unstable_shouldYield()) {
378
+ runtime.advanceTime(1);
379
+ }
380
+ runtime.log(`Yield at ${performance.now()}ms`);
381
+ }
382
+
383
+ // First show what happens when we don't request a paint
384
+ scheduleCallback(NormalPriority, () => {
385
+ runtime.log('Task with no pending input');
386
+ blockUntilSchedulerAsksToYield();
387
+ });
388
+ runtime.assertLog(['Post Message']);
389
+
390
+ runtime.fireMessageEvent();
391
+ runtime.assertLog([
392
+ 'Message Event',
393
+ 'Task with no pending input',
394
+ // Even though there's no input, eventually Scheduler will yield
395
+ // regardless in case there's a pending main thread task we don't know
396
+ // about, like a network event.
397
+ gate(flags =>
398
+ flags.enableIsInputPending
399
+ ? 'Yield at 300ms'
400
+ : // When isInputPending is disabled, we always yield quickly
401
+ 'Yield at 5ms',
402
+ ),
403
+ ]);
404
+
405
+ runtime.resetTime();
406
+
407
+ // Now do the same thing, but while the task is running, simulate a
408
+ // continuous input event.
409
+ scheduleCallback(NormalPriority, () => {
410
+ runtime.log('Task with continuous input');
411
+ runtime.scheduleContinuousEvent();
412
+ blockUntilSchedulerAsksToYield();
413
+ });
414
+ runtime.assertLog(['Post Message']);
415
+
416
+ runtime.fireMessageEvent();
417
+ runtime.assertLog([
418
+ 'Message Event',
419
+ 'Task with continuous input',
420
+ // This time we yielded quickly to unblock the continuous event. But not
421
+ // as quickly as for a discrete event.
422
+ gate(flags =>
423
+ flags.enableIsInputPending
424
+ ? 'Yield at 50ms'
425
+ : // When isInputPending is disabled, we always yield quickly
426
+ 'Yield at 5ms',
427
+ ),
428
+ 'Continuous Event',
429
+ ]);
430
+ },
431
+ );
432
+
433
+ it('requestPaint forces a yield at the end of the next frame interval', () => {
434
+ function blockUntilSchedulerAsksToYield() {
435
+ while (!Scheduler.unstable_shouldYield()) {
436
+ runtime.advanceTime(1);
437
+ }
438
+ runtime.log(`Yield at ${performance.now()}ms`);
439
+ }
440
+
441
+ // First show what happens when we don't request a paint
442
+ scheduleCallback(NormalPriority, () => {
443
+ runtime.log('Task with no paint');
444
+ blockUntilSchedulerAsksToYield();
445
+ });
446
+ runtime.assertLog(['Post Message']);
447
+
448
+ runtime.fireMessageEvent();
449
+ runtime.assertLog([
450
+ 'Message Event',
451
+ 'Task with no paint',
452
+ gate(flags =>
453
+ flags.enableIsInputPending
454
+ ? 'Yield at 300ms'
455
+ : // When isInputPending is disabled, we always yield quickly
456
+ 'Yield at 5ms',
457
+ ),
458
+ ]);
459
+
460
+ runtime.resetTime();
461
+
462
+ // Now do the same thing, but call requestPaint inside the task
463
+ scheduleCallback(NormalPriority, () => {
464
+ runtime.log('Task with paint');
465
+ requestPaint();
466
+ blockUntilSchedulerAsksToYield();
467
+ });
468
+ runtime.assertLog(['Post Message']);
469
+
470
+ runtime.fireMessageEvent();
471
+ runtime.assertLog([
472
+ 'Message Event',
473
+ 'Task with paint',
474
+ // This time we yielded quickly (5ms) because we requested a paint.
475
+ 'Yield at 5ms',
476
+ ]);
477
+ });
478
});