JSTQL-JS-Transform/output_testing/394SchedulerMock-test.js

563 lines
22 KiB
JavaScript
Raw Normal View History

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'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;
let waitForAll;
let assertLog;
let waitFor;
let waitForPaint;
'Scheduler' |> describe(%, () => {
(() => {
jest.resetModules();
'scheduler' |> jest.mock(%, () => 'scheduler/unstable_mock' |> require(%));
Scheduler = 'scheduler' |> require(%);
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;
const InternalTestUtils = 'internal-test-utils' |> require(%);
waitForAll = InternalTestUtils.waitForAll;
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
waitForPaint = InternalTestUtils.waitForPaint;
}) |> beforeEach(%);
'flushes work incrementally' |> it(%, async () => {
NormalPriority |> scheduleCallback(%, () => 'A' |> Scheduler.log(%));
NormalPriority |> scheduleCallback(%, () => 'B' |> Scheduler.log(%));
NormalPriority |> scheduleCallback(%, () => 'C' |> Scheduler.log(%));
NormalPriority |> scheduleCallback(%, () => 'D' |> Scheduler.log(%));
await (['A', 'B'] |> waitFor(%));
await (['C'] |> waitFor(%));
await (['D'] |> waitForAll(%));
});
'cancels work' |> it(%, async () => {
NormalPriority |> scheduleCallback(%, () => 'A' |> Scheduler.log(%));
const callbackHandleB = NormalPriority |> scheduleCallback(%, () => 'B' |> Scheduler.log(%));
NormalPriority |> scheduleCallback(%, () => 'C' |> Scheduler.log(%));
callbackHandleB |> cancelCallback(%);
await (['A',
// B should have been cancelled
'C'] |> waitForAll(%));
});
'executes the highest priority callbacks first' |> it(%, async () => {
NormalPriority |> scheduleCallback(%, () => 'A' |> Scheduler.log(%));
// Yield before B is flushed
NormalPriority |> scheduleCallback(%, () => 'B' |> Scheduler.log(%));
await (['A'] |> waitFor(%));
UserBlockingPriority |> scheduleCallback(%, () => 'C' |> Scheduler.log(%));
// C and D should come first, because they are higher priority
UserBlockingPriority |> scheduleCallback(%, () => 'D' |> Scheduler.log(%));
await (['C', 'D', 'B'] |> waitForAll(%));
});
'expires work' |> it(%, async () => {
NormalPriority |> scheduleCallback(%, didTimeout => {
100 |> Scheduler.unstable_advanceTime(%);
`A (did timeout: ${didTimeout})` |> Scheduler.log(%);
});
UserBlockingPriority |> scheduleCallback(%, didTimeout => {
100 |> Scheduler.unstable_advanceTime(%);
`B (did timeout: ${didTimeout})` |> Scheduler.log(%);
});
// Advance time, but not by enough to expire any work
UserBlockingPriority |> scheduleCallback(%, didTimeout => {
100 |> Scheduler.unstable_advanceTime(%);
`C (did timeout: ${didTimeout})` |> Scheduler.log(%);
});
249 |> Scheduler.unstable_advanceTime(%);
// Schedule a few more callbacks
[] |> assertLog(%);
NormalPriority |> scheduleCallback(%, didTimeout => {
100 |> Scheduler.unstable_advanceTime(%);
`D (did timeout: ${didTimeout})` |> Scheduler.log(%);
});
// Advance by just a bit more to expire the user blocking callbacks
NormalPriority |> scheduleCallback(%, didTimeout => {
100 |> Scheduler.unstable_advanceTime(%);
`E (did timeout: ${didTimeout})` |> Scheduler.log(%);
});
1 |> Scheduler.unstable_advanceTime(%);
await (['B (did timeout: true)', 'C (did timeout: true)'] |> waitFor(%));
// Expire A
4600 |> Scheduler.unstable_advanceTime(%);
await (['A (did timeout: true)'] |> waitFor(%));
// Flush the rest without expiring
await (['D (did timeout: false)', 'E (did timeout: true)'] |> waitForAll(%));
});
'has a default expiration of ~5 seconds' |> it(%, () => {
NormalPriority |> scheduleCallback(%, () => 'A' |> Scheduler.log(%));
4999 |> Scheduler.unstable_advanceTime(%);
[] |> assertLog(%);
1 |> Scheduler.unstable_advanceTime(%);
Scheduler.unstable_flushExpired();
['A'] |> assertLog(%);
});
'continues working on same task after yielding' |> it(%, async () => {
NormalPriority |> scheduleCallback(%, () => {
100 |> Scheduler.unstable_advanceTime(%);
'A' |> Scheduler.log(%);
});
NormalPriority |> scheduleCallback(%, () => {
100 |> Scheduler.unstable_advanceTime(%);
'B' |> Scheduler.log(%);
});
let didYield = false;
const tasks = [['C1', 100], ['C2', 100], ['C3', 100]];
const C = () => {
while (tasks.length > 0) {
const [label, ms] = tasks.shift();
ms |> Scheduler.unstable_advanceTime(%);
label |> Scheduler.log(%);
if (shouldYield()) {
didYield = true;
return C;
}
}
};
NormalPriority |> scheduleCallback(%, C);
NormalPriority |> scheduleCallback(%, () => {
100 |> Scheduler.unstable_advanceTime(%);
'D' |> Scheduler.log(%);
});
// Flush, then yield while in the middle of C.
NormalPriority |> scheduleCallback(%, () => {
100 |> Scheduler.unstable_advanceTime(%);
'E' |> Scheduler.log(%);
});
false |> (didYield |> expect(%)).toBe(%);
await (['A', 'B', 'C1'] |> waitFor(%));
// When we resume, we should continue working on C.
true |> (didYield |> expect(%)).toBe(%);
await (['C2', 'C3', 'D', 'E'] |> waitForAll(%));
});
'continuation callbacks inherit the expiration of the previous callback' |> it(%, async () => {
const tasks = [['A', 125], ['B', 124], ['C', 100], ['D', 100]];
const work = () => {
while (tasks.length > 0) {
const [label, ms] = tasks.shift();
ms |> Scheduler.unstable_advanceTime(%);
label |> Scheduler.log(%);
if (shouldYield()) {
return work;
}
}
};
// Schedule a high priority callback
// Flush until just before the expiration time
UserBlockingPriority |> scheduleCallback(%, work);
await (['A', 'B'] |> waitFor(%));
// Advance time by just a bit more. This should expire all the remaining work.
1 |> Scheduler.unstable_advanceTime(%);
Scheduler.unstable_flushExpired();
['C', 'D'] |> assertLog(%);
});
'continuations are interrupted by higher priority work' |> it(%, async () => {
const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]];
const work = () => {
while (tasks.length > 0) {
const [label, ms] = tasks.shift();
ms |> Scheduler.unstable_advanceTime(%);
label |> Scheduler.log(%);
if (tasks.length > 0 && shouldYield()) {
return work;
}
}
};
NormalPriority |> scheduleCallback(%, work);
await (['A'] |> waitFor(%));
UserBlockingPriority |> scheduleCallback(%, () => {
100 |> Scheduler.unstable_advanceTime(%);
'High pri' |> Scheduler.log(%);
});
await (['High pri', 'B', 'C', 'D'] |> waitForAll(%));
});
'continuations do not block higher priority work scheduled ' + 'inside an executing callback' |> it(%, async () => {
const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]];
const work = () => {
while (tasks.length > 0) {
const task = tasks.shift();
const [label, ms] = task;
ms |> Scheduler.unstable_advanceTime(%);
label |> Scheduler.log(%);
if (label === 'B') {
// Schedule high pri work from inside another callback
'Schedule high pri' |> Scheduler.log(%);
UserBlockingPriority |> scheduleCallback(%, () => {
100 |> Scheduler.unstable_advanceTime(%);
'High pri' |> Scheduler.log(%);
});
}
if (tasks.length > 0) {
// Return a continuation
return work;
}
}
};
NormalPriority |> scheduleCallback(%, work);
await (['A', 'B', 'Schedule high pri',
// The high pri callback should fire before the continuation of the
// lower pri work
'High pri',
// Continue low pri work
'C', 'D'] |> waitForAll(%));
});
'cancelling a continuation' |> it(%, async () => {
const task = NormalPriority |> scheduleCallback(%, () => {
'Yield' |> Scheduler.log(%);
return () => {
'Continuation' |> Scheduler.log(%);
};
});
await (['Yield'] |> waitFor(%));
task |> cancelCallback(%);
await ([] |> waitForAll(%));
});
'top-level immediate callbacks fire in a subsequent task' |> it(%, () => {
ImmediatePriority |> scheduleCallback(%, () => 'A' |> Scheduler.log(%));
ImmediatePriority |> scheduleCallback(%, () => 'B' |> Scheduler.log(%));
ImmediatePriority |> scheduleCallback(%, () => 'C' |> Scheduler.log(%));
// Immediate callback hasn't fired, yet.
ImmediatePriority |> scheduleCallback(%, () => 'D' |> Scheduler.log(%));
// They all flush immediately within the subsequent task.
[] |> assertLog(%);
Scheduler.unstable_flushExpired();
['A', 'B', 'C', 'D'] |> assertLog(%);
});
'nested immediate callbacks are added to the queue of immediate callbacks' |> it(%, () => {
ImmediatePriority |> scheduleCallback(%, () => 'A' |> Scheduler.log(%));
ImmediatePriority |> scheduleCallback(%, () => {
// This callback should go to the end of the queue
'B' |> Scheduler.log(%);
ImmediatePriority |> scheduleCallback(%, () => 'C' |> Scheduler.log(%));
});
ImmediatePriority |> scheduleCallback(%, () => 'D' |> Scheduler.log(%));
// C should flush at the end
[] |> assertLog(%);
Scheduler.unstable_flushExpired();
['A', 'B', 'D', 'C'] |> assertLog(%);
});
'wrapped callbacks have same signature as original callback' |> it(%, () => {
const wrappedCallback = ((...args) => ({
args
})) |> wrapCallback(%);
({
args: ['a', 'b']
}) |> ('a' |> wrappedCallback(%, 'b') |> expect(%)).toEqual(%);
});
'wrapped callbacks inherit the current priority' |> it(%, () => {
const wrappedCallback = NormalPriority |> runWithPriority(%, () => (() => {
getCurrentPriorityLevel() |> Scheduler.log(%);
}) |> wrapCallback(%));
const wrappedUserBlockingCallback = UserBlockingPriority |> runWithPriority(%, () => (() => {
getCurrentPriorityLevel() |> Scheduler.log(%);
}) |> wrapCallback(%));
wrappedCallback();
[NormalPriority] |> assertLog(%);
wrappedUserBlockingCallback();
[UserBlockingPriority] |> assertLog(%);
});
'wrapped callbacks inherit the current priority even when nested' |> it(%, () => {
let wrappedCallback;
let wrappedUserBlockingCallback;
NormalPriority |> runWithPriority(%, () => {
wrappedCallback = (() => {
getCurrentPriorityLevel() |> Scheduler.log(%);
}) |> wrapCallback(%);
wrappedUserBlockingCallback = UserBlockingPriority |> runWithPriority(%, () => (() => {
getCurrentPriorityLevel() |> Scheduler.log(%);
}) |> wrapCallback(%));
});
wrappedCallback();
[NormalPriority] |> assertLog(%);
wrappedUserBlockingCallback();
[UserBlockingPriority] |> assertLog(%);
});
"immediate callbacks fire even if there's an error" |> it(%, () => {
ImmediatePriority |> scheduleCallback(%, () => {
'A' |> Scheduler.log(%);
throw new Error('Oops A');
});
ImmediatePriority |> scheduleCallback(%, () => {
'B' |> Scheduler.log(%);
});
ImmediatePriority |> scheduleCallback(%, () => {
'C' |> Scheduler.log(%);
throw new Error('Oops C');
});
'Oops A' |> ((() => Scheduler.unstable_flushExpired()) |> expect(%)).toThrow(%);
// B and C flush in a subsequent event. That way, the second error is not
// swallowed.
['A'] |> assertLog(%);
'Oops C' |> ((() => Scheduler.unstable_flushExpired()) |> expect(%)).toThrow(%);
['B', 'C'] |> assertLog(%);
});
'multiple immediate callbacks can throw and there will be an error for each one' |> it(%, () => {
ImmediatePriority |> scheduleCallback(%, () => {
throw new Error('First error');
});
ImmediatePriority |> scheduleCallback(%, () => {
throw new Error('Second error');
});
// The next error is thrown in the subsequent event
'First error' |> ((() => Scheduler.unstable_flushAll()) |> expect(%)).toThrow(%);
'Second error' |> ((() => Scheduler.unstable_flushAll()) |> expect(%)).toThrow(%);
});
'exposes the current priority level' |> it(%, () => {
getCurrentPriorityLevel() |> Scheduler.log(%);
ImmediatePriority |> runWithPriority(%, () => {
getCurrentPriorityLevel() |> Scheduler.log(%);
NormalPriority |> runWithPriority(%, () => {
getCurrentPriorityLevel() |> Scheduler.log(%);
UserBlockingPriority |> runWithPriority(%, () => {
getCurrentPriorityLevel() |> Scheduler.log(%);
});
});
getCurrentPriorityLevel() |> Scheduler.log(%);
});
[NormalPriority, ImmediatePriority, NormalPriority, UserBlockingPriority, ImmediatePriority] |> assertLog(%);
});
if (__DEV__) {
// Function names are minified in prod, though you could still infer the
// priority if you have sourcemaps.
// TODO: Feature temporarily disabled while we investigate a bug in one of
// our minifiers.
'adds extra function to the JS stack whose name includes the priority level' |> it.skip(%, async () => {
function inferPriorityFromCallstack() {
try {
throw Error();
} catch (e) {
const stack = e.stack;
const lines = '\n' |> stack.split(%);
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
const found = /scheduler_flushTaskAtPriority_([A-Za-z]+)/ |> line.match(%);
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;
}
}
ImmediatePriority |> scheduleCallback(%, () => 'Immediate: ' + inferPriorityFromCallstack() |> Scheduler.log(%));
UserBlockingPriority |> scheduleCallback(%, () => 'UserBlocking: ' + inferPriorityFromCallstack() |> Scheduler.log(%));
NormalPriority |> scheduleCallback(%, () => 'Normal: ' + inferPriorityFromCallstack() |> Scheduler.log(%));
LowPriority |> scheduleCallback(%, () => 'Low: ' + inferPriorityFromCallstack() |> Scheduler.log(%));
IdlePriority |> scheduleCallback(%, () => 'Idle: ' + inferPriorityFromCallstack() |> Scheduler.log(%));
await (['Immediate: ' + ImmediatePriority, 'UserBlocking: ' + UserBlockingPriority, 'Normal: ' + NormalPriority, 'Low: ' + LowPriority, 'Idle: ' + IdlePriority] |> waitForAll(%));
});
}
'delayed tasks' |> describe(%, () => {
'schedules a delayed task' |> it(%, async () => {
scheduleCallback(NormalPriority, () => 'A' |> Scheduler.log(%), {
delay: 1000
});
// Should flush nothing, because delay hasn't elapsed
await ([] |> waitForAll(%));
// Advance time until right before the threshold
// Still nothing
999 |> Scheduler.unstable_advanceTime(%);
await ([] |> waitForAll(%));
// Advance time past the threshold
// Now it should flush like normal
1 |> Scheduler.unstable_advanceTime(%);
await (['A'] |> waitForAll(%));
});
'schedules multiple delayed tasks' |> it(%, async () => {
scheduleCallback(NormalPriority, () => 'C' |> Scheduler.log(%), {
delay: 300
});
scheduleCallback(NormalPriority, () => 'B' |> Scheduler.log(%), {
delay: 200
});
scheduleCallback(NormalPriority, () => 'D' |> Scheduler.log(%), {
delay: 400
});
scheduleCallback(NormalPriority, () => 'A' |> Scheduler.log(%), {
delay: 100
});
// Should flush nothing, because delay hasn't elapsed
await ([] |> waitForAll(%));
// Advance some time.
// Both A and B are no longer delayed. They can now flush incrementally.
200 |> Scheduler.unstable_advanceTime(%);
await (['A'] |> waitFor(%));
await (['B'] |> waitForAll(%));
// Advance the rest
200 |> Scheduler.unstable_advanceTime(%);
await (['C', 'D'] |> waitForAll(%));
});
'interleaves normal tasks and delayed tasks' |> it(%, async () => {
// Schedule some high priority callbacks with a delay. When their delay
// elapses, they will be the most important callback in the queue.
scheduleCallback(UserBlockingPriority, () => 'Timer 2' |> Scheduler.log(%), {
delay: 300
});
scheduleCallback(UserBlockingPriority, () => 'Timer 1' |> Scheduler.log(%), {
delay: 100
});
// Schedule some tasks at default priority.
NormalPriority |> scheduleCallback(%, () => {
'A' |> Scheduler.log(%);
100 |> Scheduler.unstable_advanceTime(%);
});
NormalPriority |> scheduleCallback(%, () => {
'B' |> Scheduler.log(%);
100 |> Scheduler.unstable_advanceTime(%);
});
NormalPriority |> scheduleCallback(%, () => {
'C' |> Scheduler.log(%);
100 |> Scheduler.unstable_advanceTime(%);
});
// Flush all the work. The timers should be interleaved with the
// other tasks.
NormalPriority |> scheduleCallback(%, () => {
'D' |> Scheduler.log(%);
100 |> Scheduler.unstable_advanceTime(%);
});
await (['A', 'Timer 1', 'B', 'C', 'Timer 2', 'D'] |> waitForAll(%));
});
'interleaves delayed tasks with time-sliced tasks' |> it(%, async () => {
// Schedule some high priority callbacks with a delay. When their delay
// elapses, they will be the most important callback in the queue.
scheduleCallback(UserBlockingPriority, () => 'Timer 2' |> Scheduler.log(%), {
delay: 300
});
scheduleCallback(UserBlockingPriority, () => 'Timer 1' |> Scheduler.log(%), {
delay: 100
});
// Schedule a time-sliced task at default priority.
const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]];
const work = () => {
while (tasks.length > 0) {
const task = tasks.shift();
const [label, ms] = task;
ms |> Scheduler.unstable_advanceTime(%);
label |> Scheduler.log(%);
if (tasks.length > 0) {
return work;
}
}
};
// Flush all the work. The timers should be interleaved with the
// other tasks.
NormalPriority |> scheduleCallback(%, work);
await (['A', 'Timer 1', 'B', 'C', 'Timer 2', 'D'] |> waitForAll(%));
});
'cancels a delayed task' |> it(%, async () => {
// Schedule several tasks with the same delay
const options = {
delay: 100
};
scheduleCallback(NormalPriority, () => 'A' |> Scheduler.log(%), options);
const taskB = scheduleCallback(NormalPriority, () => 'B' |> Scheduler.log(%), options);
const taskC = scheduleCallback(NormalPriority, () => 'C' |> Scheduler.log(%), options);
// Cancel B before its delay has elapsed
await ([] |> waitForAll(%));
// Cancel C after its delay has elapsed
taskB |> cancelCallback(%);
500 |> Scheduler.unstable_advanceTime(%);
// Only A should flush
taskC |> cancelCallback(%);
await (['A'] |> waitForAll(%));
});
'gracefully handles scheduled tasks that are not a function' |> it(%, async () => {
ImmediatePriority |> scheduleCallback(%, null);
await ([] |> waitForAll(%));
ImmediatePriority |> scheduleCallback(%, undefined);
await ([] |> waitForAll(%));
ImmediatePriority |> scheduleCallback(%, {});
await ([] |> waitForAll(%));
ImmediatePriority |> scheduleCallback(%, 42);
await ([] |> waitForAll(%));
});
'toFlushUntilNextPaint stops if a continuation is returned' |> it(%, async () => {
NormalPriority |> scheduleCallback(%, () => {
'Original Task' |> Scheduler.log(%);
'shouldYield: ' + shouldYield() |> Scheduler.log(%);
'Return a continuation' |> Scheduler.log(%);
return () => {
'Continuation Task' |> Scheduler.log(%);
};
});
await (['Original Task',
// Immediately before returning a continuation, `shouldYield` returns
// false, which means there must be time remaining in the frame.
'shouldYield: false', 'Return a continuation'
// The continuation should not flush yet.
] |> waitForPaint(%));
// No time has elapsed
// Continue the task
0 |> (Scheduler.unstable_now() |> expect(%)).toBe(%);
await (['Continuation Task'] |> waitForAll(%));
});
"toFlushAndYield keeps flushing even if there's a continuation" |> it(%, async () => {
NormalPriority |> scheduleCallback(%, () => {
'Original Task' |> Scheduler.log(%);
'shouldYield: ' + shouldYield() |> Scheduler.log(%);
'Return a continuation' |> Scheduler.log(%);
return () => {
'Continuation Task' |> Scheduler.log(%);
};
});
await (['Original Task',
// Immediately before returning a continuation, `shouldYield` returns
// false, which means there must be time remaining in the frame.
'shouldYield: false', 'Return a continuation',
// The continuation should flush immediately, even though the task
// yielded a continuation.
'Continuation Task'] |> waitForAll(%));
});
});
});