Compare commits
122 Commits
16.4.2-dev
...
radio-clic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0664d552c | ||
|
|
1609cf3432 | ||
|
|
46d5afc54d | ||
|
|
b3b80a4835 | ||
|
|
5e8beec84b | ||
|
|
470377bbdb | ||
|
|
ad17ca639b | ||
|
|
6db080154b | ||
|
|
fddb23601f | ||
|
|
95738e5cfd | ||
|
|
d79238f1ee | ||
|
|
ae63110335 | ||
|
|
e341e503b2 | ||
|
|
00cd4444e2 | ||
|
|
41f6d8cc7a | ||
|
|
0624c719f4 | ||
|
|
f60a7f722c | ||
|
|
ff41519ec2 | ||
|
|
c44c2a2161 | ||
|
|
28cd494bdf | ||
|
|
b381f41411 | ||
|
|
0182a74632 | ||
|
|
2a2ef7e0fd | ||
|
|
840cb1a268 | ||
|
|
bc1ea9cd96 | ||
|
|
0154a79fed | ||
|
|
dbd16c8a96 | ||
|
|
ca0941fce3 | ||
|
|
a32c727f2e | ||
|
|
2b509e2c8c | ||
|
|
5776fa3fcf | ||
|
|
3d3506d37d | ||
|
|
71b4e99901 | ||
|
|
8121212f0d | ||
|
|
2a1bc3f74c | ||
|
|
2c560cb995 | ||
|
|
ead08827d0 | ||
|
|
e4e58343e4 | ||
|
|
0e235bb8f7 | ||
|
|
236f608723 | ||
|
|
acbb4f93f0 | ||
|
|
171e0b7d44 | ||
|
|
606c30aa5f | ||
|
|
9f78913b20 | ||
|
|
6d3e262880 | ||
|
|
82c7ca4cca | ||
|
|
21ac62c77a | ||
|
|
d6a0626b38 | ||
|
|
fd410f43fc | ||
|
|
f9358c51c8 | ||
|
|
854c953905 | ||
|
|
467d139101 | ||
|
|
43ffae2d17 | ||
|
|
659a29cecf | ||
|
|
58f3b29d91 | ||
|
|
1c89cb62fd | ||
|
|
e6076ecf48 | ||
|
|
32f6f258ba | ||
|
|
9ca37f8431 | ||
|
|
f89f25f471 | ||
|
|
7b99ceabec | ||
|
|
6ebc8f3c07 | ||
|
|
d64d1ddb57 | ||
|
|
afd46490d0 | ||
|
|
e79366d549 | ||
|
|
1f32d3c6dc | ||
|
|
377e1a049e | ||
|
|
96d38d178a | ||
|
|
095dd5049c | ||
|
|
5662595677 | ||
|
|
ebbd221432 | ||
|
|
ddc91af795 | ||
|
|
3596e40b39 | ||
|
|
449f6ddd5c | ||
|
|
f762b3abb1 | ||
|
|
6f6b560a64 | ||
|
|
1386ccddd8 | ||
|
|
f5779bbc10 | ||
|
|
9faf389e79 | ||
|
|
85fe4ddce7 | ||
|
|
07fefe3331 | ||
|
|
88d7ed8bfb | ||
|
|
f128fdea48 | ||
|
|
aa8266c4f7 | ||
|
|
c039c16f21 | ||
|
|
6731bfbed7 | ||
|
|
64e1921aab | ||
|
|
bf32a3d195 | ||
|
|
183aefa51f | ||
|
|
3297102de6 | ||
|
|
f4b6a9f8ee | ||
|
|
3eedcb1fda | ||
|
|
6d6de6011c | ||
|
|
71a60ddb16 | ||
|
|
9e6c99ca2e | ||
|
|
baff5cc2f6 | ||
|
|
6a530e3baa | ||
|
|
c35a1e7483 | ||
|
|
076bbeace7 | ||
|
|
da5c87bdfa | ||
|
|
a960d18bc7 | ||
|
|
5b3d17a5f7 | ||
|
|
b0f60895f7 | ||
|
|
ae8c6dd534 | ||
|
|
0fcf92d06d | ||
|
|
97af3e1f3a | ||
|
|
4fe6eec15b | ||
|
|
8e87c139b4 | ||
|
|
aeda7b745d | ||
|
|
b1b3acbd6b | ||
|
|
ae14317d68 | ||
|
|
72434a7686 | ||
|
|
64c54edea4 | ||
|
|
9bd4d1fae2 | ||
|
|
9bda7b28f3 | ||
|
|
2e75779075 | ||
|
|
bc963f353d | ||
|
|
051637da61 | ||
|
|
2a8085980f | ||
|
|
e0c78344e2 | ||
|
|
74b1723df1 | ||
|
|
7b1bbe6bef |
@@ -6,6 +6,7 @@ module.exports = {
|
||||
jsxBracketSameLine: true,
|
||||
trailingComma: 'es5',
|
||||
printWidth: 80,
|
||||
parser: 'babylon',
|
||||
|
||||
overrides: [
|
||||
{
|
||||
|
||||
38
CHANGELOG.md
@@ -5,6 +5,18 @@
|
||||
Click to see more.
|
||||
</summary>
|
||||
|
||||
</details>
|
||||
|
||||
## 16.4.2 (August 1, 2018)
|
||||
|
||||
### React DOM Server
|
||||
|
||||
* Fix a [potential XSS vulnerability when the attacker controls an attribute name](https://reactjs.org/blog/2018/08/01/react-v-16-4-2.html) (`CVE-2018-6341`). This fix is available in the latest `react-dom@16.4.2`, as well as in previous affected minor versions: `react-dom@16.0.1`, `react-dom@16.1.2`, `react-dom@16.2.1`, and `react-dom@16.3.3`. ([@gaearon](https://github.com/gaearon) in [#13302](https://github.com/facebook/react/pull/13302))
|
||||
|
||||
* Fix a crash in the server renderer when an attribute is called `hasOwnProperty`. This fix is only available in `react-dom@16.4.2`. ([@gaearon](https://github.com/gaearon) in [#13303](https://github.com/facebook/react/pull/13303))
|
||||
|
||||
## 16.4.1 (June 13, 2018)
|
||||
|
||||
### React
|
||||
|
||||
* You can now assign `propTypes` to components returned by `React.ForwardRef`. ([@bvaughn](https://github.com/bvaughn) in [#12911](https://github.com/facebook/react/pull/12911))
|
||||
@@ -27,8 +39,6 @@
|
||||
* Allow multiple root children in test renderer traversal API. ([@gaearon](https://github.com/gaearon) in [#13017](https://github.com/facebook/react/pull/13017))
|
||||
* Fix `getDerivedStateFromProps()` in the shallow renderer to not discard the pending state. ([@fatfisz](https://github.com/fatfisz) in [#13030](https://github.com/facebook/react/pull/13030))
|
||||
|
||||
</details>
|
||||
|
||||
## 16.4.0 (May 23, 2018)
|
||||
|
||||
### React
|
||||
@@ -68,6 +78,12 @@
|
||||
|
||||
* The [new host config shape](https://github.com/facebook/react/blob/c601f7a64640290af85c9f0e33c78480656b46bc/packages/react-noop-renderer/src/createReactNoop.js#L82-L285) is flat and doesn't use nested objects. ([@gaearon](https://github.com/gaearon) in [#12792](https://github.com/facebook/react/pull/12792))
|
||||
|
||||
## 16.3.3 (August 1, 2018)
|
||||
|
||||
### React DOM Server
|
||||
|
||||
* Fix a [potential XSS vulnerability when the attacker controls an attribute name](https://reactjs.org/blog/2018/08/01/react-v-16-4-2.html) (`CVE-2018-6341`). This fix is available in the latest `react-dom@16.4.2`, as well as in previous affected minor versions: `react-dom@16.0.1`, `react-dom@16.1.2`, `react-dom@16.2.1`, and `react-dom@16.3.3`. ([@gaearon](https://github.com/gaearon) in [#13302](https://github.com/facebook/react/pull/13302))
|
||||
|
||||
## 16.3.2 (April 16, 2018)
|
||||
|
||||
### React
|
||||
@@ -177,6 +193,12 @@
|
||||
|
||||
* Fix a crash on updates. ([@rmhartog](https://github.com/rmhartog) in [#11955](https://github.com/facebook/react/pull/11955))
|
||||
|
||||
## 16.2.1 (August 1, 2018)
|
||||
|
||||
### React DOM Server
|
||||
|
||||
* Fix a [potential XSS vulnerability when the attacker controls an attribute name](https://reactjs.org/blog/2018/08/01/react-v-16-4-2.html) (`CVE-2018-6341`). This fix is available in the latest `react-dom@16.4.2`, as well as in previous affected minor versions: `react-dom@16.0.1`, `react-dom@16.1.2`, `react-dom@16.2.1`, and `react-dom@16.3.3`. ([@gaearon](https://github.com/gaearon) in [#13302](https://github.com/facebook/react/pull/13302))
|
||||
|
||||
## 16.2.0 (November 28, 2017)
|
||||
|
||||
### React
|
||||
@@ -201,6 +223,12 @@
|
||||
|
||||
* Many tests were rewritten against the public API. Big thanks to [everyone who contributed](https://github.com/facebook/react/issues/11299)!
|
||||
|
||||
## 16.1.2 (August 1, 2018)
|
||||
|
||||
### React DOM Server
|
||||
|
||||
* Fix a [potential XSS vulnerability when the attacker controls an attribute name](https://reactjs.org/blog/2018/08/01/react-v-16-4-2.html) (`CVE-2018-6341`). This fix is available in the latest `react-dom@16.4.2`, as well as in previous affected minor versions: `react-dom@16.0.1`, `react-dom@16.1.2`, `react-dom@16.2.1`, and `react-dom@16.3.3`. ([@gaearon](https://github.com/gaearon) in [#13302](https://github.com/facebook/react/pull/13302))
|
||||
|
||||
## 16.1.1 (November 13, 2017)
|
||||
|
||||
### React
|
||||
@@ -291,6 +319,12 @@ Starting with 16.1.0, we will no longer be publishing new releases on Bower. You
|
||||
|
||||
* First release of the [new experimental package](https://github.com/facebook/react/tree/master/packages/react-call-return) for parent-child communication. ([@gaearon](https://github.com/gaearon) in [#11364](https://github.com/facebook/react/pull/11364))
|
||||
|
||||
## 16.0.1 (August 1, 2018)
|
||||
|
||||
### React DOM Server
|
||||
|
||||
* Fix a [potential XSS vulnerability when the attacker controls an attribute name](https://reactjs.org/blog/2018/08/01/react-v-16-4-2.html) (`CVE-2018-6341`). This fix is available in the latest `react-dom@16.4.2`, as well as in previous affected minor versions: `react-dom@16.0.1`, `react-dom@16.1.2`, `react-dom@16.2.1`, and `react-dom@16.3.3`. ([@gaearon](https://github.com/gaearon) in [#13302](https://github.com/facebook/react/pull/13302))
|
||||
|
||||
## 16.0.0 (September 26, 2017)
|
||||
|
||||
### New JS Environment Requirements
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Code of Conduct
|
||||
|
||||
Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.facebook.com/pages/876921332402685/open-source-code-of-conduct) so that you can understand what actions will and will not be tolerated.
|
||||
Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.fb.com/codeofconduct/) so that you can understand what actions will and will not be tolerated.
|
||||
|
||||
37
README.md
@@ -8,15 +8,28 @@ React is a JavaScript library for building user interfaces.
|
||||
|
||||
[Learn how to use React in your own project](https://reactjs.org/docs/getting-started.html).
|
||||
|
||||
## Installation
|
||||
|
||||
React has been designed for gradual adoption from the start, and **you can use as little or as much React as you need**:
|
||||
|
||||
* Use [Online Playgrounds](https://reactjs.org/docs/getting-started.html#online-playgrounds) to get a taste of React.
|
||||
* [Add React to a Website](https://reactjs.org/docs/add-react-to-a-website.html) as a `<script>` tag in one minute.
|
||||
* [Create a New React App](https://reactjs.org/docs/create-a-new-react-app.html) if you're looking for a powerful JavaScript toolchain.
|
||||
|
||||
You can use React as a `<script>` tag from a [CDN](https://reactjs.org/docs/cdn-links.html), or as a `react` package on [npm](https://www.npmjs.com/).
|
||||
|
||||
## Documentation
|
||||
|
||||
You can find the React documentation [on the website](https://reactjs.org/docs).
|
||||
It is divided into several sections:
|
||||
|
||||
* [Quick Start](https://reactjs.org/docs/hello-world.html)
|
||||
Check out the [Getting Started](https://reactjs.org/docs/getting-started.html) page for a quick overview.
|
||||
|
||||
The documentation is divided into several sections:
|
||||
|
||||
* [Tutorial](https://reactjs.org/tutorial/tutorial.html)
|
||||
* [Main Concepts](https://reactjs.org/docs/hello-world.html)
|
||||
* [Advanced Guides](https://reactjs.org/docs/jsx-in-depth.html)
|
||||
* [API Reference](https://reactjs.org/docs/react-api.html)
|
||||
* [Tutorial](https://reactjs.org/tutorial/tutorial.html)
|
||||
* [Where to Get Support](https://reactjs.org/community/support.html)
|
||||
* [Contributing Guide](https://reactjs.org/docs/how-to-contribute.html)
|
||||
|
||||
@@ -34,26 +47,14 @@ class HelloMessage extends React.Component {
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<HelloMessage name="John" />,
|
||||
<HelloMessage name="Taylor" />,
|
||||
document.getElementById('container')
|
||||
);
|
||||
```
|
||||
|
||||
This example will render "Hello John" into a container on the page.
|
||||
This example will render "Hello Taylor" into a container on the page.
|
||||
|
||||
You'll notice that we used an HTML-like syntax; [we call it JSX](https://reactjs.org/docs/introducing-jsx.html). JSX is not required to use React, but it makes code more readable, and writing it feels like writing HTML. We recommend using [Babel](https://babeljs.io/) with a [React preset](https://babeljs.io/docs/plugins/preset-react/) to convert JSX into native JavaScript for browsers to digest.
|
||||
|
||||
## Installation
|
||||
|
||||
React is available as the `react` package on [npm](https://www.npmjs.com/). It is also available on a [CDN](https://reactjs.org/docs/cdn-links.html).
|
||||
|
||||
React is flexible and can be used in a variety of projects. You can create new apps with it, but you can also gradually introduce it into an existing codebase without doing a rewrite.
|
||||
|
||||
The recommended way to install React depends on your project. Here you can find short guides for the most common scenarios:
|
||||
|
||||
* [Trying Out React](https://reactjs.org/docs/try-react.html)
|
||||
* [Creating a New Application](https://reactjs.org/docs/add-react-to-a-new-app.html)
|
||||
* [Adding React to an Existing Application](https://reactjs.org/docs/add-react-to-an-existing-app.html)
|
||||
You'll notice that we used an HTML-like syntax; [we call it JSX](https://reactjs.org/docs/introducing-jsx.html). JSX is not required to use React, but it makes code more readable, and writing it feels like writing HTML. If you're using React as a `<script>` tag, read [this section](https://reactjs.org/docs/add-react-to-a-website.html#optional-try-react-with-jsx) on integrating JSX; otherwise, the [recommended JavaScript toolchains](https://reactjs.org/docs/create-a-new-react-app.html) handle it automatically.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ init:
|
||||
- git config --global core.autocrlf input
|
||||
|
||||
environment:
|
||||
nodejs_version: 8
|
||||
JAVA_HOME: C:\Program Files\Java\jdk1.8.0
|
||||
matrix:
|
||||
- nodejs_version: 10
|
||||
|
||||
# Finish on first failed build
|
||||
matrix:
|
||||
|
||||
@@ -20,7 +20,7 @@ The left box shows the property (or attribute) assigned by React 15.\*, and the
|
||||
right box shows the property (or attribute) assigned by the latest version of
|
||||
React 16.
|
||||
|
||||
Right now we use a purple outline to call out cases where the assigned property
|
||||
Right now, we use a purple outline to call out cases where the assigned property
|
||||
(or attribute) has changed between React 15 and 16.
|
||||
|
||||
---
|
||||
|
||||
@@ -65,6 +65,7 @@ class Header extends React.Component {
|
||||
<option value="/custom-elements">Custom Elements</option>
|
||||
<option value="/media-events">Media Events</option>
|
||||
<option value="/pointer-events">Pointer Events</option>
|
||||
<option value="/mouse-events">Mouse Events</option>
|
||||
</select>
|
||||
</label>
|
||||
<label htmlFor="react_version">
|
||||
|
||||
@@ -12,6 +12,7 @@ import EventPooling from './event-pooling';
|
||||
import CustomElementFixtures from './custom-elements';
|
||||
import MediaEventsFixtures from './media-events';
|
||||
import PointerEventsFixtures from './pointer-events';
|
||||
import MouseEventsFixtures from './mouse-events';
|
||||
|
||||
const React = window.React;
|
||||
|
||||
@@ -49,6 +50,8 @@ function FixturesPage() {
|
||||
return <MediaEventsFixtures />;
|
||||
case '/pointer-events':
|
||||
return <PointerEventsFixtures />;
|
||||
case '/mouse-events':
|
||||
return <MouseEventsFixtures />;
|
||||
default:
|
||||
return <p>Please select a test fixture.</p>;
|
||||
}
|
||||
|
||||
16
fixtures/dom/src/components/fixtures/mouse-events/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import FixtureSet from '../../FixtureSet';
|
||||
import MouseMovement from './mouse-movement';
|
||||
|
||||
const React = window.React;
|
||||
|
||||
class MouseEvents extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<FixtureSet title="Mouse Events" description="">
|
||||
<MouseMovement />
|
||||
</FixtureSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MouseEvents;
|
||||
@@ -0,0 +1,48 @@
|
||||
import TestCase from '../../TestCase';
|
||||
|
||||
const React = window.React;
|
||||
|
||||
class MouseMovement extends React.Component {
|
||||
state = {
|
||||
movement: {x: 0, y: 0},
|
||||
};
|
||||
|
||||
onMove = event => {
|
||||
this.setState({x: event.movementX, y: event.movementY});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {x, y} = this.state;
|
||||
|
||||
const boxStyle = {
|
||||
padding: '10px 20px',
|
||||
border: '1px solid #d9d9d9',
|
||||
margin: '10px 0 20px',
|
||||
};
|
||||
|
||||
return (
|
||||
<TestCase
|
||||
title="Mouse Movement"
|
||||
description="We polyfill the movementX and movementY fields."
|
||||
affectedBrowsers="IE, Safari">
|
||||
<TestCase.Steps>
|
||||
<li>Mouse over the box below</li>
|
||||
</TestCase.Steps>
|
||||
|
||||
<TestCase.ExpectedResult>
|
||||
The reported values should equal the pixel (integer) difference
|
||||
between mouse movements positions.
|
||||
</TestCase.ExpectedResult>
|
||||
|
||||
<div style={boxStyle} onMouseMove={this.onMove}>
|
||||
<p>Trace your mouse over this box.</p>
|
||||
<p>
|
||||
Last movement: {x},{y}
|
||||
</p>
|
||||
</div>
|
||||
</TestCase>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MouseMovement;
|
||||
@@ -158,6 +158,31 @@ class SelectFixture extends React.Component {
|
||||
</form>
|
||||
</div>
|
||||
</TestCase>
|
||||
|
||||
<TestCase
|
||||
title="An option which contains conditional render fails"
|
||||
relatedIssues="11911">
|
||||
<TestCase.Steps>
|
||||
<li>Select any option</li>
|
||||
</TestCase.Steps>
|
||||
<TestCase.ExpectedResult>
|
||||
Option should be set
|
||||
</TestCase.ExpectedResult>
|
||||
|
||||
<div className="test-fixture">
|
||||
<select value={this.state.value} onChange={this.onChange}>
|
||||
<option value="red">
|
||||
red {this.state.value === 'red' && 'is chosen '} TextNode
|
||||
</option>
|
||||
<option value="blue">
|
||||
blue {this.state.value === 'blue' && 'is chosen '} TextNode
|
||||
</option>
|
||||
<option value="green">
|
||||
green {this.state.value === 'green' && 'is chosen '} TextNode
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</TestCase>
|
||||
</FixtureSet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<ol>
|
||||
<li>
|
||||
<button onClick="runTestOne()">Run Test 1</button>
|
||||
<p>Calls the callback with the frame when not blocked:</p>
|
||||
<p>Calls the callback within the frame when not blocked:</p>
|
||||
<div><b>Expected:</b></div>
|
||||
<div id="test-1-expected">
|
||||
</div>
|
||||
@@ -79,6 +79,15 @@
|
||||
<p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
|
||||
<button onClick="runTestSix()">Run Test 6</button>
|
||||
</li>
|
||||
<li>
|
||||
<p>Continues calling callbacks even when user switches away from this tab</p>
|
||||
<button onClick="runTestSeven()">Run Test 7</button>
|
||||
<div><b>Click the button above, observe the counter, then switch to
|
||||
another tab and switch back:</b></div>
|
||||
<div id="test-7">
|
||||
</div>
|
||||
<div> If the counter advanced while you were away from this tab, it's correct.</div>
|
||||
</li>
|
||||
</ol>
|
||||
<script src="../../build/dist/react-scheduler.development.js"></script>
|
||||
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
|
||||
@@ -464,6 +473,22 @@ function runTestSix() {
|
||||
console.log('scheduled cbE');
|
||||
};
|
||||
}
|
||||
|
||||
function runTestSeven() {
|
||||
// Test 7
|
||||
// Calls callbacks, continues calling them even when this tab is in the
|
||||
// background
|
||||
clearTestResult(7);
|
||||
let counter = -1;
|
||||
function incrementCounterAndScheduleNextCallback() {
|
||||
const counterNode = document.getElementById('test-7');
|
||||
counter++;
|
||||
counterNode.innerHTML = counter;
|
||||
waitForTimeToPass(100);
|
||||
scheduleWork(incrementCounterAndScheduleNextCallback);
|
||||
}
|
||||
scheduleWork(incrementCounterAndScheduleNextCallback);
|
||||
}
|
||||
</script type="text/babel">
|
||||
</body>
|
||||
</html>
|
||||
|
||||
14
fixtures/unstable-async/suspense/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
|
||||
# testing
|
||||
coverage
|
||||
|
||||
# production
|
||||
build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
npm-debug.log
|
||||
33
fixtures/unstable-async/suspense/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# IO "suspense" demo
|
||||
|
||||
## What is this fixture?
|
||||
|
||||
This is a demo application based on [Dan Abramov's](https://github.com/gaearon) recent [JSConf Iceland talk](https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html) about React.
|
||||
|
||||
It depends on a local build of React and enables us to easily test async and "suspense" APIs in a more "real world app" like context.
|
||||
|
||||
## Can I use this code in production?
|
||||
|
||||
No. The APIs being tested here are unstable and some of them have still not been released to NPM. For now, this fixture is only a test harness.
|
||||
|
||||
## How do I run this fixture?
|
||||
|
||||
Clone the React repository.
|
||||
|
||||
Open `packages/shared/ReactFeatureFlags.js` locally (make sure you didn't open a similarly named file!) and set [the `enableSuspense` flag](https://github.com/facebook/react/blob/d79238f1eeb6634ba7a3df23c3b2709b56cbb8b2/packages/shared/ReactFeatureFlags.js#L19) to `true`.
|
||||
|
||||
Then follow these steps:
|
||||
|
||||
```shell
|
||||
# 1: Build react from source
|
||||
cd /path/to/react
|
||||
yarn
|
||||
yarn build dom,core,interaction,simple-cache-provider --type=NODE
|
||||
|
||||
# 2: Install fixture dependencies
|
||||
cd fixtures/unstable-async/suspense/
|
||||
yarn
|
||||
|
||||
# 3: Run the app
|
||||
yarn start
|
||||
```
|
||||
41
fixtures/unstable-async/suspense/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "io-demo",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": ".",
|
||||
"devDependencies": {
|
||||
"gh-pages": "^1.1.0",
|
||||
"react-scripts": "^1.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"clipboard": "^1.7.1",
|
||||
"github-fork-ribbon-css": "^0.2.1",
|
||||
"react": "../../../build/node_modules/react",
|
||||
"react-dom": "../../../build/node_modules/react-dom",
|
||||
"react-draggable": "^3.0.5",
|
||||
"simple-cache-provider": "../../../build/node_modules/simple-cache-provider"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
"deploy": "gh-pages -d build"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "./node_modules/react-scripts/config/eslint.js"
|
||||
},
|
||||
"browserslist": {
|
||||
"development": [
|
||||
"last 2 chrome versions",
|
||||
"last 2 firefox versions",
|
||||
"last 2 edge versions"
|
||||
],
|
||||
"production": [
|
||||
">1%",
|
||||
"last 4 versions",
|
||||
"Firefox ESR",
|
||||
"not ie < 11"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
fixtures/unstable-async/suspense/public/favicon.ico
Executable file
|
After Width: | Height: | Size: 24 KiB |
BIN
fixtures/unstable-async/suspense/public/img/acdlite.jpeg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
fixtures/unstable-async/suspense/public/img/bvaughn.jpeg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
fixtures/unstable-async/suspense/public/img/flarnie.jpeg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
fixtures/unstable-async/suspense/public/img/gaearon.jpeg
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
fixtures/unstable-async/suspense/public/img/sebmarkbage.jpeg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
fixtures/unstable-async/suspense/public/img/sophiebits.jpeg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
fixtures/unstable-async/suspense/public/img/trueadm.jpeg
Normal file
|
After Width: | Height: | Size: 26 KiB |
13
fixtures/unstable-async/suspense/public/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" href="./src/favicon.ico">
|
||||
<title>Movie List</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="debugger"></div>
|
||||
</body>
|
||||
</html>
|
||||
15
fixtures/unstable-async/suspense/public/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "Emoji Search",
|
||||
"name": "Emoji Search Example App",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
334
fixtures/unstable-async/suspense/src/api/data.js
Normal file
@@ -0,0 +1,334 @@
|
||||
export const coreContributorListJSON = [
|
||||
{
|
||||
id: 'acdlite',
|
||||
name: 'Andrew Clark',
|
||||
},
|
||||
{
|
||||
id: 'bvaughn',
|
||||
name: 'Brian Vaughn',
|
||||
},
|
||||
{
|
||||
id: 'gaearon',
|
||||
name: 'Dan Abramov',
|
||||
},
|
||||
{
|
||||
id: 'trueadm',
|
||||
name: 'Dominic Gannaway',
|
||||
},
|
||||
{
|
||||
id: 'flarnie',
|
||||
name: 'Flarnie Marchan',
|
||||
},
|
||||
{
|
||||
id: 'sebmarkbage',
|
||||
name: 'Sebastian Markbåge',
|
||||
},
|
||||
{
|
||||
id: 'sophiebits',
|
||||
name: 'Sophie Alpert',
|
||||
},
|
||||
];
|
||||
|
||||
export const userProfileJSON = {
|
||||
acdlite: {
|
||||
id: 'acdlite',
|
||||
name: 'Andrew Clark',
|
||||
image: '/img/acdlite.jpeg',
|
||||
location: 'Redwood City, CA',
|
||||
email: 'acdlite@me.com',
|
||||
tagline: 'React core at Facebook. Hi!',
|
||||
},
|
||||
bvaughn: {
|
||||
id: 'bvaughn',
|
||||
name: 'Brian Vaughn',
|
||||
image: '/img/bvaughn.jpeg',
|
||||
location: 'Mountain View, CA',
|
||||
email: 'brian.david.vaughn@gmail.com',
|
||||
tagline:
|
||||
'React JS core team at @facebook; formerly at @treasure-data and @google.',
|
||||
},
|
||||
gaearon: {
|
||||
id: 'gaearon',
|
||||
name: 'Dan Abramov',
|
||||
image: '/img/gaearon.jpeg',
|
||||
location: 'London, UK',
|
||||
email: 'dan.abramov@me.com',
|
||||
tagline:
|
||||
'Working on @reactjs. Co-author of Redux and Create React App. Building tools for humans.',
|
||||
},
|
||||
flarnie: {
|
||||
id: 'flarnie',
|
||||
name: 'Flarnie Marchan',
|
||||
image: '/img/flarnie.jpeg',
|
||||
location: 'Oakland, CA',
|
||||
email: null,
|
||||
tagline:
|
||||
'Software Engineer at Facebook React Core Team & Co-maintainer of Draft.js',
|
||||
},
|
||||
sebmarkbage: {
|
||||
id: 'sebmarkbage',
|
||||
name: 'Sebastian Markbåge',
|
||||
image: '/img/sebmarkbage.jpeg',
|
||||
location: 'San Francisco',
|
||||
email: 'sebastian@calyptus.eu',
|
||||
tagline: null,
|
||||
},
|
||||
sophiebits: {
|
||||
id: 'sophiebits',
|
||||
name: 'Sophie Alpert',
|
||||
image: '/img/sophiebits.jpeg',
|
||||
location: 'California',
|
||||
email: 'hi@sophiebits.com',
|
||||
tagline:
|
||||
'I like fixing things. eng manager of @reactjs at Facebook. ex-@khanacademy. 💎🌸 she/her. kindness, intersectional feminism, music.',
|
||||
},
|
||||
trueadm: {
|
||||
id: 'trueadm',
|
||||
name: 'Dominic Gannaway',
|
||||
image: '/img/trueadm.jpeg',
|
||||
location: 'London, United Kingdom',
|
||||
email: null,
|
||||
tagline:
|
||||
'Currently an engineer on the React core team at @facebook. Author of @infernojs and t7. Enjoys coding + being a Dad.',
|
||||
},
|
||||
};
|
||||
export const userRepositoriesListJSON = {
|
||||
acdlite: [
|
||||
{
|
||||
name: 'recompose',
|
||||
url: 'https://github.com/acdlite/recompose',
|
||||
description:
|
||||
'A React utility belt for function components and higher-order components.',
|
||||
},
|
||||
{
|
||||
name: 'react-fiber-architecture',
|
||||
url: 'https://github.com/acdlite/react-fiber-architecture',
|
||||
description: "A description of React's new core algorithm, React Fiber",
|
||||
},
|
||||
{
|
||||
name: 'redux-router',
|
||||
url: 'https://github.com/acdlite/redux-router',
|
||||
description:
|
||||
'Redux bindings for React Router – keep your router state inside your Redux store',
|
||||
},
|
||||
{
|
||||
name: 'flummox',
|
||||
url: 'https://github.com/acdlite/flummox',
|
||||
description: 'Minimal, isomorphic Flux.',
|
||||
},
|
||||
{
|
||||
name: 'redux-rx',
|
||||
url: 'https://github.com/acdlite/redux-rx',
|
||||
description: 'RxJS utilities for Redux.',
|
||||
},
|
||||
{
|
||||
name: 'react-remarkable',
|
||||
url: 'https://github.com/acdlite/react-remarkable',
|
||||
description: 'A React component for rendering Markdown with remarkable',
|
||||
},
|
||||
],
|
||||
bvaughn: [
|
||||
{
|
||||
name: 'react-virtualized',
|
||||
url: 'https://github.com/bvaughn/react-virtualized',
|
||||
description:
|
||||
'React components for efficiently rendering large lists and tabular data',
|
||||
},
|
||||
{
|
||||
name: 'redux-search',
|
||||
url: 'https://github.com/bvaughn/redux-search',
|
||||
description: 'Redux bindings for client-side search',
|
||||
},
|
||||
{
|
||||
name: 'react-window',
|
||||
url: 'https://github.com/bvaughn/react-window',
|
||||
description:
|
||||
'React components for efficiently rendering large lists and tabular data',
|
||||
},
|
||||
{
|
||||
name: 'react-virtualized-select',
|
||||
url: 'https://github.com/bvaughn/react-virtualized-select',
|
||||
description:
|
||||
'HOC that uses react-virtualized and react-select to display large lists of options in a drop-down',
|
||||
},
|
||||
{
|
||||
name: 'js-search',
|
||||
url: 'https://github.com/bvaughn/js-search',
|
||||
description:
|
||||
'JS Search is an efficient, client-side search library for JavaScript and JSON objects',
|
||||
},
|
||||
{
|
||||
name: 'react-highlight-words',
|
||||
url: 'https://github.com/bvaughn/react-highlight-words',
|
||||
description:
|
||||
'React component to highlight words within a larger body of text',
|
||||
},
|
||||
],
|
||||
gaearon: [
|
||||
{
|
||||
name: 'facebook/react',
|
||||
url: 'https://github.com/facebook/react',
|
||||
description:
|
||||
'A declarative, efficient, and flexible JavaScript library for building user interfaces.',
|
||||
},
|
||||
{
|
||||
name: 'reduxjs/redux',
|
||||
url: 'https://github.com/reduxjs/redux',
|
||||
description: 'Predictable state container for JavaScript apps',
|
||||
},
|
||||
{
|
||||
name: 'facebook/create-react-app',
|
||||
url: 'https://github.com/facebook/create-react-app',
|
||||
description: 'Create React apps with no build configuration.',
|
||||
},
|
||||
{
|
||||
name: 'reduxjs/redux-devtools',
|
||||
url: 'https://github.com/reduxjs/redux-devtools',
|
||||
description:
|
||||
'DevTools for Redux with hot reloading, action replay, and customizable UI',
|
||||
},
|
||||
{
|
||||
name: 'react-dnd/react-dnd',
|
||||
url: 'https://github.com/react-dnd/react-dnd',
|
||||
description: 'Drag and Drop for React',
|
||||
},
|
||||
{
|
||||
name: 'paularmstrong/normalizr',
|
||||
url: 'https://github.com/paularmstrong/normalizr',
|
||||
description: 'Normalizes nested JSON according to a schema',
|
||||
},
|
||||
],
|
||||
flarnie: [
|
||||
{
|
||||
name: 'diffux/diffux',
|
||||
url: 'https://github.com/diffux/diffux',
|
||||
description: 'Perceptual diffs of responsive screenshots made simple.',
|
||||
},
|
||||
{
|
||||
name: 'facebook/draft-js',
|
||||
url: 'https://github.com/facebook/draft-js',
|
||||
description: 'A React framework for building text editors.',
|
||||
},
|
||||
{
|
||||
name: 'facebook/react',
|
||||
url: 'https://github.com/facebook/react',
|
||||
description:
|
||||
'A declarative, efficient, and flexible JavaScript library for building user interfaces.',
|
||||
},
|
||||
{
|
||||
name: 'facebook/jest',
|
||||
url: 'https://github.com/facebook/jest',
|
||||
description: '🃏 Delightful JavaScript Testing.',
|
||||
},
|
||||
{
|
||||
name: 'Galooshi/import-js',
|
||||
url: 'https://github.com/Galooshi/import-js',
|
||||
description: 'A tool to simplify importing JS modules',
|
||||
},
|
||||
{
|
||||
name: 'webpack_rails_demo',
|
||||
url: 'https://github.com/flarnie/webpack_rails_demo',
|
||||
description: 'Setting up webpack with Ruby on Rails: a basic demo',
|
||||
},
|
||||
],
|
||||
sebmarkbage: [
|
||||
{
|
||||
name: 'art',
|
||||
url: 'https://github.com/sebmarkbage/art',
|
||||
description:
|
||||
"Retained mode vector drawing API designed for multiple output modes. There's also a built-in SVG parser.",
|
||||
},
|
||||
{
|
||||
name: 'ecmascript-immutable-data-structures',
|
||||
url:
|
||||
'https://github.com/sebmarkbage/ecmascript-immutable-data-structures',
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
name: 'ocamlrun-wasm',
|
||||
url: 'https://github.com/sebmarkbage/ocamlrun-wasm',
|
||||
description: 'OCamlrun WebAssembly - OCaml Bytecode Interpreter in WASM',
|
||||
},
|
||||
{
|
||||
name: 'ecmascript-generator-expression',
|
||||
url: 'https://github.com/sebmarkbage/ecmascript-generator-expression',
|
||||
description:
|
||||
'Proposal for do Generator Expressions in ECMAScript. Work in progress. Edit Add topics',
|
||||
},
|
||||
{
|
||||
name: 'ecmascript-undefined-propagation',
|
||||
url: 'https://github.com/sebmarkbage/ecmascript-undefined-propagation',
|
||||
description:
|
||||
'ECMAScript proposal to relax the rules to return `undefined` for property access on `null` or `undefined` instead of throwing.',
|
||||
},
|
||||
{
|
||||
name: 'ecmascript-shallow-equal',
|
||||
url: 'https://github.com/sebmarkbage/ecmascript-shallow-equal',
|
||||
description: 'A proposal for ECMAScript for Object.shallowEqual',
|
||||
},
|
||||
],
|
||||
sophiebits: [
|
||||
{
|
||||
name: 'facebook/react',
|
||||
url: 'https://github.com/facebook/react',
|
||||
description:
|
||||
'A declarative, efficient, and flexible JavaScript library for building user interfaces.',
|
||||
},
|
||||
{
|
||||
name: 'Khan/KaTeX',
|
||||
url: 'https://github.com/Khan/KaTeX',
|
||||
description: 'Fast math typesetting for the web.',
|
||||
},
|
||||
{
|
||||
name: 'facebook/react-devtools',
|
||||
url: 'https://github.com/facebook/react-devtools',
|
||||
description:
|
||||
'An extension that allows inspection of React component hierarchy in the Chrome and Firefox Developer Tools.',
|
||||
},
|
||||
{
|
||||
name: 'vim-awesome/vim-awesome',
|
||||
url: 'https://github.com/vim-awesome/vim-awesome',
|
||||
description: 'Awesome Vim plugins from across the universe',
|
||||
},
|
||||
{
|
||||
name: 'facebook/draft-js',
|
||||
url: 'https://github.com/facebook/draft-js',
|
||||
description: 'A React framework for building text editors.',
|
||||
},
|
||||
{
|
||||
name: 'es3ify',
|
||||
url: 'https://github.com/sophiebits/es3ify',
|
||||
description:
|
||||
'Browserify transform to convert ES5 syntax to be ES3-compatible.',
|
||||
},
|
||||
],
|
||||
trueadm: [
|
||||
{
|
||||
name: 'facebook/react',
|
||||
url: 'https://github.com/facebook/react',
|
||||
description:
|
||||
'A declarative, efficient, and flexible JavaScript library for building user interfaces.',
|
||||
},
|
||||
{
|
||||
name: 'infernojs/inferno',
|
||||
url: 'https://github.com/infernojs/inferno',
|
||||
description:
|
||||
'An extremely fast, React-like JavaScript library for building modern user interfaces',
|
||||
},
|
||||
{
|
||||
name: 'facebook/prepack',
|
||||
url: 'https://github.com/facebook/prepack',
|
||||
description: 'A JavaScript bundle optimizer.',
|
||||
},
|
||||
{
|
||||
name: 't7',
|
||||
url: 'https://github.com/trueadm/t7',
|
||||
description: 'Lightweight virtual DOM templating library',
|
||||
},
|
||||
{
|
||||
name: 'infernojs/babel-plugin-inferno',
|
||||
url: 'https://github.com/infernojs/babel-plugin-inferno',
|
||||
description: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
67
fixtures/unstable-async/suspense/src/api/index.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
coreContributorListJSON,
|
||||
userProfileJSON,
|
||||
userRepositoriesListJSON,
|
||||
} from './data';
|
||||
|
||||
export function fetchCoreContributorListJSON() {
|
||||
return makeFakeAPICall('/react/contributors', coreContributorListJSON);
|
||||
}
|
||||
|
||||
export function fetchUserProfileJSON(id) {
|
||||
return makeFakeAPICall(`/${id}/details`, userProfileJSON[id]);
|
||||
}
|
||||
|
||||
export function fetchUserRepositoriesListJSON(id) {
|
||||
return makeFakeAPICall(`/${id}/repositories`, userRepositoriesListJSON[id]);
|
||||
}
|
||||
|
||||
let fakeRequestTime = 1000;
|
||||
let onProgress = () => true;
|
||||
|
||||
export function setFakeRequestTime(val) {
|
||||
fakeRequestTime = val;
|
||||
}
|
||||
|
||||
export function setProgressHandler(handler) {
|
||||
onProgress = handler;
|
||||
}
|
||||
|
||||
export function setPauseNewRequests(value) {
|
||||
shouldPauseNewRequests = value;
|
||||
}
|
||||
|
||||
let shouldPauseNewRequests = false;
|
||||
let notifiers = {};
|
||||
let isPausedUrl = {};
|
||||
|
||||
export function setPaused(url, isPaused) {
|
||||
const wasPaused = isPausedUrl[url];
|
||||
isPausedUrl[url] = isPaused;
|
||||
if (isPaused !== wasPaused) {
|
||||
notifiers[url]();
|
||||
}
|
||||
}
|
||||
|
||||
function makeFakeAPICall(url, result) {
|
||||
let i = 1;
|
||||
return new Promise(resolve => {
|
||||
isPausedUrl[url] = shouldPauseNewRequests;
|
||||
function notify() {
|
||||
if (!isPausedUrl[url]) {
|
||||
i++;
|
||||
}
|
||||
onProgress(url, i, isPausedUrl[url]);
|
||||
if (isPausedUrl[url]) {
|
||||
return;
|
||||
}
|
||||
if (i === 100) {
|
||||
resolve(result);
|
||||
} else {
|
||||
setTimeout(notify, fakeRequestTime / 100);
|
||||
}
|
||||
}
|
||||
notifiers[url] = notify;
|
||||
notify();
|
||||
});
|
||||
}
|
||||
7
fixtures/unstable-async/suspense/src/cache.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import {createCache} from 'simple-cache-provider';
|
||||
|
||||
export let cache;
|
||||
function initCache() {
|
||||
cache = createCache(initCache);
|
||||
}
|
||||
initCache();
|
||||
82
fixtures/unstable-async/suspense/src/components/App.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, {Fragment, Placeholder, PureComponent} from 'react';
|
||||
import {unstable_deferredUpdates} from 'react-dom';
|
||||
import {createResource} from 'simple-cache-provider';
|
||||
import {cache} from '../cache';
|
||||
import Spinner from './Spinner';
|
||||
import ContributorListPage from './ContributorListPage';
|
||||
|
||||
const UserPageResource = createResource(() => import('./UserPage'));
|
||||
|
||||
function UserPageLoader(props) {
|
||||
const UserPage = UserPageResource.read(cache).default;
|
||||
return <UserPage {...props} />;
|
||||
}
|
||||
|
||||
export default class App extends PureComponent {
|
||||
state = {
|
||||
currentId: null,
|
||||
showDetail: false,
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (
|
||||
prevState.showDetail !== this.state.showDetail ||
|
||||
prevState.currentId !== this.state.currentId
|
||||
) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
handleUserClick = id => {
|
||||
this.setState({
|
||||
currentId: id,
|
||||
});
|
||||
unstable_deferredUpdates(() => {
|
||||
this.setState({
|
||||
showDetail: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
handleBackClick = () =>
|
||||
this.setState({
|
||||
currentId: null,
|
||||
showDetail: false,
|
||||
});
|
||||
|
||||
render() {
|
||||
const {currentId, showDetail} = this.state;
|
||||
return showDetail
|
||||
? this.renderDetail(currentId)
|
||||
: this.renderList(currentId);
|
||||
}
|
||||
|
||||
renderDetail(id) {
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={this.handleBackClick}
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: '1rem',
|
||||
}}>
|
||||
Return to list
|
||||
</button>
|
||||
<Placeholder delayMs={2000} fallback={<Spinner size="large" />}>
|
||||
<UserPageLoader id={id} />
|
||||
</Placeholder>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderList(loadingId) {
|
||||
return (
|
||||
<Placeholder delayMs={1500} fallback={<Spinner size="large" />}>
|
||||
<ContributorListPage
|
||||
loadingId={loadingId}
|
||||
onUserClick={this.handleUserClick}
|
||||
/>
|
||||
</Placeholder>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, {Fragment} from 'react';
|
||||
import {createResource} from 'simple-cache-provider';
|
||||
import {cache} from '../cache';
|
||||
import Spinner from './Spinner';
|
||||
import {fetchCoreContributorListJSON} from '../api';
|
||||
|
||||
const ContributorListResource = createResource(fetchCoreContributorListJSON);
|
||||
|
||||
const ContributorListPage = ({loadingId, onUserClick}) => (
|
||||
<Fragment>
|
||||
<h1>React Core Team</h1>
|
||||
<ul
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridGap: '0.5rem',
|
||||
gridTemplateColumns: 'repeat(auto-fill, 20rem)',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
}}>
|
||||
{ContributorListResource.read(cache).map(user => (
|
||||
<ContributorListItem
|
||||
key={user.id}
|
||||
onClick={() => onUserClick(user.id)}
|
||||
isLoading={loadingId && user.id === loadingId}
|
||||
user={user}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const ContributorListItem = ({isLoading, onClick, user}) => (
|
||||
<li
|
||||
onClick={onClick}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '1rem',
|
||||
backgroundColor: 'var(--color-buttonBg)',
|
||||
border: '1px solid var(--color-buttonBorder)',
|
||||
borderRadius: '1rem',
|
||||
opacity: isLoading === false ? 0.5 : 1,
|
||||
cursor: isLoading ? 'default' : 'pointer',
|
||||
}}
|
||||
tabIndex="0">
|
||||
<div>
|
||||
<strong>{user.name}</strong>
|
||||
<div style={{marginTop: '0.5rem'}}>{user.id}</div>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Spinner size="small" />
|
||||
) : (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
|
||||
</svg>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
|
||||
export default ContributorListPage;
|
||||
68
fixtures/unstable-async/suspense/src/components/Spinner.css
Normal file
@@ -0,0 +1,68 @@
|
||||
.Spinner {
|
||||
animation: rotate 1.3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(270deg); }
|
||||
}
|
||||
|
||||
.SmallSpinnerPath {
|
||||
stroke-dasharray: 100;
|
||||
stroke-dashoffset: 0;
|
||||
transform-origin: center;
|
||||
animation:
|
||||
SmallDash 1.3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes SmallDash {
|
||||
0% { stroke-dashoffset: 100; }
|
||||
50% {
|
||||
stroke-dashoffset: 50;
|
||||
transform:rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 100;
|
||||
transform:rotate(450deg);
|
||||
}
|
||||
}
|
||||
|
||||
.MediumSpinnerPath {
|
||||
stroke-dasharray: 150;
|
||||
stroke-dashoffset: 0;
|
||||
transform-origin: center;
|
||||
animation:
|
||||
MediumDash 1.3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes MediumDash {
|
||||
0% { stroke-dashoffset: 150; }
|
||||
50% {
|
||||
stroke-dashoffset: 50;
|
||||
transform:rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 150;
|
||||
transform:rotate(450deg);
|
||||
}
|
||||
}
|
||||
|
||||
.LargeSpinnerPath {
|
||||
stroke-dasharray: 200;
|
||||
stroke-dashoffset: 0;
|
||||
transform-origin: center;
|
||||
animation:
|
||||
LargeDash 1.3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes LargeDash {
|
||||
0% { stroke-dashoffset: 200; }
|
||||
50% {
|
||||
stroke-dashoffset: 50;
|
||||
transform:rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 200;
|
||||
transform:rotate(450deg);
|
||||
}
|
||||
}
|
||||
48
fixtures/unstable-async/suspense/src/components/Spinner.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import './Spinner.css';
|
||||
|
||||
const SPINNER_SIZES = {
|
||||
small: 30,
|
||||
medium: 50,
|
||||
large: 70,
|
||||
};
|
||||
|
||||
const STROKE_WIDTHS = {
|
||||
small: 4,
|
||||
medium: 5,
|
||||
large: 6,
|
||||
};
|
||||
|
||||
const PATH_CLASS_NAMES = {
|
||||
small: 'SmallSpinnerPath',
|
||||
medium: 'MediumSpinnerPath',
|
||||
large: 'LargeSpinnerPath',
|
||||
};
|
||||
|
||||
// Heavily inspired by https://codepen.io/mrrocks/pen/EiplA
|
||||
export default function Spinner({size = 'small'}) {
|
||||
const baseSize = SPINNER_SIZES[size];
|
||||
const pathSize = baseSize / 2;
|
||||
const strokeWidth = STROKE_WIDTHS[size];
|
||||
const pathRadius = `${baseSize / 2 - strokeWidth}px`;
|
||||
const className = PATH_CLASS_NAMES[size];
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width={baseSize}
|
||||
height={baseSize}
|
||||
viewBox={`0 0 ${baseSize} ${baseSize}`}>
|
||||
<circle
|
||||
className="SpinnerPath"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
cx={pathSize}
|
||||
cy={pathSize}
|
||||
r={pathRadius}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
169
fixtures/unstable-async/suspense/src/components/UserPage.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import React, {Fragment, Placeholder} from 'react';
|
||||
import {createResource} from 'simple-cache-provider';
|
||||
import Spinner from './Spinner';
|
||||
import {cache} from '../cache';
|
||||
import {fetchUserProfileJSON, fetchUserRepositoriesListJSON} from '../api';
|
||||
|
||||
export default function UserPage({id}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, 20rem)',
|
||||
gridGap: '1rem',
|
||||
alignItems: 'start',
|
||||
}}>
|
||||
<UserDetails id={id} />
|
||||
<Placeholder delayMs={1000} fallback={<Spinner size="medium" />}>
|
||||
<Repositories id={id} />
|
||||
</Placeholder>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const UserDetailsResource = createResource(fetchUserProfileJSON);
|
||||
|
||||
function UserDetails({id}) {
|
||||
const user = UserDetailsResource.read(cache, id);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridGap: '0.5rem',
|
||||
width: '20rem',
|
||||
padding: '1rem',
|
||||
backgroundColor: 'var(--color-buttonBg)',
|
||||
border: '1px solid var(--color-buttonBorder)',
|
||||
borderRadius: '1rem',
|
||||
}}>
|
||||
<UserPicture source={user.image} />
|
||||
<div
|
||||
style={{
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 'bold',
|
||||
color: 'var(--color-pageTextDark)',
|
||||
}}>
|
||||
{user.name}
|
||||
</div>
|
||||
<div style={{fontSize: '1.25rem'}}>{user.id}</div>
|
||||
{user.tagline !== null && <div>{user.tagline}</div>}
|
||||
<hr
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
border: 'none',
|
||||
backgroundColor: '#ddd',
|
||||
}}
|
||||
/>
|
||||
{user.location && <Location location={user.location} />}
|
||||
{user.email && <Email email={user.email} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Location = ({location}) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
marginRight: '0.5rem',
|
||||
fill: 'currentColor',
|
||||
}}>
|
||||
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" />
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
</svg>
|
||||
{location}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Email = ({email}) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
marginRight: '0.5rem',
|
||||
fill: 'currentColor',
|
||||
}}>
|
||||
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z" />
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
</svg>
|
||||
<a href={`mailto:${email}`}>{email}</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ImageResource = createResource(
|
||||
src =>
|
||||
new Promise(resolve => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(src);
|
||||
img.src = src;
|
||||
})
|
||||
);
|
||||
|
||||
function Img({src, ...rest}) {
|
||||
return <img src={ImageResource.read(cache, src)} {...rest} />;
|
||||
}
|
||||
|
||||
function UserPicture({source}) {
|
||||
return (
|
||||
<Placeholder delayMs={1500} fallback={<img src={source} alt="poster" />}>
|
||||
<Img
|
||||
src={source}
|
||||
alt="profile picture"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
/>
|
||||
</Placeholder>
|
||||
);
|
||||
}
|
||||
|
||||
const UserRepositoriesResource = createResource(fetchUserRepositoriesListJSON);
|
||||
|
||||
function Repositories({id}) {
|
||||
const reviews = UserRepositoriesResource.read(cache, id);
|
||||
return (
|
||||
<ul
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridGap: '1rem',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
}}>
|
||||
{reviews.map(review => <Repository key={review.id} {...review} />)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function Repository({description, name, url}) {
|
||||
return (
|
||||
<li
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridGap: '0.5rem',
|
||||
padding: '1rem',
|
||||
backgroundColor: 'var(--color-buttonBg)',
|
||||
border: '1px solid var(--color-buttonBorder)',
|
||||
borderRadius: '1rem',
|
||||
}}>
|
||||
<strong>
|
||||
<a href={url}>{name}</a>
|
||||
</strong>
|
||||
<div>{description}</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
90
fixtures/unstable-async/suspense/src/index.css
Normal file
@@ -0,0 +1,90 @@
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--color-debuggerBg: #f7f7f7;
|
||||
--color-debuggerText: #333;
|
||||
--color-debuggerBorder: #e7e7e7;
|
||||
|
||||
--color-panelBg: #f7f7f7;
|
||||
--color-panelText: #333;
|
||||
|
||||
--color-pageTextDark: #000;
|
||||
--color-pageText: #333;
|
||||
--color-pageBg: #fff;
|
||||
|
||||
--color-buttonBg: #f7f7f7;
|
||||
--color-buttonBorder: #e7e7e7;
|
||||
|
||||
--pt: 8px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: calc(var(--pt)*4);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
color: var(--color-pageText);
|
||||
background-color: var(--color-pageBg);
|
||||
}
|
||||
|
||||
/* -------------------------------- */
|
||||
/* Debugger */
|
||||
/* -------------------------------- */
|
||||
|
||||
#debugger {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
/* width: 100vw; */
|
||||
/* height: 100vh; */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.🎛 {
|
||||
position: fixed;
|
||||
max-width: calc(var(--pt)*28);
|
||||
border-radius: var(--pt);
|
||||
padding: calc(var(--pt)*2);
|
||||
background-color: var(--color-debuggerBg);
|
||||
border: 1px solid var(--color-debuggerBorder);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--color-debuggerText);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.🕹 {
|
||||
background-color: var(--color-buttonBg);
|
||||
border: 1px solid var(--color-buttonBorder);
|
||||
border-radius: var(--pt);
|
||||
padding: 0;
|
||||
width: calc(var(--pt)*5);
|
||||
height: calc(var(--pt)*5);
|
||||
font-size: calc(var(--pt)*3);
|
||||
line-height: 1;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.🕹.👶 {
|
||||
width: calc(var(--pt)*3);
|
||||
height: calc(var(--pt)*3);
|
||||
font-size: calc(var(--pt)*2);
|
||||
}
|
||||
|
||||
.🕹.🐘 {
|
||||
width: auto;
|
||||
height: calc(var(--pt)*7);
|
||||
padding: var(--pt) calc(var(--pt)*2);
|
||||
}
|
||||
|
||||
.🕹:hover {
|
||||
background-color: white;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
box-shadow: 4px 4px 0 var(--color-buttonBorder);
|
||||
}
|
||||
|
||||
.🕹:active {
|
||||
box-shadow: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
272
fixtures/unstable-async/suspense/src/index.js
Normal file
@@ -0,0 +1,272 @@
|
||||
import React, {Fragment, PureComponent} from 'react';
|
||||
import {unstable_createRoot, render} from 'react-dom';
|
||||
import {cache} from './cache';
|
||||
import {
|
||||
setFakeRequestTime,
|
||||
setPaused,
|
||||
setPauseNewRequests,
|
||||
setProgressHandler,
|
||||
} from './api';
|
||||
import App from './components/App';
|
||||
import Draggable from 'react-draggable';
|
||||
import './index.css';
|
||||
|
||||
let handleReset;
|
||||
|
||||
class Shell extends PureComponent {
|
||||
state = {
|
||||
iteration: 0,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
handleReset = this.handleReset;
|
||||
}
|
||||
|
||||
handleReset = () =>
|
||||
this.setState(prevState => ({
|
||||
iteration: prevState.iteration + 1,
|
||||
}));
|
||||
|
||||
render() {
|
||||
return <App key={this.state.iteration} />;
|
||||
}
|
||||
}
|
||||
|
||||
class Debugger extends PureComponent {
|
||||
state = {
|
||||
iteration: 0,
|
||||
strategy: 'async',
|
||||
requestTime: 1,
|
||||
showDebugger: false,
|
||||
pauseNewRequests: false,
|
||||
waitTime: 0,
|
||||
requests: {},
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
setFakeRequestTime(this.state.requestTime * 1000);
|
||||
setProgressHandler(this.handleProgress);
|
||||
window.addEventListener('keydown', e => {
|
||||
if (e.key.toLowerCase() === '/') {
|
||||
this.setState(state => ({
|
||||
showDebugger: !state.showDebugger,
|
||||
}));
|
||||
} else if (e.key.toLowerCase() === 'p') {
|
||||
this.togglePauseRequests();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.requestTime !== this.state.requestTime) {
|
||||
setFakeRequestTime(this.state.requestTime * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
cache.invalidate();
|
||||
this.setState(state => ({
|
||||
requests: {},
|
||||
}));
|
||||
handleReset();
|
||||
};
|
||||
|
||||
handleProgress = (url, progress, isPaused) => {
|
||||
this.setState(state => ({
|
||||
requests: {
|
||||
...state.requests,
|
||||
[url]: {
|
||||
url,
|
||||
progress,
|
||||
isPaused,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
togglePauseRequests = () => {
|
||||
this.setState(
|
||||
prevState => {
|
||||
return {pauseNewRequests: !prevState.pauseNewRequests};
|
||||
},
|
||||
() => {
|
||||
setPauseNewRequests(this.state.pauseNewRequests);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.showDebugger) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Draggable cancel="input">
|
||||
<div
|
||||
className="🎛"
|
||||
style={{
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
}}>
|
||||
<div>
|
||||
Latency: {this.state.requestTime} second{this.state.requestTime !==
|
||||
1
|
||||
? 's'
|
||||
: ''}{' '}
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="3"
|
||||
step="0.5"
|
||||
style={{width: '100%'}}
|
||||
value={this.state.requestTime}
|
||||
onChange={e => {
|
||||
e.stopPropagation();
|
||||
this.setState({requestTime: parseFloat(e.target.value)});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={this.state.pauseNewRequests}
|
||||
onChange={this.togglePauseRequests}
|
||||
/>
|
||||
Pause new requests
|
||||
</label>
|
||||
<br />
|
||||
<br />
|
||||
{Object.values(this.state.requests).filter(x => x.progress !== 100)
|
||||
.length > 0 ? (
|
||||
<Fragment>
|
||||
<div style={{marginBottom: 10}}>
|
||||
<b>Loading</b>
|
||||
</div>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<div style={{marginBottom: 10}}>
|
||||
<b>Loading</b>
|
||||
</div>
|
||||
<small style={{height: 20, display: 'block'}}>(None)</small>
|
||||
</Fragment>
|
||||
)}
|
||||
{Object.keys(this.state.requests)
|
||||
.reverse()
|
||||
.map(url => {
|
||||
const {progress, isPaused} = this.state.requests[url];
|
||||
if (progress === 100) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={url}
|
||||
style={{
|
||||
height: 20,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
title: isPaused ? 'Resume' : 'Pause',
|
||||
}}
|
||||
onClick={e => {
|
||||
setPaused(url, !isPaused);
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: progress + '%',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundColor: isPaused ? '#fbfb0e' : '#61dafb',
|
||||
zIndex: -1,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}>
|
||||
{url}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{Object.values(this.state.requests).filter(x => x.progress === 100)
|
||||
.length > 0 ? (
|
||||
<Fragment>
|
||||
<br />
|
||||
<div style={{marginBottom: 10}}>
|
||||
<b>Cached</b>{' '}
|
||||
<button
|
||||
style={{
|
||||
height: 16,
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={this.handleReset}>
|
||||
🗑
|
||||
</button>
|
||||
</div>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<br />
|
||||
<div style={{marginBottom: 10}}>
|
||||
<b>Cached</b>
|
||||
</div>
|
||||
<small style={{height: 20, display: 'block'}}>(None)</small>
|
||||
</Fragment>
|
||||
)}
|
||||
{Object.keys(this.state.requests)
|
||||
.reverse()
|
||||
.map(url => {
|
||||
const {progress} = this.state.requests[url];
|
||||
if (progress !== 100) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={url}
|
||||
style={{
|
||||
height: 20,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: progress + '%',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundColor:
|
||||
progress !== 100 ? '#61dafb' : 'lightgreen',
|
||||
zIndex: -1,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}>
|
||||
{url}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unstable_createRoot(document.getElementById('root')).render(<Shell />);
|
||||
|
||||
render(<Debugger />, document.getElementById('debugger'));
|
||||
7435
fixtures/unstable-async/suspense/yarn.lock
Normal file
21
fixtures/unstable-async/time-slicing/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
29
fixtures/unstable-async/time-slicing/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# CPU async rendering demo
|
||||
|
||||
## What is this fixture?
|
||||
|
||||
This is a demo application based on [Dan Abramov's](https://github.com/gaearon) recent [JSConf Iceland talk](https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html) about React.
|
||||
|
||||
It depends on a local build of React and enables us to easily test async "time slicing" APIs in a more "real world app" like context.
|
||||
|
||||
## Can I use this code in production?
|
||||
|
||||
No. The APIs being tested here are unstable and some of them have still not been released to NPM. For now, this fixture is only a test harness.
|
||||
|
||||
There are also known bugs and inefficiencies in master so **don't use this fixture for demonstration purposes either yet**. Until they are fixed, this fixture is **not** indicative of React async rendering performance.
|
||||
|
||||
## How do I run this fixture?
|
||||
|
||||
```shell
|
||||
# 1: Build react from source
|
||||
cd /path/to/react
|
||||
yarn
|
||||
yarn build dom,core,interaction,simple-cache-provider --type=NODE
|
||||
|
||||
# 2: Install fixture dependencies
|
||||
cd fixtures/unstable-async/time-slicing/
|
||||
yarn
|
||||
|
||||
# 3: Run the app
|
||||
yarn start
|
||||
```
|
||||
20
fixtures/unstable-async/time-slicing/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "cpu-demo",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"glamor": "^2.20.40",
|
||||
"react": "../../../build/node_modules/react",
|
||||
"react-dom": "../../../build/node_modules/react-dom",
|
||||
"react-markdown": "^3.2.0",
|
||||
"react-scripts": "^1.1.4",
|
||||
"simple-cache-provider": "../../../build/node_modules/simple-cache-provider",
|
||||
"victory": "^0.25.6"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
}
|
||||
}
|
||||
BIN
fixtures/unstable-async/time-slicing/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
40
fixtures/unstable-async/time-slicing/public/index.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
15
fixtures/unstable-async/time-slicing/public/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
126
fixtures/unstable-async/time-slicing/src/Charts.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import {
|
||||
VictoryArea,
|
||||
VictoryAxis,
|
||||
VictoryChart,
|
||||
VictoryBar,
|
||||
VictoryTheme,
|
||||
VictoryScatter,
|
||||
VictoryStack,
|
||||
} from 'victory';
|
||||
|
||||
const colors = ['#fff489', '#fa57c1', '#b166cc', '#7572ff', '#69a6f9'];
|
||||
|
||||
export default class Charts extends PureComponent {
|
||||
render() {
|
||||
const streamData = this.props.data;
|
||||
return (
|
||||
<div>
|
||||
<div style={{display: 'flex'}}>
|
||||
<VictoryChart
|
||||
theme={VictoryTheme.material}
|
||||
width={400}
|
||||
height={400}
|
||||
style={{
|
||||
parent: {
|
||||
backgroundColor: '#222',
|
||||
},
|
||||
}}>
|
||||
<VictoryAxis
|
||||
style={{
|
||||
axis: {stroke: 'white'},
|
||||
tickLabels: {fill: 'white'},
|
||||
}}
|
||||
/>
|
||||
<VictoryAxis
|
||||
style={{
|
||||
axis: {stroke: 'white'},
|
||||
tickLabels: {fill: 'white'},
|
||||
}}
|
||||
dependentAxis
|
||||
/>
|
||||
<VictoryScatter
|
||||
data={streamData[0]}
|
||||
size={6}
|
||||
style={{
|
||||
data: {
|
||||
fill: d => colors[d.x % 5],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</VictoryChart>
|
||||
|
||||
<VictoryChart
|
||||
theme={VictoryTheme.material}
|
||||
width={400}
|
||||
height={400}
|
||||
style={{
|
||||
parent: {
|
||||
backgroundColor: '#222',
|
||||
},
|
||||
}}
|
||||
domainPadding={[20, 20]}>
|
||||
<VictoryAxis
|
||||
style={{
|
||||
axis: {stroke: 'white'},
|
||||
tickLabels: {fill: 'white'},
|
||||
}}
|
||||
/>
|
||||
<VictoryAxis
|
||||
style={{
|
||||
axis: {stroke: 'white'},
|
||||
tickLabels: {fill: 'white'},
|
||||
}}
|
||||
dependentAxis
|
||||
/>
|
||||
<VictoryBar
|
||||
data={streamData[0]}
|
||||
style={{
|
||||
data: {
|
||||
fill: d => colors[d.x % 5],
|
||||
stroke: 'none',
|
||||
padding: 5,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</VictoryChart>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
top: '-50px',
|
||||
}}>
|
||||
<VictoryChart
|
||||
theme={VictoryTheme.material}
|
||||
width={800}
|
||||
height={350}
|
||||
style={{
|
||||
parent: {
|
||||
backgroundColor: '#222',
|
||||
},
|
||||
}}>
|
||||
<VictoryAxis
|
||||
style={{
|
||||
axis: {stroke: 'white'},
|
||||
tickLabels: {fill: 'white'},
|
||||
}}
|
||||
/>
|
||||
<VictoryAxis
|
||||
style={{
|
||||
axis: {stroke: 'white'},
|
||||
tickLabels: {fill: 'white'},
|
||||
}}
|
||||
dependentAxis
|
||||
/>
|
||||
<VictoryStack>
|
||||
{streamData.map((data, i) => (
|
||||
<VictoryArea key={i} data={data} colorScale={colors} />
|
||||
))}
|
||||
</VictoryStack>
|
||||
</VictoryChart>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
105
fixtures/unstable-async/time-slicing/src/Clock.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, {createRef, PureComponent} from 'react';
|
||||
|
||||
const SPEED = 0.003 / Math.PI;
|
||||
const FRAMES = 10;
|
||||
|
||||
export default class Clock extends PureComponent {
|
||||
faceRef = createRef();
|
||||
arcGroupRef = createRef();
|
||||
clockHandRef = createRef();
|
||||
frame = null;
|
||||
hitCounter = 0;
|
||||
rotation = 0;
|
||||
t0 = Date.now();
|
||||
arcs = [];
|
||||
|
||||
animate = () => {
|
||||
const now = Date.now();
|
||||
const td = now - this.t0;
|
||||
this.rotation = (this.rotation + SPEED * td) % (2 * Math.PI);
|
||||
this.t0 = now;
|
||||
|
||||
this.arcs.push({rotation: this.rotation, td});
|
||||
|
||||
let lx, ly, tx, ty;
|
||||
if (this.arcs.length > FRAMES) {
|
||||
this.arcs.forEach(({rotation, td}, i) => {
|
||||
lx = tx;
|
||||
ly = ty;
|
||||
const r = 145;
|
||||
tx = 155 + r * Math.cos(rotation);
|
||||
ty = 155 + r * Math.sin(rotation);
|
||||
const bigArc = SPEED * td < Math.PI ? '0' : '1';
|
||||
const path = `M${tx} ${ty}A${r} ${r} 0 ${bigArc} 0 ${lx} ${ly}L155 155`;
|
||||
const hue = 120 - Math.min(120, td / 4);
|
||||
const colour = `hsl(${hue}, 100%, ${60 - i * (30 / FRAMES)}%)`;
|
||||
if (i !== 0) {
|
||||
const arcEl = this.arcGroupRef.current.children[i - 1];
|
||||
arcEl.setAttribute('d', path);
|
||||
arcEl.setAttribute('fill', colour);
|
||||
}
|
||||
});
|
||||
this.clockHandRef.current.setAttribute('d', `M155 155L${tx} ${ty}`);
|
||||
this.arcs.shift();
|
||||
}
|
||||
|
||||
if (this.hitCounter > 0) {
|
||||
this.faceRef.current.setAttribute(
|
||||
'fill',
|
||||
`hsla(0, 0%, ${this.hitCounter}%, 0.95)`
|
||||
);
|
||||
this.hitCounter -= 1;
|
||||
} else {
|
||||
this.hitCounter = 0;
|
||||
this.faceRef.current.setAttribute('fill', 'hsla(0, 0%, 5%, 0.95)');
|
||||
}
|
||||
|
||||
this.frame = requestAnimationFrame(this.animate);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.frame = requestAnimationFrame(this.animate);
|
||||
if (this.faceRef.current) {
|
||||
this.faceRef.current.addEventListener('click', this.handleClick);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
console.log('componentDidUpdate()', this.faceRef.current);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.faceRef.current.removeEventListener('click', this.handleClick);
|
||||
if (this.frame) {
|
||||
cancelAnimationFrame(this.frame);
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
e.stopPropagation();
|
||||
this.hitCounter = 50;
|
||||
};
|
||||
|
||||
render() {
|
||||
const paths = new Array(FRAMES);
|
||||
for (let i = 0; i < FRAMES; i++) {
|
||||
paths.push(<path className="arcHand" key={i} />);
|
||||
}
|
||||
return (
|
||||
<div className="stutterer">
|
||||
<svg height="310" width="310">
|
||||
<circle
|
||||
className="clockFace"
|
||||
onClick={this.handleClick}
|
||||
cx={155}
|
||||
cy={155}
|
||||
r={150}
|
||||
ref={this.faceRef}
|
||||
/>
|
||||
<g ref={this.arcGroupRef}>{paths}</g>
|
||||
<path className="clockHand" ref={this.clockHandRef} />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
143
fixtures/unstable-async/time-slicing/src/index.css
Normal file
@@ -0,0 +1,143 @@
|
||||
html,
|
||||
body {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
user-select: none;
|
||||
font-family: Karla, Helvetica Neue, Helvetica, sans-serif;
|
||||
background: rgb(34, 34, 34);
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.VictoryContainer {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.rendering {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
zoom: 1.8;
|
||||
}
|
||||
|
||||
label {
|
||||
zoom: 1;
|
||||
margin-right: 50px;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
label.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
label:nth-child(1).selected {
|
||||
color: rgb(253, 25, 153);
|
||||
}
|
||||
|
||||
label:nth-child(2).selected {
|
||||
color: rgb(255, 240, 1);
|
||||
}
|
||||
|
||||
label:nth-child(3).selected {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 16px;
|
||||
font-size: 30px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.input.sync {
|
||||
outline-color: rgba(253, 25, 153, 0.1);
|
||||
}
|
||||
.input.debounced {
|
||||
outline-color: rgba(255, 240, 1, 0.1);
|
||||
}
|
||||
.input.async {
|
||||
outline-color: rgba(97, 218, 251, 0.1);
|
||||
}
|
||||
|
||||
|
||||
label {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
label label {
|
||||
display: 'inline-block';
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.demo {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.stutterer {
|
||||
transform: scale(1.5);
|
||||
height: 310px;
|
||||
width: 310px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -256px;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
box-shadow: 0 0 10px 10px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 200px;
|
||||
}
|
||||
|
||||
.clockHand {
|
||||
stroke: white;
|
||||
stroke-width: 10px;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.clockFace {
|
||||
stroke: white;
|
||||
stroke-width: 10px;
|
||||
}
|
||||
|
||||
.arcHand {
|
||||
}
|
||||
|
||||
.innerLine {
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
height: 149px;
|
||||
left: 47.5%;
|
||||
top: 0%;
|
||||
width: 5%;
|
||||
background-color: red;
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
156
fixtures/unstable-async/time-slicing/src/index.js
Normal file
@@ -0,0 +1,156 @@
|
||||
import React, {PureComponent, unstable_AsyncMode} from 'react';
|
||||
import {flushSync, render, unstable_deferredUpdates} from 'react-dom';
|
||||
import _ from 'lodash';
|
||||
import Charts from './Charts';
|
||||
import Clock from './Clock';
|
||||
import './index.css';
|
||||
|
||||
let cachedData = new Map();
|
||||
|
||||
class App extends PureComponent {
|
||||
state = {
|
||||
value: '',
|
||||
strategy: 'sync',
|
||||
showDemo: true,
|
||||
showClock: false,
|
||||
};
|
||||
|
||||
// Random data for the chart
|
||||
getStreamData(input) {
|
||||
if (cachedData.has(input)) {
|
||||
return cachedData.get(input);
|
||||
}
|
||||
const multiplier = input.length !== 0 ? input.length : 1;
|
||||
const complexity =
|
||||
(parseInt(window.location.search.substring(1), 10) / 100) * 25 || 25;
|
||||
const data = _.range(5).map(t =>
|
||||
_.range(complexity * multiplier).map((j, i) => {
|
||||
return {
|
||||
x: j,
|
||||
y: (t + 1) * _.random(0, 255),
|
||||
};
|
||||
})
|
||||
);
|
||||
cachedData.set(input, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('keydown', e => {
|
||||
if (e.key.toLowerCase() === '?') {
|
||||
e.preventDefault();
|
||||
this.setState(state => ({
|
||||
showClock: !state.showClock,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleChartClick = e => {
|
||||
if (this.state.showDemo) {
|
||||
if (e.shiftKey) {
|
||||
this.setState({showDemo: false});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.state.strategy !== 'async') {
|
||||
this.setState(state => ({
|
||||
showDemo: !state.showDemo,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (this._ignoreClick) {
|
||||
return;
|
||||
}
|
||||
this._ignoreClick = true;
|
||||
|
||||
// TODO: needing setTimeout here seems like a React bug.
|
||||
setTimeout(() => {
|
||||
unstable_deferredUpdates(() => {
|
||||
this.setState({showDemo: true}, () => {
|
||||
this._ignoreClick = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
debouncedHandleChange = _.debounce(value => {
|
||||
if (this.state.strategy === 'debounced') {
|
||||
flushSync(() => {
|
||||
this.setState({value: value});
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
renderOption(strategy, label) {
|
||||
const {strategy: currentStrategy} = this.state;
|
||||
return (
|
||||
<label className={strategy === currentStrategy ? 'selected' : null}>
|
||||
<input
|
||||
type="radio"
|
||||
checked={strategy === currentStrategy}
|
||||
onChange={() => this.setState({strategy})}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
handleChange = e => {
|
||||
const value = e.target.value;
|
||||
const {strategy} = this.state;
|
||||
switch (strategy) {
|
||||
case 'sync':
|
||||
this.setState({value});
|
||||
break;
|
||||
case 'debounced':
|
||||
this.debouncedHandleChange(value);
|
||||
break;
|
||||
case 'async':
|
||||
// TODO: needing setTimeout here seems like a React bug.
|
||||
setTimeout(() => {
|
||||
unstable_deferredUpdates(() => {
|
||||
this.setState({value});
|
||||
});
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const Wrapper =
|
||||
this.state.strategy === 'async' ? unstable_AsyncMode : 'div';
|
||||
const {showClock} = this.state;
|
||||
const data = this.getStreamData(this.state.value);
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="rendering">
|
||||
{this.renderOption('sync', 'Synchronous')}
|
||||
{this.renderOption('debounced', 'Debounced')}
|
||||
{this.renderOption('async', 'Asynchronous')}
|
||||
</div>
|
||||
<input
|
||||
className={'input ' + this.state.strategy}
|
||||
placeholder="longer input → more components and DOM nodes"
|
||||
defaultValue={this.state.input}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<Wrapper>
|
||||
<div className="demo" onClick={this.handleChartClick}>
|
||||
{this.state.showDemo && (
|
||||
<Charts data={data} onClick={this.handleChartClick} />
|
||||
)}
|
||||
<div style={{display: showClock ? 'block' : 'none'}}>
|
||||
<Clock />
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.getElementById('root');
|
||||
render(<App />, container);
|
||||
7078
fixtures/unstable-async/time-slicing/yarn.lock
Normal file
@@ -57,7 +57,6 @@
|
||||
"eslint-plugin-no-for-of-loops": "^1.0.0",
|
||||
"eslint-plugin-react": "^6.7.1",
|
||||
"eslint-plugin-react-internal": "link:./scripts/eslint-rules/",
|
||||
"fbjs": "^0.8.16",
|
||||
"fbjs-scripts": "^0.6.0",
|
||||
"filesize": "^3.5.6",
|
||||
"flow-bin": "^0.72.0",
|
||||
@@ -78,8 +77,8 @@
|
||||
"ncp": "^2.0.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"platform": "^1.1.0",
|
||||
"prettier": "1.11.1",
|
||||
"prop-types": "^15.6.0",
|
||||
"prettier": "1.13.7",
|
||||
"prop-types": "^15.6.2",
|
||||
"random-seed": "^0.3.0",
|
||||
"react-lifecycles-compat": "^3.0.2",
|
||||
"rimraf": "^2.6.1",
|
||||
@@ -99,7 +98,7 @@
|
||||
"yargs": "^6.3.0"
|
||||
},
|
||||
"devEngines": {
|
||||
"node": "8.x || 9.x"
|
||||
"node": "8.x || 9.x || 10.x"
|
||||
},
|
||||
"jest": {
|
||||
"testRegex": "/scripts/jest/dont-run-jest-directly\\.js$"
|
||||
|
||||
@@ -15,7 +15,7 @@ Other cases have **better long-term solutions**:
|
||||
|
||||
The main motivation for `create-subscription` is to provide a way for library authors to ensure compatibility with React's upcoming asynchronous rendering mode. `create-subscription` guarantees correctness in async mode, accounting for the subtle bugs and edge cases that a library author might otherwise miss.
|
||||
|
||||
However, it achieves correctness by sometimes de-opting to synchronous mode, obviating the benefits of async rendering. This is an inherent limitation of storing state outside of React's managed state queue and rendering in response to a change event.
|
||||
However, [it achieves correctness by sometimes de-opting to synchronous mode](https://github.com/facebook/react/issues/13186#issuecomment-403959161), obviating the benefits of async rendering. This is an inherent limitation of storing state outside of React's managed state queue and rendering in response to a change event.
|
||||
|
||||
The effect of de-opting to sync mode is that the main thread may periodically be blocked (in the case of CPU-bound work), and placeholders may appear earlier than desired (in the case of IO-bound work).
|
||||
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
"index.js",
|
||||
"cjs/"
|
||||
],
|
||||
"dependencies": {
|
||||
"fbjs": "^0.8.16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0"
|
||||
},
|
||||
|
||||
@@ -450,7 +450,9 @@ describe('createSubscription', () => {
|
||||
},
|
||||
() => null,
|
||||
);
|
||||
}).toWarnDev('Subscription must specify a getCurrentValue function');
|
||||
}).toWarnDev('Subscription must specify a getCurrentValue function', {
|
||||
withoutStack: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should warn for invalid missing subscribe', () => {
|
||||
@@ -461,7 +463,9 @@ describe('createSubscription', () => {
|
||||
},
|
||||
() => null,
|
||||
);
|
||||
}).toWarnDev('Subscription must specify a subscribe function');
|
||||
}).toWarnDev('Subscription must specify a subscribe function', {
|
||||
withoutStack: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should warn if subscribe does not return an unsubscribe method', () => {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import warning from 'fbjs/lib/warning';
|
||||
import invariant from 'shared/invariant';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
type Unsubscribe = () => void;
|
||||
|
||||
@@ -21,7 +21,7 @@ export function createSubscription<Property, Value>(
|
||||
getCurrentValue: (source: Property) => Value | void,
|
||||
|
||||
// Setup a subscription for the subscribable value in props, and return an unsubscribe function.
|
||||
// Return false to indicate the property cannot be unsubscribed from (e.g. native Promises).
|
||||
// Return empty function if the property cannot be unsubscribed from (e.g. native Promises).
|
||||
// Due to the variety of change event types, subscribers should provide their own handlers.
|
||||
// Those handlers should not attempt to update state though;
|
||||
// They should call the callback() instead when a subscription changes.
|
||||
@@ -36,11 +36,11 @@ export function createSubscription<Property, Value>(
|
||||
}> {
|
||||
const {getCurrentValue, subscribe} = config;
|
||||
|
||||
warning(
|
||||
warningWithoutStack(
|
||||
typeof getCurrentValue === 'function',
|
||||
'Subscription must specify a getCurrentValue function',
|
||||
);
|
||||
warning(
|
||||
warningWithoutStack(
|
||||
typeof subscribe === 'function',
|
||||
'Subscription must specify a subscribe function',
|
||||
);
|
||||
@@ -87,13 +87,13 @@ export function createSubscription<Property, Value>(
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.state.source !== prevState.source) {
|
||||
this.unsubscribe(prevState);
|
||||
this.unsubscribe();
|
||||
this.subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe(this.state);
|
||||
this.unsubscribe();
|
||||
|
||||
// Track mounted to avoid calling setState after unmounting
|
||||
// For source like Promises that can't be unsubscribed from.
|
||||
@@ -147,7 +147,7 @@ export function createSubscription<Property, Value>(
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribe(state: State) {
|
||||
unsubscribe() {
|
||||
if (typeof this._unsubscribe === 'function') {
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import ReactErrorUtils from 'shared/ReactErrorUtils';
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
import {
|
||||
injectEventPluginOrder,
|
||||
@@ -167,7 +167,7 @@ export function getListener(inst: Fiber, registrationName: string) {
|
||||
*/
|
||||
function extractEvents(
|
||||
topLevelType: TopLevelType,
|
||||
targetInst: Fiber,
|
||||
targetInst: null | Fiber,
|
||||
nativeEvent: AnyNativeEvent,
|
||||
nativeEventTarget: EventTarget,
|
||||
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
|
||||
@@ -229,7 +229,7 @@ export function runEventsInBatch(
|
||||
|
||||
export function runExtractedEventsInBatch(
|
||||
topLevelType: TopLevelType,
|
||||
targetInst: Fiber,
|
||||
targetInst: null | Fiber,
|
||||
nativeEvent: AnyNativeEvent,
|
||||
nativeEventTarget: EventTarget,
|
||||
) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
PluginModule,
|
||||
} from './PluginModuleType';
|
||||
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
type NamesToPlugins = {[key: PluginName]: PluginModule<AnyNativeEvent>};
|
||||
type EventPluginOrder = null | Array<PluginName>;
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
import ReactErrorUtils from 'shared/ReactErrorUtils';
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import warning from 'fbjs/lib/warning';
|
||||
import invariant from 'shared/invariant';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
export let getFiberCurrentPropsFromNode = null;
|
||||
export let getInstanceFromNode = null;
|
||||
@@ -21,7 +21,7 @@ export const injection = {
|
||||
getNodeFromInstance,
|
||||
} = Injected);
|
||||
if (__DEV__) {
|
||||
warning(
|
||||
warningWithoutStack(
|
||||
getNodeFromInstance && getInstanceFromNode,
|
||||
'EventPluginUtils.injection.injectComponentTree(...): Injected ' +
|
||||
'module is missing getNodeFromInstance or getInstanceFromNode.',
|
||||
@@ -39,14 +39,18 @@ if (__DEV__) {
|
||||
const listenersIsArr = Array.isArray(dispatchListeners);
|
||||
const listenersLen = listenersIsArr
|
||||
? dispatchListeners.length
|
||||
: dispatchListeners ? 1 : 0;
|
||||
: dispatchListeners
|
||||
? 1
|
||||
: 0;
|
||||
|
||||
const instancesIsArr = Array.isArray(dispatchInstances);
|
||||
const instancesLen = instancesIsArr
|
||||
? dispatchInstances.length
|
||||
: dispatchInstances ? 1 : 0;
|
||||
: dispatchInstances
|
||||
? 1
|
||||
: 0;
|
||||
|
||||
warning(
|
||||
warningWithoutStack(
|
||||
instancesIsArr === listenersIsArr && instancesLen === listenersLen,
|
||||
'EventPluginUtils: Invalid `event`.',
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
traverseTwoPhase,
|
||||
traverseEnterLeave,
|
||||
} from 'shared/ReactTreeTraversal';
|
||||
import warning from 'fbjs/lib/warning';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
import {getListener} from './EventPluginHub';
|
||||
import accumulateInto from './accumulateInto';
|
||||
@@ -46,7 +46,7 @@ function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
|
||||
*/
|
||||
function accumulateDirectionalDispatches(inst, phase, event) {
|
||||
if (__DEV__) {
|
||||
warning(inst, 'Dispatching inst must not be null');
|
||||
warningWithoutStack(inst, 'Dispatching inst must not be null');
|
||||
}
|
||||
const listener = listenerAtPhase(inst, event, phase);
|
||||
if (listener) {
|
||||
|
||||
@@ -24,7 +24,7 @@ export type PluginModule<NativeEvent> = {
|
||||
eventTypes: EventTypes,
|
||||
extractEvents: (
|
||||
topLevelType: TopLevelType,
|
||||
targetInst: Fiber,
|
||||
targetInst: null | Fiber,
|
||||
nativeTarget: NativeEvent,
|
||||
nativeEventTarget: EventTarget,
|
||||
) => ?ReactSyntheticEvent,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
import {
|
||||
getInstanceFromNode,
|
||||
|
||||
@@ -548,7 +548,9 @@ const ResponderEventPlugin = {
|
||||
? eventTypes.responderStart
|
||||
: isResponderTouchMove
|
||||
? eventTypes.responderMove
|
||||
: isResponderTouchEnd ? eventTypes.responderEnd : null;
|
||||
: isResponderTouchEnd
|
||||
? eventTypes.responderEnd
|
||||
: null;
|
||||
|
||||
if (incrementalTouch) {
|
||||
const gesture = ResponderSyntheticEvent.getPooled(
|
||||
@@ -571,7 +573,9 @@ const ResponderEventPlugin = {
|
||||
noResponderTouches(nativeEvent);
|
||||
const finalTouch = isResponderTerminate
|
||||
? eventTypes.responderTerminate
|
||||
: isResponderRelease ? eventTypes.responderRelease : null;
|
||||
: isResponderRelease
|
||||
? eventTypes.responderRelease
|
||||
: null;
|
||||
if (finalTouch) {
|
||||
const finalEvent = ResponderSyntheticEvent.getPooled(
|
||||
finalTouch,
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import warning from 'fbjs/lib/warning';
|
||||
import invariant from 'shared/invariant';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
import {isStartish, isMoveish, isEndish} from './ResponderTopLevelEventTypes';
|
||||
|
||||
@@ -95,7 +95,7 @@ function resetTouchRecord(touchRecord: TouchRecord, touch: Touch): void {
|
||||
function getTouchIdentifier({identifier}: Touch): number {
|
||||
invariant(identifier != null, 'Touch object is missing identifier.');
|
||||
if (__DEV__) {
|
||||
warning(
|
||||
warningWithoutStack(
|
||||
identifier <= MAX_TOUCH_BANK,
|
||||
'Touch identifier %s is greater than maximum supported %s which causes ' +
|
||||
'performance issues backfilling array locations for all of the indices.',
|
||||
@@ -200,7 +200,7 @@ const ResponderTouchHistoryStore = {
|
||||
}
|
||||
if (__DEV__) {
|
||||
const activeRecord = touchBank[touchHistory.indexOfSingleActiveTouch];
|
||||
warning(
|
||||
warningWithoutStack(
|
||||
activeRecord != null && activeRecord.touchActive,
|
||||
'Cannot find single active touch.',
|
||||
);
|
||||
|
||||
@@ -7,23 +7,11 @@
|
||||
|
||||
/* eslint valid-typeof: 0 */
|
||||
|
||||
import emptyFunction from 'fbjs/lib/emptyFunction';
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import warning from 'fbjs/lib/warning';
|
||||
import invariant from 'shared/invariant';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
let didWarnForAddedNewProperty = false;
|
||||
const EVENT_POOL_SIZE = 10;
|
||||
|
||||
const shouldBeReleasedProperties = [
|
||||
'dispatchConfig',
|
||||
'_targetInst',
|
||||
'nativeEvent',
|
||||
'isDefaultPrevented',
|
||||
'isPropagationStopped',
|
||||
'_dispatchListeners',
|
||||
'_dispatchInstances',
|
||||
];
|
||||
|
||||
/**
|
||||
* @interface Event
|
||||
* @see http://www.w3.org/TR/DOM-Level-3-Events/
|
||||
@@ -32,7 +20,9 @@ const EventInterface = {
|
||||
type: null,
|
||||
target: null,
|
||||
// currentTarget is set when dispatching; no use in copying it here
|
||||
currentTarget: emptyFunction.thatReturnsNull,
|
||||
currentTarget: function() {
|
||||
return null;
|
||||
},
|
||||
eventPhase: null,
|
||||
bubbles: null,
|
||||
cancelable: null,
|
||||
@@ -43,6 +33,14 @@ const EventInterface = {
|
||||
isTrusted: null,
|
||||
};
|
||||
|
||||
function functionThatReturnsTrue() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function functionThatReturnsFalse() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthetic events are dispatched by event plugins, typically in response to a
|
||||
* top-level event delegation handler.
|
||||
@@ -72,6 +70,8 @@ function SyntheticEvent(
|
||||
delete this.nativeEvent;
|
||||
delete this.preventDefault;
|
||||
delete this.stopPropagation;
|
||||
delete this.isDefaultPrevented;
|
||||
delete this.isPropagationStopped;
|
||||
}
|
||||
|
||||
this.dispatchConfig = dispatchConfig;
|
||||
@@ -103,11 +103,11 @@ function SyntheticEvent(
|
||||
? nativeEvent.defaultPrevented
|
||||
: nativeEvent.returnValue === false;
|
||||
if (defaultPrevented) {
|
||||
this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
|
||||
this.isDefaultPrevented = functionThatReturnsTrue;
|
||||
} else {
|
||||
this.isDefaultPrevented = emptyFunction.thatReturnsFalse;
|
||||
this.isDefaultPrevented = functionThatReturnsFalse;
|
||||
}
|
||||
this.isPropagationStopped = emptyFunction.thatReturnsFalse;
|
||||
this.isPropagationStopped = functionThatReturnsFalse;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ Object.assign(SyntheticEvent.prototype, {
|
||||
} else if (typeof event.returnValue !== 'unknown') {
|
||||
event.returnValue = false;
|
||||
}
|
||||
this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
|
||||
this.isDefaultPrevented = functionThatReturnsTrue;
|
||||
},
|
||||
|
||||
stopPropagation: function() {
|
||||
@@ -144,7 +144,7 @@ Object.assign(SyntheticEvent.prototype, {
|
||||
event.cancelBubble = true;
|
||||
}
|
||||
|
||||
this.isPropagationStopped = emptyFunction.thatReturnsTrue;
|
||||
this.isPropagationStopped = functionThatReturnsTrue;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -153,7 +153,7 @@ Object.assign(SyntheticEvent.prototype, {
|
||||
* won't be added back into the pool.
|
||||
*/
|
||||
persist: function() {
|
||||
this.isPersistent = emptyFunction.thatReturnsTrue;
|
||||
this.isPersistent = functionThatReturnsTrue;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -161,7 +161,7 @@ Object.assign(SyntheticEvent.prototype, {
|
||||
*
|
||||
* @return {boolean} True if this should not be released, false otherwise.
|
||||
*/
|
||||
isPersistent: emptyFunction.thatReturnsFalse,
|
||||
isPersistent: functionThatReturnsFalse,
|
||||
|
||||
/**
|
||||
* `PooledClass` looks for `destructor` on each instance it releases.
|
||||
@@ -179,24 +179,44 @@ Object.assign(SyntheticEvent.prototype, {
|
||||
this[propName] = null;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < shouldBeReleasedProperties.length; i++) {
|
||||
this[shouldBeReleasedProperties[i]] = null;
|
||||
}
|
||||
this.dispatchConfig = null;
|
||||
this._targetInst = null;
|
||||
this.nativeEvent = null;
|
||||
this.isDefaultPrevented = functionThatReturnsFalse;
|
||||
this.isPropagationStopped = functionThatReturnsFalse;
|
||||
this._dispatchListeners = null;
|
||||
this._dispatchInstances = null;
|
||||
if (__DEV__) {
|
||||
Object.defineProperty(
|
||||
this,
|
||||
'nativeEvent',
|
||||
getPooledWarningPropertyDefinition('nativeEvent', null),
|
||||
);
|
||||
Object.defineProperty(
|
||||
this,
|
||||
'isDefaultPrevented',
|
||||
getPooledWarningPropertyDefinition(
|
||||
'isDefaultPrevented',
|
||||
functionThatReturnsFalse,
|
||||
),
|
||||
);
|
||||
Object.defineProperty(
|
||||
this,
|
||||
'isPropagationStopped',
|
||||
getPooledWarningPropertyDefinition(
|
||||
'isPropagationStopped',
|
||||
functionThatReturnsFalse,
|
||||
),
|
||||
);
|
||||
Object.defineProperty(
|
||||
this,
|
||||
'preventDefault',
|
||||
getPooledWarningPropertyDefinition('preventDefault', emptyFunction),
|
||||
getPooledWarningPropertyDefinition('preventDefault', () => {}),
|
||||
);
|
||||
Object.defineProperty(
|
||||
this,
|
||||
'stopPropagation',
|
||||
getPooledWarningPropertyDefinition('stopPropagation', emptyFunction),
|
||||
getPooledWarningPropertyDefinition('stopPropagation', () => {}),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -228,49 +248,6 @@ SyntheticEvent.extend = function(Interface) {
|
||||
return Class;
|
||||
};
|
||||
|
||||
/** Proxying after everything set on SyntheticEvent
|
||||
* to resolve Proxy issue on some WebKit browsers
|
||||
* in which some Event properties are set to undefined (GH#10010)
|
||||
*/
|
||||
if (__DEV__) {
|
||||
const isProxySupported =
|
||||
typeof Proxy === 'function' &&
|
||||
// https://github.com/facebook/react/issues/12011
|
||||
!Object.isSealed(new Proxy({}, {}));
|
||||
|
||||
if (isProxySupported) {
|
||||
/*eslint-disable no-func-assign */
|
||||
SyntheticEvent = new Proxy(SyntheticEvent, {
|
||||
construct: function(target, args) {
|
||||
return this.apply(target, Object.create(target.prototype), args);
|
||||
},
|
||||
apply: function(constructor, that, args) {
|
||||
return new Proxy(constructor.apply(that, args), {
|
||||
set: function(target, prop, value) {
|
||||
if (
|
||||
prop !== 'isPersistent' &&
|
||||
!target.constructor.Interface.hasOwnProperty(prop) &&
|
||||
shouldBeReleasedProperties.indexOf(prop) === -1
|
||||
) {
|
||||
warning(
|
||||
didWarnForAddedNewProperty || target.isPersistent(),
|
||||
"This synthetic event is reused for performance reasons. If you're " +
|
||||
"seeing this, you're adding a new property in the synthetic event object. " +
|
||||
'The property is never released. See ' +
|
||||
'https://fb.me/react-event-pooling for more information.',
|
||||
);
|
||||
didWarnForAddedNewProperty = true;
|
||||
}
|
||||
target[prop] = value;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
/*eslint-enable no-func-assign */
|
||||
}
|
||||
}
|
||||
|
||||
addEventPoolingTo(SyntheticEvent);
|
||||
|
||||
/**
|
||||
@@ -307,7 +284,7 @@ function getPooledWarningPropertyDefinition(propName, getVal) {
|
||||
|
||||
function warn(action, result) {
|
||||
const warningCondition = false;
|
||||
warning(
|
||||
warningWithoutStack(
|
||||
warningCondition,
|
||||
"This synthetic event is reused for performance reasons. If you're seeing this, " +
|
||||
"you're %s `%s` on a released/nullified synthetic event. %s. " +
|
||||
@@ -345,7 +322,7 @@ function releasePooledEvent(event) {
|
||||
const EventConstructor = this;
|
||||
invariant(
|
||||
event instanceof EventConstructor,
|
||||
'Trying to release an event instance into a pool of a different type.',
|
||||
'Trying to release an event instance into a pool of a different type.',
|
||||
);
|
||||
event.destructor();
|
||||
if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
/**
|
||||
* Accumulates items that must not be null or undefined.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
/**
|
||||
* Accumulates items that must not be null or undefined into the first one. This
|
||||
|
||||
@@ -20,10 +20,9 @@
|
||||
"dependencies": {
|
||||
"art": "^0.10.1",
|
||||
"create-react-class": "^15.6.2",
|
||||
"fbjs": "^0.8.16",
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.0"
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0"
|
||||
|
||||
8
packages/react-art/src/ReactART.js
vendored
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactVersion from 'shared/ReactVersion';
|
||||
import * as ARTRenderer from 'react-reconciler/inline.art';
|
||||
import Transform from 'art/core/transform';
|
||||
import Mode from 'art/modes/current';
|
||||
@@ -131,6 +132,13 @@ class Text extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
ARTRenderer.injectIntoDevTools({
|
||||
findFiberByHostInstance: () => null,
|
||||
bundleType: __DEV__ ? 1 : 0,
|
||||
version: ReactVersion,
|
||||
rendererPackageName: 'react-art',
|
||||
});
|
||||
|
||||
/** API */
|
||||
|
||||
export const ClippingRectangle = TYPES.CLIPPING_RECTANGLE;
|
||||
|
||||
16
packages/react-art/src/ReactARTHostConfig.js
vendored
@@ -8,14 +8,18 @@
|
||||
import * as ReactScheduler from 'shared/ReactScheduler';
|
||||
import Transform from 'art/core/transform';
|
||||
import Mode from 'art/modes/current';
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import emptyObject from 'fbjs/lib/emptyObject';
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals';
|
||||
|
||||
const pooledTransform = new Transform();
|
||||
|
||||
const NO_CONTEXT = {};
|
||||
const UPDATE_SIGNAL = {};
|
||||
if (__DEV__) {
|
||||
Object.freeze(NO_CONTEXT);
|
||||
Object.freeze(UPDATE_SIGNAL);
|
||||
}
|
||||
|
||||
/** Helper Methods */
|
||||
|
||||
@@ -318,16 +322,20 @@ export function shouldDeprioritizeSubtree(type, props) {
|
||||
}
|
||||
|
||||
export function getRootHostContext() {
|
||||
return emptyObject;
|
||||
return NO_CONTEXT;
|
||||
}
|
||||
|
||||
export function getChildHostContext() {
|
||||
return emptyObject;
|
||||
return NO_CONTEXT;
|
||||
}
|
||||
|
||||
export const scheduleDeferredCallback = ReactScheduler.scheduleWork;
|
||||
export const cancelDeferredCallback = ReactScheduler.cancelScheduledWork;
|
||||
|
||||
export const scheduleTimeout = setTimeout;
|
||||
export const cancelTimeout = clearTimeout;
|
||||
export const noTimeout = -1;
|
||||
|
||||
export function shouldSetTextContent(type, props) {
|
||||
return (
|
||||
typeof props.children === 'string' || typeof props.children === 'number'
|
||||
|
||||
@@ -28,6 +28,10 @@ const Circle = require('react-art/Circle');
|
||||
const Rectangle = require('react-art/Rectangle');
|
||||
const Wedge = require('react-art/Wedge');
|
||||
|
||||
// Isolate the noop renderer
|
||||
jest.resetModules();
|
||||
const ReactNoop = require('react-noop-renderer');
|
||||
|
||||
let Group;
|
||||
let Shape;
|
||||
let Surface;
|
||||
@@ -71,6 +75,8 @@ describe('ReactART', () => {
|
||||
Surface = ReactART.Surface;
|
||||
|
||||
TestComponent = class extends React.Component {
|
||||
group = React.createRef();
|
||||
|
||||
render() {
|
||||
const a = (
|
||||
<Shape
|
||||
@@ -104,7 +110,7 @@ describe('ReactART', () => {
|
||||
|
||||
return (
|
||||
<Surface width={150} height={200}>
|
||||
<Group ref="group">
|
||||
<Group ref={this.group}>
|
||||
{this.props.flipped ? [b, a, c] : [a, b, c]}
|
||||
</Group>
|
||||
</Surface>
|
||||
@@ -121,7 +127,7 @@ describe('ReactART', () => {
|
||||
it('should have the correct lifecycle state', () => {
|
||||
let instance = <TestComponent />;
|
||||
instance = ReactTestUtils.renderIntoDocument(instance);
|
||||
const group = instance.refs.group;
|
||||
const group = instance.group.current;
|
||||
// Duck type test for an ART group
|
||||
expect(typeof group.indicate).toBe('function');
|
||||
});
|
||||
@@ -260,15 +266,17 @@ describe('ReactART', () => {
|
||||
let ref = null;
|
||||
|
||||
class Outer extends React.Component {
|
||||
test = React.createRef();
|
||||
|
||||
componentDidMount() {
|
||||
ref = this.refs.test;
|
||||
ref = this.test.current;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Surface>
|
||||
<Group>
|
||||
<CustomShape ref="test" />
|
||||
<CustomShape ref={this.test} />
|
||||
</Group>
|
||||
</Surface>
|
||||
);
|
||||
@@ -289,26 +297,28 @@ describe('ReactART', () => {
|
||||
let ref = {};
|
||||
|
||||
class Outer extends React.Component {
|
||||
test = React.createRef();
|
||||
|
||||
componentDidMount() {
|
||||
ref = this.refs.test;
|
||||
ref = this.test.current;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
ref = this.refs.test;
|
||||
ref = this.test.current;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Surface>
|
||||
<Group>
|
||||
{this.props.mountCustomShape && <CustomShape ref="test" />}
|
||||
{this.props.mountCustomShape && <CustomShape ref={this.test} />}
|
||||
</Group>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
}
|
||||
ReactDOM.render(<Outer />, container);
|
||||
expect(ref).not.toBeDefined();
|
||||
expect(ref).toBe(null);
|
||||
ReactDOM.render(<Outer mountCustomShape={true} />, container);
|
||||
expect(ref.constructor).toBe(CustomShape);
|
||||
});
|
||||
@@ -348,7 +358,7 @@ describe('ReactART', () => {
|
||||
const CurrentRendererContext = React.createContext(null);
|
||||
|
||||
function Yield(props) {
|
||||
testRenderer.unstable_yield(props.value);
|
||||
ReactNoop.yield(props.value);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -366,19 +376,16 @@ describe('ReactART', () => {
|
||||
|
||||
// Using test renderer instead of the DOM renderer here because async
|
||||
// testing APIs for the DOM renderer don't exist.
|
||||
const testRenderer = ReactTestRenderer.create(
|
||||
ReactNoop.render(
|
||||
<CurrentRendererContext.Provider value="Test">
|
||||
<Yield value="A" />
|
||||
<Yield value="B" />
|
||||
<LogCurrentRenderer />
|
||||
<Yield value="C" />
|
||||
</CurrentRendererContext.Provider>,
|
||||
{
|
||||
unstable_isAsync: true,
|
||||
},
|
||||
);
|
||||
|
||||
testRenderer.unstable_flushThrough(['A']);
|
||||
ReactNoop.flushThrough(['A']);
|
||||
|
||||
ReactDOM.render(
|
||||
<Surface>
|
||||
@@ -393,7 +400,7 @@ describe('ReactART', () => {
|
||||
expect(ops).toEqual([null, 'ART']);
|
||||
|
||||
ops = [];
|
||||
expect(testRenderer.unstable_flushAll()).toEqual(['B', 'C']);
|
||||
expect(ReactNoop.flush()).toEqual(['B', 'C']);
|
||||
|
||||
expect(ops).toEqual(['Test']);
|
||||
});
|
||||
|
||||
@@ -13,10 +13,9 @@
|
||||
},
|
||||
"homepage": "https://reactjs.org/",
|
||||
"dependencies": {
|
||||
"fbjs": "^0.8.16",
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.0"
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0"
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('DOMPropertyOperations', () => {
|
||||
|
||||
it('should not remove empty attributes for special properties', () => {
|
||||
const container = document.createElement('div');
|
||||
ReactDOM.render(<input value="" />, container);
|
||||
ReactDOM.render(<input value="" onChange={() => {}} />, container);
|
||||
expect(container.firstChild.getAttribute('value')).toBe('');
|
||||
expect(container.firstChild.value).toBe('');
|
||||
});
|
||||
|
||||
@@ -80,8 +80,8 @@ describe('ReactChildReconciler', () => {
|
||||
'Keys should be unique so that components maintain their identity ' +
|
||||
'across updates. Non-unique keys may cause children to be ' +
|
||||
'duplicated and/or omitted — the behavior is unsupported and ' +
|
||||
'could change in a future version.',
|
||||
' in div (at **)\n' +
|
||||
'could change in a future version.\n' +
|
||||
' in div (at **)\n' +
|
||||
' in Component (at **)\n' +
|
||||
' in Parent (at **)\n' +
|
||||
' in GrandParent (at **)',
|
||||
@@ -127,8 +127,8 @@ describe('ReactChildReconciler', () => {
|
||||
'Keys should be unique so that components maintain their identity ' +
|
||||
'across updates. Non-unique keys may cause children to be ' +
|
||||
'duplicated and/or omitted — the behavior is unsupported and ' +
|
||||
'could change in a future version.',
|
||||
' in div (at **)\n' +
|
||||
'could change in a future version.\n' +
|
||||
' in div (at **)\n' +
|
||||
' in Component (at **)\n' +
|
||||
' in Parent (at **)\n' +
|
||||
' in GrandParent (at **)',
|
||||
|
||||
@@ -43,19 +43,22 @@ describe('ReactComponentLifeCycle', () => {
|
||||
const container = document.createElement('div');
|
||||
expect(() =>
|
||||
ReactDOM.render(<MyComponent x={1} />, container),
|
||||
).toLowPriorityWarnDev([
|
||||
'componentWillMount is deprecated and will be removed in the next major version. ' +
|
||||
'Use componentDidMount instead. As a temporary workaround, ' +
|
||||
'you can rename to UNSAFE_componentWillMount.' +
|
||||
'\n\nPlease update the following components: MyComponent',
|
||||
'componentWillReceiveProps is deprecated and will be removed in the next major version. ' +
|
||||
'Use static getDerivedStateFromProps instead.' +
|
||||
'\n\nPlease update the following components: MyComponent',
|
||||
'componentWillUpdate is deprecated and will be removed in the next major version. ' +
|
||||
'Use componentDidUpdate instead. As a temporary workaround, ' +
|
||||
'you can rename to UNSAFE_componentWillUpdate.' +
|
||||
'\n\nPlease update the following components: MyComponent',
|
||||
]);
|
||||
).toLowPriorityWarnDev(
|
||||
[
|
||||
'componentWillMount is deprecated and will be removed in the next major version. ' +
|
||||
'Use componentDidMount instead. As a temporary workaround, ' +
|
||||
'you can rename to UNSAFE_componentWillMount.' +
|
||||
'\n\nPlease update the following components: MyComponent',
|
||||
'componentWillReceiveProps is deprecated and will be removed in the next major version. ' +
|
||||
'Use static getDerivedStateFromProps instead.' +
|
||||
'\n\nPlease update the following components: MyComponent',
|
||||
'componentWillUpdate is deprecated and will be removed in the next major version. ' +
|
||||
'Use componentDidUpdate instead. As a temporary workaround, ' +
|
||||
'you can rename to UNSAFE_componentWillUpdate.' +
|
||||
'\n\nPlease update the following components: MyComponent',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// Dedupe check (update and instantiate new
|
||||
ReactDOM.render(<MyComponent x={2} />, container);
|
||||
|
||||
@@ -68,7 +68,8 @@ type ComponentLifeCycle =
|
||||
* Mounted components have a DOM node representation and are capable of
|
||||
* receiving new props.
|
||||
*/
|
||||
| 'MOUNTED' /**
|
||||
| 'MOUNTED'
|
||||
/**
|
||||
* Unmounted components are inactive and cannot receive new props.
|
||||
*/
|
||||
| 'UNMOUNTED';
|
||||
@@ -218,6 +219,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
'This is a no-op, but it might indicate a bug in your application. ' +
|
||||
'Instead, assign to `this.state` directly or define a `state = {};` ' +
|
||||
'class property with the desired state in the StatefulComponent component.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// Check deduplication; (no extra warnings should be logged).
|
||||
@@ -248,7 +250,9 @@ describe('ReactComponentLifeCycle', () => {
|
||||
expect(() => {
|
||||
const instance = ReactTestUtils.renderIntoDocument(element);
|
||||
expect(instance._isMounted()).toBeTruthy();
|
||||
}).toWarnDev('Component is accessing isMounted inside its render()');
|
||||
}).toWarnDev('Component is accessing isMounted inside its render()', {
|
||||
withoutStack: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly determine if a null component is mounted', () => {
|
||||
@@ -275,7 +279,9 @@ describe('ReactComponentLifeCycle', () => {
|
||||
expect(() => {
|
||||
const instance = ReactTestUtils.renderIntoDocument(element);
|
||||
expect(instance._isMounted()).toBeTruthy();
|
||||
}).toWarnDev('Component is accessing isMounted inside its render()');
|
||||
}).toWarnDev('Component is accessing isMounted inside its render()', {
|
||||
withoutStack: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('isMounted should return false when unmounted', () => {
|
||||
@@ -313,7 +319,9 @@ describe('ReactComponentLifeCycle', () => {
|
||||
|
||||
expect(() => {
|
||||
ReactTestUtils.renderIntoDocument(<Component />);
|
||||
}).toWarnDev('Component is accessing findDOMNode inside its render()');
|
||||
}).toWarnDev('Component is accessing findDOMNode inside its render()', {
|
||||
withoutStack: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should carry through each of the phases of setup', () => {
|
||||
@@ -379,6 +387,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
instance = ReactDOM.render(<LifeCycleComponent />, container);
|
||||
}).toWarnDev(
|
||||
'LifeCycleComponent is accessing isMounted inside its render() function',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// getInitialState
|
||||
@@ -674,6 +683,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
const container = document.createElement('div');
|
||||
expect(() => ReactDOM.render(<Component />, container)).toWarnDev(
|
||||
'Unsafe legacy lifecycles will not be called for components using new component APIs.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -701,6 +711,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
const container = document.createElement('div');
|
||||
expect(() => ReactDOM.render(<Component value={1} />, container)).toWarnDev(
|
||||
'Unsafe legacy lifecycles will not be called for components using new component APIs.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
ReactDOM.render(<Component value={2} />, container);
|
||||
});
|
||||
@@ -728,6 +739,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
const container = document.createElement('div');
|
||||
expect(() => ReactDOM.render(<Component value={1} />, container)).toWarnDev(
|
||||
'Unsafe legacy lifecycles will not be called for components using new component APIs.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
ReactDOM.render(<Component value={2} />, container);
|
||||
});
|
||||
@@ -756,6 +768,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
' componentWillUpdate\n\n' +
|
||||
'The above lifecycles should be removed. Learn more about this warning here:\n' +
|
||||
'https://fb.me/react-async-component-lifecycle-hooks',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
class WillMount extends React.Component {
|
||||
@@ -775,6 +788,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
' UNSAFE_componentWillMount\n\n' +
|
||||
'The above lifecycles should be removed. Learn more about this warning here:\n' +
|
||||
'https://fb.me/react-async-component-lifecycle-hooks',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
class WillMountAndUpdate extends React.Component {
|
||||
@@ -796,6 +810,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
' UNSAFE_componentWillUpdate\n\n' +
|
||||
'The above lifecycles should be removed. Learn more about this warning here:\n' +
|
||||
'https://fb.me/react-async-component-lifecycle-hooks',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
class WillReceiveProps extends React.Component {
|
||||
@@ -815,6 +830,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
' componentWillReceiveProps\n\n' +
|
||||
'The above lifecycles should be removed. Learn more about this warning here:\n' +
|
||||
'https://fb.me/react-async-component-lifecycle-hooks',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -841,6 +857,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
' componentWillUpdate\n\n' +
|
||||
'The above lifecycles should be removed. Learn more about this warning here:\n' +
|
||||
'https://fb.me/react-async-component-lifecycle-hooks',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
class WillMount extends React.Component {
|
||||
@@ -859,6 +876,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
' UNSAFE_componentWillMount\n\n' +
|
||||
'The above lifecycles should be removed. Learn more about this warning here:\n' +
|
||||
'https://fb.me/react-async-component-lifecycle-hooks',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
class WillMountAndUpdate extends React.Component {
|
||||
@@ -879,6 +897,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
' UNSAFE_componentWillUpdate\n\n' +
|
||||
'The above lifecycles should be removed. Learn more about this warning here:\n' +
|
||||
'https://fb.me/react-async-component-lifecycle-hooks',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
class WillReceiveProps extends React.Component {
|
||||
@@ -897,6 +916,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
' componentWillReceiveProps\n\n' +
|
||||
'The above lifecycles should be removed. Learn more about this warning here:\n' +
|
||||
'https://fb.me/react-async-component-lifecycle-hooks',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -964,6 +984,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
expect(() => ReactDOM.render(<MyComponent />, div)).toWarnDev(
|
||||
'MyComponent.getDerivedStateFromProps(): A valid state object (or null) must ' +
|
||||
'be returned. You have returned undefined.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// De-duped
|
||||
@@ -984,6 +1005,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
expect(() => ReactDOM.render(<MyComponent />, div)).toWarnDev(
|
||||
'MyComponent: Did not properly initialize state during construction. ' +
|
||||
'Expected state to be an object, but it was undefined.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// De-duped
|
||||
@@ -1209,6 +1231,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
expect(() => ReactDOM.render(<MyComponent value="bar" />, div)).toWarnDev(
|
||||
'MyComponent.getSnapshotBeforeUpdate(): A snapshot value (or null) must ' +
|
||||
'be returned. You have returned undefined.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// De-duped
|
||||
@@ -1229,6 +1252,7 @@ describe('ReactComponentLifeCycle', () => {
|
||||
expect(() => ReactDOM.render(<MyComponent />, div)).toWarnDev(
|
||||
'MyComponent: getSnapshotBeforeUpdate() should be used with componentDidUpdate(). ' +
|
||||
'This component defines getSnapshotBeforeUpdate() only.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// De-duped
|
||||
|
||||
@@ -17,10 +17,50 @@ let ReactDOMServer;
|
||||
let ReactCurrentOwner;
|
||||
let ReactTestUtils;
|
||||
let PropTypes;
|
||||
let shallowEqual;
|
||||
let shallowCompare;
|
||||
|
||||
describe('ReactCompositeComponent', () => {
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
/**
|
||||
* Performs equality by iterating through keys on an object and returning false
|
||||
* when any key has values which are not strictly equal between the arguments.
|
||||
* Returns true when the values of all keys are strictly equal.
|
||||
*/
|
||||
function shallowEqual(objA: mixed, objB: mixed): boolean {
|
||||
if (Object.is(objA, objB)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
typeof objA !== 'object' ||
|
||||
objA === null ||
|
||||
typeof objB !== 'object' ||
|
||||
objB === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const keysA = Object.keys(objA);
|
||||
const keysB = Object.keys(objB);
|
||||
if (keysA.length !== keysB.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < keysA.length; i++) {
|
||||
if (
|
||||
!hasOwnProperty.call(objB, keysA[i]) ||
|
||||
!Object.is(objA[keysA[i]], objB[keysA[i]])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function shallowCompare(instance, nextProps, nextState) {
|
||||
return (
|
||||
!shallowEqual(instance.props, nextProps) ||
|
||||
!shallowEqual(instance.state, nextState)
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
@@ -30,14 +70,6 @@ describe('ReactCompositeComponent', () => {
|
||||
.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner;
|
||||
ReactTestUtils = require('react-dom/test-utils');
|
||||
PropTypes = require('prop-types');
|
||||
shallowEqual = require('fbjs/lib/shallowEqual');
|
||||
|
||||
shallowCompare = function(instance, nextProps, nextState) {
|
||||
return (
|
||||
!shallowEqual(instance.props, nextProps) ||
|
||||
!shallowEqual(instance.state, nextState)
|
||||
);
|
||||
};
|
||||
|
||||
MorphingComponent = class extends React.Component {
|
||||
state = {activated: false};
|
||||
@@ -131,6 +163,7 @@ describe('ReactCompositeComponent', () => {
|
||||
'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
|
||||
'will stop working in React v17. Replace the ReactDOM.render() call ' +
|
||||
'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// New explicit API
|
||||
@@ -247,6 +280,7 @@ describe('ReactCompositeComponent', () => {
|
||||
'This is a no-op, but it might indicate a bug in your application. ' +
|
||||
'Instead, assign to `this.state` directly or define a `state = {};` ' +
|
||||
'class property with the desired state in the MyComponent component.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// No additional warning should be recorded
|
||||
@@ -271,6 +305,7 @@ describe('ReactCompositeComponent', () => {
|
||||
'This is a no-op, but it might indicate a bug in your application. ' +
|
||||
'Instead, assign to `this.state` directly or define a `state = {};` ' +
|
||||
'class property with the desired state in the MyComponent component.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// No additional warning should be recorded
|
||||
@@ -431,9 +466,8 @@ describe('ReactCompositeComponent', () => {
|
||||
instance = ReactDOM.render(<Component />, container);
|
||||
}).toWarnDev(
|
||||
'Cannot update during an existing state transition (such as within ' +
|
||||
"`render` or another component's constructor). Render methods should " +
|
||||
'be a pure function of props and state; constructor side-effects are ' +
|
||||
'an anti-pattern, but can be moved to `componentWillMount`.',
|
||||
'`render`). Render methods should be a pure function of props and state.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// The setState call is queued and then executed as a second pass. This
|
||||
@@ -481,6 +515,7 @@ describe('ReactCompositeComponent', () => {
|
||||
instance = ReactDOM.render(<Component />, container);
|
||||
}).toWarnDev(
|
||||
'Warning: setState(...): Cannot call setState() inside getChildContext()',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
expect(renderPasses).toBe(2);
|
||||
@@ -557,6 +592,7 @@ describe('ReactCompositeComponent', () => {
|
||||
expect(() => instance.setState({bogus: true})).toWarnDev(
|
||||
'Warning: Component.shouldComponentUpdate(): Returned undefined instead of a ' +
|
||||
'boolean value. Make sure to return true or false.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -573,6 +609,7 @@ describe('ReactCompositeComponent', () => {
|
||||
'Warning: Component has a method called ' +
|
||||
'componentDidUnmount(). But there is no such lifecycle method. ' +
|
||||
'Did you mean componentWillUnmount()?',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -591,6 +628,7 @@ describe('ReactCompositeComponent', () => {
|
||||
'If you meant to update the state in response to changing props, ' +
|
||||
'use componentWillReceiveProps(). If you meant to fetch data or ' +
|
||||
'run side-effects or mutations after React has updated the UI, use componentDidUpdate().',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -609,6 +647,7 @@ describe('ReactCompositeComponent', () => {
|
||||
expect(() => ReactTestUtils.renderIntoDocument(<Component />)).toWarnDev(
|
||||
'Warning: Setting defaultProps as an instance property on Component is not supported ' +
|
||||
'and will be ignored. Instead, define defaultProps as a static property on Component.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1099,6 +1138,7 @@ describe('ReactCompositeComponent', () => {
|
||||
'triggering nested component updates from render is not allowed. If ' +
|
||||
'necessary, trigger nested updates in componentDidUpdate.\n\nCheck the ' +
|
||||
'render method of Outer.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1390,6 +1430,7 @@ describe('ReactCompositeComponent', () => {
|
||||
expect(() => ReactDOM.render(<Foo idx="qwe" />, container)).toWarnDev(
|
||||
'Foo(...): When calling super() in `Foo`, make sure to pass ' +
|
||||
"up the same props that your component's constructor was passed.",
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -410,6 +410,7 @@ describe('ReactCompositeComponent-state', () => {
|
||||
'Warning: Test.componentWillReceiveProps(): Assigning directly to ' +
|
||||
"this.state is deprecated (except inside a component's constructor). " +
|
||||
'Use setState instead.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
expect(ops).toEqual([
|
||||
@@ -451,6 +452,7 @@ describe('ReactCompositeComponent-state', () => {
|
||||
'Warning: Test.componentWillMount(): Assigning directly to ' +
|
||||
"this.state is deprecated (except inside a component's constructor). " +
|
||||
'Use setState instead.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
expect(ops).toEqual([
|
||||
|
||||
@@ -9,11 +9,20 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
let React = require('react');
|
||||
let ReactDOM = require('react-dom');
|
||||
const ReactTestUtils = require('react-dom/test-utils');
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
let ReactTestUtils;
|
||||
|
||||
describe('ReactDOM', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
ReactTestUtils = require('react-dom/test-utils');
|
||||
});
|
||||
|
||||
// TODO: uncomment this test once we can run in phantom, which
|
||||
// supports real submit events.
|
||||
/*
|
||||
@@ -434,38 +443,77 @@ describe('ReactDOM', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('warns when requestAnimationFrame is not polyfilled in the browser', () => {
|
||||
it('warns when requestAnimationFrame is not polyfilled', () => {
|
||||
const previousRAF = global.requestAnimationFrame;
|
||||
try {
|
||||
global.requestAnimationFrame = undefined;
|
||||
delete global.requestAnimationFrame;
|
||||
jest.resetModules();
|
||||
expect(() => require('react-dom')).toWarnDev(
|
||||
'React depends on requestAnimationFrame.',
|
||||
"This browser doesn't support requestAnimationFrame.",
|
||||
{withoutStack: true},
|
||||
);
|
||||
} finally {
|
||||
global.requestAnimationFrame = previousRAF;
|
||||
}
|
||||
});
|
||||
|
||||
// We're just testing importing, not using it.
|
||||
// It is important because even isomorphic components may import it.
|
||||
it('can import findDOMNode in Node environment', () => {
|
||||
const previousRAF = global.requestAnimationFrame;
|
||||
const previousRIC = global.requestIdleCallback;
|
||||
const prevWindow = global.window;
|
||||
try {
|
||||
global.requestAnimationFrame = undefined;
|
||||
global.requestIdleCallback = undefined;
|
||||
// Simulate the Node environment:
|
||||
delete global.window;
|
||||
jest.resetModules();
|
||||
expect(() => {
|
||||
require('react-dom');
|
||||
}).not.toThrow();
|
||||
} finally {
|
||||
global.requestAnimationFrame = previousRAF;
|
||||
global.requestIdleCallback = previousRIC;
|
||||
global.window = prevWindow;
|
||||
it('reports stacks with re-entrant renderToString() calls on the client', () => {
|
||||
function Child2(props) {
|
||||
return <span ariaTypo3="no">{props.children}</span>;
|
||||
}
|
||||
|
||||
function App2() {
|
||||
return (
|
||||
<Child2>
|
||||
{ReactDOMServer.renderToString(<blink ariaTypo2="no" />)}
|
||||
</Child2>
|
||||
);
|
||||
}
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<span ariaTypo4="no">{ReactDOMServer.renderToString(<App2 />)}</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ServerEntry() {
|
||||
return ReactDOMServer.renderToString(<Child />);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<span ariaTypo="no" />
|
||||
<ServerEntry />
|
||||
<font ariaTypo5="no" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
expect(() => ReactDOM.render(<App />, container)).toWarnDev([
|
||||
// ReactDOM(App > div > span)
|
||||
'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
|
||||
' in span (at **)\n' +
|
||||
' in div (at **)\n' +
|
||||
' in App (at **)',
|
||||
// ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink)
|
||||
'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
|
||||
' in blink (at **)',
|
||||
// ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child) >>> ReactDOMServer(App2 > Child2 > span)
|
||||
'Invalid ARIA attribute `ariaTypo3`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
|
||||
' in span (at **)\n' +
|
||||
' in Child2 (at **)\n' +
|
||||
' in App2 (at **)',
|
||||
// ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child > span)
|
||||
'Invalid ARIA attribute `ariaTypo4`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
|
||||
' in span (at **)\n' +
|
||||
' in Child (at **)',
|
||||
// ReactDOM(App > div > font)
|
||||
'Invalid ARIA attribute `ariaTypo5`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
|
||||
' in font (at **)\n' +
|
||||
' in div (at **)\n' +
|
||||
' in App (at **)',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -554,39 +554,174 @@ describe('ReactDOMComponent', () => {
|
||||
expect(stubStyle.color).toEqual('green');
|
||||
});
|
||||
|
||||
it('should reject attribute key injection attack on markup', () => {
|
||||
it('should reject attribute key injection attack on markup for regular DOM (SSR)', () => {
|
||||
expect(() => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const container = document.createElement('div');
|
||||
const element = React.createElement(
|
||||
const element1 = React.createElement(
|
||||
'div',
|
||||
{'blah" onclick="beevil" noise="hi': 'selected'},
|
||||
null,
|
||||
);
|
||||
const element2 = React.createElement(
|
||||
'div',
|
||||
{'></div><script>alert("hi")</script>': 'selected'},
|
||||
null,
|
||||
);
|
||||
let result1 = ReactDOMServer.renderToString(element1);
|
||||
let result2 = ReactDOMServer.renderToString(element2);
|
||||
expect(result1.toLowerCase()).not.toContain('onclick');
|
||||
expect(result2.toLowerCase()).not.toContain('script');
|
||||
}
|
||||
}).toWarnDev([
|
||||
'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`',
|
||||
'Warning: Invalid attribute name: `></div><script>alert("hi")</script>`',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should reject attribute key injection attack on markup for custom elements (SSR)', () => {
|
||||
expect(() => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const element1 = React.createElement(
|
||||
'x-foo-component',
|
||||
{'blah" onclick="beevil" noise="hi': 'selected'},
|
||||
null,
|
||||
);
|
||||
ReactDOM.render(element, container);
|
||||
const element2 = React.createElement(
|
||||
'x-foo-component',
|
||||
{'></x-foo-component><script>alert("hi")</script>': 'selected'},
|
||||
null,
|
||||
);
|
||||
let result1 = ReactDOMServer.renderToString(element1);
|
||||
let result2 = ReactDOMServer.renderToString(element2);
|
||||
expect(result1.toLowerCase()).not.toContain('onclick');
|
||||
expect(result2.toLowerCase()).not.toContain('script');
|
||||
}
|
||||
}).toWarnDev(
|
||||
}).toWarnDev([
|
||||
'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`',
|
||||
);
|
||||
'Warning: Invalid attribute name: `></x-foo-component><script>alert("hi")</script>`',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should reject attribute key injection attack on update', () => {
|
||||
it('should reject attribute key injection attack on mount for regular DOM', () => {
|
||||
expect(() => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const container = document.createElement('div');
|
||||
ReactDOM.render(
|
||||
React.createElement(
|
||||
'div',
|
||||
{'blah" onclick="beevil" noise="hi': 'selected'},
|
||||
null,
|
||||
),
|
||||
container,
|
||||
);
|
||||
expect(container.firstChild.attributes.length).toBe(0);
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
ReactDOM.render(
|
||||
React.createElement(
|
||||
'div',
|
||||
{'></div><script>alert("hi")</script>': 'selected'},
|
||||
null,
|
||||
),
|
||||
container,
|
||||
);
|
||||
expect(container.firstChild.attributes.length).toBe(0);
|
||||
}
|
||||
}).toWarnDev([
|
||||
'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`',
|
||||
'Warning: Invalid attribute name: `></div><script>alert("hi")</script>`',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should reject attribute key injection attack on mount for custom elements', () => {
|
||||
expect(() => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const container = document.createElement('div');
|
||||
ReactDOM.render(
|
||||
React.createElement(
|
||||
'x-foo-component',
|
||||
{'blah" onclick="beevil" noise="hi': 'selected'},
|
||||
null,
|
||||
),
|
||||
container,
|
||||
);
|
||||
expect(container.firstChild.attributes.length).toBe(0);
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
ReactDOM.render(
|
||||
React.createElement(
|
||||
'x-foo-component',
|
||||
{'></x-foo-component><script>alert("hi")</script>': 'selected'},
|
||||
null,
|
||||
),
|
||||
container,
|
||||
);
|
||||
expect(container.firstChild.attributes.length).toBe(0);
|
||||
}
|
||||
}).toWarnDev([
|
||||
'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`',
|
||||
'Warning: Invalid attribute name: `></x-foo-component><script>alert("hi")</script>`',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should reject attribute key injection attack on update for regular DOM', () => {
|
||||
expect(() => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const container = document.createElement('div');
|
||||
const beforeUpdate = React.createElement('div', {}, null);
|
||||
ReactDOM.render(beforeUpdate, container);
|
||||
ReactDOM.render(
|
||||
React.createElement(
|
||||
'div',
|
||||
{'blah" onclick="beevil" noise="hi': 'selected'},
|
||||
null,
|
||||
),
|
||||
container,
|
||||
);
|
||||
expect(container.firstChild.attributes.length).toBe(0);
|
||||
ReactDOM.render(
|
||||
React.createElement(
|
||||
'div',
|
||||
{'></div><script>alert("hi")</script>': 'selected'},
|
||||
null,
|
||||
),
|
||||
container,
|
||||
);
|
||||
expect(container.firstChild.attributes.length).toBe(0);
|
||||
}
|
||||
}).toWarnDev([
|
||||
'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`',
|
||||
'Warning: Invalid attribute name: `></div><script>alert("hi")</script>`',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should reject attribute key injection attack on update for custom elements', () => {
|
||||
expect(() => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const container = document.createElement('div');
|
||||
const beforeUpdate = React.createElement('x-foo-component', {}, null);
|
||||
ReactDOM.render(beforeUpdate, container);
|
||||
|
||||
const afterUpdate = React.createElement(
|
||||
'x-foo-component',
|
||||
{'blah" onclick="beevil" noise="hi': 'selected'},
|
||||
null,
|
||||
ReactDOM.render(
|
||||
React.createElement(
|
||||
'x-foo-component',
|
||||
{'blah" onclick="beevil" noise="hi': 'selected'},
|
||||
null,
|
||||
),
|
||||
container,
|
||||
);
|
||||
ReactDOM.render(afterUpdate, container);
|
||||
expect(container.firstChild.attributes.length).toBe(0);
|
||||
ReactDOM.render(
|
||||
React.createElement(
|
||||
'x-foo-component',
|
||||
{'></x-foo-component><script>alert("hi")</script>': 'selected'},
|
||||
null,
|
||||
),
|
||||
container,
|
||||
);
|
||||
expect(container.firstChild.attributes.length).toBe(0);
|
||||
}
|
||||
}).toWarnDev(
|
||||
}).toWarnDev([
|
||||
'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`',
|
||||
);
|
||||
'Warning: Invalid attribute name: `></x-foo-component><script>alert("hi")</script>`',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should update arbitrary attributes for tags containing dashes', () => {
|
||||
|
||||
@@ -195,6 +195,7 @@ describe('ReactDOMComponentTree', () => {
|
||||
'was rendered by React and is not a top-level container. You may ' +
|
||||
'have accidentally passed in a React root node instead of its ' +
|
||||
'container.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -211,6 +212,7 @@ describe('ReactDOMComponentTree', () => {
|
||||
'component. If you intended to update the children of this node, ' +
|
||||
'you should instead have the existing children update their state ' +
|
||||
'and render the new components instead of calling ReactDOM.render.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -256,6 +256,7 @@ describe('ReactDOMFiber', () => {
|
||||
'and will be removed in React 17+. Update your code to use ' +
|
||||
'ReactDOM.createPortal() instead. It has the exact same API, ' +
|
||||
'but without the "unstable_" prefix.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(portalContainer.innerHTML).toBe('<div>portal</div>');
|
||||
expect(container.innerHTML).toBe('<div></div>');
|
||||
@@ -979,8 +980,9 @@ describe('ReactDOMFiber', () => {
|
||||
expect(() => ReactDOM.render(<Example />, container)).toWarnDev(
|
||||
'Expected `onClick` listener to be a function, instead got `false`.\n\n' +
|
||||
'If you used to conditionally omit it with onClick={condition && value}, ' +
|
||||
'pass onClick={condition ? value : undefined} instead.\n',
|
||||
' in div (at **)\n' + ' in Example (at **)',
|
||||
'pass onClick={condition ? value : undefined} instead.\n' +
|
||||
' in div (at **)\n' +
|
||||
' in Example (at **)',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1100,6 +1102,7 @@ describe('ReactDOMFiber', () => {
|
||||
'removed without using React. This is not supported and will ' +
|
||||
'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
|
||||
'to empty a container.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
}).toThrowError();
|
||||
});
|
||||
@@ -1117,6 +1120,7 @@ describe('ReactDOMFiber', () => {
|
||||
'removed without using React. This is not supported and will ' +
|
||||
'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
|
||||
'to empty a container.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1133,6 +1137,7 @@ describe('ReactDOMFiber', () => {
|
||||
'removed without using React. This is not supported and will ' +
|
||||
'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
|
||||
'to empty a container.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -41,9 +41,7 @@ describe('ReactDOMOption', () => {
|
||||
expect(() => {
|
||||
node = ReactTestUtils.renderIntoDocument(el);
|
||||
}).toWarnDev(
|
||||
'<div> cannot appear as a child of <option>.\n' +
|
||||
' in div (at **)\n' +
|
||||
' in option (at **)',
|
||||
'<div> cannot appear as a child of <option>.\n' + ' in option (at **)',
|
||||
);
|
||||
expect(node.innerHTML).toBe('1 2');
|
||||
ReactTestUtils.renderIntoDocument(el);
|
||||
|
||||
@@ -143,7 +143,9 @@ describe('ReactDOMRoot', () => {
|
||||
<span />
|
||||
</div>,
|
||||
);
|
||||
expect(jest.runAllTimers).toWarnDev('Extra attributes');
|
||||
expect(jest.runAllTimers).toWarnDev('Extra attributes', {
|
||||
withoutStack: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not clear existing children', async () => {
|
||||
@@ -358,9 +360,18 @@ describe('ReactDOMRoot', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const batch = root.createBatch();
|
||||
const InvalidType = undefined;
|
||||
expect(() => batch.render(<InvalidType />)).toWarnDev([
|
||||
'React.createElement: type is invalid',
|
||||
]);
|
||||
expect(() => batch.render(<InvalidType />)).toWarnDev(
|
||||
['React.createElement: type is invalid'],
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(() => batch.commit()).toThrow('Element type is invalid');
|
||||
});
|
||||
|
||||
it('throws a good message on invalid containers', () => {
|
||||
expect(() => {
|
||||
ReactDOM.unstable_createRoot(<div>Hi</div>);
|
||||
}).toThrow(
|
||||
'unstable_createRoot(...): Target container is not a DOM element.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -443,6 +443,37 @@ describe('ReactDOMSelect', () => {
|
||||
expect(markup).not.toContain('<option selected="" value="gorilla"');
|
||||
});
|
||||
|
||||
it('should support server-side rendering with dangerouslySetInnerHTML', () => {
|
||||
const stub = (
|
||||
<select defaultValue="giraffe">
|
||||
<option
|
||||
value="monkey"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: 'A monkey!',
|
||||
}}>
|
||||
{undefined}
|
||||
</option>
|
||||
<option
|
||||
value="giraffe"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: 'A giraffe!',
|
||||
}}>
|
||||
{null}
|
||||
</option>
|
||||
<option
|
||||
value="gorilla"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: 'A gorilla!',
|
||||
}}
|
||||
/>
|
||||
</select>
|
||||
);
|
||||
const markup = ReactDOMServer.renderToString(stub);
|
||||
expect(markup).toContain('<option selected="" value="giraffe"');
|
||||
expect(markup).not.toContain('<option selected="" value="monkey"');
|
||||
expect(markup).not.toContain('<option selected="" value="gorilla"');
|
||||
});
|
||||
|
||||
it('should support server-side rendering with multiple', () => {
|
||||
const stub = (
|
||||
<select multiple={true} value={['giraffe', 'gorilla']} onChange={noop}>
|
||||
@@ -514,6 +545,38 @@ describe('ReactDOMSelect', () => {
|
||||
expect(node.options[2].selected).toBe(false); // gorilla
|
||||
});
|
||||
|
||||
it('should support options with dynamic children', () => {
|
||||
const container = document.createElement('div');
|
||||
|
||||
let node;
|
||||
|
||||
function App({value}) {
|
||||
return (
|
||||
<select value={value} ref={n => (node = n)} onChange={noop}>
|
||||
<option key="monkey" value="monkey">
|
||||
A monkey {value === 'monkey' ? 'is chosen' : null}!
|
||||
</option>
|
||||
<option key="giraffe" value="giraffe">
|
||||
A giraffe {value === 'giraffe' && 'is chosen'}!
|
||||
</option>
|
||||
<option key="gorilla" value="gorilla">
|
||||
A gorilla {value === 'gorilla' && 'is chosen'}!
|
||||
</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<App value="monkey" />, container);
|
||||
expect(node.options[0].selected).toBe(true); // monkey
|
||||
expect(node.options[1].selected).toBe(false); // giraffe
|
||||
expect(node.options[2].selected).toBe(false); // gorilla
|
||||
|
||||
ReactDOM.render(<App value="giraffe" />, container);
|
||||
expect(node.options[0].selected).toBe(false); // monkey
|
||||
expect(node.options[1].selected).toBe(true); // giraffe
|
||||
expect(node.options[2].selected).toBe(false); // gorilla
|
||||
});
|
||||
|
||||
it('should warn if value is null', () => {
|
||||
expect(() =>
|
||||
ReactTestUtils.renderIntoDocument(
|
||||
@@ -535,24 +598,21 @@ describe('ReactDOMSelect', () => {
|
||||
});
|
||||
|
||||
it('should warn if selected is set on <option>', () => {
|
||||
expect(() =>
|
||||
ReactTestUtils.renderIntoDocument(
|
||||
function App() {
|
||||
return (
|
||||
<select>
|
||||
<option selected={true} />
|
||||
<option selected={true} />
|
||||
</select>,
|
||||
),
|
||||
).toWarnDev(
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
expect(() => ReactTestUtils.renderIntoDocument(<App />)).toWarnDev(
|
||||
'Use the `defaultValue` or `value` props on <select> instead of ' +
|
||||
'setting `selected` on <option>.',
|
||||
);
|
||||
|
||||
ReactTestUtils.renderIntoDocument(
|
||||
<select>
|
||||
<option selected={true} />
|
||||
<option selected={true} />
|
||||
</select>,
|
||||
);
|
||||
ReactTestUtils.renderIntoDocument(<App />);
|
||||
});
|
||||
|
||||
it('should warn if value is null and multiple is true', () => {
|
||||
@@ -584,11 +644,20 @@ describe('ReactDOMSelect', () => {
|
||||
<option value="gorilla">A gorilla!</option>
|
||||
</select>
|
||||
);
|
||||
const node = ReactTestUtils.renderIntoDocument(stub);
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
ReactTestUtils.Simulate.change(node);
|
||||
try {
|
||||
const node = ReactDOM.render(stub, container);
|
||||
|
||||
expect(node.value).toBe('giraffe');
|
||||
node.dispatchEvent(
|
||||
new Event('change', {bubbles: true, cancelable: false}),
|
||||
);
|
||||
|
||||
expect(node.value).toBe('giraffe');
|
||||
} finally {
|
||||
document.body.removeChild(container);
|
||||
}
|
||||
});
|
||||
|
||||
it('should warn if value and defaultValue props are specified', () => {
|
||||
@@ -617,6 +686,13 @@ describe('ReactDOMSelect', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not warn about missing onChange in uncontrolled textareas', () => {
|
||||
const container = document.createElement('div');
|
||||
ReactDOM.render(<select />, container);
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
ReactDOM.render(<select value={undefined} />, container);
|
||||
});
|
||||
|
||||
it('should be able to safely remove select onChange', () => {
|
||||
function changeView() {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMSelection;
|
||||
let invariant;
|
||||
|
||||
let getModernOffsetsFromPoints;
|
||||
|
||||
@@ -21,7 +20,6 @@ describe('ReactDOMSelection', () => {
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMSelection = require('../client/ReactDOMSelection');
|
||||
invariant = require('fbjs/lib/invariant');
|
||||
|
||||
({getModernOffsetsFromPoints} = ReactDOMSelection);
|
||||
});
|
||||
@@ -68,10 +66,9 @@ describe('ReactDOMSelection', () => {
|
||||
}
|
||||
traverse(outerNode);
|
||||
|
||||
invariant(
|
||||
start !== null && end !== null,
|
||||
'Provided anchor/focus nodes were outside of root.',
|
||||
);
|
||||
if (start === null || end === null) {
|
||||
throw new Error('Provided anchor/focus nodes were outside of root.');
|
||||
}
|
||||
return {start, end};
|
||||
}
|
||||
|
||||
|
||||
@@ -885,6 +885,7 @@ describe('ReactDOMServerIntegration', () => {
|
||||
'components) but got: object. You likely forgot to export your ' +
|
||||
"component from the file it's defined in, or you might have mixed up " +
|
||||
'default and named imports.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
await render(EmptyComponent);
|
||||
},
|
||||
@@ -906,6 +907,7 @@ describe('ReactDOMServerIntegration', () => {
|
||||
'Warning: React.createElement: type is invalid -- expected a string ' +
|
||||
'(for built-in components) or a class/function (for composite ' +
|
||||
'components) but got: null.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
await render(NullComponent);
|
||||
},
|
||||
@@ -925,6 +927,7 @@ describe('ReactDOMServerIntegration', () => {
|
||||
'components) but got: undefined. You likely forgot to export your ' +
|
||||
"component from the file it's defined in, or you might have mixed up " +
|
||||
'default and named imports.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
await render(UndefinedComponent);
|
||||
|
||||
@@ -14,14 +14,12 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
let ReactTestUtils;
|
||||
|
||||
function initModules() {
|
||||
// Reset warning cache.
|
||||
jest.resetModuleRegistry();
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactTestUtils = require('react-dom/test-utils');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
|
||||
// Make them available to the helpers.
|
||||
@@ -35,6 +33,7 @@ const {
|
||||
resetModules,
|
||||
itRenders,
|
||||
itClientRenders,
|
||||
itThrowsWhenRendering,
|
||||
renderIntoDom,
|
||||
serverRender,
|
||||
} = ReactDOMServerIntegrationUtils(initModules);
|
||||
@@ -327,6 +326,81 @@ describe('ReactDOMServerIntegration', () => {
|
||||
expectSelectValue(e, 'bar');
|
||||
});
|
||||
|
||||
itRenders(
|
||||
'a select with options that use dangerouslySetInnerHTML',
|
||||
async render => {
|
||||
const e = await render(
|
||||
<select defaultValue="baz" value="bar" readOnly={true}>
|
||||
<option
|
||||
id="foo"
|
||||
value="foo"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: 'Foo',
|
||||
}}>
|
||||
{undefined}
|
||||
</option>
|
||||
<option
|
||||
id="bar"
|
||||
value="bar"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: 'Bar',
|
||||
}}>
|
||||
{null}
|
||||
</option>
|
||||
<option
|
||||
id="baz"
|
||||
value="baz"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: 'Baz',
|
||||
}}
|
||||
/>
|
||||
</select>,
|
||||
1,
|
||||
);
|
||||
expectSelectValue(e, 'bar');
|
||||
},
|
||||
);
|
||||
|
||||
itThrowsWhenRendering(
|
||||
'a select with option that uses dangerouslySetInnerHTML and 0 as child',
|
||||
async render => {
|
||||
await render(
|
||||
<select defaultValue="baz" value="foo" readOnly={true}>
|
||||
<option
|
||||
id="foo"
|
||||
value="foo"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: 'Foo',
|
||||
}}>
|
||||
{0}
|
||||
</option>
|
||||
</select>,
|
||||
1,
|
||||
);
|
||||
},
|
||||
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
|
||||
);
|
||||
|
||||
itThrowsWhenRendering(
|
||||
'a select with option that uses dangerouslySetInnerHTML and empty string as child',
|
||||
async render => {
|
||||
await render(
|
||||
<select defaultValue="baz" value="foo" readOnly={true}>
|
||||
<option
|
||||
id="foo"
|
||||
value="foo"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: 'Foo',
|
||||
}}>
|
||||
{''}
|
||||
</option>
|
||||
</select>,
|
||||
1,
|
||||
);
|
||||
},
|
||||
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
|
||||
);
|
||||
|
||||
itRenders(
|
||||
'a select value overriding defaultValue no matter the prop order',
|
||||
async render => {
|
||||
@@ -339,6 +413,36 @@ describe('ReactDOMServerIntegration', () => {
|
||||
expectSelectValue(e, 'bar');
|
||||
},
|
||||
);
|
||||
|
||||
itRenders('a select option with flattened children', async render => {
|
||||
const e = await render(
|
||||
<select value="bar" readOnly={true}>
|
||||
<option value="bar">A {'B'}</option>
|
||||
</select>,
|
||||
);
|
||||
const option = e.options[0];
|
||||
expect(option.childNodes.length).toBe(1);
|
||||
expect(option.childNodes[0].nodeType).toBe(3);
|
||||
expect(option.childNodes[0].nodeValue).toBe('A B');
|
||||
});
|
||||
|
||||
itRenders(
|
||||
'a select option with flattened children and a warning',
|
||||
async render => {
|
||||
const e = await render(
|
||||
<select readOnly={true} value="bar">
|
||||
<option value="bar">
|
||||
{['Bar', false, 'Foo', <div key="1" />, 'Baz']}
|
||||
</option>
|
||||
</select>,
|
||||
1,
|
||||
);
|
||||
expect(e.getAttribute('value')).toBe(null);
|
||||
expect(e.getAttribute('defaultValue')).toBe(null);
|
||||
expect(e.firstChild.innerHTML).toBe('BarFooBaz');
|
||||
expect(e.firstChild.selected).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('user interaction', function() {
|
||||
@@ -443,35 +547,63 @@ describe('ReactDOMServerIntegration', () => {
|
||||
|
||||
describe('user interaction with controlled inputs', function() {
|
||||
itClientRenders('a controlled text input', async render => {
|
||||
const setUntrackedValue = Object.getOwnPropertyDescriptor(
|
||||
HTMLInputElement.prototype,
|
||||
'value',
|
||||
).set;
|
||||
|
||||
let changeCount = 0;
|
||||
const e = await render(
|
||||
<ControlledInput onChange={() => changeCount++} />,
|
||||
);
|
||||
expect(changeCount).toBe(0);
|
||||
expect(e.value).toBe('Hello');
|
||||
const container = e.parentNode;
|
||||
document.body.appendChild(container);
|
||||
|
||||
// simulate a user typing.
|
||||
e.value = 'Goodbye';
|
||||
ReactTestUtils.Simulate.change(e);
|
||||
try {
|
||||
expect(changeCount).toBe(0);
|
||||
expect(e.value).toBe('Hello');
|
||||
|
||||
expect(changeCount).toBe(1);
|
||||
expect(e.value).toBe('Goodbye');
|
||||
// simulate a user typing.
|
||||
setUntrackedValue.call(e, 'Goodbye');
|
||||
e.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: false}),
|
||||
);
|
||||
|
||||
expect(changeCount).toBe(1);
|
||||
expect(e.value).toBe('Goodbye');
|
||||
} finally {
|
||||
document.body.removeChild(container);
|
||||
}
|
||||
});
|
||||
|
||||
itClientRenders('a controlled textarea', async render => {
|
||||
const setUntrackedValue = Object.getOwnPropertyDescriptor(
|
||||
HTMLTextAreaElement.prototype,
|
||||
'value',
|
||||
).set;
|
||||
|
||||
let changeCount = 0;
|
||||
const e = await render(
|
||||
<ControlledTextArea onChange={() => changeCount++} />,
|
||||
);
|
||||
expect(changeCount).toBe(0);
|
||||
expect(e.value).toBe('Hello');
|
||||
const container = e.parentNode;
|
||||
document.body.appendChild(container);
|
||||
|
||||
// simulate a user typing.
|
||||
e.value = 'Goodbye';
|
||||
ReactTestUtils.Simulate.change(e);
|
||||
try {
|
||||
expect(changeCount).toBe(0);
|
||||
expect(e.value).toBe('Hello');
|
||||
|
||||
expect(changeCount).toBe(1);
|
||||
expect(e.value).toBe('Goodbye');
|
||||
// simulate a user typing.
|
||||
setUntrackedValue.call(e, 'Goodbye');
|
||||
e.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: false}),
|
||||
);
|
||||
|
||||
expect(changeCount).toBe(1);
|
||||
expect(e.value).toBe('Goodbye');
|
||||
} finally {
|
||||
document.body.removeChild(container);
|
||||
}
|
||||
});
|
||||
|
||||
itClientRenders('a controlled checkbox', async render => {
|
||||
@@ -479,31 +611,53 @@ describe('ReactDOMServerIntegration', () => {
|
||||
const e = await render(
|
||||
<ControlledCheckbox onChange={() => changeCount++} />,
|
||||
);
|
||||
expect(changeCount).toBe(0);
|
||||
expect(e.checked).toBe(true);
|
||||
const container = e.parentNode;
|
||||
document.body.appendChild(container);
|
||||
|
||||
// simulate a user typing.
|
||||
e.checked = false;
|
||||
ReactTestUtils.Simulate.change(e);
|
||||
try {
|
||||
expect(changeCount).toBe(0);
|
||||
expect(e.checked).toBe(true);
|
||||
|
||||
expect(changeCount).toBe(1);
|
||||
expect(e.checked).toBe(false);
|
||||
// simulate a user clicking.
|
||||
e.dispatchEvent(
|
||||
new Event('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
|
||||
expect(changeCount).toBe(1);
|
||||
expect(e.checked).toBe(false);
|
||||
} finally {
|
||||
document.body.removeChild(container);
|
||||
}
|
||||
});
|
||||
|
||||
itClientRenders('a controlled select', async render => {
|
||||
const setUntrackedValue = Object.getOwnPropertyDescriptor(
|
||||
HTMLSelectElement.prototype,
|
||||
'value',
|
||||
).set;
|
||||
|
||||
let changeCount = 0;
|
||||
const e = await render(
|
||||
<ControlledSelect onChange={() => changeCount++} />,
|
||||
);
|
||||
expect(changeCount).toBe(0);
|
||||
expect(e.value).toBe('Hello');
|
||||
const container = e.parentNode;
|
||||
document.body.appendChild(container);
|
||||
|
||||
// simulate a user typing.
|
||||
e.value = 'Goodbye';
|
||||
ReactTestUtils.Simulate.change(e);
|
||||
try {
|
||||
expect(changeCount).toBe(0);
|
||||
expect(e.value).toBe('Hello');
|
||||
|
||||
expect(changeCount).toBe(1);
|
||||
expect(e.value).toBe('Goodbye');
|
||||
// simulate a user typing.
|
||||
setUntrackedValue.call(e, 'Goodbye');
|
||||
e.dispatchEvent(
|
||||
new Event('change', {bubbles: true, cancelable: false}),
|
||||
);
|
||||
|
||||
expect(changeCount).toBe(1);
|
||||
expect(e.value).toBe('Goodbye');
|
||||
} finally {
|
||||
document.body.removeChild(container);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
68
packages/react-dom/src/__tests__/ReactDOMServerIntegrationSpecialTypes-test.js
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
|
||||
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
|
||||
function initModules() {
|
||||
// Reset warning cache.
|
||||
jest.resetModuleRegistry();
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
|
||||
// Make them available to the helpers.
|
||||
return {
|
||||
ReactDOM,
|
||||
ReactDOMServer,
|
||||
};
|
||||
}
|
||||
|
||||
const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
|
||||
|
||||
describe('ReactDOMServerIntegration', () => {
|
||||
beforeEach(() => {
|
||||
resetModules();
|
||||
});
|
||||
|
||||
itRenders('a forwardedRef component and its children', async render => {
|
||||
const FunctionComponent = ({label, forwardedRef}) => (
|
||||
<div ref={forwardedRef}>{label}</div>
|
||||
);
|
||||
const WrappedFunctionComponent = React.forwardRef((props, ref) => (
|
||||
<FunctionComponent {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
const ref = React.createRef();
|
||||
const element = await render(
|
||||
<WrappedFunctionComponent ref={ref} label="Test" />,
|
||||
);
|
||||
const parent = element.parentNode;
|
||||
const div = parent.childNodes[0];
|
||||
expect(div.tagName).toBe('DIV');
|
||||
expect(div.textContent).toBe('Test');
|
||||
});
|
||||
|
||||
itRenders('a Profiler component and its children', async render => {
|
||||
const element = await render(
|
||||
<React.unstable_Profiler id="profiler" onRender={jest.fn()}>
|
||||
<div>Test</div>
|
||||
</React.unstable_Profiler>,
|
||||
);
|
||||
const parent = element.parentNode;
|
||||
const div = parent.childNodes[0];
|
||||
expect(div.tagName).toBe('DIV');
|
||||
expect(div.textContent).toBe('Test');
|
||||
});
|
||||
});
|
||||
@@ -44,6 +44,7 @@ describe('ReactDOMServerLifecycles', () => {
|
||||
ReactDOMServer.renderToString(<Component />),
|
||||
).toLowPriorityWarnDev(
|
||||
'Component: componentWillMount() is deprecated and will be removed in the next major version.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -62,6 +63,7 @@ describe('ReactDOMServerLifecycles', () => {
|
||||
).toLowPriorityWarnDev(
|
||||
'Warning: Component: componentWillMount() is deprecated and will be removed ' +
|
||||
'in the next major version.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// De-duped
|
||||
|
||||
@@ -183,6 +183,7 @@ describe('ReactDOMServerLifecycles', () => {
|
||||
expect(() => ReactDOMServer.renderToString(<Component />)).toWarnDev(
|
||||
'Component.getDerivedStateFromProps(): A valid state object (or null) must ' +
|
||||
'be returned. You have returned undefined.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// De-duped
|
||||
@@ -202,6 +203,7 @@ describe('ReactDOMServerLifecycles', () => {
|
||||
expect(() => ReactDOMServer.renderToString(<Component />)).toWarnDev(
|
||||
'Component: Did not properly initialize state during construction. ' +
|
||||
'Expected state to be an object, but it was undefined.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// De-duped
|
||||
@@ -258,6 +260,7 @@ describe('ReactDOMServerLifecycles', () => {
|
||||
'usually means you called setState() outside componentWillMount() on ' +
|
||||
'the server. This is a no-op.\n\n' +
|
||||
'Please check the code for the Outer component.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const emptyFunction = require('fbjs/lib/emptyFunction');
|
||||
function emptyFunction() {}
|
||||
|
||||
describe('ReactDOMTextarea', () => {
|
||||
let React;
|
||||
@@ -97,7 +97,7 @@ describe('ReactDOMTextarea', () => {
|
||||
});
|
||||
|
||||
it('should display `value` of number 0', () => {
|
||||
const stub = <textarea value={0} />;
|
||||
const stub = <textarea value={0} onChange={emptyFunction} />;
|
||||
const node = renderTextarea(stub);
|
||||
|
||||
expect(node.value).toBe('0');
|
||||
@@ -237,11 +237,25 @@ describe('ReactDOMTextarea', () => {
|
||||
|
||||
it('should properly control a value of number `0`', () => {
|
||||
const stub = <textarea value={0} onChange={emptyFunction} />;
|
||||
const node = renderTextarea(stub);
|
||||
const setUntrackedValue = Object.getOwnPropertyDescriptor(
|
||||
HTMLTextAreaElement.prototype,
|
||||
'value',
|
||||
).set;
|
||||
|
||||
node.value = 'giraffe';
|
||||
ReactTestUtils.Simulate.change(node);
|
||||
expect(node.value).toBe('0');
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
try {
|
||||
const node = renderTextarea(stub, container);
|
||||
|
||||
setUntrackedValue.call(node, 'giraffe');
|
||||
node.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: false}),
|
||||
);
|
||||
expect(node.value).toBe('0');
|
||||
} finally {
|
||||
document.body.removeChild(container);
|
||||
}
|
||||
});
|
||||
|
||||
it('should treat children like `defaultValue`', () => {
|
||||
@@ -402,4 +416,11 @@ describe('ReactDOMTextarea', () => {
|
||||
<textarea value="foo" defaultValue="bar" readOnly={true} />,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not warn about missing onChange in uncontrolled textareas', () => {
|
||||
const container = document.createElement('div');
|
||||
ReactDOM.render(<textarea />, container);
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
ReactDOM.render(<textarea value={undefined} />, container);
|
||||
});
|
||||
});
|
||||
|
||||
79
packages/react-dom/src/__tests__/ReactErrorLoggingRecovery-test.js
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
// This is a regression test for https://github.com/facebook/react/issues/13188.
|
||||
// It reproduces a combination of conditions that led to a problem.
|
||||
|
||||
if (global.window) {
|
||||
throw new Error('This test must run in a Node environment.');
|
||||
}
|
||||
|
||||
// The issue only reproduced when React was loaded before JSDOM.
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
|
||||
// Unlike other tests, we want to enable error logging.
|
||||
// Note this is not a real Error prototype property,
|
||||
// it's only set in our Jest environment.
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Error.prototype.suppressReactErrorLogging = false;
|
||||
|
||||
// Initialize JSDOM separately.
|
||||
// We don't use our normal JSDOM setup because we want to load React first.
|
||||
const {JSDOM} = require('jsdom');
|
||||
global.requestAnimationFrame = setTimeout;
|
||||
global.cancelAnimationFrame = clearTimeout;
|
||||
const jsdom = new JSDOM(`<div id="app-root"></div>`);
|
||||
global.window = jsdom.window;
|
||||
global.document = jsdom.window.document;
|
||||
global.navigator = jsdom.window.navigator;
|
||||
|
||||
class Bad extends React.Component {
|
||||
componentDidUpdate() {
|
||||
throw new Error('no');
|
||||
}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ReactErrorLoggingRecovery', () => {
|
||||
let originalConsoleError = console.error;
|
||||
|
||||
beforeEach(() => {
|
||||
console.error = error => {
|
||||
throw new Error('Buggy console.error');
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
it('should recover from errors in console.error', function() {
|
||||
const div = document.createElement('div');
|
||||
let didCatch = false;
|
||||
try {
|
||||
ReactDOM.render(<Bad />, div);
|
||||
ReactDOM.render(<Bad />, div);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('no');
|
||||
didCatch = true;
|
||||
}
|
||||
expect(didCatch).toBe(true);
|
||||
ReactDOM.render(<span>Hello</span>, div);
|
||||
expect(div.firstChild.textContent).toBe('Hello');
|
||||
|
||||
// Verify the console.error bug is surfaced
|
||||
expect(() => {
|
||||
jest.runAllTimers();
|
||||
}).toThrow('Buggy console.error');
|
||||
});
|
||||
});
|
||||
@@ -63,6 +63,7 @@ describe('ReactMount', () => {
|
||||
'Functions are not valid as a React child. ' +
|
||||
'This may happen if you return a Component instead of <Component /> from render. ' +
|
||||
'Or maybe you meant to call this function rather than return it.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -126,6 +127,7 @@ describe('ReactMount', () => {
|
||||
|
||||
expect(() => ReactDOM.hydrate(<div />, container)).toWarnDev(
|
||||
'Did not expect server HTML to contain the text node " " in <container>.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -135,6 +137,7 @@ describe('ReactMount', () => {
|
||||
|
||||
expect(() => ReactDOM.hydrate(<div />, container)).toWarnDev(
|
||||
'Did not expect server HTML to contain the text node " " in <container>.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -153,6 +156,7 @@ describe('ReactMount', () => {
|
||||
ReactDOM.render(<div />, iFrame.contentDocument.body),
|
||||
).toWarnDev(
|
||||
'Rendering components directly into document.body is discouraged',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -171,6 +175,7 @@ describe('ReactMount', () => {
|
||||
).toWarnDev(
|
||||
'Server: "This markup contains an nbsp entity: server text" ' +
|
||||
'Client: "This markup contains an nbsp entity: client text"',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -197,6 +202,7 @@ describe('ReactMount', () => {
|
||||
'root component. If you intended to update the children of this node, ' +
|
||||
'you should instead have the existing children update their state and ' +
|
||||
'render the new components instead of calling ReactDOM.render.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -222,6 +228,7 @@ describe('ReactMount', () => {
|
||||
expect(() => ReactDOMOther.unmountComponentAtNode(container)).toWarnDev(
|
||||
"Warning: unmountComponentAtNode(): The node you're attempting to unmount " +
|
||||
'was rendered by another copy of React.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// Don't throw a warning if the correct React copy unmounts the node
|
||||
|
||||
@@ -55,6 +55,7 @@ describe('ReactMount', () => {
|
||||
'unmount was rendered by React and is not a top-level container. You ' +
|
||||
'may have accidentally passed in a React root node instead of its ' +
|
||||
'container.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -77,6 +78,7 @@ describe('ReactMount', () => {
|
||||
'unmount was rendered by React and is not a top-level container. ' +
|
||||
'Instead, have the parent component update its state and rerender in ' +
|
||||
'order to remove this component.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -209,8 +209,8 @@ describe('ReactMultiChild', () => {
|
||||
'Keys should be unique so that components maintain their identity ' +
|
||||
'across updates. Non-unique keys may cause children to be ' +
|
||||
'duplicated and/or omitted — the behavior is unsupported and ' +
|
||||
'could change in a future version.',
|
||||
' in div (at **)\n' +
|
||||
'could change in a future version.\n' +
|
||||
' in div (at **)\n' +
|
||||
' in WrapperComponent (at **)\n' +
|
||||
' in div (at **)\n' +
|
||||
' in Parent (at **)',
|
||||
@@ -269,8 +269,8 @@ describe('ReactMultiChild', () => {
|
||||
'Keys should be unique so that components maintain their identity ' +
|
||||
'across updates. Non-unique keys may cause children to be ' +
|
||||
'duplicated and/or omitted — the behavior is unsupported and ' +
|
||||
'could change in a future version.',
|
||||
' in div (at **)\n' +
|
||||
'could change in a future version.\n' +
|
||||
' in div (at **)\n' +
|
||||
' in WrapperComponent (at **)\n' +
|
||||
' in div (at **)\n' +
|
||||
' in Parent (at **)',
|
||||
@@ -294,6 +294,67 @@ describe('ReactMultiChild', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn for using generators as children', () => {
|
||||
function* Foo() {
|
||||
yield <h1 key="1">Hello</h1>;
|
||||
yield <h1 key="2">World</h1>;
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
expect(() => {
|
||||
ReactDOM.render(<Foo />, div);
|
||||
}).toWarnDev(
|
||||
'Using Generators as children is unsupported and will likely yield ' +
|
||||
'unexpected results because enumerating a generator mutates it. You may ' +
|
||||
'convert it to an array with `Array.from()` or the `[...spread]` operator ' +
|
||||
'before rendering. Keep in mind you might need to polyfill these features for older browsers.\n' +
|
||||
' in Foo (at **)',
|
||||
);
|
||||
|
||||
// Test de-duplication
|
||||
ReactDOM.render(<Foo />, div);
|
||||
});
|
||||
|
||||
it('should not warn for using generators in legacy iterables', () => {
|
||||
const fooIterable = {
|
||||
'@@iterator': function*() {
|
||||
yield <h1 key="1">Hello</h1>;
|
||||
yield <h1 key="2">World</h1>;
|
||||
},
|
||||
};
|
||||
|
||||
function Foo() {
|
||||
return fooIterable;
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(<Foo />, div);
|
||||
expect(div.textContent).toBe('HelloWorld');
|
||||
|
||||
ReactDOM.render(<Foo />, div);
|
||||
expect(div.textContent).toBe('HelloWorld');
|
||||
});
|
||||
|
||||
it('should not warn for using generators in modern iterables', () => {
|
||||
const fooIterable = {
|
||||
[Symbol.iterator]: function*() {
|
||||
yield <h1 key="1">Hello</h1>;
|
||||
yield <h1 key="2">World</h1>;
|
||||
},
|
||||
};
|
||||
|
||||
function Foo() {
|
||||
return fooIterable;
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(<Foo />, div);
|
||||
expect(div.textContent).toBe('HelloWorld');
|
||||
|
||||
ReactDOM.render(<Foo />, div);
|
||||
expect(div.textContent).toBe('HelloWorld');
|
||||
});
|
||||
|
||||
it('should reorder bailed-out children', () => {
|
||||
class LetterInner extends React.Component {
|
||||
render() {
|
||||
|
||||
@@ -248,7 +248,7 @@ function prepareChildrenArray(childrenArray) {
|
||||
return childrenArray;
|
||||
}
|
||||
|
||||
function prepareChildrenIterable(childrenArray) {
|
||||
function prepareChildrenLegacyIterable(childrenArray) {
|
||||
return {
|
||||
'@@iterator': function*() {
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
@@ -259,9 +259,27 @@ function prepareChildrenIterable(childrenArray) {
|
||||
};
|
||||
}
|
||||
|
||||
function prepareChildrenModernIterable(childrenArray) {
|
||||
return {
|
||||
[Symbol.iterator]: function*() {
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const child of childrenArray) {
|
||||
yield child;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function testPropsSequence(sequence) {
|
||||
testPropsSequenceWithPreparedChildren(sequence, prepareChildrenArray);
|
||||
testPropsSequenceWithPreparedChildren(sequence, prepareChildrenIterable);
|
||||
testPropsSequenceWithPreparedChildren(
|
||||
sequence,
|
||||
prepareChildrenLegacyIterable,
|
||||
);
|
||||
testPropsSequenceWithPreparedChildren(
|
||||
sequence,
|
||||
prepareChildrenModernIterable,
|
||||
);
|
||||
}
|
||||
|
||||
describe('ReactMultiChildReconcile', () => {
|
||||
@@ -311,7 +329,7 @@ describe('ReactMultiChildReconcile', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should reset internal state if removed then readded in an iterable', () => {
|
||||
it('should reset internal state if removed then readded in a legacy iterable', () => {
|
||||
// Test basics.
|
||||
const props = {
|
||||
usernameToStatus: {
|
||||
@@ -323,7 +341,7 @@ describe('ReactMultiChildReconcile', () => {
|
||||
const parentInstance = ReactDOM.render(
|
||||
<FriendsStatusDisplay
|
||||
{...props}
|
||||
prepareChildren={prepareChildrenIterable}
|
||||
prepareChildren={prepareChildrenLegacyIterable}
|
||||
/>,
|
||||
container,
|
||||
);
|
||||
@@ -332,7 +350,7 @@ describe('ReactMultiChildReconcile', () => {
|
||||
|
||||
// Now remove the child.
|
||||
ReactDOM.render(
|
||||
<FriendsStatusDisplay prepareChildren={prepareChildrenIterable} />,
|
||||
<FriendsStatusDisplay prepareChildren={prepareChildrenLegacyIterable} />,
|
||||
container,
|
||||
);
|
||||
statusDisplays = parentInstance.getStatusDisplays();
|
||||
@@ -342,7 +360,49 @@ describe('ReactMultiChildReconcile', () => {
|
||||
ReactDOM.render(
|
||||
<FriendsStatusDisplay
|
||||
{...props}
|
||||
prepareChildren={prepareChildrenIterable}
|
||||
prepareChildren={prepareChildrenLegacyIterable}
|
||||
/>,
|
||||
container,
|
||||
);
|
||||
statusDisplays = parentInstance.getStatusDisplays();
|
||||
expect(statusDisplays.jcw).toBeTruthy();
|
||||
expect(statusDisplays.jcw.getInternalState()).not.toBe(
|
||||
startingInternalState,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reset internal state if removed then readded in a modern iterable', () => {
|
||||
// Test basics.
|
||||
const props = {
|
||||
usernameToStatus: {
|
||||
jcw: 'jcwStatus',
|
||||
},
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
const parentInstance = ReactDOM.render(
|
||||
<FriendsStatusDisplay
|
||||
{...props}
|
||||
prepareChildren={prepareChildrenModernIterable}
|
||||
/>,
|
||||
container,
|
||||
);
|
||||
let statusDisplays = parentInstance.getStatusDisplays();
|
||||
const startingInternalState = statusDisplays.jcw.getInternalState();
|
||||
|
||||
// Now remove the child.
|
||||
ReactDOM.render(
|
||||
<FriendsStatusDisplay prepareChildren={prepareChildrenModernIterable} />,
|
||||
container,
|
||||
);
|
||||
statusDisplays = parentInstance.getStatusDisplays();
|
||||
expect(statusDisplays.jcw).toBeFalsy();
|
||||
|
||||
// Now reset the props that cause there to be a child
|
||||
ReactDOM.render(
|
||||
<FriendsStatusDisplay
|
||||
{...props}
|
||||
prepareChildren={prepareChildrenModernIterable}
|
||||
/>,
|
||||
container,
|
||||
);
|
||||
|
||||
@@ -39,6 +39,7 @@ describe('rendering React components at document', () => {
|
||||
'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
|
||||
'will stop working in React v17. Replace the ReactDOM.render() call ' +
|
||||
'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -200,8 +201,11 @@ describe('rendering React components at document', () => {
|
||||
'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
|
||||
'will stop working in React v17. Replace the ReactDOM.render() call ' +
|
||||
'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
}).toWarnDev('Warning: Text content did not match.');
|
||||
}).toWarnDev('Warning: Text content did not match.', {
|
||||
withoutStack: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw on full document render w/ no markup', () => {
|
||||
@@ -369,6 +373,7 @@ describe('rendering React components at document', () => {
|
||||
container.textContent = 'potato';
|
||||
expect(() => ReactDOM.hydrate(<div>parsnip</div>, container)).toWarnDev(
|
||||
'Expected server HTML to contain a matching <div> in <div>.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(container.textContent).toBe('parsnip');
|
||||
});
|
||||
@@ -394,7 +399,9 @@ describe('rendering React components at document', () => {
|
||||
|
||||
expect(() =>
|
||||
ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
|
||||
).toWarnDev('Warning: Text content did not match.');
|
||||
).toWarnDev('Warning: Text content did not match.', {
|
||||
withoutStack: true,
|
||||
});
|
||||
expect(testDocument.body.innerHTML).toBe('Hello world');
|
||||
});
|
||||
|
||||
@@ -417,7 +424,9 @@ describe('rendering React components at document', () => {
|
||||
// getTestDocument() has an extra <meta> that we didn't render.
|
||||
expect(() =>
|
||||
ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
|
||||
).toWarnDev('Did not expect server HTML to contain a <meta> in <head>.');
|
||||
).toWarnDev('Did not expect server HTML to contain a <meta> in <head>.', {
|
||||
withoutStack: true,
|
||||
});
|
||||
expect(testDocument.body.innerHTML).toBe('Hello world');
|
||||
});
|
||||
|
||||
|
||||
@@ -174,6 +174,19 @@ describe('ReactDOMServer', () => {
|
||||
(__DEV__ ? '\n in iframe (at **)' : ''),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not crash on poisoned hasOwnProperty', () => {
|
||||
let html;
|
||||
expect(
|
||||
() =>
|
||||
(html = ReactDOMServer.renderToString(
|
||||
<div hasOwnProperty="poison">
|
||||
<span unknown="test" />
|
||||
</div>,
|
||||
)),
|
||||
).toWarnDev(['React does not recognize the `hasOwnProperty` prop']);
|
||||
expect(html).toContain('<span unknown="test">');
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderToStaticMarkup', () => {
|
||||
@@ -519,6 +532,7 @@ describe('ReactDOMServer', () => {
|
||||
'Warning: setState(...): Can only update a mounting component.' +
|
||||
' This usually means you called setState() outside componentWillMount() on the server.' +
|
||||
' This is a no-op.\n\nPlease check the code for the Foo component.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
const markup = ReactDOMServer.renderToStaticMarkup(<Foo />);
|
||||
@@ -546,6 +560,7 @@ describe('ReactDOMServer', () => {
|
||||
'Warning: forceUpdate(...): Can only update a mounting component. ' +
|
||||
'This usually means you called forceUpdate() outside componentWillMount() on the server. ' +
|
||||
'This is a no-op.\n\nPlease check the code for the Baz component.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
const markup = ReactDOMServer.renderToStaticMarkup(<Baz />);
|
||||
expect(markup).toBe('<div></div>');
|
||||
@@ -643,4 +658,138 @@ describe('ReactDOMServer', () => {
|
||||
ReactDOMServer.renderToString(<ClassWithRenderNotExtended />);
|
||||
}).toThrow(TypeError);
|
||||
});
|
||||
|
||||
// We're just testing importing, not using it.
|
||||
// It is important because even isomorphic components may import it.
|
||||
it('can import react-dom in Node environment', () => {
|
||||
if (
|
||||
typeof requestAnimationFrame !== 'undefined' ||
|
||||
global.hasOwnProperty('requestAnimationFrame') ||
|
||||
typeof requestIdleCallback !== 'undefined' ||
|
||||
global.hasOwnProperty('requestIdleCallback') ||
|
||||
typeof window !== 'undefined' ||
|
||||
global.hasOwnProperty('window')
|
||||
) {
|
||||
// Don't remove this. This test is specifically checking
|
||||
// what happens when they *don't* exist. It's useless otherwise.
|
||||
throw new Error('Expected this test to run in a Node environment.');
|
||||
}
|
||||
jest.resetModules();
|
||||
expect(() => {
|
||||
require('react-dom');
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('includes a useful stack in warnings', () => {
|
||||
function A() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function B() {
|
||||
return (
|
||||
<font>
|
||||
<C>
|
||||
<span ariaTypo="no" />
|
||||
</C>
|
||||
</font>
|
||||
);
|
||||
}
|
||||
|
||||
class C extends React.Component {
|
||||
render() {
|
||||
return <b>{this.props.children}</b>;
|
||||
}
|
||||
}
|
||||
|
||||
function Child() {
|
||||
return [<A key="1" />, <B key="2" />, <span ariaTypo2="no" />];
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<section />
|
||||
<span>
|
||||
<Child />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
expect(() => ReactDOMServer.renderToString(<App />)).toWarnDev([
|
||||
'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
|
||||
' in span (at **)\n' +
|
||||
' in b (at **)\n' +
|
||||
' in C (at **)\n' +
|
||||
' in font (at **)\n' +
|
||||
' in B (at **)\n' +
|
||||
' in Child (at **)\n' +
|
||||
' in span (at **)\n' +
|
||||
' in div (at **)\n' +
|
||||
' in App (at **)',
|
||||
'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
|
||||
' in span (at **)\n' +
|
||||
' in Child (at **)\n' +
|
||||
' in span (at **)\n' +
|
||||
' in div (at **)\n' +
|
||||
' in App (at **)',
|
||||
]);
|
||||
});
|
||||
|
||||
it('reports stacks with re-entrant renderToString() calls', () => {
|
||||
function Child2(props) {
|
||||
return <span ariaTypo3="no">{props.children}</span>;
|
||||
}
|
||||
|
||||
function App2() {
|
||||
return (
|
||||
<Child2>
|
||||
{ReactDOMServer.renderToString(<blink ariaTypo2="no" />)}
|
||||
</Child2>
|
||||
);
|
||||
}
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<span ariaTypo4="no">{ReactDOMServer.renderToString(<App2 />)}</span>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<span ariaTypo="no" />
|
||||
<Child />
|
||||
<font ariaTypo5="no" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
expect(() => ReactDOMServer.renderToString(<App />)).toWarnDev([
|
||||
// ReactDOMServer(App > div > span)
|
||||
'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
|
||||
' in span (at **)\n' +
|
||||
' in div (at **)\n' +
|
||||
' in App (at **)',
|
||||
// ReactDOMServer(App > div > Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink)
|
||||
'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
|
||||
' in blink (at **)',
|
||||
// ReactDOMServer(App > div > Child) >>> ReactDOMServer(App2 > Child2 > span)
|
||||
'Invalid ARIA attribute `ariaTypo3`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
|
||||
' in span (at **)\n' +
|
||||
' in Child2 (at **)\n' +
|
||||
' in App2 (at **)',
|
||||
// ReactDOMServer(App > div > Child > span)
|
||||
'Invalid ARIA attribute `ariaTypo4`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
|
||||
' in span (at **)\n' +
|
||||
' in Child (at **)\n' +
|
||||
' in div (at **)\n' +
|
||||
' in App (at **)',
|
||||
// ReactDOMServer(App > div > font)
|
||||
'Invalid ARIA attribute `ariaTypo5`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
|
||||
' in font (at **)\n' +
|
||||
' in div (at **)\n' +
|
||||
' in App (at **)',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -83,6 +83,7 @@ describe('ReactDOMServerHydration', () => {
|
||||
'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
|
||||
'will stop working in React v17. Replace the ReactDOM.render() call ' +
|
||||
'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(mountCount).toEqual(3);
|
||||
expect(element.innerHTML).toBe(lastMarkup);
|
||||
@@ -101,7 +102,9 @@ describe('ReactDOMServerHydration', () => {
|
||||
element.innerHTML = lastMarkup;
|
||||
expect(() => {
|
||||
instance = ReactDOM.render(<TestComponent name="y" />, element);
|
||||
}).toWarnDev('Text content did not match. Server: "x" Client: "y"');
|
||||
}).toWarnDev('Text content did not match. Server: "x" Client: "y"', {
|
||||
withoutStack: true,
|
||||
});
|
||||
expect(mountCount).toEqual(4);
|
||||
expect(element.innerHTML.length > 0).toBe(true);
|
||||
expect(element.innerHTML).not.toEqual(lastMarkup);
|
||||
@@ -184,7 +187,9 @@ describe('ReactDOMServerHydration', () => {
|
||||
element.innerHTML = lastMarkup;
|
||||
expect(() => {
|
||||
instance = ReactDOM.hydrate(<TestComponent name="y" />, element);
|
||||
}).toWarnDev('Text content did not match. Server: "x" Client: "y"');
|
||||
}).toWarnDev('Text content did not match. Server: "x" Client: "y"', {
|
||||
withoutStack: true,
|
||||
});
|
||||
expect(mountCount).toEqual(4);
|
||||
expect(element.innerHTML.length > 0).toBe(true);
|
||||
expect(element.innerHTML).not.toEqual(lastMarkup);
|
||||
@@ -247,6 +252,7 @@ describe('ReactDOMServerHydration', () => {
|
||||
ReactDOM.hydrate(<button autoFocus={false}>client</button>, element),
|
||||
).toWarnDev(
|
||||
'Warning: Text content did not match. Server: "server" Client: "client"',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
expect(element.firstChild.focus).not.toHaveBeenCalled();
|
||||
@@ -263,4 +269,76 @@ describe('ReactDOMServerHydration', () => {
|
||||
'Render them conditionally so that they only appear on the client render.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to render and hydrate Mode components', () => {
|
||||
class ComponentWithWarning extends React.Component {
|
||||
componentWillMount() {
|
||||
// Expected warning
|
||||
}
|
||||
render() {
|
||||
return 'Hi';
|
||||
}
|
||||
}
|
||||
|
||||
const markup = (
|
||||
<React.StrictMode>
|
||||
<ComponentWithWarning />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = ReactDOMServer.renderToString(markup);
|
||||
expect(element.textContent).toBe('Hi');
|
||||
|
||||
expect(() => ReactDOM.hydrate(markup, element)).toWarnDev(
|
||||
'Please update the following components to use componentDidMount instead: ComponentWithWarning',
|
||||
);
|
||||
expect(element.textContent).toBe('Hi');
|
||||
});
|
||||
|
||||
it('should be able to render and hydrate forwardRef components', () => {
|
||||
const FunctionComponent = ({label, forwardedRef}) => (
|
||||
<div ref={forwardedRef}>{label}</div>
|
||||
);
|
||||
const WrappedFunctionComponent = React.forwardRef((props, ref) => (
|
||||
<FunctionComponent {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
const ref = React.createRef();
|
||||
const markup = <WrappedFunctionComponent ref={ref} label="Hi" />;
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = ReactDOMServer.renderToString(markup);
|
||||
expect(element.textContent).toBe('Hi');
|
||||
expect(ref.current).toBe(null);
|
||||
|
||||
ReactDOM.hydrate(markup, element);
|
||||
expect(element.textContent).toBe('Hi');
|
||||
expect(ref.current.tagName).toBe('DIV');
|
||||
});
|
||||
|
||||
it('should be able to render and hydrate Profiler components', () => {
|
||||
const callback = jest.fn();
|
||||
const markup = (
|
||||
<React.unstable_Profiler id="profiler" onRender={callback}>
|
||||
<div>Hi</div>
|
||||
</React.unstable_Profiler>
|
||||
);
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = ReactDOMServer.renderToString(markup);
|
||||
expect(element.textContent).toBe('Hi');
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
ReactDOM.hydrate(markup, element);
|
||||
expect(element.textContent).toBe('Hi');
|
||||
if (__DEV__) {
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
const [id, phase] = callback.mock.calls[0];
|
||||
expect(id).toBe('profiler');
|
||||
expect(phase).toBe('mount');
|
||||
} else {
|
||||
expect(callback).toHaveBeenCalledTimes(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,6 +111,7 @@ describe('ReactStatelessComponent', () => {
|
||||
).toWarnDev(
|
||||
'StatelessComponentWithChildContext: Stateless ' +
|
||||
'functional components do not support getDerivedStateFromProps.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -133,6 +134,7 @@ describe('ReactStatelessComponent', () => {
|
||||
).toWarnDev(
|
||||
'StatelessComponentWithChildContext(...): childContextTypes cannot ' +
|
||||
'be defined on a functional component.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -53,6 +53,16 @@ describe('ReactTestUtils', () => {
|
||||
MockedComponent.prototype.render = jest.fn();
|
||||
|
||||
// Patch it up so it returns its children.
|
||||
expect(() =>
|
||||
ReactTestUtils.mockComponent(MockedComponent),
|
||||
).toLowPriorityWarnDev(
|
||||
'ReactTestUtils.mockComponent() is deprecated. ' +
|
||||
'Use shallow rendering or jest.mock() instead.\n\n' +
|
||||
'See https://fb.me/test-utils-mock-component for more information.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// De-duplication check
|
||||
ReactTestUtils.mockComponent(MockedComponent);
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
@@ -1226,6 +1226,7 @@ describe('ReactUpdates', () => {
|
||||
const container = document.createElement('div');
|
||||
expect(() => ReactDOM.render(<Foo />, container)).toWarnDev(
|
||||
'Cannot update during an existing state transition',
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(ops).toEqual(['base: 0, memoized: 0', 'base: 1, memoized: 1']);
|
||||
});
|
||||
@@ -1364,4 +1365,37 @@ describe('ReactUpdates', () => {
|
||||
ReactDOM.render(<NonTerminating />, container);
|
||||
}).toThrow('Maximum');
|
||||
});
|
||||
|
||||
it('can schedule ridiculously many updates within the same batch without triggering a maximum update error', () => {
|
||||
const subscribers = [];
|
||||
|
||||
class Child extends React.Component {
|
||||
state = {value: 'initial'};
|
||||
componentDidMount() {
|
||||
subscribers.push(this);
|
||||
}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
const children = [];
|
||||
for (let i = 0; i < 1200; i++) {
|
||||
children.push(<Child key={i} />);
|
||||
}
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
ReactDOM.render(<App />, container);
|
||||
|
||||
ReactDOM.unstable_batchedUpdates(() => {
|
||||
subscribers.forEach(s => {
|
||||
s.setState({value: 'update'});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||