/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/scheduler/src/__tests__/SchedulerPostTask-test.js
414 строк
11 KB
Andrew Clark
Revert "[Tests] Reset modules by default" (#28318)
13 фев 2024, 19:39
Не верифицирован
13 фев 2024, 19:39
015ff2e
Код
Авторство
О чём код?
/** * 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 * @jest-environment node */ /* eslint-disable no-for-of-loops/no-for-of-loops */ 'use strict'; let Scheduler; let runtime; let performance; let cancelCallback; let scheduleCallback; let ImmediatePriority; let NormalPriority; let UserBlockingPriority; let LowPriority; let IdlePriority; let shouldYield; // The Scheduler postTask implementation uses a new postTask browser API to // schedule work on the main thread. This test suite mocks all browser methods // used in our implementation. It assumes as little as possible about the order // and timing of events. describe('SchedulerPostTask', () => { beforeEach(() => { jest.resetModules(); jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_post_task'), ); runtime = installMockBrowserRuntime(); performance = window.performance; Scheduler = require('scheduler'); cancelCallback = Scheduler.unstable_cancelCallback; scheduleCallback = Scheduler.unstable_scheduleCallback; ImmediatePriority = Scheduler.unstable_ImmediatePriority; UserBlockingPriority = Scheduler.unstable_UserBlockingPriority; NormalPriority = Scheduler.unstable_NormalPriority; LowPriority = Scheduler.unstable_LowPriority; IdlePriority = Scheduler.unstable_IdlePriority; shouldYield = Scheduler.unstable_shouldYield; }); afterEach(() => { if (!runtime.isLogEmpty()) { throw Error('Test exited without clearing log.'); } }); function installMockBrowserRuntime() { let taskQueue = new Map(); let eventLog = []; // Mock window functions const window = {}; global.window = window; let idCounter = 0; let currentTime = 0; window.performance = { now() { return currentTime; }, }; // Note: setTimeout is used to report errors and nothing else. window.setTimeout = cb => { try { cb(); } catch (error) { runtime.log(`Error: ${error.message}`); } }; // Mock browser scheduler. const scheduler = {}; global.scheduler = scheduler; scheduler.postTask = function (callback, {signal}) { const {priority} = signal; const id = idCounter++; log( `Post Task ${id} [${priority === undefined ? '<default>' : priority}]`, ); const controller = signal._controller; return new Promise((resolve, reject) => { taskQueue.set(controller, {id, callback, resolve, reject}); }); }; scheduler.yield = function ({signal}) { const {priority} = signal; const id = idCounter++; log(`Yield ${id} [${priority === undefined ? '<default>' : priority}]`); const controller = signal._controller; let callback; return { then(cb) { callback = cb; return new Promise((resolve, reject) => { taskQueue.set(controller, {id, callback, resolve, reject}); }); }, }; }; global.TaskController = class TaskController { constructor({priority}) { this.signal = {_controller: this, priority}; } abort() { const task = taskQueue.get(this); if (task !== undefined) { taskQueue.delete(this); const reject = task.reject; reject(new Error('Aborted')); } } }; function ensureLogIsEmpty() { if (eventLog.length !== 0) { throw Error('Log is not empty. Call assertLog before continuing.'); } } function advanceTime(ms) { currentTime += ms; } function flushTasks() { ensureLogIsEmpty(); // If there's a continuation, it will call postTask again // which will set nextTask. That means we need to clear // nextTask before the invocation, otherwise we would // delete the continuation task. const prevTaskQueue = taskQueue; taskQueue = new Map(); for (const [, {id, callback, resolve}] of prevTaskQueue) { log(`Task ${id} Fired`); callback(false); resolve(); } } function log(val) { eventLog.push(val); } function isLogEmpty() { return eventLog.length === 0; } function assertLog(expected) { const actual = eventLog; eventLog = []; expect(actual).toEqual(expected); } return { advanceTime, flushTasks, log, isLogEmpty, assertLog, }; } it('task that finishes before deadline', () => { scheduleCallback(NormalPriority, () => { runtime.log('A'); }); runtime.assertLog(['Post Task 0 [user-visible]']); runtime.flushTasks(); runtime.assertLog(['Task 0 Fired', 'A']); }); it('task with continuation', () => { scheduleCallback(NormalPriority, () => { runtime.log('A'); while (!Scheduler.unstable_shouldYield()) { runtime.advanceTime(1); } runtime.log(`Yield at ${performance.now()}ms`); return () => { runtime.log('Continuation'); }; }); runtime.assertLog(['Post Task 0 [user-visible]']); runtime.flushTasks(); runtime.assertLog([ 'Task 0 Fired', 'A', 'Yield at 5ms', 'Yield 1 [user-visible]', ]); runtime.flushTasks(); runtime.assertLog(['Task 1 Fired', 'Continuation']); }); it('multiple tasks', () => { scheduleCallback(NormalPriority, () => { runtime.log('A'); }); scheduleCallback(NormalPriority, () => { runtime.log('B'); }); runtime.assertLog([ 'Post Task 0 [user-visible]', 'Post Task 1 [user-visible]', ]); runtime.flushTasks(); runtime.assertLog(['Task 0 Fired', 'A', 'Task 1 Fired', 'B']); }); it('cancels tasks', () => { const task = scheduleCallback(NormalPriority, () => { runtime.log('A'); }); runtime.assertLog(['Post Task 0 [user-visible]']); cancelCallback(task); runtime.flushTasks(); runtime.assertLog([]); }); it('an error in one task does not affect execution of other tasks', () => { scheduleCallback(NormalPriority, () => { throw Error('Oops!'); }); scheduleCallback(NormalPriority, () => { runtime.log('Yay'); }); runtime.assertLog([ 'Post Task 0 [user-visible]', 'Post Task 1 [user-visible]', ]); runtime.flushTasks(); runtime.assertLog(['Task 0 Fired', 'Error: Oops!', 'Task 1 Fired', 'Yay']); }); it('schedule new task after queue has emptied', () => { scheduleCallback(NormalPriority, () => { runtime.log('A'); }); runtime.assertLog(['Post Task 0 [user-visible]']); runtime.flushTasks(); runtime.assertLog(['Task 0 Fired', 'A']); scheduleCallback(NormalPriority, () => { runtime.log('B'); }); runtime.assertLog(['Post Task 1 [user-visible]']); runtime.flushTasks(); runtime.assertLog(['Task 1 Fired', 'B']); }); it('schedule new task after a cancellation', () => { const handle = scheduleCallback(NormalPriority, () => { runtime.log('A'); }); runtime.assertLog(['Post Task 0 [user-visible]']); cancelCallback(handle); runtime.flushTasks(); runtime.assertLog([]); scheduleCallback(NormalPriority, () => { runtime.log('B'); }); runtime.assertLog(['Post Task 1 [user-visible]']); runtime.flushTasks(); runtime.assertLog(['Task 1 Fired', 'B']); }); it('schedules tasks at different priorities', () => { scheduleCallback(ImmediatePriority, () => { runtime.log('A'); }); scheduleCallback(UserBlockingPriority, () => { runtime.log('B'); }); scheduleCallback(NormalPriority, () => { runtime.log('C'); }); scheduleCallback(LowPriority, () => { runtime.log('D'); }); scheduleCallback(IdlePriority, () => { runtime.log('E'); }); runtime.assertLog([ 'Post Task 0 [user-blocking]', 'Post Task 1 [user-blocking]', 'Post Task 2 [user-visible]', 'Post Task 3 [user-visible]', 'Post Task 4 [background]', ]); runtime.flushTasks(); runtime.assertLog([ 'Task 0 Fired', 'A', 'Task 1 Fired', 'B', 'Task 2 Fired', 'C', 'Task 3 Fired', 'D', 'Task 4 Fired', 'E', ]); }); it('yielding continues in a new task regardless of how much time is remaining', () => { scheduleCallback(NormalPriority, () => { runtime.log('Original Task'); runtime.log('shouldYield: ' + shouldYield()); runtime.log('Return a continuation'); return () => { runtime.log('Continuation Task'); }; }); runtime.assertLog(['Post Task 0 [user-visible]']); runtime.flushTasks(); runtime.assertLog([ 'Task 0 Fired', '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 be scheduled in a separate macrotask even // though there's time remaining. 'Yield 1 [user-visible]', ]); // No time has elapsed expect(performance.now()).toBe(0); runtime.flushTasks(); runtime.assertLog(['Task 1 Fired', 'Continuation Task']); }); describe('falls back to postTask for scheduling continuations when scheduler.yield is not available', () => { beforeEach(() => { delete global.scheduler.yield; }); it('task with continuation', () => { scheduleCallback(NormalPriority, () => { runtime.log('A'); while (!Scheduler.unstable_shouldYield()) { runtime.advanceTime(1); } runtime.log(`Yield at ${performance.now()}ms`); return () => { runtime.log('Continuation'); }; }); runtime.assertLog(['Post Task 0 [user-visible]']); runtime.flushTasks(); runtime.assertLog([ 'Task 0 Fired', 'A', 'Yield at 5ms', 'Post Task 1 [user-visible]', ]); runtime.flushTasks(); runtime.assertLog(['Task 1 Fired', 'Continuation']); }); it('yielding continues in a new task regardless of how much time is remaining', () => { scheduleCallback(NormalPriority, () => { runtime.log('Original Task'); runtime.log('shouldYield: ' + shouldYield()); runtime.log('Return a continuation'); return () => { runtime.log('Continuation Task'); }; }); runtime.assertLog(['Post Task 0 [user-visible]']); runtime.flushTasks(); runtime.assertLog([ 'Task 0 Fired', '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 be scheduled in a separate macrotask even // though there's time remaining. 'Post Task 1 [user-visible]', ]); // No time has elapsed expect(performance.now()).toBe(0); runtime.flushTasks(); runtime.assertLog(['Task 1 Fired', 'Continuation Task']); }); }); });