Remove concurrent apis from stable (#17088)
* Tests run in experimental mode by default For local development, you usually want experiments enabled. Unless the release channel is set with an environment variable, tests will run with __EXPERIMENTAL__ set to `true`. * Remove concurrent APIs from stable builds Those who want to try concurrent mode should use the experimental builds instead. I've left the `unstable_` prefixed APIs in the Facebook build so we can continue experimenting with them internally without blessing them for widespread use. * Turn on SSR flags in experimental build * Remove prefixed concurrent APIs from www build Instead we'll use the experimental builds when syncing to www. * Remove "canary" from internal React version string
This commit is contained in:
parent
4cb399a433
commit
30c5daf943
|
@ -98,7 +98,10 @@ jobs:
|
|||
- checkout
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --maxWorkers=2
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: stable
|
||||
command: yarn test --maxWorkers=2
|
||||
|
||||
test_source_experimental:
|
||||
docker: *docker
|
||||
|
@ -120,7 +123,10 @@ jobs:
|
|||
- checkout
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test-persistent --maxWorkers=2
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: stable
|
||||
command: yarn test-persistent --maxWorkers=2
|
||||
|
||||
test_source_prod:
|
||||
docker: *docker
|
||||
|
@ -130,7 +136,10 @@ jobs:
|
|||
- checkout
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test-prod --maxWorkers=2
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: stable
|
||||
command: yarn test-prod --maxWorkers=2
|
||||
|
||||
build:
|
||||
docker: *docker
|
||||
|
@ -217,7 +226,23 @@ jobs:
|
|||
- attach_workspace: *attach_workspace
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test-build --maxWorkers=2
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: stable
|
||||
command: yarn test-build --maxWorkers=2
|
||||
|
||||
test_build_experimental:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: experimental
|
||||
command: yarn test-build --maxWorkers=2
|
||||
|
||||
test_build_devtools:
|
||||
docker: *docker
|
||||
|
@ -227,7 +252,10 @@ jobs:
|
|||
- attach_workspace: *attach_workspace
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test-build-devtools --maxWorkers=2
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: stable
|
||||
command: yarn test-build --maxWorkers=2
|
||||
|
||||
test_dom_fixtures:
|
||||
docker: *docker
|
||||
|
@ -238,6 +266,8 @@ jobs:
|
|||
- *restore_yarn_cache
|
||||
- run:
|
||||
name: Run DOM fixture tests
|
||||
environment:
|
||||
RELEASE_CHANNEL: stable
|
||||
command: |
|
||||
cd fixtures/dom
|
||||
yarn --frozen-lockfile
|
||||
|
@ -265,7 +295,23 @@ jobs:
|
|||
- attach_workspace: *attach_workspace
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test-build-prod --maxWorkers=2
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: stable
|
||||
command: yarn test-build-prod --maxWorkers=2
|
||||
|
||||
test_build_prod_experimental:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: experimental
|
||||
command: yarn test-build-prod --maxWorkers=2
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
@ -324,6 +370,12 @@ workflows:
|
|||
- process_artifacts_experimental:
|
||||
requires:
|
||||
- build_experimental
|
||||
- test_build_experimental:
|
||||
requires:
|
||||
- build_experimental
|
||||
- test_build_prod_experimental:
|
||||
requires:
|
||||
- build_experimental
|
||||
|
||||
fuzz_tests:
|
||||
triggers:
|
||||
|
|
|
@ -17,6 +17,7 @@ let TestRenderer;
|
|||
let ARTTest;
|
||||
|
||||
global.__DEV__ = process.env.NODE_ENV !== 'production';
|
||||
global.__EXPERIMENTAL__ = process.env.RELEASE_CHANNEL === 'experimental';
|
||||
|
||||
expect.extend(require('../toWarnDev'));
|
||||
|
||||
|
@ -176,19 +177,21 @@ it("doesn't warn if you use nested acts from different renderers", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('warns when using createRoot() + .render', () => {
|
||||
const root = ReactDOM.unstable_createRoot(document.createElement('div'));
|
||||
expect(() => {
|
||||
TestRenderer.act(() => {
|
||||
root.render(<App />);
|
||||
});
|
||||
}).toWarnDev(
|
||||
[
|
||||
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked',
|
||||
"It looks like you're using the wrong act()",
|
||||
],
|
||||
{
|
||||
withoutStack: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
if (__EXPERIMENTAL__) {
|
||||
it('warns when using createRoot() + .render', () => {
|
||||
const root = ReactDOM.createRoot(document.createElement('div'));
|
||||
expect(() => {
|
||||
TestRenderer.act(() => {
|
||||
root.render(<App />);
|
||||
});
|
||||
}).toWarnDev(
|
||||
[
|
||||
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked',
|
||||
"It looks like you're using the wrong act()",
|
||||
],
|
||||
{
|
||||
withoutStack: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,493 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 1`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 2`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 3`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<C key="c">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 4`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<B key="b">
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 5`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 6`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 7`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 8`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 9`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 10`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
<Suspense>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 11`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<B key="b">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 12`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 1`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 2`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 3`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<C key="c">
|
||||
<Z>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 4`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<C key="c">
|
||||
<B key="b">
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 5`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 6`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 7`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 8`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<Z>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 9`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 10`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<Z>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 11`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<B key="b">
|
||||
<Z>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 12`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 13`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 14`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 15`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<C key="c">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 16`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<B key="b">
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 17`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 18`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 19`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 20`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 21`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 22`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
<Suspense>
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 23`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<B key="b">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 24`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test with different tree operations (Concurrent Mode): 1: abcde 1`] = `
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<C key="c">
|
||||
<D key="d">
|
||||
<E key="e">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle a stress test with different tree operations (Concurrent Mode): 2: abxde 1`] = `
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
▾ <C key="c">
|
||||
<X>
|
||||
<D key="d">
|
||||
<E key="e">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 1`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<A key="a">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 2`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<B key="b">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 3`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<C key="c">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 4`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<D key="d">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 5`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<E key="e">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 6`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<A key="a">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 7`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<B key="b">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 8`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<C key="c">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 9`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<D key="d">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 10`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<E key="e">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 11`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 12`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<B key="b">
|
||||
<A key="a">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 13`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<B key="b">
|
||||
<C key="c">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 14`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<C key="c">
|
||||
<B key="b">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 15`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<A key="a">
|
||||
<C key="c">
|
||||
`;
|
||||
|
||||
exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 16`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
`;
|
|
@ -27,6 +27,11 @@ describe('StoreStressConcurrent', () => {
|
|||
print = require('./storeSerializer').print;
|
||||
});
|
||||
|
||||
if (!__EXPERIMENTAL__) {
|
||||
it("empty test so Jest doesn't complain", () => {});
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a stress test for the tree mount/update/unmount traversal.
|
||||
// It renders different trees that should produce the same output.
|
||||
it('should handle a stress test with different tree operations (Concurrent Mode)', () => {
|
||||
|
@ -57,9 +62,19 @@ describe('StoreStressConcurrent', () => {
|
|||
// 1. Render a normal version of [a, b, c, d, e].
|
||||
let container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
let root = ReactDOM.unstable_createRoot(container);
|
||||
let root = ReactDOM.createRoot(container);
|
||||
act(() => root.render(<Parent>{[a, b, c, d, e]}</Parent>));
|
||||
expect(store).toMatchSnapshot('1: abcde');
|
||||
expect(store).toMatchInlineSnapshot(
|
||||
`
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<C key="c">
|
||||
<D key="d">
|
||||
<E key="e">
|
||||
`,
|
||||
);
|
||||
expect(container.textContent).toMatch('abcde');
|
||||
const snapshotForABCDE = print(store);
|
||||
|
||||
|
@ -68,7 +83,18 @@ describe('StoreStressConcurrent', () => {
|
|||
act(() => {
|
||||
setShowX(true);
|
||||
});
|
||||
expect(store).toMatchSnapshot('2: abxde');
|
||||
expect(store).toMatchInlineSnapshot(
|
||||
`
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
▾ <C key="c">
|
||||
<X>
|
||||
<D key="d">
|
||||
<E key="e">
|
||||
`,
|
||||
);
|
||||
expect(container.textContent).toMatch('abxde');
|
||||
const snapshotForABXDE = print(store);
|
||||
|
||||
|
@ -120,7 +146,7 @@ describe('StoreStressConcurrent', () => {
|
|||
// Ensure fresh mount.
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
|
||||
// Verify mounting 'abcde'.
|
||||
act(() => root.render(<Parent>{cases[i]}</Parent>));
|
||||
|
@ -150,7 +176,7 @@ describe('StoreStressConcurrent', () => {
|
|||
// There'll be no unmounting until the very end.
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
for (let i = 0; i < cases.length; i++) {
|
||||
// Verify mounting 'abcde'.
|
||||
act(() => root.render(<Parent>{cases[i]}</Parent>));
|
||||
|
@ -216,22 +242,80 @@ describe('StoreStressConcurrent', () => {
|
|||
let snapshots = [];
|
||||
let container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
let root = ReactDOM.unstable_createRoot(container);
|
||||
let root = ReactDOM.createRoot(container);
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
act(() => root.render(<Root>{steps[i]}</Root>));
|
||||
// We snapshot each step once so it doesn't regress.
|
||||
expect(store).toMatchSnapshot();
|
||||
snapshots.push(print(store));
|
||||
act(() => root.unmount());
|
||||
expect(print(store)).toBe('');
|
||||
}
|
||||
|
||||
expect(snapshots).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<A key=\\"a\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<B key=\\"b\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<C key=\\"c\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<D key=\\"d\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<E key=\\"e\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<A key=\\"a\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<B key=\\"b\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<C key=\\"c\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<D key=\\"d\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<E key=\\"e\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<A key=\\"a\\">
|
||||
<B key=\\"b\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<B key=\\"b\\">
|
||||
<A key=\\"a\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<B key=\\"b\\">
|
||||
<C key=\\"c\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<C key=\\"c\\">
|
||||
<B key=\\"b\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<A key=\\"a\\">
|
||||
<C key=\\"c\\">",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<C key=\\"c\\">
|
||||
<A key=\\"a\\">",
|
||||
]
|
||||
`);
|
||||
|
||||
// 2. Verify that we can update from every step to every other step and back.
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
for (let j = 0; j < steps.length; j++) {
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
act(() => root.render(<Root>{steps[i]}</Root>));
|
||||
expect(print(store)).toMatch(snapshots[i]);
|
||||
act(() => root.render(<Root>{steps[j]}</Root>));
|
||||
|
@ -248,7 +332,7 @@ describe('StoreStressConcurrent', () => {
|
|||
for (let j = 0; j < steps.length; j++) {
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
act(() =>
|
||||
root.render(
|
||||
<Root>
|
||||
|
@ -320,7 +404,7 @@ describe('StoreStressConcurrent', () => {
|
|||
let snapshots = [];
|
||||
let container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
let root = ReactDOM.unstable_createRoot(container);
|
||||
let root = ReactDOM.createRoot(container);
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
act(() =>
|
||||
root.render(
|
||||
|
@ -331,13 +415,96 @@ describe('StoreStressConcurrent', () => {
|
|||
</Root>,
|
||||
),
|
||||
);
|
||||
// We snapshot each step once so it doesn't regress.
|
||||
expect(store).toMatchSnapshot();
|
||||
// We snapshot each step once so it doesn't regress.d
|
||||
snapshots.push(print(store));
|
||||
act(() => root.unmount());
|
||||
expect(print(store)).toBe('');
|
||||
}
|
||||
|
||||
expect(snapshots).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key=\\"a\\">
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key=\\"a\\">
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key=\\"a\\">
|
||||
<B key=\\"b\\">
|
||||
<C key=\\"c\\">
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key=\\"c\\">
|
||||
<B key=\\"b\\">
|
||||
<A key=\\"a\\">
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key=\\"c\\">
|
||||
<A key=\\"a\\">
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key=\\"c\\">
|
||||
<A key=\\"a\\">
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key=\\"c\\">
|
||||
<A key=\\"a\\">
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key=\\"a\\">
|
||||
<B key=\\"b\\">
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key=\\"a\\">
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
<Suspense>
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<B key=\\"b\\">
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key=\\"a\\">
|
||||
<Y>",
|
||||
]
|
||||
`);
|
||||
|
||||
// 2. Verify check Suspense can render same steps as initial fallback content.
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
act(() =>
|
||||
|
@ -364,7 +531,7 @@ describe('StoreStressConcurrent', () => {
|
|||
// Always start with a fresh container and steps[i].
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
act(() =>
|
||||
root.render(
|
||||
<Root>
|
||||
|
@ -410,7 +577,7 @@ describe('StoreStressConcurrent', () => {
|
|||
// Always start with a fresh container and steps[i].
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
act(() =>
|
||||
root.render(
|
||||
<Root>
|
||||
|
@ -468,7 +635,7 @@ describe('StoreStressConcurrent', () => {
|
|||
// Always start with a fresh container and steps[i].
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
act(() =>
|
||||
root.render(
|
||||
<Root>
|
||||
|
@ -518,7 +685,7 @@ describe('StoreStressConcurrent', () => {
|
|||
// Always start with a fresh container and steps[i].
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
act(() =>
|
||||
root.render(
|
||||
<Root>
|
||||
|
@ -572,7 +739,7 @@ describe('StoreStressConcurrent', () => {
|
|||
// Always start with a fresh container and steps[i].
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
act(() =>
|
||||
root.render(
|
||||
<Root>
|
||||
|
@ -726,7 +893,7 @@ describe('StoreStressConcurrent', () => {
|
|||
let snapshots = [];
|
||||
let container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
let root = ReactDOM.unstable_createRoot(container);
|
||||
let root = ReactDOM.createRoot(container);
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
act(() =>
|
||||
root.render(
|
||||
|
@ -740,7 +907,6 @@ describe('StoreStressConcurrent', () => {
|
|||
),
|
||||
);
|
||||
// We snapshot each step once so it doesn't regress.
|
||||
expect(store).toMatchSnapshot();
|
||||
snapshots.push(print(store));
|
||||
act(() => root.unmount());
|
||||
expect(print(store)).toBe('');
|
||||
|
@ -765,19 +931,126 @@ describe('StoreStressConcurrent', () => {
|
|||
),
|
||||
);
|
||||
// We snapshot each step once so it doesn't regress.
|
||||
expect(store).toMatchSnapshot();
|
||||
fallbackSnapshots.push(print(store));
|
||||
act(() => root.unmount());
|
||||
expect(print(store)).toBe('');
|
||||
}
|
||||
|
||||
expect(snapshots).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<A key=\\"a\\">
|
||||
<Z>
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<A key=\\"a\\">
|
||||
<Z>
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<A key=\\"a\\">
|
||||
<B key=\\"b\\">
|
||||
<C key=\\"c\\">
|
||||
<Z>
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<C key=\\"c\\">
|
||||
<B key=\\"b\\">
|
||||
<A key=\\"a\\">
|
||||
<Z>
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<C key=\\"c\\">
|
||||
<A key=\\"a\\">
|
||||
<Z>
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<C key=\\"c\\">
|
||||
<A key=\\"a\\">
|
||||
<Z>
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<C key=\\"c\\">
|
||||
<A key=\\"a\\">
|
||||
<Z>
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<A key=\\"a\\">
|
||||
<B key=\\"b\\">
|
||||
<Z>
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<A key=\\"a\\">
|
||||
<Z>
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<Z>
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<B key=\\"b\\">
|
||||
<Z>
|
||||
<Y>",
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
▾ <MaybeSuspend>
|
||||
<A key=\\"a\\">
|
||||
<Z>
|
||||
<Y>",
|
||||
]
|
||||
`);
|
||||
|
||||
// 3. Verify we can update from each step to each step in primary mode.
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
for (let j = 0; j < steps.length; j++) {
|
||||
// Always start with a fresh container and steps[i].
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
act(() =>
|
||||
root.render(
|
||||
<Root>
|
||||
|
@ -829,7 +1102,7 @@ describe('StoreStressConcurrent', () => {
|
|||
// Always start with a fresh container and steps[i].
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
act(() =>
|
||||
root.render(
|
||||
<Root>
|
||||
|
@ -896,7 +1169,7 @@ describe('StoreStressConcurrent', () => {
|
|||
// Always start with a fresh container and steps[i].
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
act(() =>
|
||||
root.render(
|
||||
<Root>
|
||||
|
@ -948,7 +1221,7 @@ describe('StoreStressConcurrent', () => {
|
|||
// Always start with a fresh container and steps[i].
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
act(() =>
|
||||
root.render(
|
||||
<Root>
|
||||
|
@ -1000,7 +1273,7 @@ describe('StoreStressConcurrent', () => {
|
|||
// Always start with a fresh container and steps[i].
|
||||
container = document.createElement('div');
|
||||
// $FlowFixMe
|
||||
root = ReactDOM.unstable_createRoot(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
act(() =>
|
||||
root.render(
|
||||
<Root>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -105,38 +105,40 @@ describe('ReactDOMHooks', () => {
|
|||
expect(labelRef.current.innerHTML).toBe('abc');
|
||||
});
|
||||
|
||||
it('should not bail out when an update is scheduled from within an event handler in Concurrent Mode', () => {
|
||||
const {createRef, useCallback, useState} = React;
|
||||
if (__EXPERIMENTAL__) {
|
||||
it('should not bail out when an update is scheduled from within an event handler in Concurrent Mode', () => {
|
||||
const {createRef, useCallback, useState} = React;
|
||||
|
||||
const Example = ({inputRef, labelRef}) => {
|
||||
const [text, setText] = useState('');
|
||||
const handleInput = useCallback(event => {
|
||||
setText(event.target.value);
|
||||
});
|
||||
const Example = ({inputRef, labelRef}) => {
|
||||
const [text, setText] = useState('');
|
||||
const handleInput = useCallback(event => {
|
||||
setText(event.target.value);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<input ref={inputRef} onInput={handleInput} />
|
||||
<label ref={labelRef}>{text}</label>
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
<input ref={inputRef} onInput={handleInput} />
|
||||
<label ref={labelRef}>{text}</label>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const inputRef = createRef();
|
||||
const labelRef = createRef();
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Example inputRef={inputRef} labelRef={labelRef} />);
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
inputRef.current.value = 'abc';
|
||||
inputRef.current.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
};
|
||||
|
||||
const inputRef = createRef();
|
||||
const labelRef = createRef();
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
root.render(<Example inputRef={inputRef} labelRef={labelRef} />);
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
inputRef.current.value = 'abc';
|
||||
inputRef.current.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
expect(labelRef.current.innerHTML).toBe('abc');
|
||||
});
|
||||
expect(labelRef.current.innerHTML).toBe('abc');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -26,15 +26,22 @@ describe('ReactDOMRoot', () => {
|
|||
Scheduler = require('scheduler');
|
||||
});
|
||||
|
||||
if (!__EXPERIMENTAL__) {
|
||||
it('createRoot is not exposed in stable build', () => {
|
||||
expect(ReactDOM.createRoot).toBe(undefined);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
it('renders children', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<div>Hi</div>);
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(container.textContent).toEqual('Hi');
|
||||
});
|
||||
|
||||
it('unmounts children', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<div>Hi</div>);
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(container.textContent).toEqual('Hi');
|
||||
|
@ -57,7 +64,7 @@ describe('ReactDOMRoot', () => {
|
|||
// Does not hydrate by default
|
||||
const container1 = document.createElement('div');
|
||||
container1.innerHTML = markup;
|
||||
const root1 = ReactDOM.unstable_createRoot(container1);
|
||||
const root1 = ReactDOM.createRoot(container1);
|
||||
root1.render(
|
||||
<div>
|
||||
<span />
|
||||
|
@ -68,7 +75,7 @@ describe('ReactDOMRoot', () => {
|
|||
// Accepts `hydrate` option
|
||||
const container2 = document.createElement('div');
|
||||
container2.innerHTML = markup;
|
||||
const root2 = ReactDOM.unstable_createRoot(container2, {hydrate: true});
|
||||
const root2 = ReactDOM.createRoot(container2, {hydrate: true});
|
||||
root2.render(
|
||||
<div>
|
||||
<span />
|
||||
|
@ -81,7 +88,7 @@ describe('ReactDOMRoot', () => {
|
|||
|
||||
it('does not clear existing children', async () => {
|
||||
container.innerHTML = '<div>a</div><div>b</div>';
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(
|
||||
<div>
|
||||
<span>c</span>
|
||||
|
@ -102,12 +109,12 @@ describe('ReactDOMRoot', () => {
|
|||
|
||||
it('throws a good message on invalid containers', () => {
|
||||
expect(() => {
|
||||
ReactDOM.unstable_createRoot(<div>Hi</div>);
|
||||
ReactDOM.createRoot(<div>Hi</div>);
|
||||
}).toThrow('createRoot(...): Target container is not a DOM element.');
|
||||
});
|
||||
|
||||
it('warns when rendering with legacy API into createRoot() container', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<div>Hi</div>);
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(container.textContent).toEqual('Hi');
|
||||
|
@ -130,7 +137,7 @@ describe('ReactDOMRoot', () => {
|
|||
});
|
||||
|
||||
it('warns when hydrating with legacy API into createRoot() container', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<div>Hi</div>);
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(container.textContent).toEqual('Hi');
|
||||
|
@ -150,7 +157,7 @@ describe('ReactDOMRoot', () => {
|
|||
});
|
||||
|
||||
it('warns when unmounting with legacy API (no previous content)', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<div>Hi</div>);
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(container.textContent).toEqual('Hi');
|
||||
|
@ -179,7 +186,7 @@ describe('ReactDOMRoot', () => {
|
|||
// Currently createRoot().render() doesn't clear this.
|
||||
container.appendChild(document.createElement('div'));
|
||||
// The rest is the same as test above.
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<div>Hi</div>);
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(container.textContent).toEqual('Hi');
|
||||
|
@ -198,7 +205,7 @@ describe('ReactDOMRoot', () => {
|
|||
it('warns when passing legacy container to createRoot()', () => {
|
||||
ReactDOM.render(<div>Hi</div>, container);
|
||||
expect(() => {
|
||||
ReactDOM.unstable_createRoot(container);
|
||||
ReactDOM.createRoot(container);
|
||||
}).toWarnDev(
|
||||
'You are calling ReactDOM.createRoot() on a container that was previously ' +
|
||||
'passed to ReactDOM.render(). This is not supported.',
|
||||
|
|
|
@ -75,7 +75,6 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
jest.resetModuleRegistry();
|
||||
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableSuspenseServerRenderer = true;
|
||||
ReactFeatureFlags.enableSuspenseCallback = true;
|
||||
ReactFeatureFlags.enableFlareAPI = true;
|
||||
|
||||
|
@ -90,6 +89,11 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
useHover = require('react-interactions/events/hover').useHover;
|
||||
});
|
||||
|
||||
if (!__EXPERIMENTAL__) {
|
||||
it("empty test so Jest doesn't complain", () => {});
|
||||
return;
|
||||
}
|
||||
|
||||
it('hydrates a parent even if a child Suspense boundary is blocked', async () => {
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
|
@ -130,7 +134,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
@ -200,7 +204,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// hydrating anyway.
|
||||
suspend = true;
|
||||
suspend2 = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {
|
||||
let root = ReactDOM.createRoot(container, {
|
||||
hydrate: true,
|
||||
hydrationOptions: {
|
||||
onHydrated(node) {
|
||||
|
@ -273,7 +277,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {
|
||||
let root = ReactDOM.createRoot(container, {
|
||||
hydrate: true,
|
||||
hydrationOptions: {
|
||||
onDeleted(node) {
|
||||
|
@ -411,7 +415,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
suspend = true;
|
||||
|
||||
act(() => {
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
});
|
||||
|
||||
|
@ -468,7 +472,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// hydrating anyway.
|
||||
suspend = true;
|
||||
act(() => {
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
});
|
||||
|
||||
|
@ -518,7 +522,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App text="Hello" className="hello" />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
@ -587,7 +591,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App text="Hello" className="hello" />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
@ -660,7 +664,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App text="Hello" className="hello" />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
@ -732,7 +736,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App text="Hello" className="hello" />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
@ -803,7 +807,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App text="Hello" className="hello" />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
@ -889,7 +893,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(
|
||||
<Context.Provider value={{text: 'Hello', className: 'hello'}}>
|
||||
<App />
|
||||
|
@ -971,7 +975,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(
|
||||
<Context.Provider value={{text: 'Hello', className: 'hello'}}>
|
||||
<App />
|
||||
|
@ -1049,7 +1053,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
|
||||
// On the client we have the data available quickly for some reason.
|
||||
suspend = false;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
@ -1105,7 +1109,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
|
||||
// On the client we have the data available quickly for some reason.
|
||||
suspend = false;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
// This will have exceeded the suspended time so we should timeout.
|
||||
|
@ -1166,7 +1170,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
|
||||
// On the client we have the data available quickly for some reason.
|
||||
suspend = false;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
// This will have exceeded the suspended time so we should timeout.
|
||||
|
@ -1242,7 +1246,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
|
||||
// Attempt to hydrate the content.
|
||||
suspend = false;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
@ -1335,7 +1339,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
|
||||
// Attempt to hydrate the content.
|
||||
suspend = false;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
@ -1413,7 +1417,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
|
||||
let spanB = container.getElementsByTagName('span')[1];
|
||||
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
|
||||
suspend = true;
|
||||
act(() => {
|
||||
|
@ -1495,7 +1499,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
|
||||
let spanA = container.getElementsByTagName('span')[0];
|
||||
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
|
||||
suspend = true;
|
||||
act(() => {
|
||||
|
@ -1575,7 +1579,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// Put the suspense node in pending state.
|
||||
suspenseNode.data = '$?';
|
||||
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
|
||||
suspend = true;
|
||||
act(() => {
|
||||
|
@ -1652,7 +1656,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
let span = container.getElementsByTagName('span')[1];
|
||||
|
||||
suspend = false;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
@ -1695,7 +1699,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
|
||||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
@ -1748,7 +1752,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(
|
||||
<ClassName.Provider value={'hello'}>
|
||||
<App text="Hello" />
|
||||
|
@ -1840,7 +1844,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
@ -1914,7 +1918,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
// We'll do one click before hydrating.
|
||||
|
@ -1995,7 +1999,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
// We'll do one click before hydrating.
|
||||
|
@ -2072,7 +2076,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
// We'll do one click before hydrating.
|
||||
|
@ -2151,7 +2155,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
@ -2216,7 +2220,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
|
||||
// We're going to use a different root as a parent.
|
||||
// This lets us detect whether an event goes through React's event system.
|
||||
let parentRoot = ReactDOM.unstable_createRoot(parentContainer);
|
||||
let parentRoot = ReactDOM.createRoot(parentContainer);
|
||||
parentRoot.render(<Parent />);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
|
@ -2229,7 +2233,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
suspend = true;
|
||||
|
||||
// Hydrate asynchronously.
|
||||
let root = ReactDOM.unstable_createRoot(childContainer, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(childContainer, {hydrate: true});
|
||||
root.render(<App />);
|
||||
jest.runAllTimers();
|
||||
Scheduler.unstable_flushAll();
|
||||
|
@ -2319,7 +2323,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// hydrating anyway.
|
||||
suspend1 = true;
|
||||
suspend2 = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
|
@ -2434,7 +2438,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
// hydrating anyway.
|
||||
suspend1 = true;
|
||||
suspend2 = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
|
|
|
@ -13,7 +13,6 @@ let React;
|
|||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
let Scheduler;
|
||||
let ReactFeatureFlags;
|
||||
let Suspense;
|
||||
|
||||
function dispatchMouseHoverEvent(to, from) {
|
||||
|
@ -93,10 +92,6 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
|||
beforeEach(() => {
|
||||
jest.resetModuleRegistry();
|
||||
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableSuspenseServerRenderer = true;
|
||||
ReactFeatureFlags.enableSelectiveHydration = true;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
|
@ -104,6 +99,11 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
|||
Suspense = React.Suspense;
|
||||
});
|
||||
|
||||
if (!__EXPERIMENTAL__) {
|
||||
it("empty test so Jest doesn't complain", () => {});
|
||||
return;
|
||||
}
|
||||
|
||||
it('hydrates the target boundary synchronously during a click', async () => {
|
||||
function Child({text}) {
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
|
@ -144,7 +144,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
|||
|
||||
let span = container.getElementsByTagName('span')[1];
|
||||
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
// Nothing has been hydrated so far.
|
||||
|
@ -223,7 +223,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
|||
|
||||
// A and D will be suspended. We'll click on D which should take
|
||||
// priority, after we unsuspend.
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
// Nothing has been hydrated so far.
|
||||
|
@ -309,7 +309,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
|||
|
||||
// A and D will be suspended. We'll click on D which should take
|
||||
// priority, after we unsuspend.
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
// Nothing has been hydrated so far.
|
||||
|
@ -405,7 +405,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
|||
|
||||
// A and D will be suspended. We'll click on D which should take
|
||||
// priority, after we unsuspend.
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
// Nothing has been hydrated so far.
|
||||
|
@ -474,7 +474,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
|||
let spanB = container.getElementsByTagName('span')[1];
|
||||
let spanC = container.getElementsByTagName('span')[2];
|
||||
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
// Nothing has been hydrated so far.
|
||||
|
|
|
@ -14,16 +14,12 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
|
|||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
let ReactFeatureFlags;
|
||||
let ReactTestUtils;
|
||||
|
||||
function initModules() {
|
||||
// Reset warning cache.
|
||||
jest.resetModuleRegistry();
|
||||
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableSuspenseServerRenderer = true;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
|
@ -48,6 +44,11 @@ describe('ReactDOMServerSuspense', () => {
|
|||
resetModules();
|
||||
});
|
||||
|
||||
if (!__EXPERIMENTAL__) {
|
||||
it("empty test so Jest doesn't complain", () => {});
|
||||
return;
|
||||
}
|
||||
|
||||
function Text(props) {
|
||||
return <div>{props.text}</div>;
|
||||
}
|
||||
|
@ -125,7 +126,7 @@ describe('ReactDOMServerSuspense', () => {
|
|||
expect(divB.textContent).toBe('B');
|
||||
|
||||
ReactTestUtils.act(() => {
|
||||
const root = ReactDOM.unstable_createSyncRoot(parent, {hydrate: true});
|
||||
const root = ReactDOM.createSyncRoot(parent, {hydrate: true});
|
||||
root.render(example);
|
||||
});
|
||||
|
||||
|
|
|
@ -682,38 +682,40 @@ describe('ReactDOMServer', () => {
|
|||
expect(markup).toBe('<div></div>');
|
||||
});
|
||||
|
||||
it('throws for unsupported types on the server', () => {
|
||||
expect(() => {
|
||||
ReactDOMServer.renderToString(<React.Suspense />);
|
||||
}).toThrow('ReactDOMServer does not yet support Suspense.');
|
||||
if (!__EXPERIMENTAL__) {
|
||||
it('throws for unsupported types on the server', () => {
|
||||
expect(() => {
|
||||
ReactDOMServer.renderToString(<React.Suspense />);
|
||||
}).toThrow('ReactDOMServer does not yet support Suspense.');
|
||||
|
||||
async function fakeImport(result) {
|
||||
return {default: result};
|
||||
}
|
||||
async function fakeImport(result) {
|
||||
return {default: result};
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
const LazyFoo = React.lazy(() =>
|
||||
fakeImport(
|
||||
new Promise(resolve =>
|
||||
resolve(function Foo() {
|
||||
return <div />;
|
||||
}),
|
||||
expect(() => {
|
||||
const LazyFoo = React.lazy(() =>
|
||||
fakeImport(
|
||||
new Promise(resolve =>
|
||||
resolve(function Foo() {
|
||||
return <div />;
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
ReactDOMServer.renderToString(<LazyFoo />);
|
||||
}).toThrow('ReactDOMServer does not yet support lazy-loaded components.');
|
||||
});
|
||||
);
|
||||
ReactDOMServer.renderToString(<LazyFoo />);
|
||||
}).toThrow('ReactDOMServer does not yet support lazy-loaded components.');
|
||||
});
|
||||
|
||||
it('throws when suspending on the server', () => {
|
||||
function AsyncFoo() {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
it('throws when suspending on the server', () => {
|
||||
function AsyncFoo() {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
ReactDOMServer.renderToString(<AsyncFoo />);
|
||||
}).toThrow('ReactDOMServer does not yet support Suspense.');
|
||||
});
|
||||
expect(() => {
|
||||
ReactDOMServer.renderToString(<AsyncFoo />);
|
||||
}).toThrow('ReactDOMServer does not yet support Suspense.');
|
||||
});
|
||||
}
|
||||
|
||||
it('does not get confused by throwing null', () => {
|
||||
function Bad() {
|
||||
|
|
|
@ -500,165 +500,22 @@ describe('ReactDOMServerHydration', () => {
|
|||
expect(element.textContent).toBe('Hello world');
|
||||
});
|
||||
|
||||
it('does not re-enter hydration after committing the first one', () => {
|
||||
let finalHTML = ReactDOMServer.renderToString(<div />);
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
root.render(<div />);
|
||||
Scheduler.unstable_flushAll();
|
||||
root.render(null);
|
||||
Scheduler.unstable_flushAll();
|
||||
// This should not reenter hydration state and therefore not trigger hydration
|
||||
// warnings.
|
||||
root.render(<div />);
|
||||
Scheduler.unstable_flushAll();
|
||||
});
|
||||
|
||||
it('does not invoke an event on a concurrent hydrating node until it commits', () => {
|
||||
function Sibling({text}) {
|
||||
Scheduler.unstable_yieldValue('Sibling');
|
||||
return <span>Sibling</span>;
|
||||
}
|
||||
|
||||
function Sibling2({text}) {
|
||||
Scheduler.unstable_yieldValue('Sibling2');
|
||||
return null;
|
||||
}
|
||||
|
||||
let clicks = 0;
|
||||
|
||||
function Button() {
|
||||
Scheduler.unstable_yieldValue('Button');
|
||||
let [clicked, setClicked] = React.useState(false);
|
||||
if (clicked) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<a
|
||||
onClick={() => {
|
||||
setClicked(true);
|
||||
clicks++;
|
||||
}}>
|
||||
Click me
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<Button />
|
||||
<Sibling />
|
||||
<Sibling2 />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
expect(Scheduler).toHaveYielded(['Button', 'Sibling', 'Sibling2']);
|
||||
|
||||
// We need this to be in the document since we'll dispatch events on it.
|
||||
document.body.appendChild(container);
|
||||
|
||||
let a = container.getElementsByTagName('a')[0];
|
||||
|
||||
// Hydrate asynchronously.
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
// We haven't started hydrating yet.
|
||||
a.click();
|
||||
// Clicking should not invoke the event yet because we haven't committed
|
||||
// the hydration yet.
|
||||
expect(clicks).toBe(0);
|
||||
|
||||
// Flush part way through the render.
|
||||
if (__DEV__) {
|
||||
// In DEV effects gets double invoked.
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Button', 'Button', 'Sibling']);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Button', 'Sibling']);
|
||||
}
|
||||
|
||||
expect(container.textContent).toBe('Click meSibling');
|
||||
|
||||
// We're now partially hydrated.
|
||||
a.click();
|
||||
// Clicking should not invoke the event yet because we haven't committed
|
||||
// the hydration yet.
|
||||
expect(clicks).toBe(0);
|
||||
|
||||
// Finish the rest of the hydration.
|
||||
if (__DEV__) {
|
||||
// In DEV effects gets double invoked.
|
||||
expect(Scheduler).toFlushAndYield(['Sibling2', 'Button', 'Button']);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYield(['Sibling2', 'Button']);
|
||||
}
|
||||
|
||||
// We should have picked up both events now.
|
||||
expect(clicks).toBe(2);
|
||||
|
||||
expect(container.textContent).toBe('Sibling');
|
||||
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
it('does not invoke an event on a parent tree when a subtree is hydrating', () => {
|
||||
let clicks = 0;
|
||||
let childSlotRef = React.createRef();
|
||||
|
||||
function Parent() {
|
||||
return <div onClick={() => clicks++} ref={childSlotRef} />;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<a>Click me</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
|
||||
let parentContainer = document.createElement('div');
|
||||
let childContainer = document.createElement('div');
|
||||
|
||||
// We need this to be in the document since we'll dispatch events on it.
|
||||
document.body.appendChild(parentContainer);
|
||||
|
||||
// We're going to use a different root as a parent.
|
||||
// This lets us detect whether an event goes through React's event system.
|
||||
let parentRoot = ReactDOM.unstable_createRoot(parentContainer);
|
||||
parentRoot.render(<Parent />);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
childSlotRef.current.appendChild(childContainer);
|
||||
|
||||
childContainer.innerHTML = finalHTML;
|
||||
|
||||
let a = childContainer.getElementsByTagName('a')[0];
|
||||
|
||||
// Hydrate asynchronously.
|
||||
let root = ReactDOM.unstable_createRoot(childContainer, {hydrate: true});
|
||||
root.render(<App />);
|
||||
// Nothing has rendered so far.
|
||||
|
||||
a.click();
|
||||
expect(clicks).toBe(0);
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
// We're now full hydrated.
|
||||
|
||||
expect(clicks).toBe(1);
|
||||
|
||||
document.body.removeChild(parentContainer);
|
||||
});
|
||||
if (__EXPERIMENTAL__) {
|
||||
it('does not re-enter hydration after committing the first one', () => {
|
||||
let finalHTML = ReactDOMServer.renderToString(<div />);
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<div />);
|
||||
Scheduler.unstable_flushAll();
|
||||
root.render(null);
|
||||
Scheduler.unstable_flushAll();
|
||||
// This should not reenter hydration state and therefore not trigger hydration
|
||||
// warnings.
|
||||
root.render(<div />);
|
||||
Scheduler.unstable_flushAll();
|
||||
});
|
||||
}
|
||||
|
||||
it('regression test: Suspense + hydration in legacy mode ', () => {
|
||||
const element = document.createElement('div');
|
||||
|
|
|
@ -27,29 +27,31 @@ function sleep(period) {
|
|||
|
||||
describe('ReactTestUtils.act()', () => {
|
||||
// first we run all the tests with concurrent mode
|
||||
let concurrentRoot = null;
|
||||
function renderConcurrent(el, dom) {
|
||||
concurrentRoot = ReactDOM.unstable_createRoot(dom);
|
||||
concurrentRoot.render(el);
|
||||
}
|
||||
if (__EXPERIMENTAL__) {
|
||||
let concurrentRoot = null;
|
||||
const renderConcurrent = (el, dom) => {
|
||||
concurrentRoot = ReactDOM.createRoot(dom);
|
||||
concurrentRoot.render(el);
|
||||
};
|
||||
|
||||
function unmountConcurrent(_dom) {
|
||||
if (concurrentRoot !== null) {
|
||||
concurrentRoot.unmount();
|
||||
concurrentRoot = null;
|
||||
}
|
||||
}
|
||||
const unmountConcurrent = _dom => {
|
||||
if (concurrentRoot !== null) {
|
||||
concurrentRoot.unmount();
|
||||
concurrentRoot = null;
|
||||
}
|
||||
};
|
||||
|
||||
function rerenderConcurrent(el) {
|
||||
concurrentRoot.render(el);
|
||||
}
|
||||
const rerenderConcurrent = el => {
|
||||
concurrentRoot.render(el);
|
||||
};
|
||||
|
||||
runActTests(
|
||||
'concurrent mode',
|
||||
renderConcurrent,
|
||||
unmountConcurrent,
|
||||
rerenderConcurrent,
|
||||
);
|
||||
runActTests(
|
||||
'concurrent mode',
|
||||
renderConcurrent,
|
||||
unmountConcurrent,
|
||||
rerenderConcurrent,
|
||||
);
|
||||
}
|
||||
|
||||
// and then in sync mode
|
||||
|
||||
|
@ -71,24 +73,26 @@ describe('ReactTestUtils.act()', () => {
|
|||
runActTests('legacy sync mode', renderSync, unmountSync, rerenderSync);
|
||||
|
||||
// and then in batched mode
|
||||
let batchedRoot = null;
|
||||
function renderBatched(el, dom) {
|
||||
batchedRoot = ReactDOM.unstable_createSyncRoot(dom);
|
||||
batchedRoot.render(el);
|
||||
}
|
||||
if (__EXPERIMENTAL__) {
|
||||
let batchedRoot = null;
|
||||
const renderBatched = (el, dom) => {
|
||||
batchedRoot = ReactDOM.createSyncRoot(dom);
|
||||
batchedRoot.render(el);
|
||||
};
|
||||
|
||||
function unmountBatched(dom) {
|
||||
if (batchedRoot !== null) {
|
||||
batchedRoot.unmount();
|
||||
batchedRoot = null;
|
||||
}
|
||||
}
|
||||
const unmountBatched = dom => {
|
||||
if (batchedRoot !== null) {
|
||||
batchedRoot.unmount();
|
||||
batchedRoot = null;
|
||||
}
|
||||
};
|
||||
|
||||
function rerenderBatched(el) {
|
||||
batchedRoot.render(el);
|
||||
}
|
||||
const rerenderBatched = el => {
|
||||
batchedRoot.render(el);
|
||||
};
|
||||
|
||||
runActTests('batched mode', renderBatched, unmountBatched, rerenderBatched);
|
||||
runActTests('batched mode', renderBatched, unmountBatched, rerenderBatched);
|
||||
}
|
||||
|
||||
describe('unacted effects', () => {
|
||||
function App() {
|
||||
|
@ -116,31 +120,29 @@ describe('ReactTestUtils.act()', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('warns in batched mode', () => {
|
||||
expect(() => {
|
||||
const root = ReactDOM.unstable_createSyncRoot(
|
||||
document.createElement('div'),
|
||||
);
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
}).toWarnDev([
|
||||
'An update to App ran an effect, but was not wrapped in act(...)',
|
||||
'An update to App ran an effect, but was not wrapped in act(...)',
|
||||
]);
|
||||
});
|
||||
if (__EXPERIMENTAL__) {
|
||||
it('warns in batched mode', () => {
|
||||
expect(() => {
|
||||
const root = ReactDOM.createSyncRoot(document.createElement('div'));
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
}).toWarnDev([
|
||||
'An update to App ran an effect, but was not wrapped in act(...)',
|
||||
'An update to App ran an effect, but was not wrapped in act(...)',
|
||||
]);
|
||||
});
|
||||
|
||||
it('warns in concurrent mode', () => {
|
||||
expect(() => {
|
||||
const root = ReactDOM.unstable_createRoot(
|
||||
document.createElement('div'),
|
||||
);
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
}).toWarnDev([
|
||||
'An update to App ran an effect, but was not wrapped in act(...)',
|
||||
'An update to App ran an effect, but was not wrapped in act(...)',
|
||||
]);
|
||||
});
|
||||
it('warns in concurrent mode', () => {
|
||||
expect(() => {
|
||||
const root = ReactDOM.createRoot(document.createElement('div'));
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
}).toWarnDev([
|
||||
'An update to App ran an effect, but was not wrapped in act(...)',
|
||||
'An update to App ran an effect, but was not wrapped in act(...)',
|
||||
]);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -27,34 +27,32 @@ it('does not warn when rendering in sync mode', () => {
|
|||
}).toWarnDev([]);
|
||||
});
|
||||
|
||||
it('should warn when rendering in concurrent mode', () => {
|
||||
expect(() => {
|
||||
ReactDOM.unstable_createRoot(document.createElement('div')).render(<App />);
|
||||
}).toWarnDev(
|
||||
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
|
||||
'to guarantee consistent behaviour across tests and browsers.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
// does not warn twice
|
||||
expect(() => {
|
||||
ReactDOM.unstable_createRoot(document.createElement('div')).render(<App />);
|
||||
}).toWarnDev([]);
|
||||
});
|
||||
if (__EXPERIMENTAL__) {
|
||||
it('should warn when rendering in concurrent mode', () => {
|
||||
expect(() => {
|
||||
ReactDOM.createRoot(document.createElement('div')).render(<App />);
|
||||
}).toWarnDev(
|
||||
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
|
||||
'to guarantee consistent behaviour across tests and browsers.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
// does not warn twice
|
||||
expect(() => {
|
||||
ReactDOM.createRoot(document.createElement('div')).render(<App />);
|
||||
}).toWarnDev([]);
|
||||
});
|
||||
|
||||
it('should warn when rendering in batched mode', () => {
|
||||
expect(() => {
|
||||
ReactDOM.unstable_createSyncRoot(document.createElement('div')).render(
|
||||
<App />,
|
||||
it('should warn when rendering in batched mode', () => {
|
||||
expect(() => {
|
||||
ReactDOM.createSyncRoot(document.createElement('div')).render(<App />);
|
||||
}).toWarnDev(
|
||||
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
|
||||
'to guarantee consistent behaviour across tests and browsers.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
}).toWarnDev(
|
||||
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
|
||||
'to guarantee consistent behaviour across tests and browsers.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
// does not warn twice
|
||||
expect(() => {
|
||||
ReactDOM.unstable_createSyncRoot(document.createElement('div')).render(
|
||||
<App />,
|
||||
);
|
||||
}).toWarnDev([]);
|
||||
});
|
||||
// does not warn twice
|
||||
expect(() => {
|
||||
ReactDOM.createSyncRoot(document.createElement('div')).render(<App />);
|
||||
}).toWarnDev([]);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1292,78 +1292,84 @@ describe('ReactUpdates', () => {
|
|||
expect(ops).toEqual(['Foo', 'Bar', 'Baz']);
|
||||
});
|
||||
|
||||
it('delays sync updates inside hidden subtrees in Concurrent Mode', () => {
|
||||
const container = document.createElement('div');
|
||||
if (__EXPERIMENTAL__) {
|
||||
it('delays sync updates inside hidden subtrees in Concurrent Mode', () => {
|
||||
const container = document.createElement('div');
|
||||
|
||||
function Baz() {
|
||||
Scheduler.unstable_yieldValue('Baz');
|
||||
return <p>baz</p>;
|
||||
}
|
||||
|
||||
let setCounter;
|
||||
function Bar() {
|
||||
const [counter, _setCounter] = React.useState(0);
|
||||
setCounter = _setCounter;
|
||||
Scheduler.unstable_yieldValue('Bar');
|
||||
return <p>bar {counter}</p>;
|
||||
}
|
||||
|
||||
function Foo() {
|
||||
Scheduler.unstable_yieldValue('Foo');
|
||||
React.useEffect(() => {
|
||||
Scheduler.unstable_yieldValue('Foo#effect');
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div hidden={true}>
|
||||
<Bar />
|
||||
</div>
|
||||
<Baz />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
let hiddenDiv;
|
||||
act(() => {
|
||||
root.render(<Foo />);
|
||||
if (__DEV__) {
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'Foo',
|
||||
'Foo',
|
||||
'Baz',
|
||||
'Foo#effect',
|
||||
]);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Baz', 'Foo#effect']);
|
||||
function Baz() {
|
||||
Scheduler.unstable_yieldValue('Baz');
|
||||
return <p>baz</p>;
|
||||
}
|
||||
hiddenDiv = container.firstChild.firstChild;
|
||||
expect(hiddenDiv.hidden).toBe(true);
|
||||
expect(hiddenDiv.innerHTML).toBe('');
|
||||
|
||||
let setCounter;
|
||||
function Bar() {
|
||||
const [counter, _setCounter] = React.useState(0);
|
||||
setCounter = _setCounter;
|
||||
Scheduler.unstable_yieldValue('Bar');
|
||||
return <p>bar {counter}</p>;
|
||||
}
|
||||
|
||||
function Foo() {
|
||||
Scheduler.unstable_yieldValue('Foo');
|
||||
React.useEffect(() => {
|
||||
Scheduler.unstable_yieldValue('Foo#effect');
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div hidden={true}>
|
||||
<Bar />
|
||||
</div>
|
||||
<Baz />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let hiddenDiv;
|
||||
act(() => {
|
||||
root.render(<Foo />);
|
||||
if (__DEV__) {
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'Foo',
|
||||
'Foo',
|
||||
'Baz',
|
||||
'Foo#effect',
|
||||
]);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'Foo',
|
||||
'Baz',
|
||||
'Foo#effect',
|
||||
]);
|
||||
}
|
||||
hiddenDiv = container.firstChild.firstChild;
|
||||
expect(hiddenDiv.hidden).toBe(true);
|
||||
expect(hiddenDiv.innerHTML).toBe('');
|
||||
// Run offscreen update
|
||||
if (__DEV__) {
|
||||
expect(Scheduler).toFlushAndYield(['Bar', 'Bar']);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYield(['Bar']);
|
||||
}
|
||||
expect(hiddenDiv.hidden).toBe(true);
|
||||
expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
|
||||
});
|
||||
|
||||
ReactDOM.flushSync(() => {
|
||||
setCounter(1);
|
||||
});
|
||||
// Should not flush yet
|
||||
expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
|
||||
|
||||
// Run offscreen update
|
||||
if (__DEV__) {
|
||||
expect(Scheduler).toFlushAndYield(['Bar', 'Bar']);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYield(['Bar']);
|
||||
}
|
||||
expect(hiddenDiv.hidden).toBe(true);
|
||||
expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
|
||||
expect(hiddenDiv.innerHTML).toBe('<p>bar 1</p>');
|
||||
});
|
||||
|
||||
ReactDOM.flushSync(() => {
|
||||
setCounter(1);
|
||||
});
|
||||
// Should not flush yet
|
||||
expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
|
||||
|
||||
// Run offscreen update
|
||||
if (__DEV__) {
|
||||
expect(Scheduler).toFlushAndYield(['Bar', 'Bar']);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYield(['Bar']);
|
||||
}
|
||||
expect(hiddenDiv.innerHTML).toBe('<p>bar 1</p>');
|
||||
});
|
||||
}
|
||||
|
||||
it('can render ridiculously large number of roots without triggering infinite update loop error', () => {
|
||||
class Foo extends React.Component {
|
||||
|
|
|
@ -61,7 +61,7 @@ import getComponentName from 'shared/getComponentName';
|
|||
import invariant from 'shared/invariant';
|
||||
import lowPriorityWarningWithoutStack from 'shared/lowPriorityWarningWithoutStack';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
import {enableStableConcurrentModeAPIs} from 'shared/ReactFeatureFlags';
|
||||
import {exposeConcurrentModeAPIs} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {
|
||||
getInstanceFromNode,
|
||||
|
@ -593,27 +593,8 @@ const ReactDOM: Object = {
|
|||
|
||||
unstable_batchedUpdates: batchedUpdates,
|
||||
|
||||
// TODO remove this legacy method, unstable_discreteUpdates replaces it
|
||||
unstable_interactiveUpdates: (fn, a, b, c) => {
|
||||
flushDiscreteUpdates();
|
||||
return discreteUpdates(fn, a, b, c);
|
||||
},
|
||||
|
||||
unstable_discreteUpdates: discreteUpdates,
|
||||
unstable_flushDiscreteUpdates: flushDiscreteUpdates,
|
||||
|
||||
flushSync: flushSync,
|
||||
|
||||
unstable_createRoot: createRoot,
|
||||
unstable_createSyncRoot: createSyncRoot,
|
||||
unstable_flushControlled: flushControlled,
|
||||
|
||||
unstable_scheduleHydration(target: Node) {
|
||||
if (target) {
|
||||
queueExplicitHydrationTarget(target);
|
||||
}
|
||||
},
|
||||
|
||||
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
|
||||
// Keep in sync with ReactDOMUnstableNativeDependencies.js
|
||||
// ReactTestUtils.js, and ReactTestUtilsAct.js. This is an array for better minification.
|
||||
|
@ -678,9 +659,19 @@ function warnIfReactDOMContainerInDEV(container) {
|
|||
}
|
||||
}
|
||||
|
||||
if (enableStableConcurrentModeAPIs) {
|
||||
if (exposeConcurrentModeAPIs) {
|
||||
ReactDOM.createRoot = createRoot;
|
||||
ReactDOM.createSyncRoot = createSyncRoot;
|
||||
|
||||
ReactDOM.unstable_discreteUpdates = discreteUpdates;
|
||||
ReactDOM.unstable_flushDiscreteUpdates = flushDiscreteUpdates;
|
||||
ReactDOM.unstable_flushControlled = flushControlled;
|
||||
|
||||
ReactDOM.unstable_scheduleHydration = target => {
|
||||
if (target) {
|
||||
queueExplicitHydrationTarget(target);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const foundDevTools = injectIntoDevTools({
|
||||
|
|
|
@ -474,319 +474,321 @@ describe('ChangeEventPlugin', () => {
|
|||
}
|
||||
});
|
||||
|
||||
describe('concurrent mode', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestUtils = require('react-dom/test-utils');
|
||||
Scheduler = require('scheduler');
|
||||
});
|
||||
if (__EXPERIMENTAL__) {
|
||||
describe('concurrent mode', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestUtils = require('react-dom/test-utils');
|
||||
Scheduler = require('scheduler');
|
||||
});
|
||||
|
||||
it('text input', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
let input;
|
||||
it('text input', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let input;
|
||||
|
||||
let ops = [];
|
||||
let ops = [];
|
||||
|
||||
class ControlledInput extends React.Component {
|
||||
state = {value: 'initial'};
|
||||
onChange = event => this.setState({value: event.target.value});
|
||||
render() {
|
||||
ops.push(`render: ${this.state.value}`);
|
||||
const controlledValue =
|
||||
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
|
||||
return (
|
||||
<input
|
||||
ref={el => (input = el)}
|
||||
type="text"
|
||||
value={controlledValue}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledInput />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(input).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: initial']);
|
||||
expect(input.value).toBe('initial');
|
||||
|
||||
ops = [];
|
||||
|
||||
// Trigger a change event.
|
||||
setUntrackedValue.call(input, 'changed');
|
||||
input.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: changed']);
|
||||
// Value should be the controlled value, not the original one
|
||||
expect(input.value).toBe('changed [!]');
|
||||
});
|
||||
|
||||
it('checkbox input', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
let input;
|
||||
|
||||
let ops = [];
|
||||
|
||||
class ControlledInput extends React.Component {
|
||||
state = {checked: false};
|
||||
onChange = event => {
|
||||
this.setState({checked: event.target.checked});
|
||||
};
|
||||
render() {
|
||||
ops.push(`render: ${this.state.checked}`);
|
||||
const controlledValue = this.props.reverse
|
||||
? !this.state.checked
|
||||
: this.state.checked;
|
||||
return (
|
||||
<input
|
||||
ref={el => (input = el)}
|
||||
type="checkbox"
|
||||
checked={controlledValue}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledInput reverse={false} />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(input).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: false']);
|
||||
expect(input.checked).toBe(false);
|
||||
|
||||
ops = [];
|
||||
|
||||
// Trigger a change event.
|
||||
input.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: true']);
|
||||
expect(input.checked).toBe(true);
|
||||
|
||||
// Now let's make sure we're using the controlled value.
|
||||
root.render(<ControlledInput reverse={true} />);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
ops = [];
|
||||
|
||||
// Trigger another change event.
|
||||
input.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: true']);
|
||||
expect(input.checked).toBe(false);
|
||||
});
|
||||
|
||||
it('textarea', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
let textarea;
|
||||
|
||||
let ops = [];
|
||||
|
||||
class ControlledTextarea extends React.Component {
|
||||
state = {value: 'initial'};
|
||||
onChange = event => this.setState({value: event.target.value});
|
||||
render() {
|
||||
ops.push(`render: ${this.state.value}`);
|
||||
const controlledValue =
|
||||
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
|
||||
return (
|
||||
<textarea
|
||||
ref={el => (textarea = el)}
|
||||
type="text"
|
||||
value={controlledValue}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledTextarea />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(textarea).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: initial']);
|
||||
expect(textarea.value).toBe('initial');
|
||||
|
||||
ops = [];
|
||||
|
||||
// Trigger a change event.
|
||||
setUntrackedTextareaValue.call(textarea, 'changed');
|
||||
textarea.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: changed']);
|
||||
// Value should be the controlled value, not the original one
|
||||
expect(textarea.value).toBe('changed [!]');
|
||||
});
|
||||
|
||||
it('parent of input', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
let input;
|
||||
|
||||
let ops = [];
|
||||
|
||||
class ControlledInput extends React.Component {
|
||||
state = {value: 'initial'};
|
||||
onChange = event => this.setState({value: event.target.value});
|
||||
render() {
|
||||
ops.push(`render: ${this.state.value}`);
|
||||
const controlledValue =
|
||||
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
|
||||
return (
|
||||
<div onChange={this.onChange}>
|
||||
class ControlledInput extends React.Component {
|
||||
state = {value: 'initial'};
|
||||
onChange = event => this.setState({value: event.target.value});
|
||||
render() {
|
||||
ops.push(`render: ${this.state.value}`);
|
||||
const controlledValue =
|
||||
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
|
||||
return (
|
||||
<input
|
||||
ref={el => (input = el)}
|
||||
type="text"
|
||||
value={controlledValue}
|
||||
onChange={() => {
|
||||
// Does nothing. Parent handler is responsible for updating.
|
||||
}}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledInput />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(input).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: initial']);
|
||||
expect(input.value).toBe('initial');
|
||||
|
||||
ops = [];
|
||||
|
||||
// Trigger a change event.
|
||||
setUntrackedValue.call(input, 'changed');
|
||||
input.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: changed']);
|
||||
// Value should be the controlled value, not the original one
|
||||
expect(input.value).toBe('changed [!]');
|
||||
});
|
||||
|
||||
it('checkbox input', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let input;
|
||||
|
||||
let ops = [];
|
||||
|
||||
class ControlledInput extends React.Component {
|
||||
state = {checked: false};
|
||||
onChange = event => {
|
||||
this.setState({checked: event.target.checked});
|
||||
};
|
||||
render() {
|
||||
ops.push(`render: ${this.state.checked}`);
|
||||
const controlledValue = this.props.reverse
|
||||
? !this.state.checked
|
||||
: this.state.checked;
|
||||
return (
|
||||
<input
|
||||
ref={el => (input = el)}
|
||||
type="checkbox"
|
||||
checked={controlledValue}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledInput reverse={false} />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(input).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: false']);
|
||||
expect(input.checked).toBe(false);
|
||||
|
||||
ops = [];
|
||||
|
||||
// Trigger a change event.
|
||||
input.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: true']);
|
||||
expect(input.checked).toBe(true);
|
||||
|
||||
// Now let's make sure we're using the controlled value.
|
||||
root.render(<ControlledInput reverse={true} />);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
ops = [];
|
||||
|
||||
// Trigger another change event.
|
||||
input.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: true']);
|
||||
expect(input.checked).toBe(false);
|
||||
});
|
||||
|
||||
it('textarea', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let textarea;
|
||||
|
||||
let ops = [];
|
||||
|
||||
class ControlledTextarea extends React.Component {
|
||||
state = {value: 'initial'};
|
||||
onChange = event => this.setState({value: event.target.value});
|
||||
render() {
|
||||
ops.push(`render: ${this.state.value}`);
|
||||
const controlledValue =
|
||||
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
|
||||
return (
|
||||
<textarea
|
||||
ref={el => (textarea = el)}
|
||||
type="text"
|
||||
value={controlledValue}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledTextarea />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(textarea).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: initial']);
|
||||
expect(textarea.value).toBe('initial');
|
||||
|
||||
ops = [];
|
||||
|
||||
// Trigger a change event.
|
||||
setUntrackedTextareaValue.call(textarea, 'changed');
|
||||
textarea.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: changed']);
|
||||
// Value should be the controlled value, not the original one
|
||||
expect(textarea.value).toBe('changed [!]');
|
||||
});
|
||||
|
||||
it('parent of input', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let input;
|
||||
|
||||
let ops = [];
|
||||
|
||||
class ControlledInput extends React.Component {
|
||||
state = {value: 'initial'};
|
||||
onChange = event => this.setState({value: event.target.value});
|
||||
render() {
|
||||
ops.push(`render: ${this.state.value}`);
|
||||
const controlledValue =
|
||||
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
|
||||
return (
|
||||
<div onChange={this.onChange}>
|
||||
<input
|
||||
ref={el => (input = el)}
|
||||
type="text"
|
||||
value={controlledValue}
|
||||
onChange={() => {
|
||||
// Does nothing. Parent handler is responsible for updating.
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledInput />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(input).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: initial']);
|
||||
expect(input.value).toBe('initial');
|
||||
|
||||
ops = [];
|
||||
|
||||
// Trigger a change event.
|
||||
setUntrackedValue.call(input, 'changed');
|
||||
input.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: changed']);
|
||||
// Value should be the controlled value, not the original one
|
||||
expect(input.value).toBe('changed [!]');
|
||||
});
|
||||
|
||||
it('is async for non-input events', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let input;
|
||||
|
||||
let ops = [];
|
||||
|
||||
class ControlledInput extends React.Component {
|
||||
state = {value: 'initial'};
|
||||
onChange = event => this.setState({value: event.target.value});
|
||||
reset = () => {
|
||||
this.setState({value: ''});
|
||||
};
|
||||
render() {
|
||||
ops.push(`render: ${this.state.value}`);
|
||||
const controlledValue =
|
||||
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
|
||||
return (
|
||||
<input
|
||||
ref={el => (input = el)}
|
||||
type="text"
|
||||
value={controlledValue}
|
||||
onChange={this.onChange}
|
||||
onClick={this.reset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledInput />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(input).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: initial']);
|
||||
expect(input.value).toBe('initial');
|
||||
|
||||
ops = [];
|
||||
|
||||
// Trigger a click event
|
||||
input.dispatchEvent(
|
||||
new Event('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Nothing should have changed
|
||||
expect(ops).toEqual([]);
|
||||
expect(input.value).toBe('initial');
|
||||
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
// Now the click update has flushed.
|
||||
expect(ops).toEqual(['render: ']);
|
||||
expect(input.value).toBe('');
|
||||
});
|
||||
|
||||
it('mouse enter/leave should be user-blocking but not discrete', async () => {
|
||||
// This is currently behind a feature flag
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestUtils = require('react-dom/test-utils');
|
||||
Scheduler = require('scheduler');
|
||||
|
||||
const {act} = TestUtils;
|
||||
const {useState} = React;
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
|
||||
const target = React.createRef(null);
|
||||
function Foo() {
|
||||
const [isHover, setHover] = useState(false);
|
||||
return (
|
||||
<div
|
||||
ref={target}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}>
|
||||
{isHover ? 'hovered' : 'not hovered'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledInput />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(input).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: initial']);
|
||||
expect(input.value).toBe('initial');
|
||||
await act(async () => {
|
||||
root.render(<Foo />);
|
||||
});
|
||||
expect(container.textContent).toEqual('not hovered');
|
||||
|
||||
ops = [];
|
||||
await act(async () => {
|
||||
const mouseOverEvent = document.createEvent('MouseEvents');
|
||||
mouseOverEvent.initEvent('mouseover', true, true);
|
||||
target.current.dispatchEvent(mouseOverEvent);
|
||||
|
||||
// Trigger a change event.
|
||||
setUntrackedValue.call(input, 'changed');
|
||||
input.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: changed']);
|
||||
// Value should be the controlled value, not the original one
|
||||
expect(input.value).toBe('changed [!]');
|
||||
});
|
||||
|
||||
it('is async for non-input events', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
let input;
|
||||
|
||||
let ops = [];
|
||||
|
||||
class ControlledInput extends React.Component {
|
||||
state = {value: 'initial'};
|
||||
onChange = event => this.setState({value: event.target.value});
|
||||
reset = () => {
|
||||
this.setState({value: ''});
|
||||
};
|
||||
render() {
|
||||
ops.push(`render: ${this.state.value}`);
|
||||
const controlledValue =
|
||||
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
|
||||
return (
|
||||
<input
|
||||
ref={el => (input = el)}
|
||||
type="text"
|
||||
value={controlledValue}
|
||||
onChange={this.onChange}
|
||||
onClick={this.reset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledInput />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(input).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: initial']);
|
||||
expect(input.value).toBe('initial');
|
||||
|
||||
ops = [];
|
||||
|
||||
// Trigger a click event
|
||||
input.dispatchEvent(
|
||||
new Event('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Nothing should have changed
|
||||
expect(ops).toEqual([]);
|
||||
expect(input.value).toBe('initial');
|
||||
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
// Now the click update has flushed.
|
||||
expect(ops).toEqual(['render: ']);
|
||||
expect(input.value).toBe('');
|
||||
});
|
||||
|
||||
it('mouse enter/leave should be user-blocking but not discrete', async () => {
|
||||
// This is currently behind a feature flag
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestUtils = require('react-dom/test-utils');
|
||||
Scheduler = require('scheduler');
|
||||
|
||||
const {act} = TestUtils;
|
||||
const {useState} = React;
|
||||
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
|
||||
const target = React.createRef(null);
|
||||
function Foo() {
|
||||
const [isHover, setHover] = useState(false);
|
||||
return (
|
||||
<div
|
||||
ref={target}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}>
|
||||
{isHover ? 'hovered' : 'not hovered'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
root.render(<Foo />);
|
||||
});
|
||||
expect(container.textContent).toEqual('not hovered');
|
||||
|
||||
await act(async () => {
|
||||
const mouseOverEvent = document.createEvent('MouseEvents');
|
||||
mouseOverEvent.initEvent('mouseover', true, true);
|
||||
target.current.dispatchEvent(mouseOverEvent);
|
||||
|
||||
// 3s should be enough to expire the updates
|
||||
Scheduler.unstable_advanceTime(3000);
|
||||
expect(container.textContent).toEqual('hovered');
|
||||
// 3s should be enough to expire the updates
|
||||
Scheduler.unstable_advanceTime(3000);
|
||||
expect(container.textContent).toEqual('hovered');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -845,54 +845,56 @@ describe('DOMEventResponderSystem', () => {
|
|||
buttonRef.current.dispatchEvent(createEvent('foobar'));
|
||||
});
|
||||
|
||||
it('should work with concurrent mode updates', async () => {
|
||||
const log = [];
|
||||
const TestResponder = createEventResponder({
|
||||
targetEventTypes: ['click'],
|
||||
onEvent(event, context, props) {
|
||||
log.push(props);
|
||||
},
|
||||
if (__EXPERIMENTAL__) {
|
||||
it('should work with concurrent mode updates', async () => {
|
||||
const log = [];
|
||||
const TestResponder = createEventResponder({
|
||||
targetEventTypes: ['click'],
|
||||
onEvent(event, context, props) {
|
||||
log.push(props);
|
||||
},
|
||||
});
|
||||
const ref = React.createRef();
|
||||
|
||||
function Test({counter}) {
|
||||
const listener = React.unstable_useResponder(TestResponder, {counter});
|
||||
Scheduler.unstable_yieldValue('Test');
|
||||
return (
|
||||
<button listeners={listener} ref={ref}>
|
||||
Press me
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
let root = ReactDOM.createRoot(container);
|
||||
root.render(<Test counter={0} />);
|
||||
expect(Scheduler).toFlushAndYield(['Test']);
|
||||
|
||||
// Click the button
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 0}]);
|
||||
|
||||
// Clear log
|
||||
log.length = 0;
|
||||
|
||||
// Increase counter
|
||||
root.render(<Test counter={1} />);
|
||||
// Yield before committing
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Test']);
|
||||
|
||||
// Click the button again
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 0}]);
|
||||
|
||||
// Clear log
|
||||
log.length = 0;
|
||||
|
||||
// Commit
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 1}]);
|
||||
});
|
||||
const ref = React.createRef();
|
||||
|
||||
function Test({counter}) {
|
||||
const listener = React.unstable_useResponder(TestResponder, {counter});
|
||||
Scheduler.unstable_yieldValue('Test');
|
||||
return (
|
||||
<button listeners={listener} ref={ref}>
|
||||
Press me
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
let root = ReactDOM.unstable_createRoot(container);
|
||||
root.render(<Test counter={0} />);
|
||||
expect(Scheduler).toFlushAndYield(['Test']);
|
||||
|
||||
// Click the button
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 0}]);
|
||||
|
||||
// Clear log
|
||||
log.length = 0;
|
||||
|
||||
// Increase counter
|
||||
root.render(<Test counter={1} />);
|
||||
// Yield before committing
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Test']);
|
||||
|
||||
// Click the button again
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 0}]);
|
||||
|
||||
// Clear log
|
||||
log.length = 0;
|
||||
|
||||
// Commit
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 1}]);
|
||||
});
|
||||
}
|
||||
|
||||
it('should correctly pass through event properties', () => {
|
||||
const timeStamps = [];
|
||||
|
|
|
@ -230,246 +230,256 @@ describe('SimpleEventPlugin', function() {
|
|||
expect(button.textContent).toEqual('Count: 3');
|
||||
});
|
||||
|
||||
describe('interactive events, in concurrent mode', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
ReactDOM = require('react-dom');
|
||||
Scheduler = require('scheduler');
|
||||
});
|
||||
if (__EXPERIMENTAL__) {
|
||||
describe('interactive events, in concurrent mode', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
ReactDOM = require('react-dom');
|
||||
Scheduler = require('scheduler');
|
||||
});
|
||||
|
||||
it('flushes pending interactive work before extracting event handler', () => {
|
||||
container = document.createElement('div');
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
document.body.appendChild(container);
|
||||
it('flushes pending interactive work before extracting event handler', () => {
|
||||
container = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(container);
|
||||
document.body.appendChild(container);
|
||||
|
||||
let ops = [];
|
||||
let ops = [];
|
||||
|
||||
let button;
|
||||
class Button extends React.Component {
|
||||
state = {disabled: false};
|
||||
onClick = () => {
|
||||
// Perform some side-effect
|
||||
ops.push('Side-effect');
|
||||
// Disable the button
|
||||
this.setState({disabled: true});
|
||||
};
|
||||
render() {
|
||||
ops.push(
|
||||
`render button: ${this.state.disabled ? 'disabled' : 'enabled'}`,
|
||||
);
|
||||
return (
|
||||
<button
|
||||
ref={el => (button = el)}
|
||||
// Handler is removed after the first click
|
||||
onClick={this.state.disabled ? null : this.onClick}
|
||||
/>
|
||||
let button;
|
||||
class Button extends React.Component {
|
||||
state = {disabled: false};
|
||||
onClick = () => {
|
||||
// Perform some side-effect
|
||||
ops.push('Side-effect');
|
||||
// Disable the button
|
||||
this.setState({disabled: true});
|
||||
};
|
||||
render() {
|
||||
ops.push(
|
||||
`render button: ${this.state.disabled ? 'disabled' : 'enabled'}`,
|
||||
);
|
||||
return (
|
||||
<button
|
||||
ref={el => (button = el)}
|
||||
// Handler is removed after the first click
|
||||
onClick={this.state.disabled ? null : this.onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount
|
||||
root.render(<Button />);
|
||||
// Should not have flushed yet because it's async
|
||||
expect(ops).toEqual([]);
|
||||
expect(button).toBe(undefined);
|
||||
// Flush async work
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render button: enabled']);
|
||||
|
||||
ops = [];
|
||||
|
||||
function click() {
|
||||
button.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount
|
||||
root.render(<Button />);
|
||||
// Should not have flushed yet because it's async
|
||||
expect(ops).toEqual([]);
|
||||
expect(button).toBe(undefined);
|
||||
// Flush async work
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render button: enabled']);
|
||||
// Click the button to trigger the side-effect
|
||||
click();
|
||||
expect(ops).toEqual([
|
||||
// The handler fired
|
||||
'Side-effect',
|
||||
// but the component did not re-render yet, because it's async
|
||||
]);
|
||||
|
||||
ops = [];
|
||||
ops = [];
|
||||
|
||||
function click() {
|
||||
button.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
// Click the button again
|
||||
click();
|
||||
expect(ops).toEqual([
|
||||
// Before handling this second click event, the previous interactive
|
||||
// update is flushed
|
||||
'render button: disabled',
|
||||
// The event handler was removed from the button, so there's no second
|
||||
// side-effect
|
||||
]);
|
||||
|
||||
ops = [];
|
||||
|
||||
// The handler should not fire again no matter how many times we
|
||||
// click the handler.
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual([]);
|
||||
});
|
||||
|
||||
it('end result of many interactive updates is deterministic', () => {
|
||||
container = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(container);
|
||||
document.body.appendChild(container);
|
||||
|
||||
let button;
|
||||
class Button extends React.Component {
|
||||
state = {count: 0};
|
||||
render() {
|
||||
return (
|
||||
<button
|
||||
ref={el => (button = el)}
|
||||
onClick={() =>
|
||||
// Intentionally not using the updater form here
|
||||
this.setState({count: this.state.count + 1})
|
||||
}>
|
||||
Count: {this.state.count}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount
|
||||
root.render(<Button />);
|
||||
// Should not have flushed yet because it's async
|
||||
expect(button).toBe(undefined);
|
||||
// Flush async work
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(button.textContent).toEqual('Count: 0');
|
||||
|
||||
function click() {
|
||||
button.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
}
|
||||
|
||||
// Click the button a single time
|
||||
click();
|
||||
// The counter should not have updated yet because it's async
|
||||
expect(button.textContent).toEqual('Count: 0');
|
||||
|
||||
// Click the button many more times
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
|
||||
// Flush the remaining work
|
||||
Scheduler.unstable_flushAll();
|
||||
// The counter should equal the total number of clicks
|
||||
expect(button.textContent).toEqual('Count: 7');
|
||||
});
|
||||
|
||||
it('flushes discrete updates in order', () => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
let button;
|
||||
class Button extends React.Component {
|
||||
state = {lowPriCount: 0};
|
||||
render() {
|
||||
const text = `High-pri count: ${
|
||||
this.props.highPriCount
|
||||
}, Low-pri count: ${this.state.lowPriCount}`;
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
return (
|
||||
<button
|
||||
ref={el => (button = el)}
|
||||
onClick={() => {
|
||||
Scheduler.unstable_next(() => {
|
||||
this.setState(state => ({
|
||||
lowPriCount: state.lowPriCount + 1,
|
||||
}));
|
||||
});
|
||||
}}>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Wrapper extends React.Component {
|
||||
state = {highPriCount: 0};
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
onClick={
|
||||
// Intentionally not using the updater form here, to test
|
||||
// that updates are serially processed.
|
||||
() => {
|
||||
this.setState({highPriCount: this.state.highPriCount + 1});
|
||||
}
|
||||
}>
|
||||
<Button highPriCount={this.state.highPriCount} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Wrapper />);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'High-pri count: 0, Low-pri count: 0',
|
||||
]);
|
||||
expect(button.textContent).toEqual(
|
||||
'High-pri count: 0, Low-pri count: 0',
|
||||
);
|
||||
}
|
||||
|
||||
// Click the button to trigger the side-effect
|
||||
click();
|
||||
expect(ops).toEqual([
|
||||
// The handler fired
|
||||
'Side-effect',
|
||||
// but the component did not re-render yet, because it's async
|
||||
]);
|
||||
|
||||
ops = [];
|
||||
|
||||
// Click the button again
|
||||
click();
|
||||
expect(ops).toEqual([
|
||||
// Before handling this second click event, the previous interactive
|
||||
// update is flushed
|
||||
'render button: disabled',
|
||||
// The event handler was removed from the button, so there's no second
|
||||
// side-effect
|
||||
]);
|
||||
|
||||
ops = [];
|
||||
|
||||
// The handler should not fire again no matter how many times we
|
||||
// click the handler.
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual([]);
|
||||
});
|
||||
|
||||
it('end result of many interactive updates is deterministic', () => {
|
||||
container = document.createElement('div');
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
document.body.appendChild(container);
|
||||
|
||||
let button;
|
||||
class Button extends React.Component {
|
||||
state = {count: 0};
|
||||
render() {
|
||||
return (
|
||||
<button
|
||||
ref={el => (button = el)}
|
||||
onClick={() =>
|
||||
// Intentionally not using the updater form here
|
||||
this.setState({count: this.state.count + 1})
|
||||
}>
|
||||
Count: {this.state.count}
|
||||
</button>
|
||||
function click() {
|
||||
button.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount
|
||||
root.render(<Button />);
|
||||
// Should not have flushed yet because it's async
|
||||
expect(button).toBe(undefined);
|
||||
// Flush async work
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(button.textContent).toEqual('Count: 0');
|
||||
|
||||
function click() {
|
||||
button.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
// Click the button a single time
|
||||
click();
|
||||
// Nothing should flush on the first click.
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
// Click again. This will force the previous discrete update to flush. But
|
||||
// only the high-pri count will increase.
|
||||
click();
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'High-pri count: 1, Low-pri count: 0',
|
||||
]);
|
||||
expect(button.textContent).toEqual(
|
||||
'High-pri count: 1, Low-pri count: 0',
|
||||
);
|
||||
}
|
||||
|
||||
// Click the button a single time
|
||||
click();
|
||||
// The counter should not have updated yet because it's async
|
||||
expect(button.textContent).toEqual('Count: 0');
|
||||
// Click the button many more times
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
|
||||
// Click the button many more times
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
// Flush the remaining work.
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'High-pri count: 2, Low-pri count: 0',
|
||||
'High-pri count: 3, Low-pri count: 0',
|
||||
'High-pri count: 4, Low-pri count: 0',
|
||||
'High-pri count: 5, Low-pri count: 0',
|
||||
'High-pri count: 6, Low-pri count: 0',
|
||||
'High-pri count: 7, Low-pri count: 0',
|
||||
]);
|
||||
|
||||
// Flush the remaining work
|
||||
Scheduler.unstable_flushAll();
|
||||
// The counter should equal the total number of clicks
|
||||
expect(button.textContent).toEqual('Count: 7');
|
||||
});
|
||||
|
||||
it('flushes discrete updates in order', () => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
let button;
|
||||
class Button extends React.Component {
|
||||
state = {lowPriCount: 0};
|
||||
render() {
|
||||
const text = `High-pri count: ${
|
||||
this.props.highPriCount
|
||||
}, Low-pri count: ${this.state.lowPriCount}`;
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
return (
|
||||
<button
|
||||
ref={el => (button = el)}
|
||||
onClick={() => {
|
||||
Scheduler.unstable_next(() => {
|
||||
this.setState(state => ({
|
||||
lowPriCount: state.lowPriCount + 1,
|
||||
}));
|
||||
});
|
||||
}}>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Wrapper extends React.Component {
|
||||
state = {highPriCount: 0};
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
onClick={
|
||||
// Intentionally not using the updater form here, to test
|
||||
// that updates are serially processed.
|
||||
() => {
|
||||
this.setState({highPriCount: this.state.highPriCount + 1});
|
||||
}
|
||||
}>
|
||||
<Button highPriCount={this.state.highPriCount} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
root.render(<Wrapper />);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'High-pri count: 0, Low-pri count: 0',
|
||||
]);
|
||||
expect(button.textContent).toEqual('High-pri count: 0, Low-pri count: 0');
|
||||
|
||||
function click() {
|
||||
button.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
// At the end, both counters should equal the total number of clicks
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'High-pri count: 8, Low-pri count: 0',
|
||||
'High-pri count: 8, Low-pri count: 8',
|
||||
]);
|
||||
expect(button.textContent).toEqual(
|
||||
'High-pri count: 8, Low-pri count: 8',
|
||||
);
|
||||
}
|
||||
|
||||
// Click the button a single time
|
||||
click();
|
||||
// Nothing should flush on the first click.
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
// Click again. This will force the previous discrete update to flush. But
|
||||
// only the high-pri count will increase.
|
||||
click();
|
||||
expect(Scheduler).toHaveYielded(['High-pri count: 1, Low-pri count: 0']);
|
||||
expect(button.textContent).toEqual('High-pri count: 1, Low-pri count: 0');
|
||||
|
||||
// Click the button many more times
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
|
||||
// Flush the remaining work.
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'High-pri count: 2, Low-pri count: 0',
|
||||
'High-pri count: 3, Low-pri count: 0',
|
||||
'High-pri count: 4, Low-pri count: 0',
|
||||
'High-pri count: 5, Low-pri count: 0',
|
||||
'High-pri count: 6, Low-pri count: 0',
|
||||
'High-pri count: 7, Low-pri count: 0',
|
||||
]);
|
||||
|
||||
// At the end, both counters should equal the total number of clicks
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'High-pri count: 8, Low-pri count: 0',
|
||||
'High-pri count: 8, Low-pri count: 8',
|
||||
]);
|
||||
expect(button.textContent).toEqual('High-pri count: 8, Low-pri count: 8');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('iOS bubbling click fix', function() {
|
||||
// See http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
|
||||
|
|
|
@ -750,202 +750,208 @@ describe('Input event responder', () => {
|
|||
}
|
||||
});
|
||||
|
||||
describe('concurrent mode', () => {
|
||||
it('text input', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
let input;
|
||||
if (__EXPERIMENTAL__) {
|
||||
describe('concurrent mode', () => {
|
||||
it('text input', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let input;
|
||||
|
||||
let ops = [];
|
||||
let ops = [];
|
||||
|
||||
function Component({innerRef, onChange, controlledValue}) {
|
||||
const listener = useInput({
|
||||
onChange,
|
||||
});
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
ref={innerRef}
|
||||
value={controlledValue}
|
||||
listeners={listener}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
class ControlledInput extends React.Component {
|
||||
state = {value: 'initial'};
|
||||
onChange = event => this.setState({value: event.target.value});
|
||||
render() {
|
||||
ops.push(`render: ${this.state.value}`);
|
||||
const controlledValue =
|
||||
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
|
||||
function Component({innerRef, onChange, controlledValue}) {
|
||||
const listener = useInput({
|
||||
onChange,
|
||||
});
|
||||
return (
|
||||
<Component
|
||||
onChange={this.onChange}
|
||||
innerRef={el => (input = el)}
|
||||
controlledValue={controlledValue}
|
||||
<input
|
||||
type="text"
|
||||
ref={innerRef}
|
||||
value={controlledValue}
|
||||
listeners={listener}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledInput />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(input).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: initial']);
|
||||
expect(input.value).toBe('initial');
|
||||
class ControlledInput extends React.Component {
|
||||
state = {value: 'initial'};
|
||||
onChange = event => this.setState({value: event.target.value});
|
||||
render() {
|
||||
ops.push(`render: ${this.state.value}`);
|
||||
const controlledValue =
|
||||
this.state.value === 'changed'
|
||||
? 'changed [!]'
|
||||
: this.state.value;
|
||||
return (
|
||||
<Component
|
||||
onChange={this.onChange}
|
||||
innerRef={el => (input = el)}
|
||||
controlledValue={controlledValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ops = [];
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledInput />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(input).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: initial']);
|
||||
expect(input.value).toBe('initial');
|
||||
|
||||
// Trigger a change event.
|
||||
setUntrackedValue.call(input, 'changed');
|
||||
input.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: changed']);
|
||||
// Value should be the controlled value, not the original one
|
||||
expect(input.value).toBe('changed [!]');
|
||||
});
|
||||
ops = [];
|
||||
|
||||
it('checkbox input', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
let input;
|
||||
|
||||
let ops = [];
|
||||
|
||||
function Component({innerRef, onChange, controlledValue}) {
|
||||
const listener = useInput({
|
||||
onChange,
|
||||
});
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
ref={innerRef}
|
||||
checked={controlledValue}
|
||||
listeners={listener}
|
||||
/>
|
||||
// Trigger a change event.
|
||||
setUntrackedValue.call(input, 'changed');
|
||||
input.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
}
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: changed']);
|
||||
// Value should be the controlled value, not the original one
|
||||
expect(input.value).toBe('changed [!]');
|
||||
});
|
||||
|
||||
class ControlledInput extends React.Component {
|
||||
state = {checked: false};
|
||||
onChange = event => {
|
||||
this.setState({checked: event.target.checked});
|
||||
};
|
||||
render() {
|
||||
ops.push(`render: ${this.state.checked}`);
|
||||
const controlledValue = this.props.reverse
|
||||
? !this.state.checked
|
||||
: this.state.checked;
|
||||
it('checkbox input', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let input;
|
||||
|
||||
let ops = [];
|
||||
|
||||
function Component({innerRef, onChange, controlledValue}) {
|
||||
const listener = useInput({
|
||||
onChange,
|
||||
});
|
||||
return (
|
||||
<Component
|
||||
controlledValue={controlledValue}
|
||||
onChange={this.onChange}
|
||||
innerRef={el => (input = el)}
|
||||
<input
|
||||
type="checkbox"
|
||||
ref={innerRef}
|
||||
checked={controlledValue}
|
||||
listeners={listener}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledInput reverse={false} />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(input).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: false']);
|
||||
expect(input.checked).toBe(false);
|
||||
class ControlledInput extends React.Component {
|
||||
state = {checked: false};
|
||||
onChange = event => {
|
||||
this.setState({checked: event.target.checked});
|
||||
};
|
||||
render() {
|
||||
ops.push(`render: ${this.state.checked}`);
|
||||
const controlledValue = this.props.reverse
|
||||
? !this.state.checked
|
||||
: this.state.checked;
|
||||
return (
|
||||
<Component
|
||||
controlledValue={controlledValue}
|
||||
onChange={this.onChange}
|
||||
innerRef={el => (input = el)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ops = [];
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledInput reverse={false} />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(input).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: false']);
|
||||
expect(input.checked).toBe(false);
|
||||
|
||||
// Trigger a change event.
|
||||
input.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: true']);
|
||||
expect(input.checked).toBe(true);
|
||||
ops = [];
|
||||
|
||||
// Now let's make sure we're using the controlled value.
|
||||
root.render(<ControlledInput reverse={true} />);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
ops = [];
|
||||
|
||||
// Trigger another change event.
|
||||
input.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: true']);
|
||||
expect(input.checked).toBe(false);
|
||||
});
|
||||
|
||||
it('textarea', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
let textarea;
|
||||
|
||||
let ops = [];
|
||||
|
||||
function Component({innerRef, onChange, controlledValue}) {
|
||||
const listener = useInput({
|
||||
onChange,
|
||||
});
|
||||
return (
|
||||
<textarea
|
||||
type="text"
|
||||
ref={innerRef}
|
||||
value={controlledValue}
|
||||
listeners={listener}
|
||||
/>
|
||||
// Trigger a change event.
|
||||
input.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
}
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: true']);
|
||||
expect(input.checked).toBe(true);
|
||||
|
||||
class ControlledTextarea extends React.Component {
|
||||
state = {value: 'initial'};
|
||||
onChange = event => this.setState({value: event.target.value});
|
||||
render() {
|
||||
ops.push(`render: ${this.state.value}`);
|
||||
const controlledValue =
|
||||
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
|
||||
// Now let's make sure we're using the controlled value.
|
||||
root.render(<ControlledInput reverse={true} />);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
ops = [];
|
||||
|
||||
// Trigger another change event.
|
||||
input.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: true']);
|
||||
expect(input.checked).toBe(false);
|
||||
});
|
||||
|
||||
it('textarea', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let textarea;
|
||||
|
||||
let ops = [];
|
||||
|
||||
function Component({innerRef, onChange, controlledValue}) {
|
||||
const listener = useInput({
|
||||
onChange,
|
||||
});
|
||||
return (
|
||||
<Component
|
||||
onChange={this.onChange}
|
||||
innerRef={el => (textarea = el)}
|
||||
controlledValue={controlledValue}
|
||||
<textarea
|
||||
type="text"
|
||||
ref={innerRef}
|
||||
value={controlledValue}
|
||||
listeners={listener}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledTextarea />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(textarea).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: initial']);
|
||||
expect(textarea.value).toBe('initial');
|
||||
class ControlledTextarea extends React.Component {
|
||||
state = {value: 'initial'};
|
||||
onChange = event => this.setState({value: event.target.value});
|
||||
render() {
|
||||
ops.push(`render: ${this.state.value}`);
|
||||
const controlledValue =
|
||||
this.state.value === 'changed'
|
||||
? 'changed [!]'
|
||||
: this.state.value;
|
||||
return (
|
||||
<Component
|
||||
onChange={this.onChange}
|
||||
innerRef={el => (textarea = el)}
|
||||
controlledValue={controlledValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ops = [];
|
||||
// Initial mount. Test that this is async.
|
||||
root.render(<ControlledTextarea />);
|
||||
// Should not have flushed yet.
|
||||
expect(ops).toEqual([]);
|
||||
expect(textarea).toBe(undefined);
|
||||
// Flush callbacks.
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(ops).toEqual(['render: initial']);
|
||||
expect(textarea.value).toBe('initial');
|
||||
|
||||
// Trigger a change event.
|
||||
setUntrackedTextareaValue.call(textarea, 'changed');
|
||||
textarea.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: changed']);
|
||||
// Value should be the controlled value, not the original one
|
||||
expect(textarea.value).toBe('changed [!]');
|
||||
ops = [];
|
||||
|
||||
// Trigger a change event.
|
||||
setUntrackedTextareaValue.call(textarea, 'changed');
|
||||
textarea.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
// Change should synchronously flush
|
||||
expect(ops).toEqual(['render: changed']);
|
||||
// Value should be the controlled value, not the original one
|
||||
expect(textarea.value).toBe('changed [!]');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('expect displayName to show up for event component', () => {
|
||||
|
|
|
@ -34,6 +34,11 @@ describe('mixing responders with the heritage event system', () => {
|
|||
container = null;
|
||||
});
|
||||
|
||||
if (!__EXPERIMENTAL__) {
|
||||
it("empty test so Jest doesn't complain", () => {});
|
||||
return;
|
||||
}
|
||||
|
||||
it('should properly only flush sync once when the event systems are mixed', () => {
|
||||
const useTap = require('react-interactions/events/tap').useTap;
|
||||
const ref = React.createRef();
|
||||
|
@ -66,7 +71,7 @@ describe('mixing responders with the heritage event system', () => {
|
|||
}
|
||||
|
||||
const newContainer = document.createElement('div');
|
||||
const root = ReactDOM.unstable_createRoot(newContainer);
|
||||
const root = ReactDOM.createRoot(newContainer);
|
||||
document.body.appendChild(newContainer);
|
||||
root.render(<MyComponent />);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
@ -137,7 +142,7 @@ describe('mixing responders with the heritage event system', () => {
|
|||
}
|
||||
|
||||
const newContainer = document.createElement('div');
|
||||
const root = ReactDOM.unstable_createRoot(newContainer);
|
||||
const root = ReactDOM.createRoot(newContainer);
|
||||
document.body.appendChild(newContainer);
|
||||
root.render(<MyComponent />);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
@ -216,7 +221,7 @@ describe('mixing responders with the heritage event system', () => {
|
|||
|
||||
const newContainer = document.createElement('div');
|
||||
document.body.appendChild(newContainer);
|
||||
const root = ReactDOM.unstable_createRoot(newContainer);
|
||||
const root = ReactDOM.createRoot(newContainer);
|
||||
|
||||
root.render(<MyComponent />);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
@ -238,7 +243,7 @@ describe('mixing responders with the heritage event system', () => {
|
|||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
const useTap = require('react-interactions/events/tap').useTap;
|
||||
const useInput = require('react-interactions/events/input').useInput;
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let input;
|
||||
|
||||
let ops = [];
|
||||
|
|
|
@ -2394,7 +2394,7 @@ describe('ReactFresh', () => {
|
|||
});
|
||||
|
||||
it('can hot reload offscreen components', () => {
|
||||
if (__DEV__) {
|
||||
if (__DEV__ && __EXPERIMENTAL__) {
|
||||
const AppV1 = prepare(() => {
|
||||
function Hello() {
|
||||
React.useLayoutEffect(() => {
|
||||
|
@ -2421,7 +2421,7 @@ describe('ReactFresh', () => {
|
|||
};
|
||||
});
|
||||
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<AppV1 offscreen={true} />);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['App#layout']);
|
||||
const el = container.firstChild;
|
||||
|
|
|
@ -27,7 +27,6 @@ function loadModules() {
|
|||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffects = false;
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
ReactFeatureFlags.enableSuspenseServerRenderer = true;
|
||||
ReactFeatureFlags.enableProfilerTimer = true;
|
||||
ReactFeatureFlags.enableSchedulerTracing = true;
|
||||
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
||||
|
@ -64,6 +63,11 @@ describe('ReactDOMTracing', () => {
|
|||
loadModules();
|
||||
});
|
||||
|
||||
if (!__EXPERIMENTAL__) {
|
||||
it("empty test so Jest doesn't complain", () => {});
|
||||
return;
|
||||
}
|
||||
|
||||
describe('interaction tracing', () => {
|
||||
describe('hidden', () => {
|
||||
it('traces interaction through hidden subtree', () => {
|
||||
|
@ -101,7 +105,7 @@ describe('ReactDOMTracing', () => {
|
|||
const onRender = jest.fn();
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
SchedulerTracing.unstable_trace('initialization', 0, () => {
|
||||
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
|
||||
TestUtils.act(() => {
|
||||
|
@ -171,7 +175,7 @@ describe('ReactDOMTracing', () => {
|
|||
const onRender = jest.fn();
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
SchedulerTracing.unstable_trace('initialization', 0, () => {
|
||||
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
|
||||
|
||||
|
@ -250,7 +254,7 @@ describe('ReactDOMTracing', () => {
|
|||
const onRender = jest.fn();
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
SchedulerTracing.unstable_trace('initialization', 0, () => {
|
||||
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
|
||||
TestUtils.act(() => {
|
||||
|
@ -344,7 +348,7 @@ describe('ReactDOMTracing', () => {
|
|||
|
||||
const onRender = jest.fn();
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
|
||||
// Schedule some idle work without any interactions.
|
||||
TestUtils.act(() => {
|
||||
|
@ -448,7 +452,7 @@ describe('ReactDOMTracing', () => {
|
|||
|
||||
const onRender = jest.fn();
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
|
||||
TestUtils.act(() => {
|
||||
root.render(
|
||||
|
@ -545,7 +549,7 @@ describe('ReactDOMTracing', () => {
|
|||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
|
||||
let interaction;
|
||||
|
||||
|
@ -627,7 +631,7 @@ describe('ReactDOMTracing', () => {
|
|||
|
||||
let interaction;
|
||||
|
||||
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
const root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
|
||||
// Hydrate it.
|
||||
SchedulerTracing.unstable_trace('initialization', 0, () => {
|
||||
|
@ -686,7 +690,7 @@ describe('ReactDOMTracing', () => {
|
|||
|
||||
let interaction;
|
||||
|
||||
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
const root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
|
||||
// Start hydrating but simulate blocking for suspense data.
|
||||
suspend = true;
|
||||
|
@ -755,7 +759,7 @@ describe('ReactDOMTracing', () => {
|
|||
|
||||
let interaction;
|
||||
|
||||
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
const root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
|
||||
// Hydrate without suspending to fill in the client-rendered content.
|
||||
suspend = false;
|
||||
|
|
|
@ -54,6 +54,11 @@ describe('ProfilerDOM', () => {
|
|||
return props.text;
|
||||
}
|
||||
|
||||
if (!__EXPERIMENTAL__) {
|
||||
it("empty test so Jest doesn't complain", () => {});
|
||||
return;
|
||||
}
|
||||
|
||||
it('should correctly trace interactions for async roots', async () => {
|
||||
let resolve;
|
||||
let thenable = {
|
||||
|
@ -75,7 +80,7 @@ describe('ProfilerDOM', () => {
|
|||
}
|
||||
|
||||
const element = document.createElement('div');
|
||||
const root = ReactDOM.unstable_createRoot(element);
|
||||
const root = ReactDOM.createRoot(element);
|
||||
|
||||
let interaction;
|
||||
let wrappedResolve;
|
||||
|
|
|
@ -31,9 +31,9 @@ export const enableProfilerTimer = __PROFILE__;
|
|||
// Trace which interactions trigger each commit.
|
||||
export const enableSchedulerTracing = __PROFILE__;
|
||||
|
||||
// Only used in www builds.
|
||||
export const enableSuspenseServerRenderer = false; // TODO: __DEV__? Here it might just be false.
|
||||
export const enableSelectiveHydration = false;
|
||||
// SSR experiments
|
||||
export const enableSuspenseServerRenderer = __EXPERIMENTAL__;
|
||||
export const enableSelectiveHydration = __EXPERIMENTAL__;
|
||||
|
||||
// Only used in www builds.
|
||||
export const enableSchedulerDebugging = false;
|
||||
|
@ -52,7 +52,7 @@ export const disableInputAttributeSyncing = false;
|
|||
|
||||
// These APIs will no longer be "unstable" in the upcoming 16.7 release,
|
||||
// Control this behavior with a flag to support 16.6 minor releases in the meanwhile.
|
||||
export const enableStableConcurrentModeAPIs = __EXPERIMENTAL__;
|
||||
export const exposeConcurrentModeAPIs = __EXPERIMENTAL__;
|
||||
|
||||
export const warnAboutShorthandPropertyCollision = false;
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ export const enableProfilerTimer = __PROFILE__;
|
|||
export const enableSchedulerTracing = __PROFILE__;
|
||||
export const enableSuspenseServerRenderer = false;
|
||||
export const enableSelectiveHydration = false;
|
||||
export const enableStableConcurrentModeAPIs = false;
|
||||
export const exposeConcurrentModeAPIs = false;
|
||||
export const warnAboutShorthandPropertyCollision = false;
|
||||
export const enableSchedulerDebugging = false;
|
||||
export const debugRenderPhaseSideEffectsForStrictMode = true;
|
||||
|
|
|
@ -23,7 +23,7 @@ export const enableSuspenseServerRenderer = false;
|
|||
export const enableSelectiveHydration = false;
|
||||
export const disableJavaScriptURLs = false;
|
||||
export const disableInputAttributeSyncing = false;
|
||||
export const enableStableConcurrentModeAPIs = false;
|
||||
export const exposeConcurrentModeAPIs = false;
|
||||
export const warnAboutShorthandPropertyCollision = false;
|
||||
export const enableSchedulerDebugging = false;
|
||||
export const enableFlareAPI = false;
|
||||
|
|
|
@ -23,7 +23,7 @@ export const enableSuspenseServerRenderer = false;
|
|||
export const enableSelectiveHydration = false;
|
||||
export const disableJavaScriptURLs = false;
|
||||
export const disableInputAttributeSyncing = false;
|
||||
export const enableStableConcurrentModeAPIs = false;
|
||||
export const exposeConcurrentModeAPIs = false;
|
||||
export const warnAboutShorthandPropertyCollision = false;
|
||||
export const enableSchedulerDebugging = false;
|
||||
export const enableFlareAPI = false;
|
||||
|
|
|
@ -23,7 +23,7 @@ export const enableSuspenseServerRenderer = false;
|
|||
export const enableSelectiveHydration = false;
|
||||
export const disableJavaScriptURLs = false;
|
||||
export const disableInputAttributeSyncing = false;
|
||||
export const enableStableConcurrentModeAPIs = false;
|
||||
export const exposeConcurrentModeAPIs = false;
|
||||
export const warnAboutShorthandPropertyCollision = false;
|
||||
export const enableSchedulerDebugging = false;
|
||||
export const enableFlareAPI = false;
|
||||
|
|
|
@ -21,7 +21,7 @@ export const enableProfilerTimer = __PROFILE__;
|
|||
export const enableSchedulerTracing = __PROFILE__;
|
||||
export const enableSuspenseServerRenderer = false;
|
||||
export const enableSelectiveHydration = false;
|
||||
export const enableStableConcurrentModeAPIs = false;
|
||||
export const exposeConcurrentModeAPIs = false;
|
||||
export const enableSchedulerDebugging = false;
|
||||
export const disableJavaScriptURLs = false;
|
||||
export const enableFlareAPI = true;
|
||||
|
|
|
@ -39,7 +39,7 @@ export const warnAboutStringRefs = false;
|
|||
export const warnAboutDefaultPropsOnFunctionComponents = false;
|
||||
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
|
||||
|
||||
export const enableStableConcurrentModeAPIs = false;
|
||||
export const exposeConcurrentModeAPIs = false;
|
||||
|
||||
export const enableSuspenseServerRenderer = true;
|
||||
|
||||
|
|
|
@ -7,7 +7,15 @@ if (NODE_ENV !== 'development' && NODE_ENV !== 'production') {
|
|||
global.__DEV__ = NODE_ENV === 'development';
|
||||
global.__PROFILE__ = NODE_ENV === 'development';
|
||||
global.__UMD__ = false;
|
||||
global.__EXPERIMENTAL__ = process.env.RELEASE_CHANNEL === 'experimental';
|
||||
|
||||
const RELEASE_CHANNEL = process.env.RELEASE_CHANNEL;
|
||||
|
||||
// Default to running tests in experimental mode. If the release channel is
|
||||
// set via an environment variable, then check if it's "experimental".
|
||||
global.__EXPERIMENTAL__ =
|
||||
typeof RELEASE_CHANNEL === 'string'
|
||||
? RELEASE_CHANNEL === 'experimental'
|
||||
: true;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
global.requestIdleCallback = function(callback) {
|
||||
|
|
|
@ -103,8 +103,8 @@ const getBuildInfo = async () => {
|
|||
join(cwd, 'packages', 'react', 'package.json')
|
||||
);
|
||||
const reactVersion = isExperimental
|
||||
? `${packageJSON.version}-experimental-canary-${commit}`
|
||||
: `${packageJSON.version}-canary-${commit}`;
|
||||
? `${packageJSON.version}-experimental-${commit}`
|
||||
: `${packageJSON.version}-${commit}`;
|
||||
|
||||
return {branch, buildNumber, checksum, commit, reactVersion, version};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue