| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | * |
| 7 | * @emails react-core |
| 8 | */ |
| 9 | |
| 10 | 'use strict'; |
| 11 | |
| 12 | let Scheduler; |
| 13 | let runWithPriority; |
| 14 | let ImmediatePriority; |
| 15 | let UserBlockingPriority; |
| 16 | let NormalPriority; |
| 17 | let LowPriority; |
| 18 | let IdlePriority; |
| 19 | let scheduleCallback; |
| 20 | let cancelCallback; |
| 21 | let wrapCallback; |
| 22 | let getCurrentPriorityLevel; |
| 23 | let shouldYield; |
| 24 | let waitForAll; |
| 25 | let assertLog; |
| 26 | let waitFor; |
| 27 | let waitForPaint; |
| 28 | |
| 29 | describe('Scheduler', () => { |
| 30 | beforeEach(() => { |
| 31 | jest.resetModules(); |
| 32 | jest.mock('scheduler', () => require('scheduler/unstable_mock')); |
| 33 | |
| 34 | Scheduler = require('scheduler'); |
| 35 | |
| 36 | runWithPriority = Scheduler.unstable_runWithPriority; |
| 37 | ImmediatePriority = Scheduler.unstable_ImmediatePriority; |
| 38 | UserBlockingPriority = Scheduler.unstable_UserBlockingPriority; |
| 39 | NormalPriority = Scheduler.unstable_NormalPriority; |
| 40 | LowPriority = Scheduler.unstable_LowPriority; |
| 41 | IdlePriority = Scheduler.unstable_IdlePriority; |
| 42 | scheduleCallback = Scheduler.unstable_scheduleCallback; |
| 43 | cancelCallback = Scheduler.unstable_cancelCallback; |
| 44 | wrapCallback = Scheduler.unstable_wrapCallback; |
| 45 | getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel; |
| 46 | shouldYield = Scheduler.unstable_shouldYield; |
| 47 | |
| 48 | const InternalTestUtils = require('internal-test-utils'); |
| 49 | waitForAll = InternalTestUtils.waitForAll; |
| 50 | assertLog = InternalTestUtils.assertLog; |
| 51 | waitFor = InternalTestUtils.waitFor; |
| 52 | waitForPaint = InternalTestUtils.waitForPaint; |
| 53 | }); |
| 54 | |
| 55 | it('flushes work incrementally', async () => { |
| 56 | scheduleCallback(NormalPriority, () => Scheduler.log('A')); |
| 57 | scheduleCallback(NormalPriority, () => Scheduler.log('B')); |
| 58 | scheduleCallback(NormalPriority, () => Scheduler.log('C')); |
| 59 | scheduleCallback(NormalPriority, () => Scheduler.log('D')); |
| 60 | |
| 61 | await waitFor(['A', 'B']); |
| 62 | await waitFor(['C']); |
| 63 | await waitForAll(['D']); |
| 64 | }); |
| 65 | |
| 66 | it('cancels work', async () => { |
| 67 | scheduleCallback(NormalPriority, () => Scheduler.log('A')); |
| 68 | const callbackHandleB = scheduleCallback(NormalPriority, () => |
| 69 | Scheduler.log('B'), |
| 70 | ); |
| 71 | scheduleCallback(NormalPriority, () => Scheduler.log('C')); |
| 72 | |
| 73 | cancelCallback(callbackHandleB); |
| 74 | |
| 75 | await waitForAll([ |
| 76 | 'A', |
| 77 | // B should have been cancelled |
| 78 | 'C', |
| 79 | ]); |
| 80 | }); |
| 81 | |
| 82 | it('executes the highest priority callbacks first', async () => { |
| 83 | scheduleCallback(NormalPriority, () => Scheduler.log('A')); |
| 84 | scheduleCallback(NormalPriority, () => Scheduler.log('B')); |
| 85 | |
| 86 | // Yield before B is flushed |
| 87 | await waitFor(['A']); |
| 88 | |
| 89 | scheduleCallback(UserBlockingPriority, () => Scheduler.log('C')); |
| 90 | scheduleCallback(UserBlockingPriority, () => Scheduler.log('D')); |
| 91 | |
| 92 | // C and D should come first, because they are higher priority |
| 93 | await waitForAll(['C', 'D', 'B']); |
| 94 | }); |
| 95 | |
| 96 | it('expires work', async () => { |
| 97 | scheduleCallback(NormalPriority, didTimeout => { |
| 98 | Scheduler.unstable_advanceTime(100); |
| 99 | Scheduler.log(`A (did timeout: ${didTimeout})`); |
| 100 | }); |
| 101 | scheduleCallback(UserBlockingPriority, didTimeout => { |
| 102 | Scheduler.unstable_advanceTime(100); |
| 103 | Scheduler.log(`B (did timeout: ${didTimeout})`); |
| 104 | }); |
| 105 | scheduleCallback(UserBlockingPriority, didTimeout => { |
| 106 | Scheduler.unstable_advanceTime(100); |
| 107 | Scheduler.log(`C (did timeout: ${didTimeout})`); |
| 108 | }); |
| 109 | |
| 110 | // Advance time, but not by enough to expire any work |
| 111 | Scheduler.unstable_advanceTime(249); |
| 112 | assertLog([]); |
| 113 | |
| 114 | // Schedule a few more callbacks |
| 115 | scheduleCallback(NormalPriority, didTimeout => { |
| 116 | Scheduler.unstable_advanceTime(100); |
| 117 | Scheduler.log(`D (did timeout: ${didTimeout})`); |
| 118 | }); |
| 119 | scheduleCallback(NormalPriority, didTimeout => { |
| 120 | Scheduler.unstable_advanceTime(100); |
| 121 | Scheduler.log(`E (did timeout: ${didTimeout})`); |
| 122 | }); |
| 123 | |
| 124 | // Advance by just a bit more to expire the user blocking callbacks |
| 125 | Scheduler.unstable_advanceTime(1); |
| 126 | await waitFor(['B (did timeout: true)', 'C (did timeout: true)']); |
| 127 | |
| 128 | // Expire A |
| 129 | Scheduler.unstable_advanceTime(4600); |
| 130 | await waitFor(['A (did timeout: true)']); |
| 131 | |
| 132 | // Flush the rest without expiring |
| 133 | await waitForAll(['D (did timeout: false)', 'E (did timeout: true)']); |
| 134 | }); |
| 135 | |
| 136 | it('has a default expiration of ~5 seconds', () => { |
| 137 | scheduleCallback(NormalPriority, () => Scheduler.log('A')); |
| 138 | |
| 139 | Scheduler.unstable_advanceTime(4999); |
| 140 | assertLog([]); |
| 141 | |
| 142 | Scheduler.unstable_advanceTime(1); |
| 143 | Scheduler.unstable_flushExpired(); |
| 144 | assertLog(['A']); |
| 145 | }); |
| 146 | |
| 147 | it('continues working on same task after yielding', async () => { |
| 148 | scheduleCallback(NormalPriority, () => { |
| 149 | Scheduler.unstable_advanceTime(100); |
| 150 | Scheduler.log('A'); |
| 151 | }); |
| 152 | scheduleCallback(NormalPriority, () => { |
| 153 | Scheduler.unstable_advanceTime(100); |
| 154 | Scheduler.log('B'); |
| 155 | }); |
| 156 | |
| 157 | let didYield = false; |
| 158 | const tasks = [ |
| 159 | ['C1', 100], |
| 160 | ['C2', 100], |
| 161 | ['C3', 100], |
| 162 | ]; |
| 163 | const C = () => { |
| 164 | while (tasks.length > 0) { |
| 165 | const [label, ms] = tasks.shift(); |
| 166 | Scheduler.unstable_advanceTime(ms); |
| 167 | Scheduler.log(label); |
| 168 | if (shouldYield()) { |
| 169 | didYield = true; |
| 170 | return C; |
| 171 | } |
| 172 | } |
| 173 | }; |
| 174 | |
| 175 | scheduleCallback(NormalPriority, C); |
| 176 | |
| 177 | scheduleCallback(NormalPriority, () => { |
| 178 | Scheduler.unstable_advanceTime(100); |
| 179 | Scheduler.log('D'); |
| 180 | }); |
| 181 | scheduleCallback(NormalPriority, () => { |
| 182 | Scheduler.unstable_advanceTime(100); |
| 183 | Scheduler.log('E'); |
| 184 | }); |
| 185 | |
| 186 | // Flush, then yield while in the middle of C. |
| 187 | expect(didYield).toBe(false); |
| 188 | await waitFor(['A', 'B', 'C1']); |
| 189 | expect(didYield).toBe(true); |
| 190 | |
| 191 | // When we resume, we should continue working on C. |
| 192 | await waitForAll(['C2', 'C3', 'D', 'E']); |
| 193 | }); |
| 194 | |
| 195 | it('continuation callbacks inherit the expiration of the previous callback', async () => { |
| 196 | const tasks = [ |
| 197 | ['A', 125], |
| 198 | ['B', 124], |
| 199 | ['C', 100], |
| 200 | ['D', 100], |
| 201 | ]; |
| 202 | const work = () => { |
| 203 | while (tasks.length > 0) { |
| 204 | const [label, ms] = tasks.shift(); |
| 205 | Scheduler.unstable_advanceTime(ms); |
| 206 | Scheduler.log(label); |
| 207 | if (shouldYield()) { |
| 208 | return work; |
| 209 | } |
| 210 | } |
| 211 | }; |
| 212 | |
| 213 | // Schedule a high priority callback |
| 214 | scheduleCallback(UserBlockingPriority, work); |
| 215 | |
| 216 | // Flush until just before the expiration time |
| 217 | await waitFor(['A', 'B']); |
| 218 | |
| 219 | // Advance time by just a bit more. This should expire all the remaining work. |
| 220 | Scheduler.unstable_advanceTime(1); |
| 221 | Scheduler.unstable_flushExpired(); |
| 222 | assertLog(['C', 'D']); |
| 223 | }); |
| 224 | |
| 225 | it('continuations are interrupted by higher priority work', async () => { |
| 226 | const tasks = [ |
| 227 | ['A', 100], |
| 228 | ['B', 100], |
| 229 | ['C', 100], |
| 230 | ['D', 100], |
| 231 | ]; |
| 232 | const work = () => { |
| 233 | while (tasks.length > 0) { |
| 234 | const [label, ms] = tasks.shift(); |
| 235 | Scheduler.unstable_advanceTime(ms); |
| 236 | Scheduler.log(label); |
| 237 | if (tasks.length > 0 && shouldYield()) { |
| 238 | return work; |
| 239 | } |
| 240 | } |
| 241 | }; |
| 242 | scheduleCallback(NormalPriority, work); |
| 243 | await waitFor(['A']); |
| 244 | |
| 245 | scheduleCallback(UserBlockingPriority, () => { |
| 246 | Scheduler.unstable_advanceTime(100); |
| 247 | Scheduler.log('High pri'); |
| 248 | }); |
| 249 | |
| 250 | await waitForAll(['High pri', 'B', 'C', 'D']); |
| 251 | }); |
| 252 | |
| 253 | it( |
| 254 | 'continuations do not block higher priority work scheduled ' + |
| 255 | 'inside an executing callback', |
| 256 | async () => { |
| 257 | const tasks = [ |
| 258 | ['A', 100], |
| 259 | ['B', 100], |
| 260 | ['C', 100], |
| 261 | ['D', 100], |
| 262 | ]; |
| 263 | const work = () => { |
| 264 | while (tasks.length > 0) { |
| 265 | const task = tasks.shift(); |
| 266 | const [label, ms] = task; |
| 267 | Scheduler.unstable_advanceTime(ms); |
| 268 | Scheduler.log(label); |
| 269 | if (label === 'B') { |
| 270 | // Schedule high pri work from inside another callback |
| 271 | Scheduler.log('Schedule high pri'); |
| 272 | scheduleCallback(UserBlockingPriority, () => { |
| 273 | Scheduler.unstable_advanceTime(100); |
| 274 | Scheduler.log('High pri'); |
| 275 | }); |
| 276 | } |
| 277 | if (tasks.length > 0) { |
| 278 | // Return a continuation |
| 279 | return work; |
| 280 | } |
| 281 | } |
| 282 | }; |
| 283 | scheduleCallback(NormalPriority, work); |
| 284 | await waitForAll([ |
| 285 | 'A', |
| 286 | 'B', |
| 287 | 'Schedule high pri', |
| 288 | // The high pri callback should fire before the continuation of the |
| 289 | // lower pri work |
| 290 | 'High pri', |
| 291 | // Continue low pri work |
| 292 | 'C', |
| 293 | 'D', |
| 294 | ]); |
| 295 | }, |
| 296 | ); |
| 297 | |
| 298 | it('cancelling a continuation', async () => { |
| 299 | const task = scheduleCallback(NormalPriority, () => { |
| 300 | Scheduler.log('Yield'); |
| 301 | return () => { |
| 302 | Scheduler.log('Continuation'); |
| 303 | }; |
| 304 | }); |
| 305 | |
| 306 | await waitFor(['Yield']); |
| 307 | cancelCallback(task); |
| 308 | await waitForAll([]); |
| 309 | }); |
| 310 | |
| 311 | it('top-level immediate callbacks fire in a subsequent task', () => { |
| 312 | scheduleCallback(ImmediatePriority, () => Scheduler.log('A')); |
| 313 | scheduleCallback(ImmediatePriority, () => Scheduler.log('B')); |
| 314 | scheduleCallback(ImmediatePriority, () => Scheduler.log('C')); |
| 315 | scheduleCallback(ImmediatePriority, () => Scheduler.log('D')); |
| 316 | // Immediate callback hasn't fired, yet. |
| 317 | assertLog([]); |
| 318 | // They all flush immediately within the subsequent task. |
| 319 | Scheduler.unstable_flushExpired(); |
| 320 | assertLog(['A', 'B', 'C', 'D']); |
| 321 | }); |
| 322 | |
| 323 | it('nested immediate callbacks are added to the queue of immediate callbacks', () => { |
| 324 | scheduleCallback(ImmediatePriority, () => Scheduler.log('A')); |
| 325 | scheduleCallback(ImmediatePriority, () => { |
| 326 | Scheduler.log('B'); |
| 327 | // This callback should go to the end of the queue |
| 328 | scheduleCallback(ImmediatePriority, () => Scheduler.log('C')); |
| 329 | }); |
| 330 | scheduleCallback(ImmediatePriority, () => Scheduler.log('D')); |
| 331 | assertLog([]); |
| 332 | // C should flush at the end |
| 333 | Scheduler.unstable_flushExpired(); |
| 334 | assertLog(['A', 'B', 'D', 'C']); |
| 335 | }); |
| 336 | |
| 337 | it('wrapped callbacks have same signature as original callback', () => { |
| 338 | const wrappedCallback = wrapCallback((...args) => ({args})); |
| 339 | expect(wrappedCallback('a', 'b')).toEqual({args: ['a', 'b']}); |
| 340 | }); |
| 341 | |
| 342 | it('wrapped callbacks inherit the current priority', () => { |
| 343 | const wrappedCallback = runWithPriority(NormalPriority, () => |
| 344 | wrapCallback(() => { |
| 345 | Scheduler.log(getCurrentPriorityLevel()); |
| 346 | }), |
| 347 | ); |
| 348 | |
| 349 | const wrappedUserBlockingCallback = runWithPriority( |
| 350 | UserBlockingPriority, |
| 351 | () => |
| 352 | wrapCallback(() => { |
| 353 | Scheduler.log(getCurrentPriorityLevel()); |
| 354 | }), |
| 355 | ); |
| 356 | |
| 357 | wrappedCallback(); |
| 358 | assertLog([NormalPriority]); |
| 359 | |
| 360 | wrappedUserBlockingCallback(); |
| 361 | assertLog([UserBlockingPriority]); |
| 362 | }); |
| 363 | |
| 364 | it('wrapped callbacks inherit the current priority even when nested', () => { |
| 365 | let wrappedCallback; |
| 366 | let wrappedUserBlockingCallback; |
| 367 | |
| 368 | runWithPriority(NormalPriority, () => { |
| 369 | wrappedCallback = wrapCallback(() => { |
| 370 | Scheduler.log(getCurrentPriorityLevel()); |
| 371 | }); |
| 372 | wrappedUserBlockingCallback = runWithPriority(UserBlockingPriority, () => |
| 373 | wrapCallback(() => { |
| 374 | Scheduler.log(getCurrentPriorityLevel()); |
| 375 | }), |
| 376 | ); |
| 377 | }); |
| 378 | |
| 379 | wrappedCallback(); |
| 380 | assertLog([NormalPriority]); |
| 381 | |
| 382 | wrappedUserBlockingCallback(); |
| 383 | assertLog([UserBlockingPriority]); |
| 384 | }); |
| 385 | |
| 386 | it("immediate callbacks fire even if there's an error", () => { |
| 387 | scheduleCallback(ImmediatePriority, () => { |
| 388 | Scheduler.log('A'); |
| 389 | throw new Error('Oops A'); |
| 390 | }); |
| 391 | scheduleCallback(ImmediatePriority, () => { |
| 392 | Scheduler.log('B'); |
| 393 | }); |
| 394 | scheduleCallback(ImmediatePriority, () => { |
| 395 | Scheduler.log('C'); |
| 396 | throw new Error('Oops C'); |
| 397 | }); |
| 398 | |
| 399 | expect(() => Scheduler.unstable_flushExpired()).toThrow('Oops A'); |
| 400 | assertLog(['A']); |
| 401 | |
| 402 | // B and C flush in a subsequent event. That way, the second error is not |
| 403 | // swallowed. |
| 404 | expect(() => Scheduler.unstable_flushExpired()).toThrow('Oops C'); |
| 405 | assertLog(['B', 'C']); |
| 406 | }); |
| 407 | |
| 408 | it('multiple immediate callbacks can throw and there will be an error for each one', () => { |
| 409 | scheduleCallback(ImmediatePriority, () => { |
| 410 | throw new Error('First error'); |
| 411 | }); |
| 412 | scheduleCallback(ImmediatePriority, () => { |
| 413 | throw new Error('Second error'); |
| 414 | }); |
| 415 | expect(() => Scheduler.unstable_flushAll()).toThrow('First error'); |
| 416 | // The next error is thrown in the subsequent event |
| 417 | expect(() => Scheduler.unstable_flushAll()).toThrow('Second error'); |
| 418 | }); |
| 419 | |
| 420 | it('exposes the current priority level', () => { |
| 421 | Scheduler.log(getCurrentPriorityLevel()); |
| 422 | runWithPriority(ImmediatePriority, () => { |
| 423 | Scheduler.log(getCurrentPriorityLevel()); |
| 424 | runWithPriority(NormalPriority, () => { |
| 425 | Scheduler.log(getCurrentPriorityLevel()); |
| 426 | runWithPriority(UserBlockingPriority, () => { |
| 427 | Scheduler.log(getCurrentPriorityLevel()); |
| 428 | }); |
| 429 | }); |
| 430 | Scheduler.log(getCurrentPriorityLevel()); |
| 431 | }); |
| 432 | |
| 433 | assertLog([ |
| 434 | NormalPriority, |
| 435 | ImmediatePriority, |
| 436 | NormalPriority, |
| 437 | UserBlockingPriority, |
| 438 | ImmediatePriority, |
| 439 | ]); |
| 440 | }); |
| 441 | |
| 442 | if (__DEV__) { |
| 443 | // Function names are minified in prod, though you could still infer the |
| 444 | // priority if you have sourcemaps. |
| 445 | // TODO: Feature temporarily disabled while we investigate a bug in one of |
| 446 | // our minifiers. |
| 447 | // eslint-disable-next-line jest/no-disabled-tests |
| 448 | it.skip('adds extra function to the JS stack whose name includes the priority level', async () => { |
| 449 | function inferPriorityFromCallstack() { |
| 450 | try { |
| 451 | throw Error(); |
| 452 | } catch (e) { |
| 453 | const stack = e.stack; |
| 454 | const lines = stack.split('\n'); |
| 455 | for (let i = lines.length - 1; i >= 0; i--) { |
| 456 | const line = lines[i]; |
| 457 | const found = line.match( |
| 458 | /scheduler_flushTaskAtPriority_([A-Za-z]+)/, |
| 459 | ); |
| 460 | if (found !== null) { |
| 461 | const priorityStr = found[1]; |
| 462 | switch (priorityStr) { |
| 463 | case 'Immediate': |
| 464 | return ImmediatePriority; |
| 465 | case 'UserBlocking': |
| 466 | return UserBlockingPriority; |
| 467 | case 'Normal': |
| 468 | return NormalPriority; |
| 469 | case 'Low': |
| 470 | return LowPriority; |
| 471 | case 'Idle': |
| 472 | return IdlePriority; |
| 473 | } |
| 474 | } |
| 475 | } |
| 476 | return null; |
| 477 | } |
| 478 | } |
| 479 | |
| 480 | scheduleCallback(ImmediatePriority, () => |
| 481 | Scheduler.log('Immediate: ' + inferPriorityFromCallstack()), |
| 482 | ); |
| 483 | scheduleCallback(UserBlockingPriority, () => |
| 484 | Scheduler.log('UserBlocking: ' + inferPriorityFromCallstack()), |
| 485 | ); |
| 486 | scheduleCallback(NormalPriority, () => |
| 487 | Scheduler.log('Normal: ' + inferPriorityFromCallstack()), |
| 488 | ); |
| 489 | scheduleCallback(LowPriority, () => |
| 490 | Scheduler.log('Low: ' + inferPriorityFromCallstack()), |
| 491 | ); |
| 492 | scheduleCallback(IdlePriority, () => |
| 493 | Scheduler.log('Idle: ' + inferPriorityFromCallstack()), |
| 494 | ); |
| 495 | |
| 496 | await waitForAll([ |
| 497 | 'Immediate: ' + ImmediatePriority, |
| 498 | 'UserBlocking: ' + UserBlockingPriority, |
| 499 | 'Normal: ' + NormalPriority, |
| 500 | 'Low: ' + LowPriority, |
| 501 | 'Idle: ' + IdlePriority, |
| 502 | ]); |
| 503 | }); |
| 504 | } |
| 505 | |
| 506 | describe('delayed tasks', () => { |
| 507 | it('schedules a delayed task', async () => { |
| 508 | scheduleCallback(NormalPriority, () => Scheduler.log('A'), { |
| 509 | delay: 1000, |
| 510 | }); |
| 511 | |
| 512 | // Should flush nothing, because delay hasn't elapsed |
| 513 | await waitForAll([]); |
| 514 | |
| 515 | // Advance time until right before the threshold |
| 516 | Scheduler.unstable_advanceTime(999); |
| 517 | // Still nothing |
| 518 | await waitForAll([]); |
| 519 | |
| 520 | // Advance time past the threshold |
| 521 | Scheduler.unstable_advanceTime(1); |
| 522 | |
| 523 | // Now it should flush like normal |
| 524 | await waitForAll(['A']); |
| 525 | }); |
| 526 | |
| 527 | it('schedules multiple delayed tasks', async () => { |
| 528 | scheduleCallback(NormalPriority, () => Scheduler.log('C'), { |
| 529 | delay: 300, |
| 530 | }); |
| 531 | |
| 532 | scheduleCallback(NormalPriority, () => Scheduler.log('B'), { |
| 533 | delay: 200, |
| 534 | }); |
| 535 | |
| 536 | scheduleCallback(NormalPriority, () => Scheduler.log('D'), { |
| 537 | delay: 400, |
| 538 | }); |
| 539 | |
| 540 | scheduleCallback(NormalPriority, () => Scheduler.log('A'), { |
| 541 | delay: 100, |
| 542 | }); |
| 543 | |
| 544 | // Should flush nothing, because delay hasn't elapsed |
| 545 | await waitForAll([]); |
| 546 | |
| 547 | // Advance some time. |
| 548 | Scheduler.unstable_advanceTime(200); |
| 549 | // Both A and B are no longer delayed. They can now flush incrementally. |
| 550 | await waitFor(['A']); |
| 551 | await waitForAll(['B']); |
| 552 | |
| 553 | // Advance the rest |
| 554 | Scheduler.unstable_advanceTime(200); |
| 555 | await waitForAll(['C', 'D']); |
| 556 | }); |
| 557 | |
| 558 | it('interleaves normal tasks and delayed tasks', async () => { |
| 559 | // Schedule some high priority callbacks with a delay. When their delay |
| 560 | // elapses, they will be the most important callback in the queue. |
| 561 | scheduleCallback(UserBlockingPriority, () => Scheduler.log('Timer 2'), { |
| 562 | delay: 300, |
| 563 | }); |
| 564 | scheduleCallback(UserBlockingPriority, () => Scheduler.log('Timer 1'), { |
| 565 | delay: 100, |
| 566 | }); |
| 567 | |
| 568 | // Schedule some tasks at default priority. |
| 569 | scheduleCallback(NormalPriority, () => { |
| 570 | Scheduler.log('A'); |
| 571 | Scheduler.unstable_advanceTime(100); |
| 572 | }); |
| 573 | scheduleCallback(NormalPriority, () => { |
| 574 | Scheduler.log('B'); |
| 575 | Scheduler.unstable_advanceTime(100); |
| 576 | }); |
| 577 | scheduleCallback(NormalPriority, () => { |
| 578 | Scheduler.log('C'); |
| 579 | Scheduler.unstable_advanceTime(100); |
| 580 | }); |
| 581 | scheduleCallback(NormalPriority, () => { |
| 582 | Scheduler.log('D'); |
| 583 | Scheduler.unstable_advanceTime(100); |
| 584 | }); |
| 585 | |
| 586 | // Flush all the work. The timers should be interleaved with the |
| 587 | // other tasks. |
| 588 | await waitForAll(['A', 'Timer 1', 'B', 'C', 'Timer 2', 'D']); |
| 589 | }); |
| 590 | |
| 591 | it('interleaves delayed tasks with time-sliced tasks', async () => { |
| 592 | // Schedule some high priority callbacks with a delay. When their delay |
| 593 | // elapses, they will be the most important callback in the queue. |
| 594 | scheduleCallback(UserBlockingPriority, () => Scheduler.log('Timer 2'), { |
| 595 | delay: 300, |
| 596 | }); |
| 597 | scheduleCallback(UserBlockingPriority, () => Scheduler.log('Timer 1'), { |
| 598 | delay: 100, |
| 599 | }); |
| 600 | |
| 601 | // Schedule a time-sliced task at default priority. |
| 602 | const tasks = [ |
| 603 | ['A', 100], |
| 604 | ['B', 100], |
| 605 | ['C', 100], |
| 606 | ['D', 100], |
| 607 | ]; |
| 608 | const work = () => { |
| 609 | while (tasks.length > 0) { |
| 610 | const task = tasks.shift(); |
| 611 | const [label, ms] = task; |
| 612 | Scheduler.unstable_advanceTime(ms); |
| 613 | Scheduler.log(label); |
| 614 | if (tasks.length > 0) { |
| 615 | return work; |
| 616 | } |
| 617 | } |
| 618 | }; |
| 619 | scheduleCallback(NormalPriority, work); |
| 620 | |
| 621 | // Flush all the work. The timers should be interleaved with the |
| 622 | // other tasks. |
| 623 | await waitForAll(['A', 'Timer 1', 'B', 'C', 'Timer 2', 'D']); |
| 624 | }); |
| 625 | |
| 626 | it('cancels a delayed task', async () => { |
| 627 | // Schedule several tasks with the same delay |
| 628 | const options = {delay: 100}; |
| 629 | |
| 630 | scheduleCallback(NormalPriority, () => Scheduler.log('A'), options); |
| 631 | const taskB = scheduleCallback( |
| 632 | NormalPriority, |
| 633 | () => Scheduler.log('B'), |
| 634 | options, |
| 635 | ); |
| 636 | const taskC = scheduleCallback( |
| 637 | NormalPriority, |
| 638 | () => Scheduler.log('C'), |
| 639 | options, |
| 640 | ); |
| 641 | |
| 642 | // Cancel B before its delay has elapsed |
| 643 | await waitForAll([]); |
| 644 | cancelCallback(taskB); |
| 645 | |
| 646 | // Cancel C after its delay has elapsed |
| 647 | Scheduler.unstable_advanceTime(500); |
| 648 | cancelCallback(taskC); |
| 649 | |
| 650 | // Only A should flush |
| 651 | await waitForAll(['A']); |
| 652 | }); |
| 653 | |
| 654 | it('gracefully handles scheduled tasks that are not a function', async () => { |
| 655 | scheduleCallback(ImmediatePriority, null); |
| 656 | await waitForAll([]); |
| 657 | |
| 658 | scheduleCallback(ImmediatePriority, undefined); |
| 659 | await waitForAll([]); |
| 660 | |
| 661 | scheduleCallback(ImmediatePriority, {}); |
| 662 | await waitForAll([]); |
| 663 | |
| 664 | scheduleCallback(ImmediatePriority, 42); |
| 665 | await waitForAll([]); |
| 666 | }); |
| 667 | |
| 668 | it('toFlushUntilNextPaint stops if a continuation is returned', async () => { |
| 669 | scheduleCallback(NormalPriority, () => { |
| 670 | Scheduler.log('Original Task'); |
| 671 | Scheduler.log('shouldYield: ' + shouldYield()); |
| 672 | Scheduler.log('Return a continuation'); |
| 673 | return () => { |
| 674 | Scheduler.log('Continuation Task'); |
| 675 | }; |
| 676 | }); |
| 677 | |
| 678 | await waitForPaint([ |
| 679 | 'Original Task', |
| 680 | // Immediately before returning a continuation, `shouldYield` returns |
| 681 | // false, which means there must be time remaining in the frame. |
| 682 | 'shouldYield: false', |
| 683 | 'Return a continuation', |
| 684 | |
| 685 | // The continuation should not flush yet. |
| 686 | ]); |
| 687 | |
| 688 | // No time has elapsed |
| 689 | expect(Scheduler.unstable_now()).toBe(0); |
| 690 | |
| 691 | // Continue the task |
| 692 | await waitForAll(['Continuation Task']); |
| 693 | }); |
| 694 | |
| 695 | it("toFlushAndYield keeps flushing even if there's a continuation", async () => { |
| 696 | scheduleCallback(NormalPriority, () => { |
| 697 | Scheduler.log('Original Task'); |
| 698 | Scheduler.log('shouldYield: ' + shouldYield()); |
| 699 | Scheduler.log('Return a continuation'); |
| 700 | return () => { |
| 701 | Scheduler.log('Continuation Task'); |
| 702 | }; |
| 703 | }); |
| 704 | |
| 705 | await waitForAll([ |
| 706 | 'Original Task', |
| 707 | // Immediately before returning a continuation, `shouldYield` returns |
| 708 | // false, which means there must be time remaining in the frame. |
| 709 | 'shouldYield: false', |
| 710 | 'Return a continuation', |
| 711 | |
| 712 | // The continuation should flush immediately, even though the task |
| 713 | // yielded a continuation. |
| 714 | 'Continuation Task', |
| 715 | ]); |
| 716 | }); |
| 717 | }); |
| 718 | }); |