'use strict';
let Scheduler;
let runWithPriority;
let ImmediatePriority;
let UserBlockingPriority;
let NormalPriority;
let LowPriority;
let IdlePriority;
let scheduleCallback;
let cancelCallback;
let wrapCallback;
let getCurrentPriorityLevel;
let shouldYield;
describe('Scheduler', () => {
beforeEach(() => {
jest.resetModules();
jest.mock('scheduler', () => require('scheduler/unstable_mock'));
Scheduler = require('scheduler');
runWithPriority = Scheduler.unstable_runWithPriority;
ImmediatePriority = Scheduler.unstable_ImmediatePriority;
UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
NormalPriority = Scheduler.unstable_NormalPriority;
LowPriority = Scheduler.unstable_LowPriority;
IdlePriority = Scheduler.unstable_IdlePriority;
scheduleCallback = Scheduler.unstable_scheduleCallback;
cancelCallback = Scheduler.unstable_cancelCallback;
wrapCallback = Scheduler.unstable_wrapCallback;
getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel;
shouldYield = Scheduler.unstable_shouldYield;
});
it('flushes work incrementally', () => {
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('B'));
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('C'));
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('D'));
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
expect(Scheduler).toFlushAndYieldThrough(['C']);
expect(Scheduler).toFlushAndYield(['D']);
});
it('cancels work', () => {
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
const callbackHandleB = scheduleCallback(NormalPriority, () =>
Scheduler.unstable_yieldValue('B'),
);
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('C'));
cancelCallback(callbackHandleB);
expect(Scheduler).toFlushAndYield([
'A',
'C',
]);
});
it('executes the highest priority callbacks first', () => {
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('B'));
expect(Scheduler).toFlushAndYieldThrough(['A']);
scheduleCallback(UserBlockingPriority, () =>
Scheduler.unstable_yieldValue('C'),
);
scheduleCallback(UserBlockingPriority, () =>
Scheduler.unstable_yieldValue('D'),
);
expect(Scheduler).toFlushAndYield(['C', 'D', 'B']);
});
it('expires work', () => {
scheduleCallback(NormalPriority, didTimeout => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue(`A (did timeout: ${didTimeout})`);
});
scheduleCallback(UserBlockingPriority, didTimeout => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue(`B (did timeout: ${didTimeout})`);
});
scheduleCallback(UserBlockingPriority, didTimeout => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue(`C (did timeout: ${didTimeout})`);
});
Scheduler.unstable_advanceTime(249);
expect(Scheduler).toHaveYielded([]);
scheduleCallback(NormalPriority, didTimeout => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue(`D (did timeout: ${didTimeout})`);
});
scheduleCallback(NormalPriority, didTimeout => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue(`E (did timeout: ${didTimeout})`);
});
Scheduler.unstable_advanceTime(1);
expect(Scheduler).toFlushAndYieldThrough([
'B (did timeout: true)',
'C (did timeout: true)',
]);
Scheduler.unstable_advanceTime(4600);
expect(Scheduler).toFlushAndYieldThrough(['A (did timeout: true)']);
expect(Scheduler).toFlushAndYield([
'D (did timeout: false)',
'E (did timeout: true)',
]);
});
it('has a default expiration of ~5 seconds', () => {
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
Scheduler.unstable_advanceTime(4999);
expect(Scheduler).toHaveYielded([]);
Scheduler.unstable_advanceTime(1);
expect(Scheduler).toFlushExpired(['A']);
});
it('continues working on same task after yielding', () => {
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue('A');
});
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue('B');
});
let didYield = false;
const tasks = [
['C1', 100],
['C2', 100],
['C3', 100],
];
const C = () => {
while (tasks.length > 0) {
const [label, ms] = tasks.shift();
Scheduler.unstable_advanceTime(ms);
Scheduler.unstable_yieldValue(label);
if (shouldYield()) {
didYield = true;
return C;
}
}
};
scheduleCallback(NormalPriority, C);
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue('D');
});
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue('E');
});
expect(didYield).toBe(false);
expect(Scheduler).toFlushAndYieldThrough(['A', 'B', 'C1']);
expect(didYield).toBe(true);
expect(Scheduler).toFlushAndYield(['C2', 'C3', 'D', 'E']);
});
it('continuation callbacks inherit the expiration of the previous callback', () => {
const tasks = [
['A', 125],
['B', 124],
['C', 100],
['D', 100],
];
const work = () => {
while (tasks.length > 0) {
const [label, ms] = tasks.shift();
Scheduler.unstable_advanceTime(ms);
Scheduler.unstable_yieldValue(label);
if (shouldYield()) {
return work;
}
}
};
scheduleCallback(UserBlockingPriority, work);
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
Scheduler.unstable_advanceTime(1);
expect(Scheduler).toFlushExpired(['C', 'D']);
});
it('continuations are interrupted by higher priority work', () => {
const tasks = [
['A', 100],
['B', 100],
['C', 100],
['D', 100],
];
const work = () => {
while (tasks.length > 0) {
const [label, ms] = tasks.shift();
Scheduler.unstable_advanceTime(ms);
Scheduler.unstable_yieldValue(label);
if (tasks.length > 0 && shouldYield()) {
return work;
}
}
};
scheduleCallback(NormalPriority, work);
expect(Scheduler).toFlushAndYieldThrough(['A']);
scheduleCallback(UserBlockingPriority, () => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue('High pri');
});
expect(Scheduler).toFlushAndYield(['High pri', 'B', 'C', 'D']);
});
it(
'continuations do not block higher priority work scheduled ' +
'inside an executing callback',
() => {
const tasks = [
['A', 100],
['B', 100],
['C', 100],
['D', 100],
];
const work = () => {
while (tasks.length > 0) {
const task = tasks.shift();
const [label, ms] = task;
Scheduler.unstable_advanceTime(ms);
Scheduler.unstable_yieldValue(label);
if (label === 'B') {
Scheduler.unstable_yieldValue('Schedule high pri');
scheduleCallback(UserBlockingPriority, () => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue('High pri');
});
}
if (tasks.length > 0) {
return work;
}
}
};
scheduleCallback(NormalPriority, work);
expect(Scheduler).toFlushAndYield([
'A',
'B',
'Schedule high pri',
'High pri',
'C',
'D',
]);
},
);
it('cancelling a continuation', () => {
const task = scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('Yield');
return () => {
Scheduler.unstable_yieldValue('Continuation');
};
});
expect(Scheduler).toFlushAndYieldThrough(['Yield']);
cancelCallback(task);
expect(Scheduler).toFlushWithoutYielding();
});
it('top-level immediate callbacks fire in a subsequent task', () => {
scheduleCallback(ImmediatePriority, () =>
Scheduler.unstable_yieldValue('A'),
);
scheduleCallback(ImmediatePriority, () =>
Scheduler.unstable_yieldValue('B'),
);
scheduleCallback(ImmediatePriority, () =>
Scheduler.unstable_yieldValue('C'),
);
scheduleCallback(ImmediatePriority, () =>
Scheduler.unstable_yieldValue('D'),
);
expect(Scheduler).toHaveYielded([]);
expect(Scheduler).toFlushExpired(['A', 'B', 'C', 'D']);
});
it('nested immediate callbacks are added to the queue of immediate callbacks', () => {
scheduleCallback(ImmediatePriority, () =>
Scheduler.unstable_yieldValue('A'),
);
scheduleCallback(ImmediatePriority, () => {
Scheduler.unstable_yieldValue('B');
scheduleCallback(ImmediatePriority, () =>
Scheduler.unstable_yieldValue('C'),
);
});
scheduleCallback(ImmediatePriority, () =>
Scheduler.unstable_yieldValue('D'),
);
expect(Scheduler).toHaveYielded([]);
expect(Scheduler).toFlushExpired(['A', 'B', 'D', 'C']);
});
it('wrapped callbacks have same signature as original callback', () => {
const wrappedCallback = wrapCallback((...args) => ({args}));
expect(wrappedCallback('a', 'b')).toEqual({args: ['a', 'b']});
});
it('wrapped callbacks inherit the current priority', () => {
const wrappedCallback = runWithPriority(NormalPriority, () =>
wrapCallback(() => {
Scheduler.unstable_yieldValue(getCurrentPriorityLevel());
}),
);
const wrappedUserBlockingCallback = runWithPriority(
UserBlockingPriority,
() =>
wrapCallback(() => {
Scheduler.unstable_yieldValue(getCurrentPriorityLevel());
}),
);
wrappedCallback();
expect(Scheduler).toHaveYielded([NormalPriority]);
wrappedUserBlockingCallback();
expect(Scheduler).toHaveYielded([UserBlockingPriority]);
});
it('wrapped callbacks inherit the current priority even when nested', () => {
let wrappedCallback;
let wrappedUserBlockingCallback;
runWithPriority(NormalPriority, () => {
wrappedCallback = wrapCallback(() => {
Scheduler.unstable_yieldValue(getCurrentPriorityLevel());
});
wrappedUserBlockingCallback = runWithPriority(UserBlockingPriority, () =>
wrapCallback(() => {
Scheduler.unstable_yieldValue(getCurrentPriorityLevel());
}),
);
});
wrappedCallback();
expect(Scheduler).toHaveYielded([NormalPriority]);
wrappedUserBlockingCallback();
expect(Scheduler).toHaveYielded([UserBlockingPriority]);
});
it("immediate callbacks fire even if there's an error", () => {
scheduleCallback(ImmediatePriority, () => {
Scheduler.unstable_yieldValue('A');
throw new Error('Oops A');
});
scheduleCallback(ImmediatePriority, () => {
Scheduler.unstable_yieldValue('B');
});
scheduleCallback(ImmediatePriority, () => {
Scheduler.unstable_yieldValue('C');
throw new Error('Oops C');
});
expect(() => expect(Scheduler).toFlushExpired()).toThrow('Oops A');
expect(Scheduler).toHaveYielded(['A']);
expect(() => expect(Scheduler).toFlushExpired()).toThrow('Oops C');
expect(Scheduler).toHaveYielded(['B', 'C']);
});
it('multiple immediate callbacks can throw and there will be an error for each one', () => {
scheduleCallback(ImmediatePriority, () => {
throw new Error('First error');
});
scheduleCallback(ImmediatePriority, () => {
throw new Error('Second error');
});
expect(() => Scheduler.unstable_flushAll()).toThrow('First error');
expect(() => Scheduler.unstable_flushAll()).toThrow('Second error');
});
it('exposes the current priority level', () => {
Scheduler.unstable_yieldValue(getCurrentPriorityLevel());
runWithPriority(ImmediatePriority, () => {
Scheduler.unstable_yieldValue(getCurrentPriorityLevel());
runWithPriority(NormalPriority, () => {
Scheduler.unstable_yieldValue(getCurrentPriorityLevel());
runWithPriority(UserBlockingPriority, () => {
Scheduler.unstable_yieldValue(getCurrentPriorityLevel());
});
});
Scheduler.unstable_yieldValue(getCurrentPriorityLevel());
});
expect(Scheduler).toHaveYielded([
NormalPriority,
ImmediatePriority,
NormalPriority,
UserBlockingPriority,
ImmediatePriority,
]);
});
if (__DEV__) {
it.skip('adds extra function to the JS stack whose name includes the priority level', () => {
function inferPriorityFromCallstack() {
try {
throw Error();
} catch (e) {
const stack = e.stack;
const lines = stack.split('\n');
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
const found = line.match(
/scheduler_flushTaskAtPriority_([A-Za-z]+)/,
);
if (found !== null) {
const priorityStr = found[1];
switch (priorityStr) {
case 'Immediate':
return ImmediatePriority;
case 'UserBlocking':
return UserBlockingPriority;
case 'Normal':
return NormalPriority;
case 'Low':
return LowPriority;
case 'Idle':
return IdlePriority;
}
}
}
return null;
}
}
scheduleCallback(ImmediatePriority, () =>
Scheduler.unstable_yieldValue(
'Immediate: ' + inferPriorityFromCallstack(),
),
);
scheduleCallback(UserBlockingPriority, () =>
Scheduler.unstable_yieldValue(
'UserBlocking: ' + inferPriorityFromCallstack(),
),
);
scheduleCallback(NormalPriority, () =>
Scheduler.unstable_yieldValue(
'Normal: ' + inferPriorityFromCallstack(),
),
);
scheduleCallback(LowPriority, () =>
Scheduler.unstable_yieldValue('Low: ' + inferPriorityFromCallstack()),
);
scheduleCallback(IdlePriority, () =>
Scheduler.unstable_yieldValue('Idle: ' + inferPriorityFromCallstack()),
);
expect(Scheduler).toFlushAndYield([
'Immediate: ' + ImmediatePriority,
'UserBlocking: ' + UserBlockingPriority,
'Normal: ' + NormalPriority,
'Low: ' + LowPriority,
'Idle: ' + IdlePriority,
]);
});
}
describe('delayed tasks', () => {
it('schedules a delayed task', () => {
scheduleCallback(
NormalPriority,
() => Scheduler.unstable_yieldValue('A'),
{
delay: 1000,
},
);
expect(Scheduler).toFlushAndYield([]);
Scheduler.unstable_advanceTime(999);
expect(Scheduler).toFlushAndYield([]);
Scheduler.unstable_advanceTime(1);
expect(Scheduler).toFlushAndYield(['A']);
});
it('schedules multiple delayed tasks', () => {
scheduleCallback(
NormalPriority,
() => Scheduler.unstable_yieldValue('C'),
{
delay: 300,
},
);
scheduleCallback(
NormalPriority,
() => Scheduler.unstable_yieldValue('B'),
{
delay: 200,
},
);
scheduleCallback(
NormalPriority,
() => Scheduler.unstable_yieldValue('D'),
{
delay: 400,
},
);
scheduleCallback(
NormalPriority,
() => Scheduler.unstable_yieldValue('A'),
{
delay: 100,
},
);
expect(Scheduler).toFlushAndYield([]);
Scheduler.unstable_advanceTime(200);
expect(Scheduler).toFlushAndYieldThrough(['A']);
expect(Scheduler).toFlushAndYield(['B']);
Scheduler.unstable_advanceTime(200);
expect(Scheduler).toFlushAndYield(['C', 'D']);
});
it('interleaves normal tasks and delayed tasks', () => {
scheduleCallback(
UserBlockingPriority,
() => Scheduler.unstable_yieldValue('Timer 2'),
{delay: 300},
);
scheduleCallback(
UserBlockingPriority,
() => Scheduler.unstable_yieldValue('Timer 1'),
{delay: 100},
);
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('A');
Scheduler.unstable_advanceTime(100);
});
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('B');
Scheduler.unstable_advanceTime(100);
});
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('C');
Scheduler.unstable_advanceTime(100);
});
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('D');
Scheduler.unstable_advanceTime(100);
});
expect(Scheduler).toFlushAndYield([
'A',
'Timer 1',
'B',
'C',
'Timer 2',
'D',
]);
});
it('interleaves delayed tasks with time-sliced tasks', () => {
scheduleCallback(
UserBlockingPriority,
() => Scheduler.unstable_yieldValue('Timer 2'),
{delay: 300},
);
scheduleCallback(
UserBlockingPriority,
() => Scheduler.unstable_yieldValue('Timer 1'),
{delay: 100},
);
const tasks = [
['A', 100],
['B', 100],
['C', 100],
['D', 100],
];
const work = () => {
while (tasks.length > 0) {
const task = tasks.shift();
const [label, ms] = task;
Scheduler.unstable_advanceTime(ms);
Scheduler.unstable_yieldValue(label);
if (tasks.length > 0) {
return work;
}
}
};
scheduleCallback(NormalPriority, work);
expect(Scheduler).toFlushAndYield([
'A',
'Timer 1',
'B',
'C',
'Timer 2',
'D',
]);
});
it('cancels a delayed task', () => {
const options = {delay: 100};
scheduleCallback(
NormalPriority,
() => Scheduler.unstable_yieldValue('A'),
options,
);
const taskB = scheduleCallback(
NormalPriority,
() => Scheduler.unstable_yieldValue('B'),
options,
);
const taskC = scheduleCallback(
NormalPriority,
() => Scheduler.unstable_yieldValue('C'),
options,
);
expect(Scheduler).toFlushAndYield([]);
cancelCallback(taskB);
Scheduler.unstable_advanceTime(500);
cancelCallback(taskC);
expect(Scheduler).toFlushAndYield(['A']);
});
it('gracefully handles scheduled tasks that are not a function', () => {
scheduleCallback(ImmediatePriority, null);
expect(Scheduler).toFlushWithoutYielding();
scheduleCallback(ImmediatePriority, undefined);
expect(Scheduler).toFlushWithoutYielding();
scheduleCallback(ImmediatePriority, {});
expect(Scheduler).toFlushWithoutYielding();
scheduleCallback(ImmediatePriority, 42);
expect(Scheduler).toFlushWithoutYielding();
});
it('toFlushUntilNextPaint stops if a continuation is returned', () => {
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('Original Task');
Scheduler.unstable_yieldValue('shouldYield: ' + shouldYield());
Scheduler.unstable_yieldValue('Return a continuation');
return () => {
Scheduler.unstable_yieldValue('Continuation Task');
};
});
expect(Scheduler).toFlushUntilNextPaint([
'Original Task',
'shouldYield: false',
'Return a continuation',
]);
expect(Scheduler.unstable_now()).toBe(0);
expect(Scheduler).toFlushAndYield(['Continuation Task']);
});
it("toFlushAndYield keeps flushing even if there's a continuation", () => {
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('Original Task');
Scheduler.unstable_yieldValue('shouldYield: ' + shouldYield());
Scheduler.unstable_yieldValue('Return a continuation');
return () => {
Scheduler.unstable_yieldValue('Continuation Task');
};
});
expect(Scheduler).toFlushAndYield([
'Original Task',
'shouldYield: false',
'Return a continuation',
'Continuation Task',
]);
});
});
});