Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding more schedule tests #12858

Merged
merged 1 commit into from
May 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/react-scheduler/src/ReactScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to improve the API for writing these tests; instead of drainPostMessageQueue and runRAFCallbacks it should be more generic and descriptive, like so:

const shorterTimeout = 2;
const longerTimeout = 100;
scheduleWork(callbackA);
scheduleWork(callbackB, {timeout: longerTimeout});
scheduleWork(callbackC, {timeout: shorterTimeout});

// generic API for advancing a frame and configuring how much idle time there will be
advanceByOneFrame({timeRemaining: -1 * (longerTimeout * 2)});

// idle time was negative, so there was none and we passed the timeout threshhold for the shorterTimeout
expect(callbackLog).toEqual(['C']);

// ... etc.


// 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