diff --git a/packages/scheduler/src/__tests__/Scheduler-test.js b/packages/scheduler/src/__tests__/Scheduler-test.js index 29abe65c1eae8..6df0a3ba1d246 100644 --- a/packages/scheduler/src/__tests__/Scheduler-test.js +++ b/packages/scheduler/src/__tests__/Scheduler-test.js @@ -17,6 +17,7 @@ let runtime; let performance; let cancelCallback; let scheduleCallback; +let requestPaint; let NormalPriority; // The Scheduler implementation uses browser APIs like `MessageChannel` and @@ -40,6 +41,7 @@ describe('SchedulerBrowser', () => { cancelCallback = Scheduler.unstable_cancelCallback; scheduleCallback = Scheduler.unstable_scheduleCallback; NormalPriority = Scheduler.unstable_NormalPriority; + requestPaint = Scheduler.unstable_requestPaint; }); afterEach(() => { @@ -52,6 +54,9 @@ describe('SchedulerBrowser', () => { function installMockBrowserRuntime() { let hasPendingMessageEvent = false; + let isFiringMessageEvent = false; + let hasPendingDiscreteEvent = false; + let hasPendingContinuousEvent = false; let timerIDCounter = 0; // let timerIDs = new Map(); @@ -94,6 +99,23 @@ describe('SchedulerBrowser', () => { this.port2 = port2; }; + const scheduling = { + isInputPending(options) { + if (this !== scheduling) { + throw new Error( + 'isInputPending called with incorrect `this` context', + ); + } + + return ( + hasPendingDiscreteEvent || + (options && options.includeContinuous && hasPendingContinuousEvent) + ); + }, + }; + + global.navigator = {scheduling}; + function ensureLogIsEmpty() { if (eventLog.length !== 0) { throw Error('Log is not empty. Call assertLog before continuing.'); @@ -102,6 +124,9 @@ describe('SchedulerBrowser', () => { function advanceTime(ms) { currentTime += ms; } + function resetTime() { + currentTime = 0; + } function fireMessageEvent() { ensureLogIsEmpty(); if (!hasPendingMessageEvent) { @@ -110,7 +135,35 @@ describe('SchedulerBrowser', () => { hasPendingMessageEvent = false; const onMessage = port1.onmessage; log('Message Event'); - onMessage(); + + isFiringMessageEvent = true; + try { + onMessage(); + } finally { + isFiringMessageEvent = false; + if (hasPendingDiscreteEvent) { + log('Discrete Event'); + hasPendingDiscreteEvent = false; + } + if (hasPendingContinuousEvent) { + log('Continuous Event'); + hasPendingContinuousEvent = false; + } + } + } + function scheduleDiscreteEvent() { + if (isFiringMessageEvent) { + hasPendingDiscreteEvent = true; + } else { + log('Discrete Event'); + } + } + function scheduleContinuousEvent() { + if (isFiringMessageEvent) { + hasPendingContinuousEvent = true; + } else { + log('Continuous Event'); + } } function log(val) { eventLog.push(val); @@ -125,10 +178,13 @@ describe('SchedulerBrowser', () => { } return { advanceTime, + resetTime, fireMessageEvent, log, isLogEmpty, assertLog, + scheduleDiscreteEvent, + scheduleContinuousEvent, }; } @@ -144,6 +200,8 @@ describe('SchedulerBrowser', () => { it('task with continuation', () => { scheduleCallback(NormalPriority, () => { runtime.log('Task'); + // Request paint so that we yield at the end of the frame interval + requestPaint(); while (!Scheduler.unstable_shouldYield()) { runtime.advanceTime(1); } @@ -259,4 +317,162 @@ describe('SchedulerBrowser', () => { runtime.fireMessageEvent(); runtime.assertLog(['Message Event', 'B']); }); + + it('when isInputPending is available, we can wait longer before yielding', () => { + function blockUntilSchedulerAsksToYield() { + while (!Scheduler.unstable_shouldYield()) { + runtime.advanceTime(1); + } + runtime.log(`Yield at ${performance.now()}ms`); + } + + // First show what happens when we don't request a paint + scheduleCallback(NormalPriority, () => { + runtime.log('Task with no pending input'); + blockUntilSchedulerAsksToYield(); + }); + runtime.assertLog(['Post Message']); + + runtime.fireMessageEvent(); + runtime.assertLog([ + 'Message Event', + 'Task with no pending input', + // Even though there's no input, eventually Scheduler will yield + // regardless in case there's a pending main thread task we don't know + // about, like a network event. + gate(flags => + flags.enableIsInputPending + ? 'Yield at 300ms' + : // When isInputPending is disabled, we always yield quickly + 'Yield at 5ms', + ), + ]); + + runtime.resetTime(); + + // Now do the same thing, but while the task is running, simulate an + // input event. + scheduleCallback(NormalPriority, () => { + runtime.log('Task with pending input'); + runtime.scheduleDiscreteEvent(); + blockUntilSchedulerAsksToYield(); + }); + runtime.assertLog(['Post Message']); + + runtime.fireMessageEvent(); + runtime.assertLog([ + 'Message Event', + 'Task with pending input', + // This time we yielded quickly to unblock the discrete event. + 'Yield at 5ms', + 'Discrete Event', + ]); + }); + + it( + 'isInputPending will also check for continuous inputs, but after a ' + + 'slightly larger threshold', + () => { + function blockUntilSchedulerAsksToYield() { + while (!Scheduler.unstable_shouldYield()) { + runtime.advanceTime(1); + } + runtime.log(`Yield at ${performance.now()}ms`); + } + + // First show what happens when we don't request a paint + scheduleCallback(NormalPriority, () => { + runtime.log('Task with no pending input'); + blockUntilSchedulerAsksToYield(); + }); + runtime.assertLog(['Post Message']); + + runtime.fireMessageEvent(); + runtime.assertLog([ + 'Message Event', + 'Task with no pending input', + // Even though there's no input, eventually Scheduler will yield + // regardless in case there's a pending main thread task we don't know + // about, like a network event. + gate(flags => + flags.enableIsInputPending + ? 'Yield at 300ms' + : // When isInputPending is disabled, we always yield quickly + 'Yield at 5ms', + ), + ]); + + runtime.resetTime(); + + // Now do the same thing, but while the task is running, simulate a + // continuous input event. + scheduleCallback(NormalPriority, () => { + runtime.log('Task with continuous input'); + runtime.scheduleContinuousEvent(); + blockUntilSchedulerAsksToYield(); + }); + runtime.assertLog(['Post Message']); + + runtime.fireMessageEvent(); + runtime.assertLog([ + 'Message Event', + 'Task with continuous input', + // This time we yielded quickly to unblock the continuous event. But not + // as quickly as for a discrete event. + gate(flags => + flags.enableIsInputPending + ? 'Yield at 50ms' + : // When isInputPending is disabled, we always yield quickly + 'Yield at 5ms', + ), + 'Continuous Event', + ]); + }, + ); + + it('requestPaint forces a yield at the end of the next frame interval', () => { + function blockUntilSchedulerAsksToYield() { + while (!Scheduler.unstable_shouldYield()) { + runtime.advanceTime(1); + } + runtime.log(`Yield at ${performance.now()}ms`); + } + + // First show what happens when we don't request a paint + scheduleCallback(NormalPriority, () => { + runtime.log('Task with no paint'); + blockUntilSchedulerAsksToYield(); + }); + runtime.assertLog(['Post Message']); + + runtime.fireMessageEvent(); + runtime.assertLog([ + 'Message Event', + 'Task with no paint', + gate(flags => + flags.enableIsInputPending + ? 'Yield at 300ms' + : // When isInputPending is disabled, we always yield quickly + 'Yield at 5ms', + ), + ]); + + runtime.resetTime(); + + // Now do the same thing, but call requestPaint inside the task + scheduleCallback(NormalPriority, () => { + runtime.log('Task with paint'); + requestPaint(); + blockUntilSchedulerAsksToYield(); + }); + runtime.assertLog(['Post Message']); + + runtime.fireMessageEvent(); + runtime.assertLog([ + 'Message Event', + 'Task with paint', + // This time we yielded quickly (5ms) because we requested a paint. + 'Yield at 5ms', + ]); + }); });