From 9a52cc8bcd580238f4a8b12036e698823847fd65 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sat, 4 Mar 2023 11:44:41 -0500 Subject: [PATCH] Convert ReactLazy-test to waitFor pattern (#26304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I'm in the process of codemodding our test suite to the waitFor pattern. See #26285 for full context. This module required a lot of manual changes so I'm doing it as its own PR. The reason is that most of the tests involved simulating an async import by wrapping them in `Promise.resolve()`, which means they would immediately resolve the next time the microtask queue was flushed. I rewrote the tests to resolve the simulated import explicitly. While converting these tests, I also realized that the `waitFor` helpers weren't properly waiting for the entire microtask queue to recursively finish — if a microtask schedules another microtask, the subsequent one wouldn't fire until after `waitFor` had resolved. To fix this, I used the same strategy as `act` — wait for a real task to finish before proceeding, such as a message event. --- .../ReactInternalTestUtils.js | 33 +- packages/internal-test-utils/enqueueTask.js | 50 ++ .../__tests__/ReactCacheOld-test.internal.js | 2 +- .../ReactIncrementalScheduling-test.js | 4 +- .../__tests__/ReactIncrementalUpdates-test.js | 4 +- .../src/__tests__/ReactLazy-test.internal.js | 474 +++++++++--------- 6 files changed, 305 insertions(+), 262 deletions(-) create mode 100644 packages/internal-test-utils/enqueueTask.js diff --git a/packages/internal-test-utils/ReactInternalTestUtils.js b/packages/internal-test-utils/ReactInternalTestUtils.js index db47ff910ca86..2677312d4b2af 100644 --- a/packages/internal-test-utils/ReactInternalTestUtils.js +++ b/packages/internal-test-utils/ReactInternalTestUtils.js @@ -10,6 +10,7 @@ import * as SchedulerMock from 'scheduler/unstable_mock'; import {diff} from 'jest-diff'; import {equals} from '@jest/expect-utils'; +import enqueueTask from './enqueueTask'; function assertYieldsWereCleared(Scheduler) { const actualYields = Scheduler.unstable_clearYields(); @@ -22,6 +23,12 @@ function assertYieldsWereCleared(Scheduler) { } } +async function waitForMicrotasks() { + return new Promise(resolve => { + enqueueTask(() => resolve()); + }); +} + export async function waitFor(expectedLog) { assertYieldsWereCleared(SchedulerMock); @@ -33,7 +40,7 @@ export async function waitFor(expectedLog) { const actualLog = []; do { // Wait until end of current task/microtask. - await null; + await waitForMicrotasks(); if (SchedulerMock.unstable_hasPendingWork()) { SchedulerMock.unstable_flushNumberOfYields( expectedLog.length - actualLog.length, @@ -44,7 +51,7 @@ export async function waitFor(expectedLog) { } else { // Once we've reached the expected sequence, wait one more microtask to // flush any remaining synchronous work. - await null; + await waitForMicrotasks(); actualLog.push(...SchedulerMock.unstable_clearYields()); break; } @@ -72,11 +79,11 @@ export async function waitForAll(expectedLog) { // Create the error object before doing any async work, to get a better // stack trace. const error = new Error(); - Error.captureStackTrace(error, waitFor); + Error.captureStackTrace(error, waitForAll); do { // Wait until end of current task/microtask. - await null; + await waitForMicrotasks(); if (!SchedulerMock.unstable_hasPendingWork()) { // There's no pending work, even after a microtask. Stop flushing. break; @@ -103,11 +110,11 @@ export async function waitForThrow(expectedError: mixed) { // Create the error object before doing any async work, to get a better // stack trace. const error = new Error(); - Error.captureStackTrace(error, waitFor); + Error.captureStackTrace(error, waitForThrow); do { // Wait until end of current task/microtask. - await null; + await waitForMicrotasks(); if (!SchedulerMock.unstable_hasPendingWork()) { // There's no pending work, even after a microtask. Stop flushing. error.message = 'Expected something to throw, but nothing did.'; @@ -119,7 +126,13 @@ export async function waitForThrow(expectedError: mixed) { if (equals(x, expectedError)) { return; } - if (typeof x === 'object' && x !== null && x.message === expectedError) { + if ( + typeof expectedError === 'string' && + typeof x === 'object' && + x !== null && + typeof x.message === 'string' && + x.message.includes(expectedError) + ) { return; } error.message = ` @@ -142,15 +155,15 @@ export async function waitForPaint(expectedLog) { // Create the error object before doing any async work, to get a better // stack trace. const error = new Error(); - Error.captureStackTrace(error, waitFor); + Error.captureStackTrace(error, waitForPaint); // Wait until end of current task/microtask. - await null; + await waitForMicrotasks(); if (SchedulerMock.unstable_hasPendingWork()) { // Flush until React yields. SchedulerMock.unstable_flushUntilNextPaint(); // Wait one more microtask to flush any remaining synchronous work. - await null; + await waitForMicrotasks(); } const actualLog = SchedulerMock.unstable_clearYields(); diff --git a/packages/internal-test-utils/enqueueTask.js b/packages/internal-test-utils/enqueueTask.js new file mode 100644 index 0000000000000..6f7f00fee68ca --- /dev/null +++ b/packages/internal-test-utils/enqueueTask.js @@ -0,0 +1,50 @@ +/** + * 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. + * + * @flow + */ + +let didWarnAboutMessageChannel = false; +let enqueueTaskImpl = null; + +// Same as shared/enqeuueTask, but while that one used by the public +// implementation of `act`, this is only used by our internal testing helpers. +export default function enqueueTask(task: () => void): void { + if (enqueueTaskImpl === null) { + try { + // read require off the module object to get around the bundlers. + // we don't want them to detect a require and bundle a Node polyfill. + const requireString = ('require' + Math.random()).slice(0, 7); + const nodeRequire = module && module[requireString]; + // assuming we're in node, let's try to get node's + // version of setImmediate, bypassing fake timers if any. + enqueueTaskImpl = nodeRequire.call(module, 'timers').setImmediate; + } catch (_err) { + // we're in a browser + // we can't use regular timers because they may still be faked + // so we try MessageChannel+postMessage instead + enqueueTaskImpl = function (callback: () => void) { + if (__DEV__) { + if (didWarnAboutMessageChannel === false) { + didWarnAboutMessageChannel = true; + if (typeof MessageChannel === 'undefined') { + console['error']( + 'This browser does not have a MessageChannel implementation, ' + + 'so enqueuing tasks via await act(async () => ...) will fail. ' + + 'Please file an issue at https://github.com/facebook/react/issues ' + + 'if you encounter this warning.', + ); + } + } + } + const channel = new MessageChannel(); + channel.port1.onmessage = callback; + channel.port2.postMessage(undefined); + }; + } + } + return enqueueTaskImpl(task); +} diff --git a/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js b/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js index 303516013b6d0..82956027cca66 100644 --- a/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js +++ b/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js @@ -183,7 +183,7 @@ describe('ReactCache', () => { ); if (__DEV__) { - expect(async () => { + await expect(async () => { await waitForAll(['App', 'Loading...']); }).toErrorDev([ 'Invalid key type. Expected a string, number, symbol, or ' + diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js index bed7b61e54528..3d4751b6b3fa8 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js @@ -93,7 +93,7 @@ describe('ReactIncrementalScheduling', () => { expect(ReactNoop).toMatchRenderedOutput(); }); - it('works on deferred roots in the order they were scheduled', () => { + it('works on deferred roots in the order they were scheduled', async () => { const {useEffect} = React; function Text({text}) { useEffect(() => { @@ -114,7 +114,7 @@ describe('ReactIncrementalScheduling', () => { expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:1'); // Schedule deferred work in the reverse order - act(async () => { + await act(async () => { React.startTransition(() => { ReactNoop.renderToRootWithID(, 'c'); ReactNoop.renderToRootWithID(, 'b'); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js index 3c548469fb67a..6f81c28ea9656 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js @@ -518,7 +518,7 @@ describe('ReactIncrementalUpdates', () => { expect(ReactNoop).toMatchRenderedOutput(); }); - it('regression: does not expire soon due to layout effects in the last batch', () => { + it('regression: does not expire soon due to layout effects in the last batch', async () => { const {useState, useLayoutEffect} = React; let setCount; @@ -533,7 +533,7 @@ describe('ReactIncrementalUpdates', () => { return null; } - act(async () => { + await act(async () => { React.startTransition(() => { ReactNoop.render(); }); diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index 627bab2368dc3..5ee887ee1081f 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -5,6 +5,11 @@ let Scheduler; let ReactFeatureFlags; let Suspense; let lazy; +let waitForAll; +let waitForThrow; +let assertLog; + +let fakeModuleCache; function normalizeCodeLocInfo(str) { return ( @@ -27,6 +32,13 @@ describe('ReactLazy', () => { lazy = React.lazy; ReactTestRenderer = require('react-test-renderer'); Scheduler = require('scheduler'); + + const InternalTestUtils = require('internal-test-utils'); + waitForAll = InternalTestUtils.waitForAll; + waitForThrow = InternalTestUtils.waitForThrow; + assertLog = InternalTestUtils.assertLog; + + fakeModuleCache = new Map(); }); function Text(props) { @@ -34,12 +46,45 @@ describe('ReactLazy', () => { return props.text; } - function delay(ms) { - return new Promise(resolve => setTimeout(() => resolve(), ms)); + async function fakeImport(Component) { + const record = fakeModuleCache.get(Component); + if (record === undefined) { + const newRecord = { + status: 'pending', + value: {default: Component}, + pings: [], + then(ping) { + switch (newRecord.status) { + case 'pending': { + newRecord.pings.push(ping); + return; + } + case 'resolved': { + ping(newRecord.value); + return; + } + case 'rejected': { + throw newRecord.value; + } + } + }, + }; + fakeModuleCache.set(Component, newRecord); + return newRecord; + } + return record; } - async function fakeImport(result) { - return {default: result}; + function resolveFakeImport(moduleName) { + const record = fakeModuleCache.get(moduleName); + if (record === undefined) { + throw new Error('Module not found'); + } + if (record.status !== 'pending') { + throw new Error('Module already resolved'); + } + record.status = 'resolved'; + record.pings.forEach(ping => ping(record.value)); } it('suspends until module has loaded', async () => { @@ -54,12 +99,12 @@ describe('ReactLazy', () => { }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('Hi'); - await Promise.resolve(); + await resolveFakeImport(Text); - expect(Scheduler).toFlushAndYield(['Hi']); + await waitForAll(['Hi']); expect(root).toMatchRenderedOutput('Hi'); // Should not suspend on update @@ -68,7 +113,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toFlushAndYield(['Hi again']); + await waitForAll(['Hi again']); expect(root).toMatchRenderedOutput('Hi again'); }); @@ -85,7 +130,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toHaveYielded(['Hi']); + assertLog(['Hi']); expect(root).toMatchRenderedOutput('Hi'); }); @@ -115,7 +160,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toHaveYielded([]); + assertLog([]); expect(root).toMatchRenderedOutput('Error: oh no'); }); @@ -128,11 +173,8 @@ describe('ReactLazy', () => { return ; } - const promiseForFoo = delay(100).then(() => fakeImport(Foo)); - const promiseForBar = delay(500).then(() => fakeImport(Bar)); - - const LazyFoo = lazy(() => promiseForFoo); - const LazyBar = lazy(() => promiseForBar); + const LazyFoo = lazy(() => fakeImport(Foo)); + const LazyBar = lazy(() => fakeImport(Bar)); const root = ReactTestRenderer.create( }> @@ -144,19 +186,17 @@ describe('ReactLazy', () => { }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('FooBar'); - jest.advanceTimersByTime(100); - await promiseForFoo; + await resolveFakeImport(Foo); - expect(Scheduler).toFlushAndYield(['Foo']); + await waitForAll(['Foo']); expect(root).not.toMatchRenderedOutput('FooBar'); - jest.advanceTimersByTime(500); - await promiseForBar; + await resolveFakeImport(Bar); - expect(Scheduler).toFlushAndYield(['Foo', 'Bar']); + await waitForAll(['Foo', 'Bar']); expect(root).toMatchRenderedOutput('FooBar'); }); @@ -173,12 +213,9 @@ describe('ReactLazy', () => { unstable_isConcurrent: true, }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForThrow('Element type is invalid'); + assertLog(['Loading...']); expect(root).not.toMatchRenderedOutput('Hi'); - - await Promise.resolve(); - - expect(Scheduler).toFlushAndThrow('Element type is invalid'); if (__DEV__) { expect(console.error).toHaveBeenCalledTimes(3); expect(console.error.mock.calls[0][0]).toContain( @@ -201,14 +238,9 @@ describe('ReactLazy', () => { }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForThrow('Bad network'); + assertLog(['Loading...']); expect(root).not.toMatchRenderedOutput('Hi'); - - try { - await Promise.resolve(); - } catch (e) {} - - expect(Scheduler).toFlushAndThrow('Bad network'); }); it('mount and reorder', async () => { @@ -247,28 +279,17 @@ describe('ReactLazy', () => { unstable_isConcurrent: true, }); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('AB'); - await LazyChildA; - await LazyChildB; + await resolveFakeImport(Child); - expect(Scheduler).toFlushAndYield([ - 'A', - 'B', - 'Did mount: A', - 'Did mount: B', - ]); + await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']); expect(root).toMatchRenderedOutput('AB'); // Swap the position of A and B root.update(); - expect(Scheduler).toFlushAndYield([ - 'B', - 'A', - 'Did update: B', - 'Did update: A', - ]); + await waitForAll(['B', 'A', 'Did update: B', 'Did update: A']); expect(root).toMatchRenderedOutput('BA'); }); @@ -288,10 +309,10 @@ describe('ReactLazy', () => { }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('Hi'); - await Promise.resolve(); + await resolveFakeImport(T); expect(() => expect(Scheduler).toFlushAndYield(['Hi'])).toErrorDev( 'Warning: T: Support for defaultProps ' + @@ -307,7 +328,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toFlushAndYield(['Hi again']); + await waitForAll(['Hi again']); expect(root).toMatchRenderedOutput('Hi again'); }); @@ -343,10 +364,10 @@ describe('ReactLazy', () => { unstable_isConcurrent: true, }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('SiblingA'); - await Promise.resolve(); + await resolveFakeImport(LazyImpl); expect(() => expect(Scheduler).toFlushAndYield(['Lazy', 'Sibling', 'A']), @@ -360,7 +381,7 @@ describe('ReactLazy', () => { // Lazy should not re-render stateful.current.setState({text: 'B'}); - expect(Scheduler).toFlushAndYield(['B']); + await waitForAll(['B']); expect(root).toMatchRenderedOutput('SiblingB'); }); @@ -390,22 +411,22 @@ describe('ReactLazy', () => { unstable_isConcurrent: true, }, ); - expect(Scheduler).toFlushAndYield(['Not lazy: 0', 'Loading...']); + await waitForAll(['Not lazy: 0', 'Loading...']); expect(root).not.toMatchRenderedOutput('Not lazy: 0Lazy: 0'); - await Promise.resolve(); + await resolveFakeImport(LazyImpl); - expect(Scheduler).toFlushAndYield(['Lazy: 0']); + await waitForAll(['Lazy: 0']); expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); // Should bailout due to unchanged props and state instance1.current.setState(null); - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); // Should bailout due to unchanged props and state instance2.current.setState(null); - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); }); @@ -436,22 +457,22 @@ describe('ReactLazy', () => { unstable_isConcurrent: true, }, ); - expect(Scheduler).toFlushAndYield(['Not lazy: 0', 'Loading...']); + await waitForAll(['Not lazy: 0', 'Loading...']); expect(root).not.toMatchRenderedOutput('Not lazy: 0Lazy: 0'); - await Promise.resolve(); + await resolveFakeImport(LazyImpl); - expect(Scheduler).toFlushAndYield(['Lazy: 0']); + await waitForAll(['Lazy: 0']); expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); // Should bailout due to shallow equal props and state instance1.current.setState({}); - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); // Should bailout due to shallow equal props and state instance2.current.setState({}); - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); }); @@ -518,12 +539,12 @@ describe('ReactLazy', () => { }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('A1'); - await Promise.resolve(); + await resolveFakeImport(C); - expect(Scheduler).toFlushAndYield([ + await waitForAll([ 'constructor: A', 'getDerivedStateFromProps: A', 'A1', @@ -535,7 +556,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toFlushAndYield([ + await waitForAll([ 'getDerivedStateFromProps: A', 'shouldComponentUpdate: A -> A', 'A2', @@ -549,7 +570,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toFlushAndYield([ + await waitForAll([ 'getDerivedStateFromProps: A', 'shouldComponentUpdate: A -> A', 'A3', @@ -595,13 +616,13 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toHaveYielded(['Loading...']); - expect(Scheduler).toFlushAndYield([]); + assertLog(['Loading...']); + await waitForAll([]); expect(root).toMatchRenderedOutput('Loading...'); - await Promise.resolve(); + await resolveFakeImport(C); - expect(Scheduler).toHaveYielded([]); + assertLog([]); root.update( }> @@ -609,7 +630,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toHaveYielded(['UNSAFE_componentWillMount: A', 'A2']); + assertLog(['UNSAFE_componentWillMount: A', 'A2']); expect(root).toMatchRenderedOutput('A2'); root.update( @@ -617,12 +638,12 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toHaveYielded([ + assertLog([ 'UNSAFE_componentWillReceiveProps: A -> A', 'UNSAFE_componentWillUpdate: A -> A', 'A3', ]); - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); expect(root).toMatchRenderedOutput('A3'); }); @@ -651,10 +672,10 @@ describe('ReactLazy', () => { }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('Hi Bye'); - await Promise.resolve(); + await resolveFakeImport(T); expect(() => expect(Scheduler).toFlushAndYield(['Hi Bye'])).toErrorDev( 'Warning: T: Support for defaultProps ' + 'will be removed from function components in a future major ' + @@ -668,7 +689,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toFlushAndYield(['Hi World']); + await waitForAll(['Hi World']); expect(root).toMatchRenderedOutput('Hi World'); root.update( @@ -676,7 +697,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toFlushAndYield(['Friends Bye']); + await waitForAll(['Friends Bye']); expect(root).toMatchRenderedOutput('Friends Bye'); }); @@ -692,9 +713,9 @@ describe('ReactLazy', () => { }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); - await Promise.resolve(); + await resolveFakeImport(42); root.update( }> @@ -719,10 +740,10 @@ describe('ReactLazy', () => { }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('Hello'); - await Promise.resolve(); + await resolveFakeImport(Lazy1); root.update( }> @@ -773,14 +794,14 @@ describe('ReactLazy', () => { }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('22'); // Mount - await Promise.resolve(); - expect(() => { - Scheduler.unstable_flushAll(); + await resolveFakeImport(Add); + await expect(async () => { + await waitForAll([]); }).toErrorDev( shouldWarnAboutFunctionDefaultProps ? [ @@ -799,13 +820,13 @@ describe('ReactLazy', () => { expect(root).toMatchRenderedOutput('22'); // Update - expect(() => { + await expect(async () => { root.update( }> , ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); }).toErrorDev( 'Invalid prop `inner` of type `boolean` supplied to `Add`, expected `number`.', ); @@ -971,13 +992,13 @@ describe('ReactLazy', () => { }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('Inner default text'); // Mount - await Promise.resolve(); - expect(() => { - expect(Scheduler).toFlushAndYield(['Inner default text']); + await resolveFakeImport(T); + await expect(async () => { + await waitForAll(['Inner default text']); }).toErrorDev([ 'T: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', 'The prop `text` is marked as required in `T`, but its value is `undefined`', @@ -985,13 +1006,13 @@ describe('ReactLazy', () => { expect(root).toMatchRenderedOutput('Inner default text'); // Update - expect(() => { + await expect(async () => { root.update( }> , ); - expect(Scheduler).toFlushAndYield([null]); + await waitForAll([null]); }).toErrorDev( 'The prop `text` is marked as required in `T`, but its value is `null`', ); @@ -999,9 +1020,9 @@ describe('ReactLazy', () => { }); it('includes lazy-loaded component in warning stack', async () => { + const Foo = props =>
{[, ]}
; const LazyFoo = lazy(() => { Scheduler.unstable_yieldValue('Started loading'); - const Foo = props =>
{[, ]}
; return fakeImport(Foo); }); @@ -1014,39 +1035,39 @@ describe('ReactLazy', () => { }, ); - expect(Scheduler).toFlushAndYield(['Started loading', 'Loading...']); + await waitForAll(['Started loading', 'Loading...']); expect(root).not.toMatchRenderedOutput(
AB
); - await Promise.resolve(); + await resolveFakeImport(Foo); - expect(() => { - expect(Scheduler).toFlushAndYield(['A', 'B']); + await expect(async () => { + await waitForAll(['A', 'B']); }).toErrorDev(' in Text (at **)\n' + ' in Foo (at **)'); expect(root).toMatchRenderedOutput(
AB
); }); it('supports class and forwardRef components', async () => { - const LazyClass = lazy(() => { - class Foo extends React.Component { - render() { - return ; - } + class Foo extends React.Component { + render() { + return ; } + } + const LazyClass = lazy(() => { return fakeImport(Foo); }); - const LazyForwardRef = lazy(() => { - class Bar extends React.Component { - render() { - return ; - } + class Bar extends React.Component { + render() { + return ; } - return fakeImport( - React.forwardRef((props, ref) => { - Scheduler.unstable_yieldValue('forwardRef'); - return ; - }), - ); + } + const ForwardRefBar = React.forwardRef((props, ref) => { + Scheduler.unstable_yieldValue('forwardRef'); + return ; + }); + + const LazyForwardRef = lazy(() => { + return fakeImport(ForwardRefBar); }); const ref = React.createRef(); @@ -1060,13 +1081,16 @@ describe('ReactLazy', () => { }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('FooBar'); expect(ref.current).toBe(null); - await Promise.resolve(); + await resolveFakeImport(Foo); + await waitForAll(['Foo']); - expect(Scheduler).toFlushAndYield(['Foo', 'forwardRef', 'Bar']); + await resolveFakeImport(ForwardRefBar); + + await waitForAll(['Foo', 'forwardRef', 'Bar']); expect(root).toMatchRenderedOutput('FooBar'); expect(ref.current).not.toBe(null); }); @@ -1088,13 +1112,13 @@ describe('ReactLazy', () => { unstable_isConcurrent: true, }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('4'); // Mount - await Promise.resolve(); - expect(() => { - expect(Scheduler).toFlushWithoutYielding(); + await resolveFakeImport(Add); + await expect(async () => { + await waitForAll([]); }).toErrorDev( 'Unknown: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', ); @@ -1106,7 +1130,7 @@ describe('ReactLazy', () => {
, ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(root).toMatchRenderedOutput('4'); // Update @@ -1115,7 +1139,7 @@ describe('ReactLazy', () => {
, ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(root).toMatchRenderedOutput('5'); // Update (shallowly equal) @@ -1124,7 +1148,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(root).toMatchRenderedOutput('5'); // Update (explicit props) @@ -1133,7 +1157,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(root).toMatchRenderedOutput('2'); // Update (explicit props, shallowly equal) @@ -1142,7 +1166,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(root).toMatchRenderedOutput('2'); // Update @@ -1151,7 +1175,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(root).toMatchRenderedOutput('3'); }); @@ -1176,13 +1200,13 @@ describe('ReactLazy', () => { unstable_isConcurrent: true, }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); + await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('4'); // Mount - await Promise.resolve(); - expect(() => { - expect(Scheduler).toFlushWithoutYielding(); + await resolveFakeImport(Add); + await expect(async () => { + await waitForAll([]); }).toErrorDev([ 'Memo: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', 'Unknown: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', @@ -1195,7 +1219,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(root).toMatchRenderedOutput('5'); // Update @@ -1204,13 +1228,13 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(root).toMatchRenderedOutput('2'); }); it('warns about ref on functions for lazy-loaded components', async () => { + const Foo = props =>
; const LazyFoo = lazy(() => { - const Foo = props =>
; return fakeImport(Foo); }); @@ -1224,21 +1248,20 @@ describe('ReactLazy', () => { }, ); - expect(Scheduler).toFlushAndYield(['Loading...']); - await Promise.resolve(); - expect(() => { - expect(Scheduler).toFlushAndYield([]); + await waitForAll(['Loading...']); + await resolveFakeImport(Foo); + await expect(async () => { + await waitForAll([]); }).toErrorDev('Function components cannot be given refs'); }); it('should error with a component stack naming the resolved component', async () => { let componentStackMessage; - const LazyText = lazy(() => - fakeImport(function ResolvedText() { - throw new Error('oh no'); - }), - ); + function ResolvedText() { + throw new Error('oh no'); + } + const LazyText = lazy(() => fakeImport(ResolvedText)); class ErrorBoundary extends React.Component { state = {error: null}; @@ -1264,13 +1287,10 @@ describe('ReactLazy', () => { {unstable_isConcurrent: true}, ); - expect(Scheduler).toFlushAndYield(['Loading...']); - - try { - await Promise.resolve(); - } catch (e) {} + await waitForAll(['Loading...']); - expect(Scheduler).toFlushAndYield([]); + await resolveFakeImport(ResolvedText); + await waitForAll([]); expect(componentStackMessage).toContain('in ResolvedText'); }); @@ -1307,7 +1327,7 @@ describe('ReactLazy', () => { , ); - expect(Scheduler).toHaveYielded([]); + assertLog([]); expect(componentStackMessage).toContain('in Lazy'); }); @@ -1375,41 +1395,28 @@ describe('ReactLazy', () => { unstable_isConcurrent: true, }); - expect(Scheduler).toFlushAndYield(['Init A', 'Init B', 'Loading...']); + await waitForAll(['Init A', 'Init B', 'Loading...']); expect(root).not.toMatchRenderedOutput('AB'); - await LazyChildA; - await LazyChildB; + await resolveFakeImport(ChildA); + await resolveFakeImport(ChildB); - expect(Scheduler).toFlushAndYield([ - 'A', - 'B', - 'Did mount: A', - 'Did mount: B', - ]); + await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']); expect(root).toMatchRenderedOutput('AB'); // Swap the position of A and B root.update(); - expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']); + await waitForAll(['Init B2', 'Loading...']); jest.runAllTimers(); - expect(Scheduler).toHaveYielded(['Did unmount: A', 'Did unmount: B']); + assertLog(['Did unmount: A', 'Did unmount: B']); // The suspense boundary should've triggered now. expect(root).toMatchRenderedOutput('Loading...'); await resolveB2({default: ChildB}); // We need to flush to trigger the second one to load. - expect(Scheduler).toFlushAndYield(['Init A2']); - await LazyChildA2; - - expect(Scheduler).toFlushAndYield([ - 'b', - 'a', - 'Did mount: b', - 'Did mount: a', - ]); + await waitForAll(['Init A2', 'b', 'a', 'Did mount: b', 'Did mount: a']); expect(root).toMatchRenderedOutput('ba'); }); @@ -1470,34 +1477,19 @@ describe('ReactLazy', () => { unstable_isConcurrent: false, }); - expect(Scheduler).toHaveYielded(['Init A', 'Init B', 'Loading...']); + assertLog(['Init A', 'Init B', 'Loading...']); expect(root).not.toMatchRenderedOutput('AB'); - await LazyChildA; - await LazyChildB; + await resolveFakeImport(ChildA); + await resolveFakeImport(ChildB); - expect(Scheduler).toFlushAndYield([ - 'A', - 'B', - 'Did mount: A', - 'Did mount: B', - ]); + await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']); expect(root).toMatchRenderedOutput('AB'); // Swap the position of A and B root.update(); - expect(Scheduler).toHaveYielded(['Init B2', 'Loading...']); - await LazyChildB2; - // We need to flush to trigger the second one to load. - expect(Scheduler).toFlushAndYield(['Init A2']); - await LazyChildA2; - - expect(Scheduler).toFlushAndYield([ - 'b', - 'a', - 'Did update: b', - 'Did update: a', - ]); + assertLog(['Init B2', 'Loading...']); + await waitForAll(['Init A2', 'b', 'a', 'Did update: b', 'Did update: a']); expect(root).toMatchRenderedOutput('ba'); }); @@ -1514,21 +1506,25 @@ describe('ReactLazy', () => { } } + const ChildA = ; const lazyChildA = lazy(() => { Scheduler.unstable_yieldValue('Init A'); - return fakeImport(); + return fakeImport(ChildA); }); + const ChildB = ; const lazyChildB = lazy(() => { Scheduler.unstable_yieldValue('Init B'); - return fakeImport(); + return fakeImport(ChildB); }); + const ChildA2 = ; const lazyChildA2 = lazy(() => { Scheduler.unstable_yieldValue('Init A2'); - return fakeImport(); + return fakeImport(ChildA2); }); + const ChildB2 = ; const lazyChildB2 = lazy(() => { Scheduler.unstable_yieldValue('Init B2'); - return fakeImport(); + return fakeImport(ChildB2); }); function Parent({swap}) { @@ -1543,38 +1539,28 @@ describe('ReactLazy', () => { unstable_isConcurrent: true, }); - expect(Scheduler).toFlushAndYield(['Init A', 'Loading...']); + await waitForAll(['Init A', 'Loading...']); expect(root).not.toMatchRenderedOutput('AB'); - await lazyChildA; + await resolveFakeImport(ChildA); // We need to flush to trigger the B to load. - expect(Scheduler).toFlushAndYield(['Init B']); - await lazyChildB; - - expect(Scheduler).toFlushAndYield([ - 'A', - 'B', - 'Did mount: A', - 'Did mount: B', - ]); + await waitForAll(['Init B']); + await resolveFakeImport(ChildB); + + await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']); expect(root).toMatchRenderedOutput('AB'); // Swap the position of A and B React.startTransition(() => { root.update(); }); - expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']); - await lazyChildB2; + await waitForAll(['Init B2', 'Loading...']); + await resolveFakeImport(ChildB2); // We need to flush to trigger the second one to load. - expect(Scheduler).toFlushAndYield(['Init A2', 'Loading...']); - await lazyChildA2; - - expect(Scheduler).toFlushAndYield([ - 'b', - 'a', - 'Did update: b', - 'Did update: a', - ]); + await waitForAll(['Init A2', 'Loading...']); + await resolveFakeImport(ChildA2); + + await waitForAll(['b', 'a', 'Did update: b', 'Did update: a']); expect(root).toMatchRenderedOutput('ba'); }); @@ -1591,21 +1577,25 @@ describe('ReactLazy', () => { } } + const ChildA = ; const lazyChildA = lazy(() => { Scheduler.unstable_yieldValue('Init A'); - return fakeImport(); + return fakeImport(ChildA); }); + const ChildB = ; const lazyChildB = lazy(() => { Scheduler.unstable_yieldValue('Init B'); - return fakeImport(); + return fakeImport(ChildB); }); + const ChildA2 = ; const lazyChildA2 = lazy(() => { Scheduler.unstable_yieldValue('Init A2'); - return fakeImport(); + return fakeImport(ChildA2); }); + const ChildB2 = ; const lazyChildB2 = lazy(() => { Scheduler.unstable_yieldValue('Init B2'); - return fakeImport(); + return fakeImport(ChildB2); }); function Parent({swap}) { @@ -1620,36 +1610,26 @@ describe('ReactLazy', () => { unstable_isConcurrent: false, }); - expect(Scheduler).toHaveYielded(['Init A', 'Loading...']); + assertLog(['Init A', 'Loading...']); expect(root).not.toMatchRenderedOutput('AB'); - await lazyChildA; + await resolveFakeImport(ChildA); // We need to flush to trigger the B to load. - expect(Scheduler).toFlushAndYield(['Init B']); - await lazyChildB; - - expect(Scheduler).toFlushAndYield([ - 'A', - 'B', - 'Did mount: A', - 'Did mount: B', - ]); + await waitForAll(['Init B']); + await resolveFakeImport(ChildB); + + await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']); expect(root).toMatchRenderedOutput('AB'); // Swap the position of A and B root.update(); - expect(Scheduler).toHaveYielded(['Init B2', 'Loading...']); - await lazyChildB2; + assertLog(['Init B2', 'Loading...']); + await resolveFakeImport(ChildB2); // We need to flush to trigger the second one to load. - expect(Scheduler).toFlushAndYield(['Init A2']); - await lazyChildA2; - - expect(Scheduler).toFlushAndYield([ - 'b', - 'a', - 'Did update: b', - 'Did update: a', - ]); + await waitForAll(['Init A2']); + await resolveFakeImport(ChildA2); + + await waitForAll(['b', 'a', 'Did update: b', 'Did update: a']); expect(root).toMatchRenderedOutput('ba'); }); });