Codemod tests to waitFor pattern (8/?) (#26308)

This converts some of our test suite to use the `waitFor` test pattern,
instead of the `expect(Scheduler).toFlushAndYield` pattern. Most of
these changes are automated with jscodeshift, with some slight manual
cleanup in certain cases.

See #26285 for full context.
This commit is contained in:
Andrew Clark
2023-03-04 18:04:43 -05:00
committed by GitHub
parent 3cb5afb82e
commit 64dde70827
17 changed files with 594 additions and 560 deletions

View File

@@ -17,6 +17,8 @@ describe('Timeline profiler', () => {
let ReactDOMClient;
let Scheduler;
let utils;
let assertLog;
let waitFor;
describe('User Timing API', () => {
let clearedMarks;
@@ -82,6 +84,10 @@ describe('Timeline profiler', () => {
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
setPerformanceMock =
require('react-devtools-shared/src/backend/profilingHooks').setPerformanceMock_ONLY_FOR_TESTING;
setPerformanceMock(createUserTimingPolyfill());
@@ -1372,28 +1378,29 @@ describe('Timeline profiler', () => {
<Yield id="B" value={1} />
</>,
);
expect(Scheduler).toFlushAndYieldThrough(['A:1']);
testMarks.push(...createUserTimingData(clearedMarks));
clearPendingMarks();
// Advance the clock some more to make the pending React update seem long.
startTime += 20000;
// Fake a long "click" event in the middle
// and schedule a sync update that will also flush the previous work.
testMarks.push(createNativeEventEntry('click', 25000));
ReactDOM.flushSync(() => {
root.render(
<>
<Yield id="A" value={2} />
<Yield id="B" value={2} />
</>,
);
});
});
expect(Scheduler).toHaveYielded(['A:2', 'B:2']);
await waitFor(['A:1']);
testMarks.push(...createUserTimingData(clearedMarks));
clearPendingMarks();
// Advance the clock some more to make the pending React update seem long.
startTime += 20000;
// Fake a long "click" event in the middle
// and schedule a sync update that will also flush the previous work.
testMarks.push(createNativeEventEntry('click', 25000));
ReactDOM.flushSync(() => {
root.render(
<>
<Yield id="A" value={2} />
<Yield id="B" value={2} />
</>,
);
});
assertLog(['A:2', 'B:2']);
testMarks.push(...createUserTimingData(clearedMarks));
@@ -1424,10 +1431,7 @@ describe('Timeline profiler', () => {
root.render(<Component />);
});
expect(Scheduler).toHaveYielded([
'Component mount',
'Component update',
]);
assertLog(['Component mount', 'Component update']);
const data = await preprocessData([
...createBoilerplateEntries(),
@@ -1463,10 +1467,7 @@ describe('Timeline profiler', () => {
root.render(<Component />);
});
expect(Scheduler).toHaveYielded([
'Component mount',
'Component update',
]);
assertLog(['Component mount', 'Component update']);
const data = await preprocessData([
...createBoilerplateEntries(),
@@ -1504,10 +1505,7 @@ describe('Timeline profiler', () => {
root.render(<Component />);
});
expect(Scheduler).toHaveYielded([
'Component mount',
'Component update',
]);
assertLog(['Component mount', 'Component update']);
const testMarks = [];
clearedMarks.forEach(markName => {
@@ -1567,10 +1565,7 @@ describe('Timeline profiler', () => {
root.render(<Component />);
});
expect(Scheduler).toHaveYielded([
'Component mount',
'Component update',
]);
assertLog(['Component mount', 'Component update']);
const testMarks = [];
clearedMarks.forEach(markName => {
@@ -1639,7 +1634,7 @@ describe('Timeline profiler', () => {
root.render(<Component />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Component rendered with value 0',
'Component rendered with value 0',
'Component rendered with value 1',
@@ -1708,7 +1703,7 @@ describe('Timeline profiler', () => {
root.render(<Component />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Component rendered with value 0 and deferredValue 0',
'Component rendered with value 1 and deferredValue 0',
'Component rendered with value 1 and deferredValue 1',

View File

@@ -23,6 +23,7 @@ let buffer = '';
let hasErrored = false;
let fatalError = undefined;
let textCache;
let assertLog;
describe('ReactDOMFizzShellHydration', () => {
beforeEach(() => {
@@ -35,6 +36,9 @@ describe('ReactDOMFizzShellHydration', () => {
ReactDOMFizzServer = require('react-dom/server');
Stream = require('stream');
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
startTransition = React.startTransition;
textCache = new Map();
@@ -180,7 +184,7 @@ describe('ReactDOMFizzShellHydration', () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
expect(Scheduler).toHaveYielded(['Shell']);
assertLog(['Shell']);
const dehydratedDiv = container.getElementsByTagName('div')[0];
// Clear the cache and start rendering on the client
@@ -190,7 +194,7 @@ describe('ReactDOMFizzShellHydration', () => {
await clientAct(async () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
expect(Scheduler).toHaveYielded(['Suspend! [Shell]']);
assertLog(['Suspend! [Shell]']);
expect(div.current).toBe(null);
expect(container.textContent).toBe('Shell');
@@ -198,7 +202,7 @@ describe('ReactDOMFizzShellHydration', () => {
await clientAct(async () => {
await resolveText('Shell');
});
expect(Scheduler).toHaveYielded(['Shell']);
assertLog(['Shell']);
expect(div.current).toBe(dehydratedDiv);
expect(container.textContent).toBe('Shell');
});
@@ -213,12 +217,12 @@ describe('ReactDOMFizzShellHydration', () => {
await clientAct(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['Suspend! [Shell]']);
assertLog(['Suspend! [Shell]']);
await clientAct(async () => {
await resolveText('Shell');
});
expect(Scheduler).toHaveYielded(['Shell']);
assertLog(['Shell']);
expect(container.textContent).toBe('Shell');
});
@@ -236,7 +240,7 @@ describe('ReactDOMFizzShellHydration', () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
expect(Scheduler).toHaveYielded(['Initial']);
assertLog(['Initial']);
await clientAct(async () => {
const root = ReactDOMClient.hydrateRoot(container, <App />);
@@ -246,7 +250,7 @@ describe('ReactDOMFizzShellHydration', () => {
root.render(<Text text="Updated" />);
});
});
expect(Scheduler).toHaveYielded(['Initial', 'Updated']);
assertLog(['Initial', 'Updated']);
expect(container.textContent).toBe('Updated');
},
);
@@ -262,7 +266,7 @@ describe('ReactDOMFizzShellHydration', () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
expect(Scheduler).toHaveYielded(['Shell']);
assertLog(['Shell']);
// Clear the cache and start rendering on the client
resetTextCache();
@@ -275,13 +279,13 @@ describe('ReactDOMFizzShellHydration', () => {
},
});
});
expect(Scheduler).toHaveYielded(['Suspend! [Shell]']);
assertLog(['Suspend! [Shell]']);
expect(container.textContent).toBe('Shell');
await clientAct(async () => {
root.render(<Text text="New screen" />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'New screen',
'This root received an early update, before anything was able ' +
'hydrate. Switched the entire root to client rendering.',

View File

@@ -16,6 +16,8 @@ let getCacheForType;
let caches;
let seededCache;
let ErrorBoundary;
let waitForAll;
let assertLog;
// TODO: These tests don't pass in persistent mode yet. Need to implement.
@@ -31,6 +33,10 @@ describe('ReactSuspenseEffectsSemantics', () => {
getCacheForType = React.unstable_getCacheForType;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
assertLog = InternalTestUtils.assertLog;
caches = [];
seededCache = null;
@@ -256,7 +262,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</App>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Text:Inside:Before render',
'Suspend:Async',
@@ -281,7 +287,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inside:Before render',
'AsyncText:Async render',
'ClassText:Inside:After render',
@@ -305,7 +311,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App destroy layout',
'Text:Inside:Before destroy layout',
'AsyncText:Async destroy layout',
@@ -377,7 +383,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</App>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Text:Inside:Before render',
'Suspend:Async',
@@ -407,7 +413,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'AsyncText:Async render',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
@@ -426,7 +432,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.renderLegacySyncRoot(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App destroy layout',
'Text:Inside:Before destroy layout',
'AsyncText:Async destroy layout',
@@ -474,7 +480,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
act(() => {
ReactNoop.renderLegacySyncRoot(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Text:Inside:Before render',
'Text:Inside:After render',
@@ -504,7 +510,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</App>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Text:Inside:Before render',
'Suspend:Async',
@@ -526,7 +532,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await advanceTimers(1000);
// Noop since sync root has already committed
expect(Scheduler).toHaveYielded([]);
assertLog([]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside:Before" hidden={true} />
@@ -540,7 +546,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'AsyncText:Async render',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
@@ -559,7 +565,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.renderLegacySyncRoot(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App destroy layout',
'Text:Inside:Before destroy layout',
'AsyncText:Async destroy layout',
@@ -604,7 +610,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Text:Inside:Before render',
'Text:Inside:After render',
@@ -634,7 +640,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</App>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Text:Inside:Before render',
'Suspend:Async',
@@ -653,12 +659,12 @@ describe('ReactSuspenseEffectsSemantics', () => {
await advanceTimers(1000);
// Timing out should commit the fallback and destroy inner layout effects.
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inside:Before destroy layout',
'Text:Inside:After destroy layout',
'Text:Fallback create layout',
]);
expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside:Before" hidden={true} />
@@ -672,7 +678,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inside:Before render',
'AsyncText:Async render',
'Text:Inside:After render',
@@ -695,7 +701,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App destroy layout',
'Text:Inside:Before destroy layout',
'AsyncText:Async destroy layout',
@@ -763,7 +769,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'ClassText:Inside:Before render',
'ClassText:Inside:After render',
@@ -790,7 +796,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</App>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'ClassText:Inside:Before render',
'Suspend:Async',
@@ -809,7 +815,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await advanceTimers(1000);
// Timing out should commit the fallback and destroy inner layout effects.
expect(Scheduler).toHaveYielded([
assertLog([
'ClassText:Inside:Before componentWillUnmount',
'ClassText:Inside:After componentWillUnmount',
'ClassText:Fallback componentDidMount',
@@ -828,7 +834,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'ClassText:Inside:Before render',
'AsyncText:Async render',
'ClassText:Inside:After render',
@@ -849,7 +855,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App destroy layout',
'ClassText:Inside:Before componentWillUnmount',
'AsyncText:Async destroy layout',
@@ -890,7 +896,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Text:Outer render',
'Text:Inner render',
@@ -915,7 +921,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</App>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Suspend:Async',
'Text:Outer render',
@@ -931,12 +937,12 @@ describe('ReactSuspenseEffectsSemantics', () => {
await advanceTimers(1000);
// Timing out should commit the fallback and destroy inner layout effects.
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Outer destroy layout',
'Text:Inner destroy layout',
'Text:Fallback create layout',
]);
expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span hidden={true} prop="Outer">
@@ -950,7 +956,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'AsyncText:Async render',
'Text:Outer render',
'Text:Inner render',
@@ -973,7 +979,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App destroy layout',
'AsyncText:Async destroy layout',
'Text:Outer destroy layout',
@@ -1017,7 +1023,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Text:Outer render',
'Text:MemoizedInner render',
@@ -1042,7 +1048,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</App>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Suspend:Async',
'Text:Outer render',
@@ -1059,12 +1065,12 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Timing out should commit the fallback and destroy inner layout effects.
// Even though the innermost layout effects are beneath a hidden HostComponent.
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Outer destroy layout',
'Text:MemoizedInner destroy layout',
'Text:Fallback create layout',
]);
expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span hidden={true} prop="Outer">
@@ -1078,7 +1084,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'AsyncText:Async render',
'Text:Outer render',
'Text:Fallback destroy layout',
@@ -1100,7 +1106,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App destroy layout',
'AsyncText:Async destroy layout',
'Text:Outer destroy layout',
@@ -1131,7 +1137,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Outer render',
'Text:Inner render',
'Text:Outer create layout',
@@ -1153,7 +1159,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Outer render',
'Text:Inner render',
'Suspend:InnerAsync_1',
@@ -1161,7 +1167,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Inner destroy layout',
'Text:InnerFallback create layout',
]);
expect(Scheduler).toFlushAndYield(['Text:InnerFallback create passive']);
await waitForAll(['Text:InnerFallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" />
@@ -1181,7 +1187,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:Inner render',
@@ -1192,7 +1198,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:InnerFallback destroy layout',
'Text:OuterFallback create layout',
]);
expect(Scheduler).toFlushAndYield(['Text:OuterFallback create passive']);
await waitForAll(['Text:OuterFallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" hidden={true} />
@@ -1206,7 +1212,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('InnerAsync_1');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:Inner render',
@@ -1231,7 +1237,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:Inner render',
@@ -1252,7 +1258,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('OuterAsync_1');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Outer render',
'AsyncText:OuterAsync_1 render',
'Text:Inner render',
@@ -1278,7 +1284,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('InnerAsync_2');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inner render',
'AsyncText:InnerAsync_2 render',
'Text:InnerFallback destroy layout',
@@ -1306,7 +1312,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_2',
'Text:Inner render',
@@ -1332,7 +1338,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('OuterAsync_2');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:OuterFallback create passive',
'Text:Outer render',
'AsyncText:OuterAsync_2 render',
@@ -1374,7 +1380,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Outer render',
'Text:Inner render',
'Text:Outer create layout',
@@ -1396,7 +1402,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Outer render',
'Text:Inner render',
'Suspend:InnerAsync_1',
@@ -1404,7 +1410,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Inner destroy layout',
'Text:InnerFallback create layout',
]);
expect(Scheduler).toFlushAndYield(['Text:InnerFallback create passive']);
await waitForAll(['Text:InnerFallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" />
@@ -1424,7 +1430,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:Inner render',
@@ -1435,7 +1441,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:InnerFallback destroy layout',
'Text:OuterFallback create layout',
]);
expect(Scheduler).toFlushAndYield(['Text:OuterFallback create passive']);
await waitForAll(['Text:OuterFallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" hidden={true} />
@@ -1450,7 +1456,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await resolveText('OuterAsync_1');
await resolveText('InnerAsync_1');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Outer render',
'AsyncText:OuterAsync_1 render',
'Text:Inner render',
@@ -1502,7 +1508,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inside render',
'Text:Outside render',
'Text:Inside create layout',
@@ -1523,7 +1529,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
<App outerChildren={<AsyncText text="OutsideAsync" ms={1000} />} />,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inside render',
'Suspend:OutsideAsync',
'Text:Fallback:Inside render',
@@ -1539,12 +1545,12 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Timing out should commit the fallback and destroy inner layout effects.
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inside destroy layout',
'Text:Fallback:Inside create layout',
'Text:Fallback:Outside create layout',
]);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'Text:Fallback:Inside create passive',
'Text:Fallback:Outside create passive',
]);
@@ -1566,7 +1572,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
/>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inside render',
'Suspend:OutsideAsync',
'Text:Fallback:Inside render',
@@ -1586,13 +1592,11 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Timing out should commit the inner fallback and destroy outer fallback layout effects.
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Fallback:Inside destroy layout',
'Text:Fallback:Fallback create layout',
]);
expect(Scheduler).toFlushAndYield([
'Text:Fallback:Fallback create passive',
]);
await waitForAll(['Text:Fallback:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" hidden={true} />
@@ -1608,7 +1612,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await resolveText('FallbackAsync');
await resolveText('OutsideAsync');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inside render',
'AsyncText:OutsideAsync render',
'Text:Fallback:Fallback destroy layout',
@@ -1656,7 +1660,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inside render',
'Text:Outside render',
'Text:Inside create layout',
@@ -1681,7 +1685,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inside render',
'Suspend:OutsideAsync',
'Text:Fallback:Inside render',
@@ -1693,7 +1697,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Fallback:Fallback create layout',
'Text:Fallback:Outside create layout',
]);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'Text:Fallback:Fallback create passive',
'Text:Fallback:Outside create passive',
]);
@@ -1710,7 +1714,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('FallbackAsync');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Fallback:Inside render',
'AsyncText:FallbackAsync render',
'Text:Fallback:Fallback destroy layout',
@@ -1734,7 +1738,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('OutsideAsync');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inside render',
'AsyncText:OutsideAsync render',
'Text:Fallback:Inside destroy layout',
@@ -1780,7 +1784,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App shouldSuspend={false} />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inside render',
'Text:Outside render',
'Text:Inside create layout',
@@ -1800,7 +1804,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
act(() => {
ReactNoop.render(<App shouldSuspend={true} />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Suspend:Suspend',
'Text:Fallback render',
'Text:Outside render',
@@ -1814,11 +1818,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Timing out should commit the inner fallback and destroy outer fallback layout effects.
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
'Text:Inside destroy layout',
'Text:Fallback create layout',
]);
expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
assertLog(['Text:Inside destroy layout', 'Text:Fallback create layout']);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" hidden={true} />
@@ -1831,7 +1832,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Suspend');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Inside render',
'Text:Fallback destroy layout',
'Text:Inside create layout',
@@ -1895,7 +1896,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'ErrorBoundary render: try',
'App render',
'ThrowsInDidMount render',
@@ -1926,7 +1927,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
@@ -1953,7 +1954,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'AsyncText:Async render',
'ThrowsInDidMount render',
'Text:Inside render',
@@ -2035,7 +2036,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'ErrorBoundary render: try',
'App render',
'ThrowsInWillUnmount render',
@@ -2066,7 +2067,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
@@ -2151,7 +2152,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'ErrorBoundary render: try',
'App render',
'ThrowsInLayoutEffect render',
@@ -2182,7 +2183,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
@@ -2209,7 +2210,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'AsyncText:Async render',
'ThrowsInLayoutEffect render',
'Text:Inside render',
@@ -2290,7 +2291,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'ErrorBoundary render: try',
'App render',
'ThrowsInLayoutEffectDestroy render',
@@ -2321,7 +2322,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
@@ -2394,7 +2395,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Function render',
'ClassText:Class render',
'Text:Function create layout',
@@ -2417,7 +2418,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</App>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Function render',
'Suspend:Async_1',
'Suspend:Async_2',
@@ -2434,7 +2435,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await advanceTimers(1000);
// Timing out should commit the fallback and destroy inner layout effects.
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'ClassText:Fallback componentDidMount',
@@ -2451,7 +2452,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async_1');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Function render',
'AsyncText:Async_1 render',
'Suspend:Async_2',
@@ -2469,7 +2470,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async_2');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Function render',
'AsyncText:Async_1 render',
'AsyncText:Async_2 render',
@@ -2494,7 +2495,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Function destroy layout',
'AsyncText:Async_1 destroy layout',
'AsyncText:Async_2 destroy layout',
@@ -2552,7 +2553,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Function render',
'Suspender "null" render',
'ClassText:Class render',
@@ -2573,7 +2574,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
act(() => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Function render',
'Suspender "A" render',
'Suspend:A',
@@ -2591,7 +2592,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await advanceTimers(1000);
// Timing out should commit the fallback and destroy inner layout effects.
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'ClassText:Fallback componentDidMount',
@@ -2610,7 +2611,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Function render',
'Suspender "B" render',
'Suspend:B',
@@ -2629,7 +2630,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('B');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Function render',
'Suspender "B" render',
'ClassText:Class render',
@@ -2648,7 +2649,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'Text:Function destroy passive',
@@ -2740,7 +2741,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
act(() => {
ReactNoop.renderLegacySyncRoot(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'RefCheckerOuter render',
'ClassComponent:refObject render',
@@ -2761,7 +2762,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Suspend:Async',
'RefCheckerOuter render',
@@ -2779,7 +2780,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'AsyncText:Async render',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
@@ -2791,7 +2792,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.renderLegacySyncRoot(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'AsyncText:Async destroy layout',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
@@ -2818,7 +2819,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'RefCheckerOuter render',
'RefCheckerInner:refObject render',
@@ -2842,7 +2843,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Suspend:Async',
'RefCheckerOuter render',
@@ -2867,7 +2868,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Fallback create passive',
'AsyncText:Async render',
'RefCheckerOuter render',
@@ -2893,7 +2894,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'AsyncText:Async destroy layout',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
@@ -2929,7 +2930,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'RefCheckerOuter render',
'ClassComponent:refObject render',
@@ -2950,7 +2951,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Suspend:Async',
'RefCheckerOuter render',
@@ -2971,7 +2972,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Fallback create passive',
'AsyncText:Async render',
'RefCheckerOuter render',
@@ -2993,7 +2994,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'AsyncText:Async destroy layout',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
@@ -3033,7 +3034,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'RefCheckerOuter render',
'FunctionComponent render',
@@ -3054,7 +3055,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Suspend:Async',
'RefCheckerOuter render',
@@ -3075,7 +3076,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Fallback create passive',
'AsyncText:Async render',
'RefCheckerOuter render',
@@ -3097,7 +3098,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'AsyncText:Async destroy layout',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
@@ -3152,7 +3153,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'RefChecker render',
'RefChecker create layout ref? true',
@@ -3167,7 +3168,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
});
await advanceTimers(1000);
expect(Scheduler).toHaveYielded([
assertLog([
'App render',
'Suspend:Async',
'RefChecker render',
@@ -3181,7 +3182,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Text:Fallback create passive',
'AsyncText:Async render',
'RefChecker render',
@@ -3196,7 +3197,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
assertLog([
'App destroy layout ref? true',
'AsyncText:Async destroy layout',
'RefChecker destroy layout ref? true',
@@ -3251,7 +3252,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'ErrorBoundary render: try',
'App render',
'ThrowsInRefCallback render',
@@ -3282,7 +3283,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
</ErrorBoundary>,
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
@@ -3309,7 +3310,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded([
assertLog([
'AsyncText:Async render',
'ThrowsInRefCallback render',
'Text:Inside render',

View File

@@ -19,6 +19,10 @@ let Scheduler;
let act;
let createMutableSource;
let useMutableSource;
let waitFor;
let waitForAll;
let assertLog;
let waitForPaint;
function loadModules() {
jest.resetModules();
@@ -32,6 +36,12 @@ function loadModules() {
Scheduler = require('scheduler');
act = require('jest-react').act;
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
waitForAll = InternalTestUtils.waitForAll;
waitForPaint = InternalTestUtils.waitForPaint;
assertLog = InternalTestUtils.assertLog;
// Stable entrypoints export with "unstable_" prefix.
createMutableSource =
React.createMutableSource || React.unstable_createMutableSource;
@@ -142,11 +152,11 @@ describe('useMutableSource', () => {
beforeEach(loadModules);
// @gate enableUseMutableSource
it('should subscribe to a source and schedule updates when it changes', () => {
it('should subscribe to a source and schedule updates when it changes', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
act(() => {
await act(async () => {
ReactNoop.renderToRootWithID(
<>
<Component
@@ -165,11 +175,7 @@ describe('useMutableSource', () => {
'root',
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYieldThrough([
'a:one',
'b:one',
'Sync effect',
]);
await waitFor(['a:one', 'b:one', 'Sync effect']);
// Subscriptions should be passive
expect(source.listenerCount).toBe(0);
@@ -178,7 +184,7 @@ describe('useMutableSource', () => {
// Changing values should schedule an update with React
source.value = 'two';
expect(Scheduler).toFlushAndYieldThrough(['a:two', 'b:two']);
await waitFor(['a:two', 'b:two']);
// Unmounting a component should remove its subscription.
ReactNoop.renderToRootWithID(
@@ -193,28 +199,28 @@ describe('useMutableSource', () => {
'root',
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['a:two', 'Sync effect']);
await waitForAll(['a:two', 'Sync effect']);
ReactNoop.flushPassiveEffects();
expect(source.listenerCount).toBe(1);
// Unmounting a root should remove the remaining event listeners
ReactNoop.unmountRootWithID('root');
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
ReactNoop.flushPassiveEffects();
expect(source.listenerCount).toBe(0);
// Changes to source should not trigger an updates or warnings.
source.value = 'three';
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
});
});
// @gate enableUseMutableSource
it('should restart work if a new source is mutated during render', () => {
it('should restart work if a new source is mutated during render', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
act(() => {
await act(async () => {
React.startTransition(() => {
ReactNoop.render(
<>
@@ -235,22 +241,22 @@ describe('useMutableSource', () => {
);
});
// Do enough work to read from one component
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
await waitFor(['a:one']);
// Mutate source before continuing work
source.value = 'two';
// Render work should restart and the updated value should be used
expect(Scheduler).toFlushAndYield(['a:two', 'b:two', 'Sync effect']);
await waitForAll(['a:two', 'b:two', 'Sync effect']);
});
});
// @gate enableUseMutableSource
it('should schedule an update if a new source is mutated between render and commit (subscription)', () => {
it('should schedule an update if a new source is mutated between render and commit (subscription)', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
act(() => {
await act(async () => {
ReactNoop.render(
<>
<Component
@@ -270,23 +276,19 @@ describe('useMutableSource', () => {
);
// Finish rendering
expect(Scheduler).toFlushAndYieldThrough([
'a:one',
'b:one',
'Sync effect',
]);
await waitFor(['a:one', 'b:one', 'Sync effect']);
// Mutate source before subscriptions are attached
expect(source.listenerCount).toBe(0);
source.value = 'two';
// Mutation should be detected, and a new render should be scheduled
expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
await waitForAll(['a:two', 'b:two']);
});
});
// @gate enableUseMutableSource
it('should unsubscribe and resubscribe if a new source is used', () => {
it('should unsubscribe and resubscribe if a new source is used', async () => {
const sourceA = createSource('a-one');
const mutableSourceA = createMutableSource(
sourceA,
@@ -299,7 +301,7 @@ describe('useMutableSource', () => {
param => param.versionB,
);
act(() => {
await act(async () => {
ReactNoop.render(
<Component
label="only"
@@ -309,13 +311,13 @@ describe('useMutableSource', () => {
/>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
await waitForAll(['only:a-one', 'Sync effect']);
ReactNoop.flushPassiveEffects();
expect(sourceA.listenerCount).toBe(1);
// Changing values should schedule an update with React
sourceA.value = 'a-two';
expect(Scheduler).toFlushAndYield(['only:a-two']);
await waitForAll(['only:a-two']);
// If we re-render with a new source, the old one should be unsubscribed.
ReactNoop.render(
@@ -327,23 +329,23 @@ describe('useMutableSource', () => {
/>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['only:b-one', 'Sync effect']);
await waitForAll(['only:b-one', 'Sync effect']);
ReactNoop.flushPassiveEffects();
expect(sourceA.listenerCount).toBe(0);
expect(sourceB.listenerCount).toBe(1);
// Changing to original source should not schedule updates with React
sourceA.value = 'a-three';
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
// Changing new source value should schedule an update with React
sourceB.value = 'b-two';
expect(Scheduler).toFlushAndYield(['only:b-two']);
await waitForAll(['only:b-two']);
});
});
// @gate enableUseMutableSource
it('should unsubscribe and resubscribe if a new subscribe function is provided', () => {
it('should unsubscribe and resubscribe if a new subscribe function is provided', async () => {
const source = createSource('a-one');
const mutableSource = createMutableSource(source, param => param.version);
@@ -364,7 +366,7 @@ describe('useMutableSource', () => {
};
});
act(() => {
await act(async () => {
ReactNoop.renderToRootWithID(
<Component
label="only"
@@ -375,7 +377,7 @@ describe('useMutableSource', () => {
'root',
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
await waitForAll(['only:a-one', 'Sync effect']);
ReactNoop.flushPassiveEffects();
expect(source.listenerCount).toBe(1);
expect(subscribeA).toHaveBeenCalledTimes(1);
@@ -392,7 +394,7 @@ describe('useMutableSource', () => {
'root',
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
await waitForAll(['only:a-one', 'Sync effect']);
ReactNoop.flushPassiveEffects();
expect(source.listenerCount).toBe(1);
expect(unsubscribeA).toHaveBeenCalledTimes(1);
@@ -400,7 +402,7 @@ describe('useMutableSource', () => {
// Unmounting should call the newer unsubscribe.
ReactNoop.unmountRootWithID('root');
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
ReactNoop.flushPassiveEffects();
expect(source.listenerCount).toBe(0);
expect(unsubscribeB).toHaveBeenCalledTimes(1);
@@ -408,11 +410,11 @@ describe('useMutableSource', () => {
});
// @gate enableUseMutableSource
it('should re-use previously read snapshot value when reading is unsafe', () => {
it('should re-use previously read snapshot value when reading is unsafe', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
act(() => {
await act(async () => {
ReactNoop.render(
<>
<Component
@@ -430,14 +432,14 @@ describe('useMutableSource', () => {
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
await waitForAll(['a:one', 'b:one', 'Sync effect']);
// Changing values should schedule an update with React.
// Start working on this update but don't finish it.
React.startTransition(() => {
source.value = 'two';
});
expect(Scheduler).toFlushAndYieldThrough(['a:two']);
await waitFor(['a:two']);
// Re-renders that occur before the update is processed
// should reuse snapshot so long as the config has not changed
@@ -460,18 +462,18 @@ describe('useMutableSource', () => {
() => Scheduler.unstable_yieldValue('Sync effect'),
);
});
expect(Scheduler).toHaveYielded(['a:one', 'b:one', 'Sync effect']);
assertLog(['a:one', 'b:one', 'Sync effect']);
expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
await waitForAll(['a:two', 'b:two']);
});
});
// @gate enableUseMutableSource
it('should read from source on newly mounted subtree if no pending updates are scheduled for source', () => {
it('should read from source on newly mounted subtree if no pending updates are scheduled for source', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
act(() => {
await act(async () => {
ReactNoop.render(
<>
<Component
@@ -483,7 +485,7 @@ describe('useMutableSource', () => {
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['a:one', 'Sync effect']);
await waitForAll(['a:one', 'Sync effect']);
ReactNoop.render(
<>
@@ -502,16 +504,16 @@ describe('useMutableSource', () => {
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
await waitForAll(['a:one', 'b:one', 'Sync effect']);
});
});
// @gate enableUseMutableSource
it('should throw and restart render if source and snapshot are unavailable during an update', () => {
it('should throw and restart render if source and snapshot are unavailable during an update', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
act(() => {
await act(async () => {
ReactNoop.render(
<>
<Component
@@ -529,16 +531,17 @@ describe('useMutableSource', () => {
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
await waitForAll(['a:one', 'b:one', 'Sync effect']);
ReactNoop.flushPassiveEffects();
// Changing values should schedule an update with React.
// Start working on this update but don't finish it.
ReactNoop.idleUpdates(() => {
source.value = 'two';
expect(Scheduler).toFlushAndYieldThrough(['a:two']);
});
// Start working on this update but don't finish it.
await waitFor(['a:two']);
const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
// Force a higher priority render with a new config.
@@ -562,20 +565,16 @@ describe('useMutableSource', () => {
() => Scheduler.unstable_yieldValue('Sync effect'),
);
});
expect(Scheduler).toHaveYielded([
'a:new:two',
'b:new:two',
'Sync effect',
]);
assertLog(['a:new:two', 'b:new:two', 'Sync effect']);
});
});
// @gate enableUseMutableSource
it('should throw and restart render if source and snapshot are unavailable during a sync update', () => {
it('should throw and restart render if source and snapshot are unavailable during a sync update', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
act(() => {
await act(async () => {
ReactNoop.render(
<>
<Component
@@ -593,16 +592,17 @@ describe('useMutableSource', () => {
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
await waitForAll(['a:one', 'b:one', 'Sync effect']);
ReactNoop.flushPassiveEffects();
// Changing values should schedule an update with React.
// Start working on this update but don't finish it.
ReactNoop.idleUpdates(() => {
source.value = 'two';
expect(Scheduler).toFlushAndYieldThrough(['a:two']);
});
// Start working on this update but don't finish it.
await waitFor(['a:two']);
const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
// Force a higher priority render with a new config.
@@ -626,16 +626,12 @@ describe('useMutableSource', () => {
() => Scheduler.unstable_yieldValue('Sync effect'),
);
});
expect(Scheduler).toHaveYielded([
'a:new:two',
'b:new:two',
'Sync effect',
]);
assertLog(['a:new:two', 'b:new:two', 'Sync effect']);
});
});
// @gate enableUseMutableSource
it('should only update components whose subscriptions fire', () => {
it('should only update components whose subscriptions fire', async () => {
const source = createComplexSource('a:one', 'b:one');
const mutableSource = createMutableSource(source, param => param.version);
@@ -645,7 +641,7 @@ describe('useMutableSource', () => {
const getSnapshotB = s => s.valueB;
const subscribeB = (s, callback) => s.subscribeB(callback);
act(() => {
await act(async () => {
ReactNoop.render(
<>
<Component
@@ -663,18 +659,18 @@ describe('useMutableSource', () => {
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['a:a:one', 'b:b:one', 'Sync effect']);
await waitForAll(['a:a:one', 'b:b:one', 'Sync effect']);
// Changes to part of the store (e.g. A) should not render other parts.
source.valueA = 'a:two';
expect(Scheduler).toFlushAndYield(['a:a:two']);
await waitForAll(['a:a:two']);
source.valueB = 'b:two';
expect(Scheduler).toFlushAndYield(['b:b:two']);
await waitForAll(['b:b:two']);
});
});
// @gate enableUseMutableSource
it('should detect tearing in part of the store not yet subscribed to', () => {
it('should detect tearing in part of the store not yet subscribed to', async () => {
const source = createComplexSource('a:one', 'b:one');
const mutableSource = createMutableSource(source, param => param.version);
@@ -684,7 +680,7 @@ describe('useMutableSource', () => {
const getSnapshotB = s => s.valueB;
const subscribeB = (s, callback) => s.subscribeB(callback);
act(() => {
await act(async () => {
ReactNoop.render(
<>
<Component
@@ -696,7 +692,7 @@ describe('useMutableSource', () => {
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['a:a:one', 'Sync effect']);
await waitForAll(['a:a:one', 'Sync effect']);
// Because the store has not changed yet, there are no pending updates,
// so it is considered safe to read from when we start this render.
@@ -725,29 +721,24 @@ describe('useMutableSource', () => {
() => Scheduler.unstable_yieldValue('Sync effect'),
);
});
expect(Scheduler).toFlushAndYieldThrough(['a:a:one', 'b:b:one']);
await waitFor(['a:a:one', 'b:b:one']);
// Mutating the source should trigger a tear detection on the next read,
// which should throw and re-render the entire tree.
source.valueB = 'b:two';
expect(Scheduler).toFlushAndYield([
'a:a:one',
'b:b:two',
'c:b:two',
'Sync effect',
]);
await waitForAll(['a:a:one', 'b:b:two', 'c:b:two', 'Sync effect']);
});
});
// @gate enableUseMutableSource
it('does not schedule an update for subscriptions that fire with an unchanged snapshot', () => {
it('does not schedule an update for subscriptions that fire with an unchanged snapshot', async () => {
const MockComponent = jest.fn(Component);
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
act(() => {
await act(async () => {
ReactNoop.render(
<MockComponent
label="only"
@@ -757,18 +748,18 @@ describe('useMutableSource', () => {
/>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYieldThrough(['only:one', 'Sync effect']);
await waitFor(['only:one', 'Sync effect']);
ReactNoop.flushPassiveEffects();
expect(source.listenerCount).toBe(1);
// Notify subscribe function but don't change the value
source.value = 'one';
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
});
});
// @gate enableUseMutableSource
it('should throw and restart if getSnapshot changes between scheduled update and re-render', () => {
it('should throw and restart if getSnapshot changes between scheduled update and re-render', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
@@ -789,11 +780,11 @@ describe('useMutableSource', () => {
);
}
act(() => {
await act(async () => {
ReactNoop.render(<WrapperWithState />, () =>
Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
await waitForAll(['only:one', 'Sync effect']);
ReactNoop.flushPassiveEffects();
// Change the source (and schedule an update).
@@ -804,16 +795,16 @@ describe('useMutableSource', () => {
updateGetSnapshot(() => newGetSnapshot);
});
expect(Scheduler).toHaveYielded(['only:new:two']);
assertLog(['only:new:two']);
});
});
// @gate enableUseMutableSource
it('should recover from a mutation during yield when other work is scheduled', () => {
it('should recover from a mutation during yield when other work is scheduled', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
act(() => {
await act(async () => {
// Start a render that uses the mutable source.
React.startTransition(() => {
ReactNoop.render(
@@ -833,19 +824,19 @@ describe('useMutableSource', () => {
</>,
);
});
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
await waitFor(['a:one']);
// Mutate source
source.value = 'two';
// Now render something different.
ReactNoop.render(<div />);
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
});
});
// @gate enableUseMutableSource
it('should not throw if the new getSnapshot returns the same snapshot value', () => {
it('should not throw if the new getSnapshot returns the same snapshot value', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
@@ -867,7 +858,7 @@ describe('useMutableSource', () => {
);
}
act(() => {
await act(async () => {
ReactNoop.render(
<>
<React.Profiler id="a" onRender={onRenderA}>
@@ -884,7 +875,7 @@ describe('useMutableSource', () => {
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
await waitForAll(['a:one', 'b:one', 'Sync effect']);
ReactNoop.flushPassiveEffects();
expect(onRenderA).toHaveBeenCalledTimes(1);
expect(onRenderB).toHaveBeenCalledTimes(1);
@@ -892,7 +883,7 @@ describe('useMutableSource', () => {
// If B's getSnapshot function updates, but the snapshot it returns is the same,
// only B should re-render (to update its state).
updateGetSnapshot(() => s => defaultGetSnapshot(s));
expect(Scheduler).toFlushAndYield(['b:one']);
await waitForAll(['b:one']);
ReactNoop.flushPassiveEffects();
expect(onRenderA).toHaveBeenCalledTimes(1);
expect(onRenderB).toHaveBeenCalledTimes(2);
@@ -900,7 +891,7 @@ describe('useMutableSource', () => {
});
// @gate enableUseMutableSource
it('should not throw if getSnapshot changes but the source can be safely read from anyway', () => {
it('should not throw if getSnapshot changes but the source can be safely read from anyway', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
@@ -921,11 +912,11 @@ describe('useMutableSource', () => {
);
}
act(() => {
await act(async () => {
ReactNoop.render(<WrapperWithState />, () =>
Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
await waitForAll(['only:one', 'Sync effect']);
ReactNoop.flushPassiveEffects();
// Change the source (and schedule an update)
@@ -935,12 +926,12 @@ describe('useMutableSource', () => {
updateGetSnapshot(() => newGetSnapshot);
});
expect(Scheduler).toFlushAndYield(['only:new:two']);
await waitForAll(['only:new:two']);
});
});
// @gate enableUseMutableSource
it('should still schedule an update if an eager selector throws after a mutation', () => {
it('should still schedule an update if an eager selector throws after a mutation', async () => {
const source = createSource({
friends: [
{id: 1, name: 'Foo'},
@@ -986,11 +977,11 @@ describe('useMutableSource', () => {
return <li>{name}</li>;
}
act(() => {
await act(async () => {
ReactNoop.render(<FriendsList />, () =>
Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['1:Foo', '2:Bar', 'Sync effect']);
await waitForAll(['1:Foo', '2:Bar', 'Sync effect']);
// This mutation will cause the "Bar" component to throw,
// since its value will no longer be a part of the store.
@@ -1002,12 +993,12 @@ describe('useMutableSource', () => {
{id: 3, name: 'Baz'},
],
};
expect(Scheduler).toFlushAndYield(['1:Foo', '3:Baz']);
await waitForAll(['1:Foo', '3:Baz']);
});
});
// @gate enableUseMutableSource
it('should not warn about updates that fire between unmount and passive unsubscribe', () => {
it('should not warn about updates that fire between unmount and passive unsubscribe', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
@@ -1025,21 +1016,21 @@ describe('useMutableSource', () => {
);
}
act(() => {
await act(async () => {
ReactNoop.renderToRootWithID(<Wrapper />, 'root', () =>
Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
await waitForAll(['only:one', 'Sync effect']);
ReactNoop.flushPassiveEffects();
// Unmounting a root should remove the remaining event listeners in a passive effect
ReactNoop.unmountRootWithID('root');
expect(Scheduler).toFlushAndYieldThrough(['layout unmount']);
await waitFor(['layout unmount']);
// Changes to source should not cause a warning,
// even though the unsubscribe hasn't run yet (since it's a pending passive effect).
source.value = 'two';
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
});
});
@@ -1247,7 +1238,7 @@ describe('useMutableSource', () => {
);
});
// x and y start out reading from different parts of the store.
expect(Scheduler).toHaveYielded(['x: foo, y: bar']);
assertLog(['x: foo, y: bar']);
await act(async () => {
ReactNoop.discreteUpdates(() => {
@@ -1277,7 +1268,7 @@ describe('useMutableSource', () => {
// The actual sequence of work will be:
// 1. React renders the high-pri update, sees a new getSnapshot, detects the source has been further mutated, and throws
// 2. React re-renders with all pending updates, including the second mutation, and renders "bar" and "bar".
expect(Scheduler).toHaveYielded(['x: bar, y: bar']);
assertLog(['x: bar, y: bar']);
});
// @gate enableUseMutableSource
@@ -1317,18 +1308,18 @@ describe('useMutableSource', () => {
await act(async () => {
root.render(<App getSnapshot={getSnapshotA} />);
});
expect(Scheduler).toHaveYielded(['Render: foo', 'Commit: foo']);
assertLog(['Render: foo', 'Commit: foo']);
await act(async () => {
// Switch getSnapshot to read from B instead
root.render(<App getSnapshot={getSnapshotB} />);
// Render and finish the tree, but yield right after paint, before
// the passive effects have fired.
expect(Scheduler).toFlushUntilNextPaint(['Render: bar']);
await waitForPaint(['Render: bar']);
// Then mutate B.
mutateB('baz');
});
expect(Scheduler).toHaveYielded([
assertLog([
// Fires the effect from the previous render
'Commit: bar',
// During that effect, it should detect that the snapshot has changed
@@ -1393,7 +1384,7 @@ describe('useMutableSource', () => {
);
// Render and finish the tree, but yield right after paint, before
// the passive effects have fired.
expect(Scheduler).toFlushUntilNextPaint([]);
await waitForPaint([]);
// Now mutate A. Both hooks should update.
// This is at high priority so that it doesn't get batched with default
@@ -1462,7 +1453,7 @@ describe('useMutableSource', () => {
</>,
);
});
expect(Scheduler).toHaveYielded(['a0']);
assertLog(['a0']);
expect(root).toMatchRenderedOutput('a0');
await act(async () => {
@@ -1476,7 +1467,7 @@ describe('useMutableSource', () => {
);
});
expect(Scheduler).toFlushAndYieldThrough(['a0', 'b0']);
await waitFor(['a0', 'b0']);
// Mutate in an event. This schedules a subscription update on a, which
// already mounted, but not b, which hasn't subscribed yet.
if (gate(flags => flags.enableUnifiedSyncLane)) {
@@ -1499,14 +1490,14 @@ describe('useMutableSource', () => {
mutateB('b0');
});
// Finish the current render
expect(Scheduler).toFlushUntilNextPaint(['c']);
await waitForPaint(['c']);
// a0 will re-render because of the mutation update. But it should show
// the latest value, not the intermediate one, to avoid tearing with b.
expect(Scheduler).toFlushUntilNextPaint(['a0']);
await waitForPaint(['a0']);
expect(root).toMatchRenderedOutput('a0b0c');
// We should be done.
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
expect(root).toMatchRenderedOutput('a0b0c');
});
},
@@ -1606,7 +1597,7 @@ describe('useMutableSource', () => {
await act(async () => {
root.render(<App parentConfig={configA} childConfig={configB} />);
});
expect(Scheduler).toHaveYielded(['Parent: 1', 'Child: 2', 'Commit: 1, 2']);
assertLog(['Parent: 1', 'Child: 2', 'Commit: 1, 2']);
await act(async () => {
// Switch the parent and the child to read using the same config
@@ -1614,7 +1605,7 @@ describe('useMutableSource', () => {
root.render(<App parentConfig={configB} childConfig={configB} />);
});
// Start rendering the parent, but yield before rendering the child
expect(Scheduler).toFlushAndYieldThrough(['Parent: 2']);
await waitFor(['Parent: 2']);
// Mutate the config. This is at lower priority so that 1) to make sure
// it doesn't happen to get batched with the in-progress render, and 2)
@@ -1624,7 +1615,7 @@ describe('useMutableSource', () => {
});
// In default sync mode, all of the updates flush sync.
expect(Scheduler).toFlushAndYieldThrough([
await waitFor([
// The partial render completes
'Child: 2',
'Commit: 2, 2',
@@ -1632,7 +1623,7 @@ describe('useMutableSource', () => {
'Child: 3',
]);
expect(Scheduler).toFlushAndYield([
await waitForAll([
// Now finish the rest of the update
'Commit: 3, 3',
]);
@@ -1687,11 +1678,11 @@ describe('useMutableSource', () => {
() => Scheduler.unstable_yieldValue('Sync effect'),
);
});
expect(Scheduler).toHaveYielded(['a:one', 'Sync effect']);
assertLog(['a:one', 'Sync effect']);
expect(source.listenerCount).toBe(1);
// Mount ComponentB with version 1 (but don't commit it)
act(() => {
await act(async () => {
ReactNoop.render(
<React.Profiler id="root" onRender={onRender}>
<ComponentA />
@@ -1699,11 +1690,7 @@ describe('useMutableSource', () => {
</React.Profiler>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYieldThrough([
'a:one',
'b:one',
'Sync effect',
]);
await waitFor(['a:one', 'b:one', 'Sync effect']);
expect(source.listenerCount).toBe(1);
// Mutate -> schedule update for ComponentA
@@ -1712,7 +1699,7 @@ describe('useMutableSource', () => {
});
// Commit ComponentB -> notice the change and schedule an update for ComponentB
expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
await waitForAll(['a:two', 'b:two']);
expect(source.listenerCount).toBe(2);
});
});
@@ -1746,14 +1733,14 @@ describe('useMutableSource', () => {
});
// @gate enableUseMutableSource
it('should error if multiple renderers of the same type use a mutable source at the same time', () => {
it('should error if multiple renderers of the same type use a mutable source at the same time', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(
source,
param => param.version,
);
act(() => {
await act(async () => {
// Start a render that uses the mutable source.
React.startTransition(() => {
ReactNoop.render(
@@ -1773,7 +1760,7 @@ describe('useMutableSource', () => {
</>,
);
});
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
await waitFor(['a:one']);
const PrevScheduler = Scheduler;
@@ -1791,7 +1778,7 @@ describe('useMutableSource', () => {
subscribe={defaultSubscribe}
/>,
);
expect(Scheduler).toFlushAndYieldThrough(['c:one']);
await waitFor(['c:one']);
expect(console.error.mock.calls[0][0]).toContain(
'Detected multiple renderers concurrently rendering the ' +
@@ -1808,14 +1795,14 @@ describe('useMutableSource', () => {
});
// @gate enableUseMutableSource
it('should error if multiple renderers of the same type use a mutable source at the same time with mutation between', () => {
it('should error if multiple renderers of the same type use a mutable source at the same time with mutation between', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(
source,
param => param.version,
);
act(() => {
await act(async () => {
// Start a render that uses the mutable source.
React.startTransition(() => {
ReactNoop.render(
@@ -1835,7 +1822,7 @@ describe('useMutableSource', () => {
</>,
);
});
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
await waitFor(['a:one']);
const PrevScheduler = Scheduler;
@@ -1856,7 +1843,7 @@ describe('useMutableSource', () => {
subscribe={defaultSubscribe}
/>,
);
expect(Scheduler).toFlushAndYieldThrough(['c:two']);
await waitFor(['c:two']);
expect(console.error.mock.calls[0][0]).toContain(
'Detected multiple renderers concurrently rendering the ' +

View File

@@ -16,6 +16,8 @@ let Scheduler;
let act;
let createMutableSource;
let useMutableSource;
let waitFor;
let assertLog;
describe('useMutableSourceHydration', () => {
beforeEach(() => {
@@ -33,6 +35,10 @@ describe('useMutableSourceHydration', () => {
React.createMutableSource || React.unstable_createMutableSource;
useMutableSource =
React.useMutableSource || React.unstable_useMutableSource;
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
});
const defaultGetSnapshot = source => source.value;
@@ -156,7 +162,7 @@ describe('useMutableSourceHydration', () => {
const htmlString = ReactDOMServer.renderToString(<TestComponent />);
container.innerHTML = htmlString;
expect(Scheduler).toHaveYielded(['only:one']);
assertLog(['only:one']);
expect(source.listenerCount).toBe(0);
act(() => {
@@ -164,7 +170,7 @@ describe('useMutableSourceHydration', () => {
mutableSources: [mutableSource],
});
});
expect(Scheduler).toHaveYielded(['only:one']);
assertLog(['only:one']);
expect(source.listenerCount).toBe(1);
});
@@ -190,7 +196,7 @@ describe('useMutableSourceHydration', () => {
const htmlString = ReactDOMServer.renderToString(<TestComponent />);
container.innerHTML = htmlString;
expect(Scheduler).toHaveYielded(['only:one']);
assertLog(['only:one']);
expect(source.listenerCount).toBe(0);
expect(() => {
@@ -211,7 +217,7 @@ describe('useMutableSourceHydration', () => {
],
{withoutStack: 1},
);
expect(Scheduler).toHaveYielded([
assertLog([
'only:two',
'only:two',
'Log error: Text content does not match server-rendered HTML.',
@@ -221,7 +227,7 @@ describe('useMutableSourceHydration', () => {
});
// @gate enableUseMutableSource
it('should detect a tear between hydrating components', () => {
it('should detect a tear between hydrating components', async () => {
const source = createSource('one');
const mutableSource = createMutableSource(source, param => param.version);
@@ -249,11 +255,11 @@ describe('useMutableSourceHydration', () => {
const htmlString = ReactDOMServer.renderToString(<TestComponent />);
container.innerHTML = htmlString;
expect(Scheduler).toHaveYielded(['a:one', 'b:one']);
assertLog(['a:one', 'b:one']);
expect(source.listenerCount).toBe(0);
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
React.startTransition(() => {
ReactDOMClient.hydrateRoot(container, <TestComponent />, {
mutableSources: [mutableSource],
@@ -262,7 +268,7 @@ describe('useMutableSourceHydration', () => {
},
});
});
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
await waitFor(['a:one']);
source.value = 'two';
});
}).toErrorDev(
@@ -270,7 +276,7 @@ describe('useMutableSourceHydration', () => {
'The server HTML was replaced with client content in <div>.',
{withoutStack: true},
);
expect(Scheduler).toHaveYielded([
assertLog([
'a:two',
'b:two',
// TODO: Before onRecoverableError, this error was never surfaced to the
@@ -288,7 +294,7 @@ describe('useMutableSourceHydration', () => {
});
// @gate enableUseMutableSource
it('should detect a tear between hydrating components reading from different parts of a source', () => {
it('should detect a tear between hydrating components reading from different parts of a source', async () => {
const source = createComplexSource('a:one', 'b:one');
const mutableSource = createMutableSource(source, param => param.version);
@@ -318,10 +324,10 @@ describe('useMutableSourceHydration', () => {
</>,
);
container.innerHTML = htmlString;
expect(Scheduler).toHaveYielded(['0:a:one', '1:b:one']);
assertLog(['0:a:one', '1:b:one']);
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
const fragment = (
<>
<Component
@@ -346,7 +352,7 @@ describe('useMutableSourceHydration', () => {
},
});
});
expect(Scheduler).toFlushAndYieldThrough(['0:a:one']);
await waitFor(['0:a:one']);
source.valueB = 'b:two';
});
}).toErrorDev(
@@ -354,7 +360,7 @@ describe('useMutableSourceHydration', () => {
'The server HTML was replaced with client content in <div>.',
{withoutStack: true},
);
expect(Scheduler).toHaveYielded([
assertLog([
'0:a:one',
'1:b:two',
// TODO: Before onRecoverableError, this error was never surfaced to the

View File

@@ -22,6 +22,8 @@ describe('useRef', () => {
let useLayoutEffect;
let useRef;
let useState;
let waitForAll;
let assertLog;
beforeEach(() => {
React = require('react');
@@ -37,6 +39,10 @@ describe('useRef', () => {
useLayoutEffect = React.useLayoutEffect;
useRef = React.useRef;
useState = React.useState;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
assertLog = InternalTestUtils.assertLog;
});
function Text(props) {
@@ -79,17 +85,17 @@ describe('useRef', () => {
act(() => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded([]);
assertLog([]);
ping(1);
ping(2);
ping(3);
expect(Scheduler).toHaveYielded([]);
assertLog([]);
jest.advanceTimersByTime(100);
expect(Scheduler).toHaveYielded(['ping: 3']);
assertLog(['ping: 3']);
ping(4);
jest.advanceTimersByTime(20);
@@ -97,13 +103,13 @@ describe('useRef', () => {
ping(6);
jest.advanceTimersByTime(80);
expect(Scheduler).toHaveYielded([]);
assertLog([]);
jest.advanceTimersByTime(20);
expect(Scheduler).toHaveYielded(['ping: 6']);
assertLog(['ping: 6']);
});
it('should return the same ref during re-renders', () => {
it('should return the same ref during re-renders', async () => {
function Counter() {
const ref = useRef('val');
const [count, setCount] = useState(0);
@@ -121,10 +127,10 @@ describe('useRef', () => {
}
ReactNoop.render(<Counter />);
expect(Scheduler).toFlushAndYield([3]);
await waitForAll([3]);
ReactNoop.render(<Counter />);
expect(Scheduler).toFlushAndYield([3]);
await waitForAll([3]);
});
if (__DEV__) {

View File

@@ -20,6 +20,9 @@ let useImperativeHandle;
let useRef;
let useState;
let startTransition;
let waitFor;
let waitForAll;
let assertLog;
// This tests the native useSyncExternalStore implementation, not the shim.
// Tests that apply to both the native implementation and the shim should go
@@ -41,6 +44,11 @@ describe('useSyncExternalStore', () => {
useSyncExternalStore = React.useSyncExternalStore;
startTransition = React.startTransition;
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
waitForAll = InternalTestUtils.waitForAll;
assertLog = InternalTestUtils.assertLog;
act = require('jest-react').act;
});
@@ -122,13 +130,13 @@ describe('useSyncExternalStore', () => {
root.render(<App store={store1} />);
});
expect(Scheduler).toFlushAndYieldThrough(['A0', 'B0']);
await waitFor(['A0', 'B0']);
// During an interleaved event, the store is mutated.
store1.set(1);
// Then we continue rendering.
expect(Scheduler).toFlushAndYield([
await waitForAll([
// C reads a newer value from the store than A or B, which means they
// are inconsistent.
'C1',
@@ -152,13 +160,13 @@ describe('useSyncExternalStore', () => {
});
// Start a concurrent render that reads from the store, then yield.
expect(Scheduler).toFlushAndYieldThrough(['A0', 'B0']);
await waitFor(['A0', 'B0']);
// During an interleaved event, the store is mutated.
store2.set(1);
// Then we continue rendering.
expect(Scheduler).toFlushAndYield([
await waitForAll([
// C reads a newer value from the store than A or B, which means they
// are inconsistent.
'C1',
@@ -191,17 +199,17 @@ describe('useSyncExternalStore', () => {
// Start a render that reads from the store and yields value
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['value:initial']);
assertLog(['value:initial']);
await act(() => {
store.set('value:changed');
});
expect(Scheduler).toHaveYielded(['value:changed']);
assertLog(['value:changed']);
// If cached value was updated, we expect a re-render
await act(() => {
store.set('value:initial');
});
expect(Scheduler).toHaveYielded(['value:initial']);
assertLog(['value:initial']);
});
});

View File

@@ -18,6 +18,8 @@ let ReactFreshRuntime;
let Scheduler;
let act;
let createReactClass;
let waitFor;
let assertLog;
describe('ReactFresh', () => {
let container;
@@ -32,6 +34,11 @@ describe('ReactFresh', () => {
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
act = require('jest-react').act;
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
createReactClass = require('create-react-class/factory')(
React.Component,
React.isValidElement,
@@ -2441,7 +2448,7 @@ describe('ReactFresh', () => {
const root = ReactDOMClient.createRoot(container);
root.render(<AppV1 offscreen={true} />);
expect(Scheduler).toFlushAndYieldThrough(['App#layout']);
await waitFor(['App#layout']);
const el = container.firstChild;
expect(el.hidden).toBe(true);
expect(el.firstChild).toBe(null); // Offscreen content not flushed yet.
@@ -2468,7 +2475,7 @@ describe('ReactFresh', () => {
expect(el.firstChild).toBe(null);
// Process the offscreen updates.
expect(Scheduler).toFlushAndYieldThrough(['Hello#layout']);
await waitFor(['Hello#layout']);
expect(container.firstChild).toBe(el);
expect(el.firstChild.textContent).toBe('0');
expect(el.firstChild.style.color).toBe('red');
@@ -2481,7 +2488,7 @@ describe('ReactFresh', () => {
);
});
expect(Scheduler).toHaveYielded(['Hello#layout']);
assertLog(['Hello#layout']);
expect(el.firstChild.textContent).toBe('1');
expect(el.firstChild.style.color).toBe('red');
@@ -2507,7 +2514,7 @@ describe('ReactFresh', () => {
expect(el.firstChild.style.color).toBe('red');
// Process the offscreen updates.
expect(Scheduler).toFlushAndYieldThrough(['Hello#layout']);
await waitFor(['Hello#layout']);
expect(container.firstChild).toBe(el);
expect(el.firstChild.textContent).toBe('1');
expect(el.firstChild.style.color).toBe('orange');

View File

@@ -19,7 +19,9 @@ const {format: prettyFormat} = require('pretty-format');
// Isolate noop renderer
jest.resetModules();
const ReactNoop = require('react-noop-renderer');
const Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
const waitForAll = InternalTestUtils.waitForAll;
// Kind of hacky, but we nullify all the instances to test the tree structure
// with jasmine's deep equality function, and test the instances separate. We
@@ -1015,7 +1017,7 @@ describe('ReactTestRenderer', () => {
);
});
it('can concurrently render context with a "primary" renderer', () => {
it('can concurrently render context with a "primary" renderer', async () => {
const Context = React.createContext(null);
const Indirection = React.Fragment;
const App = () => (
@@ -1026,7 +1028,7 @@ describe('ReactTestRenderer', () => {
</Context.Provider>
);
ReactNoop.render(<App />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
ReactTestRenderer.create(<App />);
});

View File

@@ -4,6 +4,7 @@ let React;
let ReactTestRenderer;
let Scheduler;
let act;
let assertLog;
describe('ReactTestRenderer.act()', () => {
beforeEach(() => {
@@ -12,6 +13,9 @@ describe('ReactTestRenderer.act()', () => {
ReactTestRenderer = require('react-test-renderer');
Scheduler = require('scheduler');
act = ReactTestRenderer.act;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
});
// @gate __DEV__
@@ -91,7 +95,7 @@ describe('ReactTestRenderer.act()', () => {
await act(async () => {
root.update(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
// Should not flush effects without also flushing microtasks
// First render:
'Effect',

View File

@@ -13,6 +13,9 @@
let React;
let ReactTestRenderer;
let Scheduler;
let waitForAll;
let waitFor;
let assertLog;
describe('ReactTestRendererAsync', () => {
beforeEach(() => {
@@ -21,9 +24,14 @@ describe('ReactTestRendererAsync', () => {
React = require('react');
ReactTestRenderer = require('react-test-renderer');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
});
it('flushAll flushes all work', () => {
it('flushAll flushes all work', async () => {
function Foo(props) {
return props.children;
}
@@ -35,7 +43,7 @@ describe('ReactTestRendererAsync', () => {
expect(renderer.toJSON()).toEqual(null);
// Flush initial mount.
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(renderer.toJSON()).toEqual('Hi');
// Update
@@ -43,11 +51,11 @@ describe('ReactTestRendererAsync', () => {
// Not yet updated.
expect(renderer.toJSON()).toEqual('Hi');
// Flush update.
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(renderer.toJSON()).toEqual('Bye');
});
it('flushAll returns array of yielded values', () => {
it('flushAll returns array of yielded values', async () => {
function Child(props) {
Scheduler.unstable_yieldValue(props.children);
return props.children;
@@ -65,15 +73,15 @@ describe('ReactTestRendererAsync', () => {
unstable_isConcurrent: true,
});
expect(Scheduler).toFlushAndYield(['A:1', 'B:1', 'C:1']);
await waitForAll(['A:1', 'B:1', 'C:1']);
expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']);
renderer.update(<Parent step={2} />);
expect(Scheduler).toFlushAndYield(['A:2', 'B:2', 'C:2']);
await waitForAll(['A:2', 'B:2', 'C:2']);
expect(renderer.toJSON()).toEqual(['A:2', 'B:2', 'C:2']);
});
it('flushThrough flushes until the expected values is yielded', () => {
it('flushThrough flushes until the expected values is yielded', async () => {
function Child(props) {
Scheduler.unstable_yieldValue(props.children);
return props.children;
@@ -96,16 +104,16 @@ describe('ReactTestRendererAsync', () => {
});
// Flush the first two siblings
expect(Scheduler).toFlushAndYieldThrough(['A:1', 'B:1']);
await waitFor(['A:1', 'B:1']);
// Did not commit yet.
expect(renderer.toJSON()).toEqual(null);
// Flush the remaining work
expect(Scheduler).toFlushAndYield(['C:1']);
await waitForAll(['C:1']);
expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']);
});
it('supports high priority interruptions', () => {
it('supports high priority interruptions', async () => {
function Child(props) {
Scheduler.unstable_yieldValue(props.children);
return props.children;
@@ -136,7 +144,7 @@ describe('ReactTestRendererAsync', () => {
});
// Flush the some of the changes, but don't commit
expect(Scheduler).toFlushAndYieldThrough(['A:1']);
await waitFor(['A:1']);
expect(renderer.toJSON()).toEqual(null);
// Interrupt with higher priority properties
@@ -232,12 +240,12 @@ describe('ReactTestRendererAsync', () => {
});
expect(Scheduler).toFlushAndThrow('Oh no!');
expect(Scheduler).toHaveYielded(['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D']);
assertLog(['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D']);
renderer.update(<App />);
expect(Scheduler).toFlushAndThrow('Oh no!');
expect(Scheduler).toHaveYielded(['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D']);
assertLog(['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D']);
renderer.update(<App />);
expect(Scheduler).toFlushAndThrow('Oh no!');

View File

@@ -21,6 +21,10 @@ let cancelCallback;
let wrapCallback;
let getCurrentPriorityLevel;
let shouldYield;
let waitForAll;
let assertLog;
let waitFor;
let waitForPaint;
describe('Scheduler', () => {
beforeEach(() => {
@@ -40,20 +44,26 @@ describe('Scheduler', () => {
wrapCallback = Scheduler.unstable_wrapCallback;
getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel;
shouldYield = Scheduler.unstable_shouldYield;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
waitForPaint = InternalTestUtils.waitForPaint;
});
it('flushes work incrementally', () => {
it('flushes work incrementally', async () => {
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('B'));
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('C'));
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('D'));
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
expect(Scheduler).toFlushAndYieldThrough(['C']);
expect(Scheduler).toFlushAndYield(['D']);
await waitFor(['A', 'B']);
await waitFor(['C']);
await waitForAll(['D']);
});
it('cancels work', () => {
it('cancels work', async () => {
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
const callbackHandleB = scheduleCallback(NormalPriority, () =>
Scheduler.unstable_yieldValue('B'),
@@ -62,19 +72,19 @@ describe('Scheduler', () => {
cancelCallback(callbackHandleB);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'A',
// B should have been cancelled
'C',
]);
});
it('executes the highest priority callbacks first', () => {
it('executes the highest priority callbacks first', async () => {
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('B'));
// Yield before B is flushed
expect(Scheduler).toFlushAndYieldThrough(['A']);
await waitFor(['A']);
scheduleCallback(UserBlockingPriority, () =>
Scheduler.unstable_yieldValue('C'),
@@ -84,10 +94,10 @@ describe('Scheduler', () => {
);
// C and D should come first, because they are higher priority
expect(Scheduler).toFlushAndYield(['C', 'D', 'B']);
await waitForAll(['C', 'D', 'B']);
});
it('expires work', () => {
it('expires work', async () => {
scheduleCallback(NormalPriority, didTimeout => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue(`A (did timeout: ${didTimeout})`);
@@ -103,7 +113,7 @@ describe('Scheduler', () => {
// Advance time, but not by enough to expire any work
Scheduler.unstable_advanceTime(249);
expect(Scheduler).toHaveYielded([]);
assertLog([]);
// Schedule a few more callbacks
scheduleCallback(NormalPriority, didTimeout => {
@@ -117,33 +127,27 @@ describe('Scheduler', () => {
// Advance by just a bit more to expire the user blocking callbacks
Scheduler.unstable_advanceTime(1);
expect(Scheduler).toFlushAndYieldThrough([
'B (did timeout: true)',
'C (did timeout: true)',
]);
await waitFor(['B (did timeout: true)', 'C (did timeout: true)']);
// Expire A
Scheduler.unstable_advanceTime(4600);
expect(Scheduler).toFlushAndYieldThrough(['A (did timeout: true)']);
await waitFor(['A (did timeout: true)']);
// Flush the rest without expiring
expect(Scheduler).toFlushAndYield([
'D (did timeout: false)',
'E (did timeout: true)',
]);
await waitForAll(['D (did timeout: false)', 'E (did timeout: true)']);
});
it('has a default expiration of ~5 seconds', () => {
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
Scheduler.unstable_advanceTime(4999);
expect(Scheduler).toHaveYielded([]);
assertLog([]);
Scheduler.unstable_advanceTime(1);
expect(Scheduler).toFlushExpired(['A']);
});
it('continues working on same task after yielding', () => {
it('continues working on same task after yielding', async () => {
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue('A');
@@ -184,14 +188,14 @@ describe('Scheduler', () => {
// Flush, then yield while in the middle of C.
expect(didYield).toBe(false);
expect(Scheduler).toFlushAndYieldThrough(['A', 'B', 'C1']);
await waitFor(['A', 'B', 'C1']);
expect(didYield).toBe(true);
// When we resume, we should continue working on C.
expect(Scheduler).toFlushAndYield(['C2', 'C3', 'D', 'E']);
await waitForAll(['C2', 'C3', 'D', 'E']);
});
it('continuation callbacks inherit the expiration of the previous callback', () => {
it('continuation callbacks inherit the expiration of the previous callback', async () => {
const tasks = [
['A', 125],
['B', 124],
@@ -213,14 +217,14 @@ describe('Scheduler', () => {
scheduleCallback(UserBlockingPriority, work);
// Flush until just before the expiration time
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
await waitFor(['A', 'B']);
// Advance time by just a bit more. This should expire all the remaining work.
Scheduler.unstable_advanceTime(1);
expect(Scheduler).toFlushExpired(['C', 'D']);
});
it('continuations are interrupted by higher priority work', () => {
it('continuations are interrupted by higher priority work', async () => {
const tasks = [
['A', 100],
['B', 100],
@@ -238,20 +242,20 @@ describe('Scheduler', () => {
}
};
scheduleCallback(NormalPriority, work);
expect(Scheduler).toFlushAndYieldThrough(['A']);
await waitFor(['A']);
scheduleCallback(UserBlockingPriority, () => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue('High pri');
});
expect(Scheduler).toFlushAndYield(['High pri', 'B', 'C', 'D']);
await waitForAll(['High pri', 'B', 'C', 'D']);
});
it(
'continuations do not block higher priority work scheduled ' +
'inside an executing callback',
() => {
async () => {
const tasks = [
['A', 100],
['B', 100],
@@ -279,7 +283,7 @@ describe('Scheduler', () => {
}
};
scheduleCallback(NormalPriority, work);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'A',
'B',
'Schedule high pri',
@@ -293,7 +297,7 @@ describe('Scheduler', () => {
},
);
it('cancelling a continuation', () => {
it('cancelling a continuation', async () => {
const task = scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('Yield');
return () => {
@@ -301,9 +305,9 @@ describe('Scheduler', () => {
};
});
expect(Scheduler).toFlushAndYieldThrough(['Yield']);
await waitFor(['Yield']);
cancelCallback(task);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
});
it('top-level immediate callbacks fire in a subsequent task', () => {
@@ -320,7 +324,7 @@ describe('Scheduler', () => {
Scheduler.unstable_yieldValue('D'),
);
// Immediate callback hasn't fired, yet.
expect(Scheduler).toHaveYielded([]);
assertLog([]);
// They all flush immediately within the subsequent task.
expect(Scheduler).toFlushExpired(['A', 'B', 'C', 'D']);
});
@@ -339,7 +343,7 @@ describe('Scheduler', () => {
scheduleCallback(ImmediatePriority, () =>
Scheduler.unstable_yieldValue('D'),
);
expect(Scheduler).toHaveYielded([]);
assertLog([]);
// C should flush at the end
expect(Scheduler).toFlushExpired(['A', 'B', 'D', 'C']);
});
@@ -365,10 +369,10 @@ describe('Scheduler', () => {
);
wrappedCallback();
expect(Scheduler).toHaveYielded([NormalPriority]);
assertLog([NormalPriority]);
wrappedUserBlockingCallback();
expect(Scheduler).toHaveYielded([UserBlockingPriority]);
assertLog([UserBlockingPriority]);
});
it('wrapped callbacks inherit the current priority even when nested', () => {
@@ -387,10 +391,10 @@ describe('Scheduler', () => {
});
wrappedCallback();
expect(Scheduler).toHaveYielded([NormalPriority]);
assertLog([NormalPriority]);
wrappedUserBlockingCallback();
expect(Scheduler).toHaveYielded([UserBlockingPriority]);
assertLog([UserBlockingPriority]);
});
it("immediate callbacks fire even if there's an error", () => {
@@ -407,12 +411,12 @@ describe('Scheduler', () => {
});
expect(() => expect(Scheduler).toFlushExpired()).toThrow('Oops A');
expect(Scheduler).toHaveYielded(['A']);
assertLog(['A']);
// B and C flush in a subsequent event. That way, the second error is not
// swallowed.
expect(() => expect(Scheduler).toFlushExpired()).toThrow('Oops C');
expect(Scheduler).toHaveYielded(['B', 'C']);
assertLog(['B', 'C']);
});
it('multiple immediate callbacks can throw and there will be an error for each one', () => {
@@ -440,7 +444,7 @@ describe('Scheduler', () => {
Scheduler.unstable_yieldValue(getCurrentPriorityLevel());
});
expect(Scheduler).toHaveYielded([
assertLog([
NormalPriority,
ImmediatePriority,
NormalPriority,
@@ -454,7 +458,7 @@ describe('Scheduler', () => {
// priority if you have sourcemaps.
// TODO: Feature temporarily disabled while we investigate a bug in one of
// our minifiers.
it.skip('adds extra function to the JS stack whose name includes the priority level', () => {
it.skip('adds extra function to the JS stack whose name includes the priority level', async () => {
function inferPriorityFromCallstack() {
try {
throw Error();
@@ -508,7 +512,7 @@ describe('Scheduler', () => {
Scheduler.unstable_yieldValue('Idle: ' + inferPriorityFromCallstack()),
);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'Immediate: ' + ImmediatePriority,
'UserBlocking: ' + UserBlockingPriority,
'Normal: ' + NormalPriority,
@@ -519,7 +523,7 @@ describe('Scheduler', () => {
}
describe('delayed tasks', () => {
it('schedules a delayed task', () => {
it('schedules a delayed task', async () => {
scheduleCallback(
NormalPriority,
() => Scheduler.unstable_yieldValue('A'),
@@ -529,21 +533,21 @@ describe('Scheduler', () => {
);
// Should flush nothing, because delay hasn't elapsed
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
// Advance time until right before the threshold
Scheduler.unstable_advanceTime(999);
// Still nothing
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
// Advance time past the threshold
Scheduler.unstable_advanceTime(1);
// Now it should flush like normal
expect(Scheduler).toFlushAndYield(['A']);
await waitForAll(['A']);
});
it('schedules multiple delayed tasks', () => {
it('schedules multiple delayed tasks', async () => {
scheduleCallback(
NormalPriority,
() => Scheduler.unstable_yieldValue('C'),
@@ -577,20 +581,20 @@ describe('Scheduler', () => {
);
// Should flush nothing, because delay hasn't elapsed
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
// Advance some time.
Scheduler.unstable_advanceTime(200);
// Both A and B are no longer delayed. They can now flush incrementally.
expect(Scheduler).toFlushAndYieldThrough(['A']);
expect(Scheduler).toFlushAndYield(['B']);
await waitFor(['A']);
await waitForAll(['B']);
// Advance the rest
Scheduler.unstable_advanceTime(200);
expect(Scheduler).toFlushAndYield(['C', 'D']);
await waitForAll(['C', 'D']);
});
it('interleaves normal tasks and delayed tasks', () => {
it('interleaves normal tasks and delayed tasks', async () => {
// Schedule some high priority callbacks with a delay. When their delay
// elapses, they will be the most important callback in the queue.
scheduleCallback(
@@ -624,17 +628,10 @@ describe('Scheduler', () => {
// Flush all the work. The timers should be interleaved with the
// other tasks.
expect(Scheduler).toFlushAndYield([
'A',
'Timer 1',
'B',
'C',
'Timer 2',
'D',
]);
await waitForAll(['A', 'Timer 1', 'B', 'C', 'Timer 2', 'D']);
});
it('interleaves delayed tasks with time-sliced tasks', () => {
it('interleaves delayed tasks with time-sliced tasks', async () => {
// Schedule some high priority callbacks with a delay. When their delay
// elapses, they will be the most important callback in the queue.
scheduleCallback(
@@ -670,17 +667,10 @@ describe('Scheduler', () => {
// Flush all the work. The timers should be interleaved with the
// other tasks.
expect(Scheduler).toFlushAndYield([
'A',
'Timer 1',
'B',
'C',
'Timer 2',
'D',
]);
await waitForAll(['A', 'Timer 1', 'B', 'C', 'Timer 2', 'D']);
});
it('cancels a delayed task', () => {
it('cancels a delayed task', async () => {
// Schedule several tasks with the same delay
const options = {delay: 100};
@@ -701,7 +691,7 @@ describe('Scheduler', () => {
);
// Cancel B before its delay has elapsed
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
cancelCallback(taskB);
// Cancel C after its delay has elapsed
@@ -709,24 +699,24 @@ describe('Scheduler', () => {
cancelCallback(taskC);
// Only A should flush
expect(Scheduler).toFlushAndYield(['A']);
await waitForAll(['A']);
});
it('gracefully handles scheduled tasks that are not a function', () => {
it('gracefully handles scheduled tasks that are not a function', async () => {
scheduleCallback(ImmediatePriority, null);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
scheduleCallback(ImmediatePriority, undefined);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
scheduleCallback(ImmediatePriority, {});
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
scheduleCallback(ImmediatePriority, 42);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
});
it('toFlushUntilNextPaint stops if a continuation is returned', () => {
it('toFlushUntilNextPaint stops if a continuation is returned', async () => {
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('Original Task');
Scheduler.unstable_yieldValue('shouldYield: ' + shouldYield());
@@ -736,7 +726,7 @@ describe('Scheduler', () => {
};
});
expect(Scheduler).toFlushUntilNextPaint([
await waitForPaint([
'Original Task',
// Immediately before returning a continuation, `shouldYield` returns
// false, which means there must be time remaining in the frame.
@@ -750,10 +740,10 @@ describe('Scheduler', () => {
expect(Scheduler.unstable_now()).toBe(0);
// Continue the task
expect(Scheduler).toFlushAndYield(['Continuation Task']);
await waitForAll(['Continuation Task']);
});
it("toFlushAndYield keeps flushing even if there's a continuation", () => {
it("toFlushAndYield keeps flushing even if there's a continuation", async () => {
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('Original Task');
Scheduler.unstable_yieldValue('shouldYield: ' + shouldYield());
@@ -763,7 +753,7 @@ describe('Scheduler', () => {
};
});
expect(Scheduler).toFlushAndYield([
await waitForAll([
'Original Task',
// Immediately before returning a continuation, `shouldYield` returns
// false, which means there must be time remaining in the frame.

View File

@@ -24,6 +24,8 @@ let cancelCallback;
// let wrapCallback;
// let getCurrentPriorityLevel;
// let shouldYield;
let waitForAll;
let waitFor;
function priorityLevelToString(priorityLevel) {
switch (priorityLevel) {
@@ -69,6 +71,10 @@ describe('Scheduler', () => {
// wrapCallback = Scheduler.unstable_wrapCallback;
// getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel;
// shouldYield = Scheduler.unstable_shouldYield;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
});
const TaskStartEvent = 1;
@@ -254,7 +260,7 @@ describe('Scheduler', () => {
return '\n' + result;
}
it('creates a basic flamegraph', () => {
it('creates a basic flamegraph', async () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
Scheduler.unstable_advanceTime(100);
@@ -280,9 +286,9 @@ describe('Scheduler', () => {
},
{label: 'Foo'},
);
expect(Scheduler).toFlushAndYieldThrough(['Yield 1', 'Yield 3']);
await waitFor(['Yield 1', 'Yield 3']);
Scheduler.unstable_advanceTime(100);
expect(Scheduler).toFlushAndYield(['Yield 2', 'Yield 4']);
await waitForAll(['Yield 2', 'Yield 4']);
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
@@ -293,7 +299,7 @@ Task 1 [Normal] │ ████████░░░░░░░
);
});
it('marks when a task is canceled', () => {
it('marks when a task is canceled', async () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
const task = scheduleCallback(NormalPriority, () => {
@@ -306,13 +312,13 @@ Task 1 [Normal] │ ████████░░░░░░░
};
});
expect(Scheduler).toFlushAndYieldThrough(['Yield 1', 'Yield 2']);
await waitFor(['Yield 1', 'Yield 2']);
Scheduler.unstable_advanceTime(100);
cancelCallback(task);
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
!!! Main thread │░░░░░░██████████████████████
@@ -321,7 +327,7 @@ Task 1 [Normal] │██████░░🡐 canceled
);
});
it('marks when a task errors', () => {
it('marks when a task errors', async () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
scheduleCallback(NormalPriority, () => {
@@ -333,7 +339,7 @@ Task 1 [Normal] │██████░░🡐 canceled
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
!!! Main thread │░░░░░░██████████████████████
@@ -342,7 +348,7 @@ Task 1 [Normal] │██████🡐 errored
);
});
it('marks when multiple tasks are canceled', () => {
it('marks when multiple tasks are canceled', async () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
const task1 = scheduleCallback(NormalPriority, () => {
@@ -364,7 +370,7 @@ Task 1 [Normal] │██████🡐 errored
};
});
expect(Scheduler).toFlushAndYieldThrough(['Yield 1', 'Yield 2']);
await waitFor(['Yield 1', 'Yield 2']);
Scheduler.unstable_advanceTime(100);
cancelCallback(task1);
@@ -373,7 +379,7 @@ Task 1 [Normal] │██████🡐 errored
// Advance more time. This should not affect the size of the main
// thread row, since the Scheduler queue is empty.
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
// The main thread row should end when the callback is cancelled.
expect(stopProfilingAndPrintFlamegraph()).toEqual(
@@ -385,14 +391,14 @@ Task 2 [Normal] │░░░░░░░░🡐 canceled
);
});
it('handles cancelling a task that already finished', () => {
it('handles cancelling a task that already finished', async () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
const task = scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('A');
Scheduler.unstable_advanceTime(1000);
});
expect(Scheduler).toFlushAndYield(['A']);
await waitForAll(['A']);
cancelCallback(task);
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
@@ -402,7 +408,7 @@ Task 1 [Normal] │████████████████
);
});
it('handles cancelling a task multiple times', () => {
it('handles cancelling a task multiple times', async () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
scheduleCallback(
@@ -426,7 +432,7 @@ Task 1 [Normal] │████████████████
cancelCallback(task);
cancelCallback(task);
cancelCallback(task);
expect(Scheduler).toFlushAndYield(['A']);
await waitForAll(['A']);
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
!!! Main thread │████████████░░░░░░░░░░░░░░░░░░░░
@@ -436,7 +442,7 @@ Task 2 [Normal] │ ░░░░░░░░🡐 canceled
);
});
it('handles delayed tasks', () => {
it('handles delayed tasks', async () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
scheduleCallback(
NormalPriority,
@@ -448,11 +454,11 @@ Task 2 [Normal] │ ░░░░░░░░🡐 canceled
delay: 1000,
},
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushAndYield(['A']);
await waitForAll(['A']);
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
@@ -462,7 +468,7 @@ Task 1 [Normal] │ █████████
);
});
it('handles cancelling a delayed task', () => {
it('handles cancelling a delayed task', async () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
const task = scheduleCallback(
NormalPriority,
@@ -470,7 +476,7 @@ Task 1 [Normal] │ █████████
{delay: 1000},
);
cancelCallback(task);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
!!! Main thread │
@@ -492,7 +498,7 @@ Task 1 [Normal] │ █████████
taskId++;
const task = scheduleCallback(NormalPriority, () => {});
cancelCallback(task);
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
}
expect(console.error).toHaveBeenCalledTimes(1);
@@ -509,7 +515,7 @@ Task 1 [Normal] │ █████████
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_advanceTime(1000);
});
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
// Note: The exact task id is not super important. That just how many tasks
// it happens to take before the array is resized.

View File

@@ -16,6 +16,9 @@ let React;
let ReactTestRenderer;
let Scheduler;
let ReplaySubject;
let assertLog;
let waitForAll;
let waitFor;
describe('useSubscription', () => {
beforeEach(() => {
@@ -31,6 +34,11 @@ describe('useSubscription', () => {
BehaviorSubject = require('rxjs').BehaviorSubject;
ReplaySubject = require('rxjs').ReplaySubject;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
});
function createBehaviorSubject(initialValue) {
@@ -49,7 +57,7 @@ describe('useSubscription', () => {
return replaySubject;
}
it('supports basic subscription pattern', () => {
it('supports basic subscription pattern', async () => {
function Child({value = 'default'}) {
Scheduler.unstable_yieldValue(value);
return null;
@@ -79,21 +87,21 @@ describe('useSubscription', () => {
{unstable_isConcurrent: true},
);
});
expect(Scheduler).toHaveYielded(['default']);
assertLog(['default']);
// Updates while subscribed should re-render the child component
act(() => observable.next(123));
expect(Scheduler).toHaveYielded([123]);
assertLog([123]);
act(() => observable.next('abc'));
expect(Scheduler).toHaveYielded(['abc']);
assertLog(['abc']);
// Unmounting the subscriber should remove listeners
act(() => renderer.update(<div />));
act(() => observable.next(456));
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
});
it('should support observable types like RxJS ReplaySubject', () => {
it('should support observable types like RxJS ReplaySubject', async () => {
function Child({value = 'default'}) {
Scheduler.unstable_yieldValue(value);
return null;
@@ -131,19 +139,19 @@ describe('useSubscription', () => {
{unstable_isConcurrent: true},
);
});
expect(Scheduler).toHaveYielded(['initial']);
assertLog(['initial']);
act(() => observable.next('updated'));
expect(Scheduler).toHaveYielded(['updated']);
assertLog(['updated']);
Scheduler.unstable_flushAll();
await waitForAll([]);
// Unsetting the subscriber prop should reset subscribed values
observable = createReplaySubject(undefined);
act(() => renderer.update(<Subscription source={observable} />));
expect(Scheduler).toHaveYielded(['default']);
assertLog(['default']);
});
it('should unsubscribe from old sources and subscribe to new sources when memoized props change', () => {
it('should unsubscribe from old sources and subscribe to new sources when memoized props change', async () => {
function Child({value = 'default'}) {
Scheduler.unstable_yieldValue(value);
return null;
@@ -182,29 +190,29 @@ describe('useSubscription', () => {
});
// Updates while subscribed should re-render the child component
expect(Scheduler).toHaveYielded(['a-0']);
assertLog(['a-0']);
expect(subscriptions).toHaveLength(1);
expect(subscriptions[0]).toBe(observableA);
// Unsetting the subscriber prop should reset subscribed values
act(() => renderer.update(<Subscription source={observableB} />));
expect(Scheduler).toHaveYielded(['b-0']);
assertLog(['b-0']);
expect(subscriptions).toHaveLength(2);
expect(subscriptions[1]).toBe(observableB);
// Updates to the old subscribable should not re-render the child component
act(() => observableA.next('a-1'));
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
// Updates to the bew subscribable should re-render the child component
act(() => observableB.next('b-1'));
expect(Scheduler).toHaveYielded(['b-1']);
assertLog(['b-1']);
expect(subscriptions).toHaveLength(2);
});
it('should unsubscribe from old sources and subscribe to new sources when useCallback functions change', () => {
it('should unsubscribe from old sources and subscribe to new sources when useCallback functions change', async () => {
function Child({value = 'default'}) {
Scheduler.unstable_yieldValue(value);
return null;
@@ -241,28 +249,28 @@ describe('useSubscription', () => {
});
// Updates while subscribed should re-render the child component
expect(Scheduler).toHaveYielded(['a-0']);
assertLog(['a-0']);
expect(subscriptions).toHaveLength(1);
expect(subscriptions[0]).toBe(observableA);
// Unsetting the subscriber prop should reset subscribed values
act(() => renderer.update(<Subscription source={observableB} />));
expect(Scheduler).toHaveYielded(['b-0']);
assertLog(['b-0']);
expect(subscriptions).toHaveLength(2);
expect(subscriptions[1]).toBe(observableB);
// Updates to the old subscribable should not re-render the child component
act(() => observableA.next('a-1'));
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
// Updates to the bew subscribable should re-render the child component
act(() => observableB.next('b-1'));
expect(Scheduler).toHaveYielded(['b-1']);
assertLog(['b-1']);
expect(subscriptions).toHaveLength(2);
});
it('should ignore values emitted by a new subscribable until the commit phase', () => {
it('should ignore values emitted by a new subscribable until the commit phase', async () => {
const log = [];
function Grandchild({value}) {
@@ -326,16 +334,16 @@ describe('useSubscription', () => {
unstable_isConcurrent: true,
});
});
expect(Scheduler).toHaveYielded(['Child: a-0', 'Grandchild: a-0']);
assertLog(['Child: a-0', 'Grandchild: a-0']);
expect(log).toEqual(['Parent.componentDidMount']);
// Start React update, but don't finish
act(() => {
await act(async () => {
React.startTransition(() => {
renderer.update(<Parent observed={observableB} />);
});
expect(Scheduler).toFlushAndYieldThrough(['Child: b-0']);
await waitFor(['Child: b-0']);
expect(log).toEqual(['Parent.componentDidMount']);
// Emit some updates from the uncommitted subscribable
@@ -351,7 +359,7 @@ describe('useSubscription', () => {
// We expect the last emitted update to be rendered (because of the commit phase value check)
// But the intermediate ones should be ignored,
// And the final rendered output should be the higher-priority observable.
expect(Scheduler).toHaveYielded([
assertLog([
'Grandchild: b-0',
'Child: b-3',
'Grandchild: b-3',
@@ -365,7 +373,7 @@ describe('useSubscription', () => {
]);
});
it('should not drop values emitted between updates', () => {
it('should not drop values emitted between updates', async () => {
const log = [];
function Grandchild({value}) {
@@ -429,16 +437,16 @@ describe('useSubscription', () => {
unstable_isConcurrent: true,
});
});
expect(Scheduler).toHaveYielded(['Child: a-0', 'Grandchild: a-0']);
assertLog(['Child: a-0', 'Grandchild: a-0']);
expect(log).toEqual(['Parent.componentDidMount:a-0']);
log.splice(0);
// Start React update, but don't finish
act(() => {
await act(async () => {
React.startTransition(() => {
renderer.update(<Parent observed={observableB} />);
});
expect(Scheduler).toFlushAndYieldThrough(['Child: b-0']);
await waitFor(['Child: b-0']);
expect(log).toEqual([]);
// Emit some updates from the old subscribable
@@ -455,7 +463,7 @@ describe('useSubscription', () => {
}
// Flush everything and ensure that the correct subscribable is used
expect(Scheduler).toFlushAndYield([
await waitForAll([
'Child: a-2',
'Grandchild: a-2',
'Child: a-2',
@@ -467,7 +475,7 @@ describe('useSubscription', () => {
// Updates from the new subscribable should be ignored.
log.splice(0);
act(() => observableB.next('b-1'));
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
expect(log).toEqual([]);
});
@@ -514,10 +522,9 @@ describe('useSubscription', () => {
},
};
eventHandler.subscribe(value => {
eventHandler.subscribe(async value => {
if (value === false) {
renderer.unmount();
expect(Scheduler).toFlushAndYield([]);
}
});
@@ -528,13 +535,13 @@ describe('useSubscription', () => {
{unstable_isConcurrent: true},
);
});
expect(Scheduler).toHaveYielded([true]);
assertLog([true]);
// This event should unmount
eventHandler.change(false);
});
it('does not return a value from the previous subscription if the source is updated', () => {
it('does not return a value from the previous subscription if the source is updated', async () => {
const subscription1 = {
getCurrentValue: () => 'one',
subscribe: () => () => {},
@@ -562,13 +569,13 @@ describe('useSubscription', () => {
{unstable_isConcurrent: true},
);
});
Scheduler.unstable_flushAll();
await waitForAll([]);
act(() => renderer.update(<Subscription subscription={subscription2} />));
Scheduler.unstable_flushAll();
await waitForAll([]);
});
it('should not tear if a mutation occurs during a concurrent update', () => {
it('should not tear if a mutation occurs during a concurrent update', async () => {
const input = document.createElement('input');
const mutate = value => {
@@ -590,7 +597,7 @@ describe('useSubscription', () => {
return value;
};
act(() => {
await act(async () => {
// Initial render of "A"
mutate('A');
ReactTestRenderer.create(
@@ -600,13 +607,13 @@ describe('useSubscription', () => {
</React.Fragment>,
{unstable_isConcurrent: true},
);
expect(Scheduler).toFlushAndYield(['render:first:A', 'render:second:A']);
await waitForAll(['render:first:A', 'render:second:A']);
// Update state "A" -> "B"
// This update will be eagerly evaluated,
// so the tearing case this test is guarding against would not happen.
mutate('B');
expect(Scheduler).toFlushAndYield(['render:first:B', 'render:second:B']);
await waitForAll(['render:first:B', 'render:second:B']);
// No more pending updates
jest.runAllTimers();
@@ -618,14 +625,11 @@ describe('useSubscription', () => {
React.startTransition(() => {
mutate('C');
});
expect(Scheduler).toFlushAndYieldThrough([
'render:first:C',
'render:second:C',
]);
await waitFor(['render:first:C', 'render:second:C']);
React.startTransition(() => {
mutate('D');
});
expect(Scheduler).toFlushAndYield(['render:first:D', 'render:second:D']);
await waitForAll(['render:first:D', 'render:second:D']);
// No more pending updates
jest.runAllTimers();

View File

@@ -17,6 +17,7 @@ let Scheduler;
let useSyncExternalStore;
let useSyncExternalStoreWithSelector;
let act;
let assertLog;
// This tests the userspace shim of `useSyncExternalStore` in a server-rendering
// (Node) environment
@@ -50,6 +51,9 @@ describe('useSyncExternalStore (userspace shim, server rendering)', () => {
Scheduler = require('scheduler');
act = require('jest-react').act;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
if (gate(flags => flags.source)) {
// The `shim/with-selector` module composes the main
// `use-sync-external-store` entrypoint. In the compiled artifacts, this
@@ -116,7 +120,7 @@ describe('useSyncExternalStore (userspace shim, server rendering)', () => {
await act(() => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['client']);
assertLog(['client']);
expect(root).toMatchRenderedOutput('client');
});
@@ -159,7 +163,7 @@ describe('useSyncExternalStore (userspace shim, server rendering)', () => {
const root = ReactNoop.createRoot();
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['A0', 'B0']);
assertLog(['A0', 'B0']);
expect(root).toMatchRenderedOutput('A0B0');
// Update b but not a
@@ -167,7 +171,7 @@ describe('useSyncExternalStore (userspace shim, server rendering)', () => {
store.set({a: 0, b: 1});
});
// Only b re-renders
expect(Scheduler).toHaveYielded(['B1']);
assertLog(['B1']);
expect(root).toMatchRenderedOutput('A0B1');
// Update a but not b
@@ -175,7 +179,7 @@ describe('useSyncExternalStore (userspace shim, server rendering)', () => {
store.set({a: 1, b: 1});
});
// Only a re-renders
expect(Scheduler).toHaveYielded(['A1']);
assertLog(['A1']);
expect(root).toMatchRenderedOutput('A1B1');
});
});

View File

@@ -20,6 +20,7 @@ let act;
let useState;
let useEffect;
let useLayoutEffect;
let assertLog;
// This tests shared behavior between the built-in and shim implementations of
// of useSyncExternalStore.
@@ -55,6 +56,9 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
useEffect = React.useEffect;
useLayoutEffect = React.useLayoutEffect;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
const internalAct = require('jest-react').act;
// The internal act implementation doesn't batch updates by default, since
@@ -140,13 +144,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const root = createRoot(container);
await act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['Initial']);
assertLog(['Initial']);
expect(container.textContent).toEqual('Initial');
await act(() => {
store.set('Updated');
});
expect(Scheduler).toHaveYielded(['Updated']);
assertLog(['Updated']);
expect(container.textContent).toEqual('Updated');
});
@@ -162,7 +166,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const root = createRoot(container);
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['Initial']);
assertLog(['Initial']);
expect(container.textContent).toEqual('Initial');
// Update to the same value
@@ -170,7 +174,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
store.set('Initial');
});
// Should not re-render
expect(Scheduler).toHaveYielded([]);
assertLog([]);
expect(container.textContent).toEqual('Initial');
});
@@ -190,13 +194,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const root = createRoot(container);
await act(() => root.render(<App />));
expect(Scheduler).toHaveYielded([0]);
assertLog([0]);
expect(container.textContent).toEqual('0');
await act(() => {
storeA.set(1);
});
expect(Scheduler).toHaveYielded([1]);
assertLog([1]);
expect(container.textContent).toEqual('1');
// Switch stores and update in the same batch
@@ -208,7 +212,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
});
});
// Now reading from B instead of A
expect(Scheduler).toHaveYielded([0]);
assertLog([0]);
expect(container.textContent).toEqual('0');
// Update A
@@ -216,14 +220,14 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
storeA.set(3);
});
// Nothing happened, because we're no longer subscribed to A
expect(Scheduler).toHaveYielded([]);
assertLog([]);
expect(container.textContent).toEqual('0');
// Update B
await act(() => {
storeB.set(1);
});
expect(Scheduler).toHaveYielded([1]);
assertLog([1]);
expect(container.textContent).toEqual('1');
});
@@ -252,7 +256,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const root = createRoot(container);
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['A0', 'B0']);
assertLog(['A0', 'B0']);
expect(container.textContent).toEqual('A0B0');
// Update b but not a
@@ -260,7 +264,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
store.set({a: 0, b: 1});
});
// Only b re-renders
expect(Scheduler).toHaveYielded(['B1']);
assertLog(['B1']);
expect(container.textContent).toEqual('A0B1');
// Update a but not b
@@ -268,7 +272,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
store.set({a: 1, b: 1});
});
// Only a re-renders
expect(Scheduler).toHaveYielded(['A1']);
assertLog(['A1']);
expect(container.textContent).toEqual('A1B1');
});
@@ -292,13 +296,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const container = document.createElement('div');
const root = createRoot(container);
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded([0, 'Passive effect: 0']);
assertLog([0, 'Passive effect: 0']);
// Schedule an update. We'll intentionally not use `act` so that we can
// insert a mutation before React subscribes to the store in a
// passive effect.
store.set(1);
expect(Scheduler).toHaveYielded([
assertLog([
1,
// Passive effect hasn't fired yet
]);
@@ -306,7 +310,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
// Flip the store state back to the previous value.
store.set(0);
expect(Scheduler).toHaveYielded([
assertLog([
'Passive effect: 1',
// Re-render. If the current state were tracked by updating a ref in a
// passive effect, then this would break because the previous render's
@@ -362,14 +366,14 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const container = document.createElement('div');
const root = createRoot(container);
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['A1']);
assertLog(['A1']);
expect(container.textContent).toEqual('A1');
act(() => {
// Change getSnapshot and update the store in the same batch
setStep(1);
});
expect(Scheduler).toHaveYielded([
assertLog([
'B1',
'Update B in commit phase',
// If Child2 had used the old getSnapshot to bail out, then it would have
@@ -420,7 +424,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const container = document.createElement('div');
const root = createRoot(container);
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['A1']);
assertLog(['A1']);
expect(container.textContent).toEqual('A1');
// This will cause a layout effect, and in the layout effect we'll update
@@ -428,7 +432,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
act(() => {
setStep(1);
});
expect(Scheduler).toHaveYielded([
assertLog([
'A1',
// This updates B, but since Child2 doesn't subscribe to B, it doesn't
// need to re-render.
@@ -467,13 +471,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
</>,
),
);
expect(Scheduler).toHaveYielded([0, 0]);
assertLog([0, 0]);
expect(container.textContent).toEqual('00');
await act(() => {
store.set(1);
});
expect(Scheduler).toHaveYielded([1, 1, 'Reset back to 0', 0, 0]);
assertLog([1, 1, 'Reset back to 0', 0, 0]);
expect(container.textContent).toEqual('00');
});
@@ -494,7 +498,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const container = document.createElement('div');
const root = createRoot(container);
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded([0]);
assertLog([0]);
// Update the store and getSnapshot at the same time
act(() => {
@@ -504,7 +508,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
});
});
// It should read from B instead of A
expect(Scheduler).toHaveYielded([2]);
assertLog([2]);
expect(container.textContent).toEqual('2');
});
@@ -549,7 +553,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
</ErrorBoundary>,
),
);
expect(Scheduler).toHaveYielded([0]);
assertLog([0]);
expect(container.textContent).toEqual('0');
// Update that throws in a getSnapshot. We can catch it with an error boundary.
@@ -557,14 +561,14 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
store.set({value: 1, throwInGetSnapshot: true, throwInIsEqual: false});
});
if (gate(flags => !flags.enableUseSyncExternalStoreShim)) {
expect(Scheduler).toHaveYielded([
assertLog([
'Error in getSnapshot',
// In a concurrent root, React renders a second time to attempt to
// recover from the error.
'Error in getSnapshot',
]);
} else {
expect(Scheduler).toHaveYielded(['Error in getSnapshot']);
assertLog(['Error in getSnapshot']);
}
expect(container.textContent).toEqual('Error in getSnapshot');
});
@@ -644,14 +648,14 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const root = createRoot(container);
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['App', 'Selector', 'A0']);
assertLog(['App', 'Selector', 'A0']);
expect(container.textContent).toEqual('A0');
// Update the store
await act(() => {
store.set({a: 1, b: 0});
});
expect(Scheduler).toHaveYielded([
assertLog([
// The selector runs before React starts rendering
'Selector',
'App',
@@ -703,7 +707,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const root = createRoot(container);
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['A0', 'B0']);
assertLog(['A0', 'B0']);
expect(container.textContent).toEqual('A0B0');
// Update b but not a
@@ -711,7 +715,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
store.set({a: 0, b: 1});
});
// Only b re-renders
expect(Scheduler).toHaveYielded(['B1']);
assertLog(['B1']);
expect(container.textContent).toEqual('A0B1');
// Update a but not b
@@ -719,7 +723,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
store.set({a: 1, b: 1});
});
// Only a re-renders
expect(Scheduler).toHaveYielded(['A1']);
assertLog(['A1']);
expect(container.textContent).toEqual('A1B1');
});
@@ -751,7 +755,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
act(() => {
ReactDOMClient.hydrateRoot(container, <App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
// First it hydrates the server rendered HTML
'server',
'Passive effect: server',
@@ -769,7 +773,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
'Text content did not match',
);
});
expect(Scheduler).toHaveYielded(['client', 'Passive effect: client']);
assertLog(['client', 'Passive effect: client']);
}
expect(container.textContent).toEqual('client');
expect(ref.current).toEqual(serverRenderedDiv);
@@ -793,13 +797,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const root = createRoot(container);
await act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['INITIAL']);
assertLog(['INITIAL']);
expect(container.textContent).toEqual('INITIAL');
await act(() => {
store.set('Updated');
});
expect(Scheduler).toHaveYielded(['UPDATED']);
assertLog(['UPDATED']);
expect(container.textContent).toEqual('UPDATED');
});
@@ -857,18 +861,12 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
await act(() => {
root.render(<App step={0} />);
});
expect(Scheduler).toHaveYielded([
'Inline selector',
'A',
'B',
'C',
'Sibling: 0',
]);
assertLog(['Inline selector', 'A', 'B', 'C', 'Sibling: 0']);
await act(() => {
root.render(<App step={1} />);
});
expect(Scheduler).toHaveYielded([
assertLog([
// We had to call the selector again because it's not memoized
'Inline selector',

View File

@@ -16,6 +16,7 @@ let React;
let ReactDOM;
let ReactDOMServer;
let Scheduler;
let assertLog;
// This tests the userspace shim of `useSyncExternalStore` in a server-rendering
// (Node) environment
@@ -45,6 +46,9 @@ describe('useSyncExternalStore (userspace shim, server rendering)', () => {
ReactDOMServer = require('react-dom/server');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
useSyncExternalStore =
require('use-sync-external-store/shim').useSyncExternalStore;
});
@@ -92,7 +96,7 @@ describe('useSyncExternalStore (userspace shim, server rendering)', () => {
const html = ReactDOMServer.renderToString(<App />);
// We don't call getServerSnapshot in the shim
expect(Scheduler).toHaveYielded(['client']);
assertLog(['client']);
expect(html).toEqual('client');
});
});