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:
Andrew Clark 2019-10-15 15:09:19 -07:00 committed by GitHub
parent 4cb399a433
commit 30c5daf943
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1978 additions and 2230 deletions

View File

@ -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:

View File

@ -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,
}
);
});
}

View File

@ -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">
`;

View File

@ -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>

View File

@ -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');
});
}
});

View File

@ -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.',

View File

@ -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();

View File

@ -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.

View File

@ -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);
});

View File

@ -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() {

View File

@ -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');

View File

@ -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(...)',
]);
});
}
});
});

View File

@ -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([]);
});
}

View File

@ -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 {

View File

@ -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({

View File

@ -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');
});
});
});
});
}
});

View File

@ -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 = [];

View File

@ -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

View File

@ -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', () => {

View File

@ -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 = [];

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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};
};