Compare commits

...

1 Commits

Author SHA1 Message Date
Rick Hanlon
f9f6a6a2a6 Fix tests 2024-06-13 22:47:37 -04:00
23 changed files with 1166 additions and 401 deletions

View File

@@ -203,17 +203,35 @@ describe('ReactCache', () => {
<AsyncText ms={100} text={3} />
</Suspense>,
);
await waitForAll(['Suspend! [1]', 'Loading...']);
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [1]']);
await waitForAll([1, 'Suspend! [2]']);
if (gate(flags => flags.disableLegacySuspenseThrowSemantics)) {
await waitForAll(['Suspend! [1]', 'Loading...']);
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [1]']);
await waitForAll([1, 'Suspend! [2]']);
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [2]']);
await waitForAll([1, 2, 'Suspend! [3]']);
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [2]']);
await waitForAll([1, 2, 'Suspend! [3]']);
await act(() => jest.advanceTimersByTime(100));
assertLog(['Promise resolved [3]', 1, 2, 3]);
await act(() => jest.advanceTimersByTime(100));
assertLog(['Promise resolved [3]', 1, 2, 3]);
} else {
await waitForAll([
'Suspend! [1]',
'Suspend! [2]',
'Suspend! [3]',
'Loading...',
]);
await act(() => jest.advanceTimersByTime(100));
assertLog([
'Promise resolved [1]',
'Promise resolved [2]',
'Promise resolved [3]',
1,
2,
3,
]);
}
expect(root).toMatchRenderedOutput('123');
@@ -226,19 +244,26 @@ describe('ReactCache', () => {
</Suspense>,
);
await waitForAll([1, 'Suspend! [4]', 'Loading...']);
if (gate(flags => flags.disableLegacySuspenseThrowSemantics)) {
await waitForAll([1, 'Suspend! [4]', 'Loading...']);
await act(() => jest.advanceTimersByTime(100));
assertLog([
'Promise resolved [4]',
1,
4,
'Suspend! [5]',
'Promise resolved [5]',
1,
4,
5,
]);
await act(() => jest.advanceTimersByTime(100));
assertLog([
'Promise resolved [4]',
1,
4,
'Suspend! [5]',
'Promise resolved [5]',
1,
4,
5,
]);
} else {
await waitForAll([1, 'Suspend! [4]', 'Suspend! [5]', 'Loading...']);
await act(() => jest.advanceTimersByTime(100));
assertLog(['Promise resolved [4]', 'Promise resolved [5]', 1, 4, 5]);
}
expect(root).toMatchRenderedOutput('145');
@@ -253,25 +278,39 @@ describe('ReactCache', () => {
</Suspense>,
);
await waitForAll([
// 1 is still cached
1,
// 2 and 3 suspend because they were evicted from the cache
'Suspend! [2]',
'Loading...',
]);
if (gate(flags => flags.disableLegacySuspenseThrowSemantics)) {
await waitForAll([
// 1 is still cached
1,
// 2 and 3 suspend because they were evicted from the cache
'Suspend! [2]',
'Loading...',
]);
await act(() => jest.advanceTimersByTime(100));
assertLog([
'Promise resolved [2]',
1,
2,
'Suspend! [3]',
'Promise resolved [3]',
1,
2,
3,
]);
await act(() => jest.advanceTimersByTime(100));
assertLog([
'Promise resolved [2]',
1,
2,
'Suspend! [3]',
'Promise resolved [3]',
1,
2,
3,
]);
} else {
await waitForAll([
// 1 is still cached
1,
// 2 and 3 suspend because they were evicted from the cache
'Suspend! [2]',
'Suspend! [3]',
'Loading...',
]);
await act(() => jest.advanceTimersByTime(100));
assertLog(['Promise resolved [2]', 'Promise resolved [3]', 1, 2, 3]);
}
expect(root).toMatchRenderedOutput('123');
});

View File

@@ -5222,7 +5222,7 @@ describe('ReactDOMFizzServer', () => {
);
resolve({default: () => <p>lazy</p>});
await waitForAll([]);
await waitFor([]);
expect(errors).toEqual([]);
expect(getVisibleChildren(container)).toEqual(
<div>

View File

@@ -160,7 +160,11 @@ describe('ReactDOMSuspensePlaceholder', () => {
});
expect(container.textContent).toEqual('Loading...');
assertLog(['A', 'Suspend! [B]', 'Loading...']);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['A', 'Suspend! [B]', 'Loading...']
: ['A', 'Suspend! [B]', 'C', 'Loading...'],
);
await act(() => {
resolveText('B');
});

View File

@@ -43,6 +43,7 @@ import {
disableLegacyMode,
disableDefaultPropsExceptForClasses,
disableStringRefs,
disableLegacySuspenseThrowSemantics,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import is from 'shared/objectIs';
@@ -333,6 +334,8 @@ let workInProgressThrownValue: mixed = null;
// listeners to a promise we've already seen (per root and lane).
let workInProgressRootDidAttachPingListener: boolean = false;
let workInProgressRootSuspendedReason: SuspendedReason = NotSuspended;
// A contextual version of workInProgressRootRenderLanes. It is a superset of
// the lanes that we started working on at the root. When we enter a subtree
// that is currently hidden, we add the lanes that would have committed if
@@ -1639,6 +1642,9 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
workInProgressSuspendedReason = NotSuspended;
workInProgressThrownValue = null;
workInProgressRootDidAttachPingListener = false;
if (!disableLegacySuspenseThrowSemantics) {
workInProgressRootSuspendedReason = NotSuspended;
}
workInProgressRootExitStatus = RootInProgress;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
@@ -1738,13 +1744,24 @@ function handleThrow(root: FiberRoot, thrownValue: any): void {
typeof thrownValue === 'object' &&
typeof thrownValue.then === 'function';
workInProgressSuspendedReason = isWakeable
? // A wakeable object was thrown by a legacy Suspense implementation.
// This has slightly different behavior than suspending with `use`.
SuspendedOnDeprecatedThrowPromise
: // This is a regular error. If something earlier in the component already
// suspended, we must clear the thenable state to unblock the work loop.
SuspendedOnError;
if (!disableLegacySuspenseThrowSemantics) {
workInProgressSuspendedReason = workInProgressRootSuspendedReason =
isWakeable
? // A wakeable object was thrown by a legacy Suspense implementation.
// This has slightly different behavior than suspending with `use`.
SuspendedOnDeprecatedThrowPromise
: // This is a regular error. If something earlier in the component already
// suspended, we must clear the thenable state to unblock the work loop.
SuspendedOnError;
} else {
workInProgressSuspendedReason = isWakeable
? // A wakeable object was thrown by a legacy Suspense implementation.
// This has slightly different behavior than suspending with `use`.
SuspendedOnDeprecatedThrowPromise
: // This is a regular error. If something earlier in the component already
// suspended, we must clear the thenable state to unblock the work loop.
SuspendedOnError;
}
}
workInProgressThrownValue = thrownValue;
@@ -2032,6 +2049,17 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
workInProgressRootExitStatus = RootDidNotComplete;
break outer;
}
case SuspendedOnDeprecatedThrowPromise: {
// Unwind then continue with the normal work loop.
workInProgressSuspendedReason = NotSuspended;
if (!disableLegacySuspenseThrowSemantics) {
workInProgressRootSuspendedReason =
SuspendedOnDeprecatedThrowPromise;
}
workInProgressThrownValue = null;
throwAndUnwindWorkLoop(root, unitOfWork, thrownValue);
break;
}
case SuspendedOnImmediate:
case SuspendedOnData: {
if (!didSuspendInShell && getSuspenseHandler() === null) {
@@ -2293,6 +2321,10 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// like infinite ping loops. So we maintain the old behavior and
// always unwind.
workInProgressSuspendedReason = NotSuspended;
if (!disableLegacySuspenseThrowSemantics) {
workInProgressRootSuspendedReason =
SuspendedOnDeprecatedThrowPromise;
}
workInProgressThrownValue = null;
throwAndUnwindWorkLoop(root, unitOfWork, thrownValue);
break;
@@ -2623,7 +2655,27 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
// sibling. If there are no more siblings, return to the parent fiber.
let completedWork: Fiber = unitOfWork;
do {
if (__DEV__) {
if (!disableLegacySuspenseThrowSemantics) {
if (
(completedWork.flags & Incomplete) !== NoFlags &&
// TODO: Where is the best place to store this?
// The root isn't the right place, because if there are `use` calls
// then those boundaries show switch to not pre-rendering.
workInProgressRootSuspendedReason ===
SuspendedOnDeprecatedThrowPromise &&
!getIsHydrating()
) {
// This fiber did not complete, because one of its children did not
// complete. Switch to unwinding the stack instead of completing it.
//
// The reason "unwind" and "complete" is interleaved is because when
// something suspends, we continue rendering the siblings even though
// they will be replaced by a fallback.
// TODO: Disable sibling prerendering, then remove this branch.
unwindUnitOfWork(completedWork);
return;
}
} else if (__DEV__) {
if ((completedWork.flags & Incomplete) !== NoFlags) {
// NOTE: If we re-enable sibling prerendering in some cases, this branch
// is where we would switch to the unwinding path.
@@ -2755,6 +2807,21 @@ function unwindUnitOfWork(unitOfWork: Fiber): void {
// NOTE: If we re-enable sibling prerendering in some cases, here we
// would switch to the normal completion path: check if a sibling
// exists, and if so, begin work on it.
if (
!disableLegacySuspenseThrowSemantics &&
// TODO: Where is the best place to store this?
// The root isn't the right place, because if there are `use` calls
// then those boundaries show switch to not pre-rendering.
workInProgressRootSuspendedReason === SuspendedOnDeprecatedThrowPromise &&
!getIsHydrating()
) {
const siblingFiber = incompleteWork.sibling;
if (siblingFiber !== null) {
// This branch will return us to the normal work loop.
workInProgress = siblingFiber;
return;
}
}
// Otherwise, return to the parent
// $FlowFixMe[incompatible-type] we bail out when we get a null

View File

@@ -297,7 +297,17 @@ describe('ReactAsyncActions', () => {
// This will schedule an update on C, and also the async action scope
// will end. This will allow React to attempt to render the updates.
await act(() => resolveText('Wait before updating C'));
assertLog(['Async action ended', 'Pending: false', 'Suspend! [A1]']);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Async action ended', 'Pending: false', 'Suspend! [A1]']
: [
'Async action ended',
'Pending: false',
'Suspend! [A1]',
'Suspend! [B1]',
'Suspend! [C1]',
],
);
expect(root).toMatchRenderedOutput(
<>
<span>Pending: true</span>
@@ -309,7 +319,11 @@ describe('ReactAsyncActions', () => {
// together, only when the all of A, B, and C updates are unblocked is the
// render allowed to proceed.
await act(() => resolveText('A1'));
assertLog(['Pending: false', 'A1', 'Suspend! [B1]']);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Pending: false', 'A1', 'Suspend! [B1]']
: ['Pending: false', 'A1', 'Suspend! [B1]', 'Suspend! [C1]'],
);
expect(root).toMatchRenderedOutput(
<>
<span>Pending: true</span>

View File

@@ -109,7 +109,11 @@ describe('ReactBlockingMode', () => {
</Suspense>,
);
await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['A', 'Suspend! [B]', 'Loading...']
: ['A', 'Suspend! [B]', 'C', 'Loading...'],
);
// In Legacy Mode, A and B would mount in a hidden primary tree. In
// Concurrent Mode, nothing in the primary tree should mount. But the
// fallback should mount immediately.

View File

@@ -397,7 +397,17 @@ describe('ReactConcurrentErrorRecovery', () => {
);
});
});
assertLog(['Suspend! [Async]']);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Suspend! [Async]']
: [
'Suspend! [Async]',
// TODO: Ideally we would skip this second render pass to render the
// error UI, since it's not going to commit anyway. The same goes for
// Suspense fallbacks during a refresh transition.
'Caught an error: Oops!',
],
);
// The render suspended without committing or surfacing the error.
expect(root).toMatchRenderedOutput(null);
@@ -414,7 +424,11 @@ describe('ReactConcurrentErrorRecovery', () => {
);
});
});
assertLog(['Suspend! [Async]']);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Suspend! [Async]']
: ['Suspend! [Async]', 'Caught an error: Oops!'],
);
expect(root).toMatchRenderedOutput(null);
await act(async () => {

View File

@@ -699,7 +699,11 @@ describe('ReactExpiration', () => {
React.startTransition(() => {
root.render(<App step={1} />);
});
await waitForAll(['Suspend! [A1]', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Suspend! [A1]', 'Loading...']
: ['Suspend! [A1]', 'B', 'C', 'Loading...'],
);
// Lots of time elapses before the promise resolves
Scheduler.unstable_advanceTime(10000);

View File

@@ -305,7 +305,11 @@ describe('ReactLazy', () => {
unstable_isConcurrent: true,
});
await waitForAll(['Suspend! [LazyChildA]', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Suspend! [LazyChildA]', 'Loading...']
: ['Suspend! [LazyChildA]', 'Suspend! [LazyChildB]', 'Loading...'],
);
expect(root).not.toMatchRenderedOutput('AB');
await act(async () => {
@@ -314,9 +318,17 @@ describe('ReactLazy', () => {
// B suspends even though it happens to share the same import as A.
// TODO: React.lazy should implement the `status` and `value` fields, so
// we can unwrap the result synchronously if it already loaded. Like `use`.
await waitFor(['A', 'Suspend! [LazyChildB]']);
await waitFor(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['A', 'Suspend! [LazyChildB]']
: ['A', 'B'],
);
});
assertLog(['A', 'B', 'Did mount: A', 'Did mount: B']);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['A', 'B', 'Did mount: A', 'Did mount: B']
: ['Did mount: A', 'Did mount: B'],
);
expect(root).toMatchRenderedOutput('AB');
// Swap the position of A and B
@@ -1354,12 +1366,19 @@ describe('ReactLazy', () => {
unstable_isConcurrent: true,
});
await waitForAll(['Init A', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Init A', 'Loading...']
: ['Init A', 'Init B', 'Loading...'],
);
expect(root).not.toMatchRenderedOutput('AB');
await act(() => resolveFakeImport(ChildA));
assertLog(['A', 'Init B']);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['A', 'Init B']
: ['A'],
);
await act(() => resolveFakeImport(ChildB));
assertLog(['A', 'B', 'Did mount: A', 'Did mount: B']);
expect(root).toMatchRenderedOutput('AB');

View File

@@ -130,13 +130,24 @@ describe('ReactSuspense', () => {
root.render(<Foo renderBar={true} />);
});
await waitForAll([
'Foo',
'Bar',
// A suspends
'Suspend! [A]',
'Loading...',
]);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Foo',
'Bar',
// A suspends
'Suspend! [A]',
'Loading...',
]
: [
'Foo',
'Bar',
// A suspends
'Suspend! [A]',
'B',
'Loading...',
],
);
expect(container.textContent).toEqual('');
await waitForAll([]);
@@ -271,7 +282,17 @@ describe('ReactSuspense', () => {
root.render(<Foo />);
});
assertLog(['Foo', 'Suspend! [A]', 'Loading...']);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Foo', 'Suspend! [A]', 'Loading...']
: [
'Foo',
'Suspend! [A]',
'Suspend! [B]',
'Loading more...',
'Loading...',
],
);
expect(container.textContent).toEqual('Loading...');
await resolveText('A');
@@ -316,7 +337,17 @@ describe('ReactSuspense', () => {
// Render an empty shell
const root = ReactDOMClient.createRoot(container);
root.render(<Foo />);
await waitForAll(['Foo', 'Suspend! [A]', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Foo', 'Suspend! [A]', 'Loading...']
: [
'Foo',
'Suspend! [A]',
'Suspend! [B]',
'Loading more...',
'Loading...',
],
);
expect(container.textContent).toEqual('Loading...');
// Now resolve A
@@ -357,7 +388,17 @@ describe('ReactSuspense', () => {
await act(() => {
root.render(<Foo />);
});
assertLog(['Foo', 'Suspend! [A]', 'Loading...']);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Foo', 'Suspend! [A]', 'Loading...']
: [
'Foo',
'Suspend! [A]',
'Suspend! [B]',
'Loading more...',
'Loading...',
],
);
expect(container.textContent).toEqual('Loading...');
await resolveText('A');
@@ -721,7 +762,11 @@ describe('ReactSuspense', () => {
root.render(<App />);
});
assertLog(['Suspend! [Child 1]', 'Loading...']);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Suspend! [Child 1]', 'Loading...']
: ['Suspend! [Child 1]', 'Suspend! [Child 2]', 'Loading...'],
);
await resolveText('Child 1');
await waitForAll(['Child 1', 'Suspend! [Child 2]']);

View File

@@ -128,7 +128,13 @@ describe('ReactSuspense', () => {
ReactNoop.render(element);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput('Waiting Tier 1');
expect(ops).toEqual([new Set([promise1])]);
expect(ops).toEqual([
new Set(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [promise1]
: [promise1, promise2],
),
]);
ops = [];
await act(() => resolve1());

View File

@@ -262,19 +262,36 @@ describe('ReactSuspenseEffectsSemantics', () => {
</App>,
);
});
assertLog([
'App render',
'Text:Inside:Before render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
'Text:Fallback create layout',
'Text:Outside create layout',
'App create layout',
'Text:Fallback create passive',
'Text:Outside create passive',
'App create passive',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'App render',
'Text:Inside:Before render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
'Text:Fallback create layout',
'Text:Outside create layout',
'App create layout',
'Text:Fallback create passive',
'Text:Outside create passive',
'App create passive',
]
: [
'App render',
'Text:Inside:Before render',
'Suspend:Async',
'ClassText:Inside:After render',
'Text:Fallback render',
'Text:Outside render',
'Text:Fallback create layout',
'Text:Outside create layout',
'App create layout',
'Text:Fallback create passive',
'Text:Outside create passive',
'App create passive',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Fallback" />
@@ -636,16 +653,30 @@ describe('ReactSuspenseEffectsSemantics', () => {
<AsyncText text="Async" />
</App>,
);
await waitFor([
'App render',
'Text:Inside:Before render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
'Text:Inside:Before destroy layout',
'Text:Inside:After destroy layout',
'Text:Fallback create layout',
]);
await waitFor(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'App render',
'Text:Inside:Before render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
'Text:Inside:Before destroy layout',
'Text:Inside:After destroy layout',
'Text:Fallback create layout',
]
: [
'App render',
'Text:Inside:Before render',
'Suspend:Async',
'Text:Inside:After render',
'Text:Fallback render',
'Text:Outside render',
'Text:Inside:Before destroy layout',
'Text:Inside:After destroy layout',
'Text:Fallback create layout',
],
);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -777,17 +808,32 @@ describe('ReactSuspenseEffectsSemantics', () => {
</App>,
);
await waitFor([
'App render',
'ClassText:Inside:Before render',
'Suspend:Async',
'ClassText:Fallback render',
'ClassText:Outside render',
'ClassText:Inside:Before componentWillUnmount',
'ClassText:Inside:After componentWillUnmount',
'ClassText:Fallback componentDidMount',
'ClassText:Outside componentDidUpdate',
]);
await waitFor(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'App render',
'ClassText:Inside:Before render',
'Suspend:Async',
'ClassText:Fallback render',
'ClassText:Outside render',
'ClassText:Inside:Before componentWillUnmount',
'ClassText:Inside:After componentWillUnmount',
'ClassText:Fallback componentDidMount',
'ClassText:Outside componentDidUpdate',
]
: [
'App render',
'ClassText:Inside:Before render',
'Suspend:Async',
'ClassText:Inside:After render',
'ClassText:Fallback render',
'ClassText:Outside render',
'ClassText:Inside:Before componentWillUnmount',
'ClassText:Inside:After componentWillUnmount',
'ClassText:Fallback componentDidMount',
'ClassText:Outside componentDidUpdate',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside:Before" hidden={true} />
@@ -888,14 +934,27 @@ describe('ReactSuspenseEffectsSemantics', () => {
<AsyncText text="Async" />
</App>,
);
await waitFor([
'App render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outer destroy layout',
'Text:Inner destroy layout',
'Text:Fallback create layout',
]);
await waitFor(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'App render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outer destroy layout',
'Text:Inner destroy layout',
'Text:Fallback create layout',
]
: [
'App render',
'Suspend:Async',
'Text:Outer render',
'Text:Inner render',
'Text:Fallback render',
'Text:Outer destroy layout',
'Text:Inner destroy layout',
'Text:Fallback create layout',
],
);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -1002,15 +1061,28 @@ describe('ReactSuspenseEffectsSemantics', () => {
<AsyncText text="Async" />
</App>,
);
await waitFor([
'App render',
'Suspend:Async',
// Text:MemoizedInner is memoized
'Text:Fallback render',
'Text:Outer destroy layout',
'Text:MemoizedInner destroy layout',
'Text:Fallback create layout',
]);
await waitFor(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'App render',
'Suspend:Async',
// Text:MemoizedInner is memoized
'Text:Fallback render',
'Text:Outer destroy layout',
'Text:MemoizedInner destroy layout',
'Text:Fallback create layout',
]
: [
'App render',
'Suspend:Async',
'Text:Outer render',
// Text:MemoizedInner is memoized
'Text:Fallback render',
'Text:Outer destroy layout',
'Text:MemoizedInner destroy layout',
'Text:Fallback create layout',
],
);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -1128,15 +1200,30 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:OuterFallback render',
'Text:Outer destroy layout',
'Text:InnerFallback destroy layout',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:OuterFallback render',
'Text:Outer destroy layout',
'Text:InnerFallback destroy layout',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
]
: [
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:Inner render',
'Suspend:InnerAsync_1',
'Text:InnerFallback render',
'Text:OuterFallback render',
'Text:Outer destroy layout',
'Text:InnerFallback destroy layout',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" hidden={true} />
@@ -1150,7 +1237,16 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('InnerAsync_1');
});
assertLog(['Text:Outer render', 'Suspend:OuterAsync_1']);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Text:Outer render', 'Suspend:OuterAsync_1']
: [
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:Inner render',
'AsyncText:InnerAsync_1 render',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" hidden={true} />
@@ -1170,11 +1266,22 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:OuterFallback render',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:OuterFallback render',
]
: [
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:Inner render',
'Suspend:InnerAsync_2',
'Text:InnerFallback render',
'Text:OuterFallback render',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" hidden={true} />
@@ -1241,17 +1348,33 @@ describe('ReactSuspenseEffectsSemantics', () => {
/>,
);
});
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_2',
'Text:OuterFallback render',
'Text:Outer destroy layout',
'AsyncText:OuterAsync_1 destroy layout',
'Text:Inner destroy layout',
'AsyncText:InnerAsync_2 destroy layout',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Text:Outer render',
'Suspend:OuterAsync_2',
'Text:OuterFallback render',
'Text:Outer destroy layout',
'AsyncText:OuterAsync_1 destroy layout',
'Text:Inner destroy layout',
'AsyncText:InnerAsync_2 destroy layout',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
]
: [
'Text:Outer render',
'Suspend:OuterAsync_2',
'Text:Inner render',
'AsyncText:InnerAsync_2 render',
'Text:OuterFallback render',
'Text:Outer destroy layout',
'AsyncText:OuterAsync_1 destroy layout',
'Text:Inner destroy layout',
'AsyncText:InnerAsync_2 destroy layout',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" hidden={true} />
@@ -1355,15 +1478,30 @@ describe('ReactSuspenseEffectsSemantics', () => {
/>,
);
});
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:OuterFallback render',
'Text:Outer destroy layout',
'Text:InnerFallback destroy layout',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:OuterFallback render',
'Text:Outer destroy layout',
'Text:InnerFallback destroy layout',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
]
: [
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:Inner render',
'Suspend:InnerAsync_1',
'Text:InnerFallback render',
'Text:OuterFallback render',
'Text:Outer destroy layout',
'Text:InnerFallback destroy layout',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" hidden={true} />
@@ -1812,17 +1950,33 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
assertLog([
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
'ThrowsInDidMount componentWillUnmount',
'Text:Inside destroy layout',
'Text:Fallback create layout',
'Text:Fallback create passive',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
'ThrowsInDidMount componentWillUnmount',
'Text:Inside destroy layout',
'Text:Fallback create layout',
'Text:Fallback create passive',
]
: [
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'ThrowsInDidMount render',
'Text:Inside render',
'Text:Fallback render',
'Text:Outside render',
'ThrowsInDidMount componentWillUnmount',
'Text:Inside destroy layout',
'Text:Fallback create layout',
'Text:Fallback create passive',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="ThrowsInDidMount" hidden={true} />
@@ -1946,36 +2100,71 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
assertLog([
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
// Even though an error was thrown in componentWillUnmount,
// subsequent layout effects should still be destroyed.
'ThrowsInWillUnmount componentWillUnmount',
'Text:Inside destroy layout',
// Even though an error was thrown in componentWillUnmount,
// subsequent layout effects should still be destroyed.
'ThrowsInWillUnmount componentWillUnmount',
'Text:Inside destroy layout',
// Finish the in-progress commit
'Text:Fallback create layout',
'Text:Fallback create passive',
// Finish the in-progress commit
'Text:Fallback create layout',
'Text:Fallback create passive',
// Destroy layout and passive effects in the errored tree.
'App destroy layout',
'Text:Fallback destroy layout',
'Text:Outside destroy layout',
'Text:Inside destroy passive',
'Text:Fallback destroy passive',
'Text:Outside destroy passive',
// Destroy layout and passive effects in the errored tree.
'App destroy layout',
'Text:Fallback destroy layout',
'Text:Outside destroy layout',
'Text:Inside destroy passive',
'Text:Fallback destroy passive',
'Text:Outside destroy passive',
// Render fallback
'ErrorBoundary render: catch',
'Text:Error render',
'Text:Error create layout',
'Text:Error create passive',
]);
// Render fallback
'ErrorBoundary render: catch',
'Text:Error render',
'Text:Error create layout',
'Text:Error create passive',
]
: [
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'ThrowsInWillUnmount render',
'Text:Inside render',
'Text:Fallback render',
'Text:Outside render',
// Even though an error was thrown in componentWillUnmount,
// subsequent layout effects should still be destroyed.
'ThrowsInWillUnmount componentWillUnmount',
'Text:Inside destroy layout',
// Finish the in-progress commit
'Text:Fallback create layout',
'Text:Fallback create passive',
// Destroy layout and passive effects in the errored tree.
'App destroy layout',
'Text:Fallback destroy layout',
'Text:Outside destroy layout',
'Text:Inside destroy passive',
'Text:Fallback destroy passive',
'Text:Outside destroy passive',
// Render fallback
'ErrorBoundary render: catch',
'Text:Error render',
'Text:Error create layout',
'Text:Error create passive',
],
);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Error" />);
});
@@ -2055,17 +2244,33 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
assertLog([
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
'ThrowsInLayoutEffect useLayoutEffect destroy',
'Text:Inside destroy layout',
'Text:Fallback create layout',
'Text:Fallback create passive',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
'ThrowsInLayoutEffect useLayoutEffect destroy',
'Text:Inside destroy layout',
'Text:Fallback create layout',
'Text:Fallback create passive',
]
: [
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'ThrowsInLayoutEffect render',
'Text:Inside render',
'Text:Fallback render',
'Text:Outside render',
'ThrowsInLayoutEffect useLayoutEffect destroy',
'Text:Inside destroy layout',
'Text:Fallback create layout',
'Text:Fallback create passive',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="ThrowsInLayoutEffect" hidden={true} />
@@ -2189,36 +2394,71 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
assertLog([
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
// Even though an error was thrown in useLayoutEffect destroy,
// subsequent layout effects should still be destroyed.
'ThrowsInLayoutEffectDestroy useLayoutEffect destroy',
'Text:Inside destroy layout',
// Even though an error was thrown in useLayoutEffect destroy,
// subsequent layout effects should still be destroyed.
'ThrowsInLayoutEffectDestroy useLayoutEffect destroy',
'Text:Inside destroy layout',
// Finish the in-progress commit
'Text:Fallback create layout',
'Text:Fallback create passive',
// Finish the in-progress commit
'Text:Fallback create layout',
'Text:Fallback create passive',
// Destroy layout and passive effects in the errored tree.
'App destroy layout',
'Text:Fallback destroy layout',
'Text:Outside destroy layout',
'Text:Inside destroy passive',
'Text:Fallback destroy passive',
'Text:Outside destroy passive',
// Destroy layout and passive effects in the errored tree.
'App destroy layout',
'Text:Fallback destroy layout',
'Text:Outside destroy layout',
'Text:Inside destroy passive',
'Text:Fallback destroy passive',
'Text:Outside destroy passive',
// Render fallback
'ErrorBoundary render: catch',
'Text:Error render',
'Text:Error create layout',
'Text:Error create passive',
]);
// Render fallback
'ErrorBoundary render: catch',
'Text:Error render',
'Text:Error create layout',
'Text:Error create passive',
]
: [
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'ThrowsInLayoutEffectDestroy render',
'Text:Inside render',
'Text:Fallback render',
'Text:Outside render',
// Even though an error was thrown in useLayoutEffect destroy,
// subsequent layout effects should still be destroyed.
'ThrowsInLayoutEffectDestroy useLayoutEffect destroy',
'Text:Inside destroy layout',
// Finish the in-progress commit
'Text:Fallback create layout',
'Text:Fallback create passive',
// Destroy layout and passive effects in the errored tree.
'App destroy layout',
'Text:Fallback destroy layout',
'Text:Outside destroy layout',
'Text:Inside destroy passive',
'Text:Fallback destroy passive',
'Text:Outside destroy passive',
// Render fallback
'ErrorBoundary render: catch',
'Text:Error render',
'Text:Error create layout',
'Text:Error create passive',
],
);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Error" />);
});
});
@@ -2280,14 +2520,27 @@ describe('ReactSuspenseEffectsSemantics', () => {
<AsyncText text="Async_2" />
</App>,
);
await waitFor([
'Text:Function render',
'Suspend:Async_1',
'ClassText:Fallback render',
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'ClassText:Fallback componentDidMount',
]);
await waitFor(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Text:Function render',
'Suspend:Async_1',
'ClassText:Fallback render',
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'ClassText:Fallback componentDidMount',
]
: [
'Text:Function render',
'Suspend:Async_1',
'Suspend:Async_2',
'ClassText:Class render',
'ClassText:Fallback render',
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'ClassText:Fallback componentDidMount',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Function" hidden={true} />
@@ -2301,11 +2554,20 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async_1');
});
assertLog([
'Text:Function render',
'AsyncText:Async_1 render',
'Suspend:Async_2',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Text:Function render',
'AsyncText:Async_1 render',
'Suspend:Async_2',
]
: [
'Text:Function render',
'AsyncText:Async_1 render',
'Suspend:Async_2',
'ClassText:Class render',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Function" hidden={true} />
@@ -2419,15 +2681,28 @@ describe('ReactSuspenseEffectsSemantics', () => {
textToRead = 'A';
await act(async () => {
ReactNoop.render(<App />);
await waitFor([
'Text:Function render',
'Suspender "A" render',
'Suspend:A',
'ClassText:Fallback render',
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'ClassText:Fallback componentDidMount',
]);
await waitFor(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Text:Function render',
'Suspender "A" render',
'Suspend:A',
'ClassText:Fallback render',
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'ClassText:Fallback componentDidMount',
]
: [
'Text:Function render',
'Suspender "A" render',
'Suspend:A',
'ClassText:Class render',
'ClassText:Fallback render',
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'ClassText:Fallback componentDidMount',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Function" hidden={true} />
@@ -2443,7 +2718,16 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('A');
});
assertLog(['Text:Function render', 'Suspender "B" render', 'Suspend:B']);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Text:Function render', 'Suspender "B" render', 'Suspend:B']
: [
'Text:Function render',
'Suspender "B" render',
'Suspend:B',
'ClassText:Class render',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Function" hidden={true} />
@@ -2664,17 +2948,34 @@ describe('ReactSuspenseEffectsSemantics', () => {
ReactNoop.render(<App children={<AsyncText text="Async" />} />);
});
await advanceTimers(1000);
assertLog([
'App render',
'Suspend:Async',
'Text:Fallback render',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
'Text:Fallback create layout',
'Text:Fallback create passive',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'App render',
'Suspend:Async',
'Text:Fallback render',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
'Text:Fallback create layout',
'Text:Fallback create passive',
]
: [
'App render',
'Suspend:Async',
'RefCheckerOuter render',
'RefCheckerInner:refObject render',
'RefCheckerInner:refCallback render',
'Text:Fallback render',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
'Text:Fallback create layout',
'Text:Fallback create passive',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="refObject" hidden={true} />
@@ -2765,17 +3066,36 @@ describe('ReactSuspenseEffectsSemantics', () => {
ReactNoop.render(<App children={<AsyncText text="Async" />} />);
});
await advanceTimers(1000);
assertLog([
'App render',
'Suspend:Async',
'Text:Fallback render',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
'Text:Fallback create layout',
'Text:Fallback create passive',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'App render',
'Suspend:Async',
'Text:Fallback render',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
'Text:Fallback create layout',
'Text:Fallback create passive',
]
: [
'App render',
'Suspend:Async',
'RefCheckerOuter render',
'ClassComponent:refObject render',
'RefCheckerInner:refObject render',
'ClassComponent:refCallback render',
'RefCheckerInner:refCallback render',
'Text:Fallback render',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
'Text:Fallback create layout',
'Text:Fallback create passive',
],
);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
// Resolving the suspended resource should re-create inner layout effects.
@@ -2862,17 +3182,36 @@ describe('ReactSuspenseEffectsSemantics', () => {
ReactNoop.render(<App children={<AsyncText text="Async" />} />);
});
await advanceTimers(1000);
assertLog([
'App render',
'Suspend:Async',
'Text:Fallback render',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
'Text:Fallback create layout',
'Text:Fallback create passive',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'App render',
'Suspend:Async',
'Text:Fallback render',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
'Text:Fallback create layout',
'Text:Fallback create passive',
]
: [
'App render',
'Suspend:Async',
'RefCheckerOuter render',
'FunctionComponent render',
'RefCheckerInner:refObject render',
'FunctionComponent render',
'RefCheckerInner:refCallback render',
'Text:Fallback render',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
'Text:Fallback create layout',
'Text:Fallback create passive',
],
);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
// Resolving the suspended resource should re-create inner layout effects.
@@ -2964,14 +3303,26 @@ describe('ReactSuspenseEffectsSemantics', () => {
ReactNoop.render(<App children={<AsyncText text="Async" />} />);
});
await advanceTimers(1000);
assertLog([
'App render',
'Suspend:Async',
'Text:Fallback render',
'RefChecker destroy layout ref? true',
'Text:Fallback create layout',
'Text:Fallback create passive',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'App render',
'Suspend:Async',
'Text:Fallback render',
'RefChecker destroy layout ref? true',
'Text:Fallback create layout',
'Text:Fallback create passive',
]
: [
'App render',
'Suspend:Async',
'RefChecker render',
'Text:Fallback render',
'RefChecker destroy layout ref? true',
'Text:Fallback create layout',
'Text:Fallback create passive',
],
);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
// Resolving the suspended resource should re-create inner layout effects.
@@ -3075,17 +3426,33 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
assertLog([
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
'ThrowsInRefCallback refCallback ref? false',
'Text:Inside destroy layout',
'Text:Fallback create layout',
'Text:Fallback create passive',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
'ThrowsInRefCallback refCallback ref? false',
'Text:Inside destroy layout',
'Text:Fallback create layout',
'Text:Fallback create passive',
]
: [
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
'ThrowsInRefCallback render',
'Text:Inside render',
'Text:Fallback render',
'Text:Outside render',
'ThrowsInRefCallback refCallback ref? false',
'Text:Inside destroy layout',
'Text:Fallback create layout',
'Text:Fallback create passive',
],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="ThrowsInRefCallback" hidden={true} />

View File

@@ -135,7 +135,11 @@ describe('ReactSuspensePlaceholder', () => {
// Initial mount
ReactNoop.render(<App middleText="B" />);
await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['A', 'Suspend! [B]', 'Loading...']
: ['A', 'Suspend! [B]', 'C', 'Loading...'],
);
expect(ReactNoop).toMatchRenderedOutput('Loading...');
await act(() => jest.advanceTimersByTime(1000));
@@ -151,7 +155,11 @@ describe('ReactSuspensePlaceholder', () => {
// Update
ReactNoop.render(<App middleText="B2" />);
await waitForAll(['Suspend! [B2]', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Suspend! [B2]', 'Loading...']
: ['Suspend! [B2]', 'C', 'Loading...'],
);
// Time out the update
jest.advanceTimersByTime(750);
@@ -194,7 +202,11 @@ describe('ReactSuspensePlaceholder', () => {
// Initial mount
ReactNoop.render(<App middleText="B" />);
await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['A', 'Suspend! [B]', 'Loading...']
: ['A', 'Suspend! [B]', 'C', 'Loading...'],
);
expect(ReactNoop).not.toMatchRenderedOutput('ABC');
@@ -204,7 +216,11 @@ describe('ReactSuspensePlaceholder', () => {
// Update
ReactNoop.render(<App middleText="B2" />);
await waitForAll(['A', 'Suspend! [B2]', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['A', 'Suspend! [B2]', 'Loading...']
: ['A', 'Suspend! [B2]', 'C', 'Loading...'],
);
// Time out the update
jest.advanceTimersByTime(750);
await waitForAll([]);
@@ -237,7 +253,11 @@ describe('ReactSuspensePlaceholder', () => {
// Initial mount
ReactNoop.render(<App middleText="b" />);
await waitForAll(['a', 'Suspend! [b]', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['a', 'Suspend! [b]', 'Loading...']
: ['a', 'Suspend! [b]', 'c', 'Loading...'],
);
expect(ReactNoop).toMatchRenderedOutput(<uppercase>LOADING...</uppercase>);
@@ -247,7 +267,11 @@ describe('ReactSuspensePlaceholder', () => {
// Update
ReactNoop.render(<App middleText="b2" />);
await waitForAll(['a', 'Suspend! [b2]', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['a', 'Suspend! [b2]', 'Loading...']
: ['a', 'Suspend! [b2]', 'c', 'Loading...'],
);
// Time out the update
jest.advanceTimersByTime(750);
await waitForAll([]);
@@ -335,12 +359,11 @@ describe('ReactSuspensePlaceholder', () => {
it('properly accounts for base durations when a suspended times out in a concurrent tree', async () => {
ReactNoop.render(<App shouldSuspend={true} />);
await waitForAll([
'App',
'Suspending',
'Suspend! [Loaded]',
'Fallback',
]);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['App', 'Suspending', 'Suspend! [Loaded]', 'Fallback']
: ['App', 'Suspending', 'Suspend! [Loaded]', 'Text', 'Fallback'],
);
// Since this is initial render we immediately commit the fallback. Another test below
// deals with the update case where this suspends.
expect(ReactNoop).toMatchRenderedOutput('Loading...');
@@ -349,7 +372,9 @@ describe('ReactSuspensePlaceholder', () => {
// Initial mount only shows the "Loading..." Fallback.
// The treeBaseDuration then should be 10ms spent rendering Fallback,
// but the actualDuration should also include the 3ms spent rendering the hidden tree.
expect(onRender.mock.calls[0][2]).toBe(13);
expect(onRender.mock.calls[0][2]).toBe(
gate(flags => flags.disableLegacySuspenseThrowSemantics) ? 13 : 18,
);
expect(onRender.mock.calls[0][3]).toBe(10);
// Resolve the pending promise.
@@ -482,12 +507,11 @@ describe('ReactSuspensePlaceholder', () => {
<Suspense fallback={null} />
</>,
);
await waitForAll([
'App',
'Suspending',
'Suspend! [Loaded]',
'Fallback',
]);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['App', 'Suspending', 'Suspend! [Loaded]', 'Fallback']
: ['App', 'Suspending', 'Suspend! [Loaded]', 'Text', 'Fallback'],
);
// Show the fallback UI.
expect(ReactNoop).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(2);
@@ -498,7 +522,9 @@ describe('ReactSuspensePlaceholder', () => {
// The actual duration should include 10ms spent rendering Fallback,
// plus the 3ms render all of the partially rendered suspended subtree.
// But the tree base duration should only include 10ms spent rendering Fallback.
expect(onRender.mock.calls[1][2]).toBe(13);
expect(onRender.mock.calls[1][2]).toBe(
gate(flags => flags.disableLegacySuspenseThrowSemantics) ? 13 : 18,
);
expect(onRender.mock.calls[1][3]).toBe(10);
// Update again while timed out.
@@ -520,13 +546,24 @@ describe('ReactSuspensePlaceholder', () => {
// from timers.
Scheduler.unstable_advanceTime(200);
await waitForAll([
'App',
'Suspending',
'Suspend! [Loaded]',
'Fallback',
'Suspend! [Sibling]',
]);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'App',
'Suspending',
'Suspend! [Loaded]',
'Fallback',
'Suspend! [Sibling]',
]
: [
'App',
'Suspending',
'Suspend! [Loaded]',
'New',
'Fallback',
'Suspend! [Sibling]',
],
);
expect(ReactNoop).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(3);

View File

@@ -221,17 +221,31 @@ describe('ReactSuspenseWithNoopRenderer', () => {
React.startTransition(() => {
ReactNoop.render(<Foo />);
});
await waitFor([
'Foo',
'Bar',
// A suspends
'Suspend! [A]',
// We immediately unwind and switch to a fallback without
// rendering siblings.
'Loading...',
'C',
// Yield before rendering D
]);
await waitFor(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Foo',
'Bar',
// A suspends
'Suspend! [A]',
// We immediately unwind and switch to a fallback without
// rendering siblings.
'Loading...',
'C',
// Yield before rendering D
]
: [
'Foo',
'Bar',
// A suspends
'Suspend! [A]',
// But we keep rendering the siblings
'B',
'Loading...',
'C',
// We leave D incomplete.
],
);
expect(ReactNoop).toMatchRenderedOutput(null);
// Flush the promise completely
@@ -290,15 +304,27 @@ describe('ReactSuspenseWithNoopRenderer', () => {
React.startTransition(() => {
ReactNoop.render(<Foo renderBar={true} />);
});
await waitForAll([
'Foo',
'Bar',
// A suspends
'Suspend! [A]',
// We immediately unwind and switch to a fallback without
// rendering siblings.
'Loading...',
]);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Foo',
'Bar',
// A suspends
'Suspend! [A]',
// We immediately unwind and switch to a fallback without
// rendering siblings.
'Loading...',
]
: [
'Foo',
'Bar',
// A suspends
'Suspend! [A]',
// But we keep rendering the siblings
'B',
'Loading...',
],
);
expect(ReactNoop).toMatchRenderedOutput(null);
// Resolve the data
@@ -379,7 +405,25 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// B suspends. Render a fallback
await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'A',
// B suspends.
'Suspend! [B]',
'Loading...',
]
: [
'A',
// B suspends.
'Suspend! [B]',
// Continue rendering the remaining siblings.
'C',
'D',
'Loading...',
],
);
// Did not commit yet.
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -598,7 +642,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
React.startTransition(() => {
ReactNoop.render(<App showA={true} showB={true} />);
});
await waitForAll(['Suspend! [A]', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Suspend! [A]', 'Loading...']
: ['Suspend! [A]', 'B', 'Loading...'],
);
expect(ReactNoop).toMatchRenderedOutput(null);
await resolveText('A');
@@ -723,12 +771,24 @@ describe('ReactSuspenseWithNoopRenderer', () => {
</Fragment>,
);
await waitForAll([
'Sync',
// The async content suspends
'Suspend! [Outer content]',
'Loading outer...',
]);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Sync',
// The async content suspends
'Suspend! [Outer content]',
'Loading outer...',
]
: [
'Sync',
// The async content suspends
'Suspend! [Outer content]',
// Continue rendering the remaining siblings.
'Suspend! [Inner content]',
'Loading inner...',
'Loading outer...',
],
);
// The outer loading state finishes immediately.
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -832,11 +892,20 @@ describe('ReactSuspenseWithNoopRenderer', () => {
</Fragment>,
),
);
assertLog([
'Suspend! [Async]',
'Suspend! [Loading (inner)...]',
'Loading (outer)...',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Suspend! [Async]',
'Suspend! [Loading (inner)...]',
'Loading (outer)...',
]
: [
'Suspend! [Async]',
'Suspend! [Loading (inner)...]',
'Sync',
'Loading (outer)...',
],
);
// The tree commits synchronously
expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading (outer)..." />);
});
@@ -904,7 +973,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
<AsyncText text="B" />
</Suspense>,
);
await waitForAll(['Suspend! [A]', 'Loading...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Suspend! [A]', 'Loading...']
: ['Suspend! [A]', 'Suspend! [B]', 'Loading...'],
);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
await act(() => {
@@ -1014,7 +1087,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}
ReactNoop.render(<App />);
await waitForAll(['Suspend! [A]']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Suspend! [A]']
: ['Suspend! [A]', 'Suspend! [B]', 'Suspend! [C]'],
);
expect(ReactNoop).toMatchRenderedOutput('Loading...');
await resolveText('A');
@@ -1672,12 +1749,23 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Foo />);
// Start rendering
await waitForAll([
'Foo',
// A suspends
'Suspend! [A]',
'Loading...',
]);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Foo',
// A suspends
'Suspend! [A]',
'Loading...',
]
: [
'Foo',
// A suspends
'Suspend! [A]',
'Suspend! [B]',
'Loading more...',
'Loading...',
],
);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
// Wait a long time.
@@ -1730,12 +1818,23 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Foo />);
// Start rendering
await waitForAll([
'Foo',
// A suspends
'Suspend! [A]',
'Loading...',
]);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Foo',
// A suspends
'Suspend! [A]',
'Loading...',
]
: [
'Foo',
// A suspends
'Suspend! [A]',
'Suspend! [B]',
'Loading more...',
'Loading...',
],
);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
await act(async () => {
@@ -1964,7 +2063,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.flushSync(() => showB());
});
assertLog(['Suspend! [A]']);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Suspend! [A]']
: ['Suspend! [A]', 'Suspend! [B]'],
);
});
// TODO: flip to "warns" when this is implemented again.
@@ -2070,7 +2173,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}
ReactNoop.render(<Foo />);
await waitForAll(['Foo', 'Suspend! [A]', 'Initial load...']);
await waitForAll(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? ['Foo', 'Suspend! [A]', 'Initial load...']
: ['Foo', 'Suspend! [A]', 'B', 'Initial load...'],
);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Initial load..." />);
// Eventually we resolve and show the data.
@@ -3354,12 +3461,22 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(() => {
setText('B');
});
assertLog([
'Outer text: B',
'Outer step: 0',
'Suspend! [Inner text: B]',
'Loading...',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Outer text: B',
'Outer step: 0',
'Suspend! [Inner text: B]',
'Loading...',
]
: [
'Outer text: B',
'Outer step: 0',
'Suspend! [Inner text: B]',
'Inner step: 0',
'Loading...',
],
);
// Commit the placeholder
await advanceTimers(250);
expect(root).toMatchRenderedOutput(
@@ -3382,12 +3499,22 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Only the outer part can update. The inner part should still show a
// fallback because we haven't finished loading B yet. Otherwise, the
// inner text would be inconsistent with the outer text.
assertLog([
'Outer text: B',
'Outer step: 1',
'Suspend! [Inner text: B]',
'Loading...',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
'Outer text: B',
'Outer step: 1',
'Suspend! [Inner text: B]',
'Loading...',
]
: [
'Outer text: B',
'Outer step: 1',
'Suspend! [Inner text: B]',
'Inner step: 1',
'Loading...',
],
);
expect(root).toMatchRenderedOutput(
<>
<span prop="Outer text: B" />

View File

@@ -857,11 +857,20 @@ describe('ReactTransition', () => {
updateTransitionPri();
});
assertLog([
// Suspend.
'Suspend! [Async]',
'Loading...',
]);
assertLog(
gate(flags => flags.disableLegacySuspenseThrowSemantics)
? [
// Suspend.
'Suspend! [Async]',
'Loading...',
]
: [
// Suspend.
'Suspend! [Async]',
'Normal pri: 0',
'Loading...',
],
);
expect(root).toMatchRenderedOutput('(empty), Normal pri: 0');
await act(async () => {

View File

@@ -272,3 +272,5 @@ export const enableGetInspectorDataForInstanceInProduction = false;
export const consoleManagedByDevToolsDuringStrictMode = true;
export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
export const disableLegacySuspenseThrowSemantics = false;

View File

@@ -100,6 +100,7 @@ export const disableStringRefs = true;
export const enableReactTestRendererWarning = false;
export const disableLegacyMode = false;
export const enableOwnerStacks = false;
export const disableLegacySuspenseThrowSemantics = true;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

View File

@@ -104,6 +104,7 @@ export const enableAsyncIterableChildren = false;
export const enableAddPropertiesFastPath = false;
export const enableShallowPropDiffing = false;
export const renameElementSymbol = true;
export const disableLegacySuspenseThrowSemantics = true;
export const enableOwnerStacks = __EXPERIMENTAL__;

View File

@@ -96,6 +96,7 @@ export const disableLegacyContext = true;
export const enableRenderableContext = true;
export const enableReactTestRendererWarning = true;
export const disableDefaultPropsExceptForClasses = true;
export const disableLegacySuspenseThrowSemantics = true;
export const enableOwnerStacks = false;

View File

@@ -93,6 +93,7 @@ export const renameElementSymbol = false;
export const enableOwnerStacks = false;
export const enableShallowPropDiffing = false;
export const disableLegacySuspenseThrowSemantics = true;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

View File

@@ -93,6 +93,7 @@ export const renameElementSymbol = false;
export const enableOwnerStacks = false;
export const enableShallowPropDiffing = false;
export const disableLegacySuspenseThrowSemantics = true;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

View File

@@ -114,6 +114,7 @@ export const enableServerComponentLogs = true;
export const enableReactTestRendererWarning = false;
export const useModernStrictMode = true;
export const disableLegacySuspenseThrowSemantics = true;
// TODO: Roll out with GK. Don't keep as dynamic flag for too long, though,
// because JSX is an extremely hot path.

View File

@@ -14,6 +14,7 @@ jest.mock('shared/ReactFeatureFlags', () => {
// Flags that aren't currently used, but we still want to force variants to keep the
// code live.
actual.disableInputAttributeSyncing = __VARIANT__;
actual.disableLegacySuspenseThrowSemantics = __VARIANT__;
// These are hardcoded to true for the next release,
// but still run the tests against both variants until