Skip to content

Commit

Permalink
Tests and fixes for 'timing out' behavior (facebook#12858)
Browse files Browse the repository at this point in the history
**what is the change?:**
Test coverage checking that callbacks are called when they time out.

This test surfaced a bug and this commit includes the fix.

I want to refine this approach, but basically we can simulate time outs
by controlling the return value of 'now()' and the argument passed to
the rAF callback.

Next we will write a browser fixture to further test this against real
browser APIs.

**why make this change?:**
Better tests will keep this module working smoothly while we continue
refactoring and improving it.

**test plan:**
Run the new tests, see that it fails without the bug fix.
  • Loading branch information
flarnie authored May 22, 2018
1 parent ad27845 commit 33289b5
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 5 deletions.
5 changes: 4 additions & 1 deletion packages/react-scheduler/src/ReactScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,10 @@ if (!ExecutionEnvironment.canUseDOM) {
if (options != null && typeof options.timeout === 'number') {
timeoutTime = now() + options.timeout;
}
if (timeoutTime > nextSoonestTimeoutTime) {
if (
nextSoonestTimeoutTime === -1 ||
(timeoutTime !== -1 && timeoutTime < nextSoonestTimeoutTime)
) {
nextSoonestTimeoutTime = timeoutTime;
}

Expand Down
122 changes: 118 additions & 4 deletions packages/react-scheduler/src/__tests__/ReactScheduler-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,43 @@
let ReactScheduler;

describe('ReactScheduler', () => {
let rAFCallbacks = [];
let postMessageCallback;
let postMessageEvents = [];

function drainPostMessageQueue() {
// default to this falling about 15 ms before next frame
currentTime = startOfLatestFrame + frameSize - 15;
if (postMessageCallback) {
while (postMessageEvents.length) {
postMessageCallback(postMessageEvents.shift());
}
}
}
function runRAFCallbacks() {
startOfLatestFrame += frameSize;
currentTime = startOfLatestFrame;
rAFCallbacks.forEach(cb => cb());
rAFCallbacks = [];
}
function advanceAll() {
jest.runAllTimers();
runRAFCallbacks();
drainPostMessageQueue();
}

let frameSize = 33;
let startOfLatestFrame = Date.now();
let currentTime = Date.now();

beforeEach(() => {
// TODO pull this into helper method, reduce repetition.
// mock the browser APIs which are used in react-scheduler:
// - requestAnimationFrame should pass the DOMHighResTimeStamp argument
// - calling 'window.postMessage' should actually fire postmessage handlers
// - Date.now should return the correct thing
global.requestAnimationFrame = function(cb) {
return setTimeout(() => {
cb(Date.now());
return rAFCallbacks.push(() => {
cb(startOfLatestFrame);
});
};
const originalAddEventListener = global.addEventListener;
Expand All @@ -50,6 +65,9 @@ describe('ReactScheduler', () => {
const postMessageEvent = {source: window, data: messageKey};
postMessageEvents.push(postMessageEvent);
};
global.Date.now = function() {
return currentTime;
};
jest.resetModules();
ReactScheduler = require('react-scheduler');
});
Expand Down Expand Up @@ -102,7 +120,7 @@ describe('ReactScheduler', () => {
scheduleWork(callbackA);
// initially waits to call the callback
expect(callbackLog).toEqual([]);
jest.runAllTimers();
runRAFCallbacks();
// this should schedule work *after* the requestAnimationFrame but before the message handler
scheduleWork(callbackB);
expect(callbackLog).toEqual([]);
Expand Down Expand Up @@ -208,6 +226,102 @@ describe('ReactScheduler', () => {
expect(callbackLog).toEqual(['A0', 'B', 'A1']);
});
});

describe('when callbacks time out: ', () => {
// USEFUL INFO:
// startOfLatestFrame is a global that goes up every time rAF runs
// currentTime defaults to startOfLatestFrame inside rAF callback
// and currentTime defaults to 15 before next frame inside idleTick

describe('when there is no more time left in the frame', () => {
it('calls any callback which has timed out, waits for others', () => {
const {scheduleWork} = ReactScheduler;
startOfLatestFrame = 1000000000000;
currentTime = startOfLatestFrame - 10;
const callbackLog = [];
// simple case of one callback which times out, another that won't.
const callbackA = jest.fn(() => callbackLog.push('A'));
const callbackB = jest.fn(() => callbackLog.push('B'));
const callbackC = jest.fn(() => callbackLog.push('C'));

scheduleWork(callbackA); // won't time out
scheduleWork(callbackB, {timeout: 100}); // times out later
scheduleWork(callbackC, {timeout: 2}); // will time out fast

runRAFCallbacks(); // runs rAF callback
// push time ahead a bit so that we have no idle time
startOfLatestFrame += 16;
drainPostMessageQueue(); // runs postMessage callback, idleTick

// callbackC should have timed out
expect(callbackLog).toEqual(['C']);

runRAFCallbacks(); // runs rAF callback
// push time ahead a bit so that we have no idle time
startOfLatestFrame += 16;
drainPostMessageQueue(); // runs postMessage callback, idleTick

// callbackB should have timed out
expect(callbackLog).toEqual(['C', 'B']);

runRAFCallbacks(); // runs rAF callback
drainPostMessageQueue(); // runs postMessage callback, idleTick

// we should have run callbackA in the idle time
expect(callbackLog).toEqual(['C', 'B', 'A']);
});
});

describe('when there is some time left in the frame', () => {
it('calls timed out callbacks and then any more pending callbacks, defers others if time runs out', () => {
// TODO first call timed out callbacks
// then any non-timed out callbacks if there is time
const {scheduleWork} = ReactScheduler;
startOfLatestFrame = 1000000000000;
currentTime = startOfLatestFrame - 10;
const callbackLog = [];
// simple case of one callback which times out, others that won't.
const callbackA = jest.fn(() => {
callbackLog.push('A');
// time passes, causing us to run out of idle time
currentTime += 25;
});
const callbackB = jest.fn(() => callbackLog.push('B'));
const callbackC = jest.fn(() => callbackLog.push('C'));
const callbackD = jest.fn(() => callbackLog.push('D'));

scheduleWork(callbackA); // won't time out
scheduleWork(callbackB, {timeout: 100}); // times out later
scheduleWork(callbackC, {timeout: 2}); // will time out fast
scheduleWork(callbackD); // won't time out

advanceAll(); // runs rAF and postMessage callbacks

// callbackC should have timed out
// we should have had time to call A also, then we run out of time
expect(callbackLog).toEqual(['C', 'A']);

runRAFCallbacks(); // runs rAF callback
// push time ahead a bit so that we have no idle time
startOfLatestFrame += 16;
drainPostMessageQueue(); // runs postMessage callback, idleTick

// callbackB should have timed out
// but we should not run callbackD because we have no idle time
expect(callbackLog).toEqual(['C', 'A', 'B']);

advanceAll(); // runs rAF and postMessage callbacks

// we should have run callbackD in the idle time
expect(callbackLog).toEqual(['C', 'A', 'B', 'D']);

advanceAll(); // runs rAF and postMessage callbacks

// we should not have run anything again, nothing is scheduled
expect(callbackLog).toEqual(['C', 'A', 'B', 'D']);
});
});
});
});

describe('cancelScheduledWork', () => {
Expand Down

0 comments on commit 33289b5

Please sign in to comment.