Skip to content

Commit d248d0a

Browse files
committed
add CM tests
1 parent 647195d commit d248d0a

File tree

13 files changed

+11806
-0
lines changed

13 files changed

+11806
-0
lines changed

test/cm/.eslintrc.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"plugins": [
3+
"react-hooks"
4+
],
5+
"extends": [
6+
"airbnb"
7+
],
8+
"env": {
9+
"browser": true
10+
},
11+
"settings": {
12+
"import/resolver": {
13+
"node": {
14+
"extensions": [".js", ".ts", ".tsx"]
15+
}
16+
}
17+
},
18+
"rules": {
19+
"react-hooks/rules-of-hooks": "error",
20+
"react-hooks/exhaustive-deps": "error",
21+
"react/jsx-filename-extension": ["error", { "extensions": [".js"] }],
22+
"react/prop-types": "off",
23+
"react/jsx-one-expression-per-line": "off",
24+
"import/prefer-default-export": "off",
25+
"no-param-reassign": "off",
26+
"default-case": "off",
27+
"arrow-body-style": "off",
28+
"no-plusplus": "off",
29+
"import/no-useless-path-segments": 0,
30+
"no-console": "off",
31+
"no-await-in-loop": "off",
32+
"arrow-parens": ["error", "as-needed", { "requireForBlockBody": true }],
33+
"react/jsx-fragments": "off"
34+
},
35+
"overrides": [{
36+
"files": ["__tests__/**/*"],
37+
"env": {
38+
"jest": true
39+
},
40+
"rules": {
41+
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
42+
}
43+
}]
44+
}

test/cm/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*~
2+
*.swp
3+
node_modules
4+
/dist

test/cm/.travis.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
language: node_js
2+
node_js:
3+
- 8
4+
- 10

test/cm/README.md

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
# Will this React global state work in concurrent mode?
2+
3+
Checking tearing in React concurrent mode
4+
5+
## Introduction
6+
7+
In react-redux, there's a theoretical issue called "tearing"
8+
that might occur in React concurrent mode.
9+
10+
Let's try to check it!
11+
12+
## What is tearing?
13+
14+
- [Stack Overflow](https://stackoverflow.com/questions/54891675/what-is-tearing-in-the-context-of-the-react-redux)
15+
- [Talk by Mark Erikson](https://www.youtube.com/watch?v=yOZ4Ml9LlWE&t=933s)
16+
- [Talk by Flarnie Marchan](https://www.youtube.com/watch?v=V1Ly-8Z1wQA&t=1079s)
17+
18+
## How does it work?
19+
20+
A small app is implemented with each library.
21+
The state has one count.
22+
The app shows the count in fifty components.
23+
24+
There's a button outside of React and
25+
if it's clicked it will trigger state mutation.
26+
This is to emulate mutating an external state outside of React,
27+
for example updating state by Redux middleware.
28+
29+
The render has intentionaly expensive computation.
30+
If the mutation happens during rendering with in a tree,
31+
there could be an inconsistency in the state.
32+
If it finds the inconsistency, the test will fail.
33+
34+
## How to run
35+
36+
```bash
37+
git clone https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode.git
38+
cd will-this-react-global-state-work-in-concurrent-mode
39+
npm install
40+
npm run build-all
41+
PORT=8080 npm run http-server &
42+
PORT=8080 npm run jest
43+
```
44+
45+
You can also test it by opening `http://localhost:8080/react-redux`
46+
in your browser, and click the button very quickly. (check the console log)
47+
48+
## Screencast
49+
50+
<img src="https://user-images.githubusercontent.com/490574/61502196-ce109200-aa0d-11e9-9efc-6203545d367c.gif" alt="Preview" width="350" />
51+
52+
## Result
53+
54+
<details>
55+
<summary>Raw Output</summary>
56+
57+
```
58+
react-redux
59+
✓ check1: updated properly (3533ms)
60+
✕ check2: no tearing during update (22ms)
61+
✓ check3: ability to interrupt render
62+
✕ check4: proper update after interrupt (5152ms)
63+
reactive-react-redux
64+
✓ check1: updated properly (3626ms)
65+
✓ check2: no tearing during update (1ms)
66+
✓ check3: ability to interrupt render
67+
✓ check4: proper update after interrupt (1464ms)
68+
react-tracked
69+
✓ check1: updated properly (8303ms)
70+
✓ check2: no tearing during update (1ms)
71+
✓ check3: ability to interrupt render (1ms)
72+
✓ check4: proper update after interrupt (1195ms)
73+
constate
74+
✓ check1: updated properly (8382ms)
75+
✓ check2: no tearing during update (1ms)
76+
✓ check3: ability to interrupt render
77+
✓ check4: proper update after interrupt (2401ms)
78+
unstated-next
79+
✓ check1: updated properly (8720ms)
80+
✓ check2: no tearing during update (1ms)
81+
✓ check3: ability to interrupt render
82+
✓ check4: proper update after interrupt (1255ms)
83+
zustand
84+
✓ check1: updated properly (3613ms)
85+
✕ check2: no tearing during update (22ms)
86+
✓ check3: ability to interrupt render (1ms)
87+
✕ check4: proper update after interrupt (5138ms)
88+
react-sweet-state
89+
✓ check1: updated properly (9351ms)
90+
✕ check2: no tearing during update (2ms)
91+
✕ check3: ability to interrupt render (1ms)
92+
✕ check4: proper update after interrupt (5203ms)
93+
storeon
94+
✓ check1: updated properly (3415ms)
95+
✕ check2: no tearing during update (20ms)
96+
✓ check3: ability to interrupt render
97+
✕ check4: proper update after interrupt (5152ms)
98+
react-hooks-global-state
99+
✓ check1: updated properly (8967ms)
100+
✓ check2: no tearing during update (1ms)
101+
✓ check3: ability to interrupt render
102+
✓ check4: proper update after interrupt (1205ms)
103+
use-context-selector
104+
✓ check1: updated properly (8604ms)
105+
✓ check2: no tearing during update (1ms)
106+
✓ check3: ability to interrupt render
107+
✓ check4: proper update after interrupt (2246ms)
108+
mobx-react-lite
109+
✕ check1: updated properly (11839ms)
110+
✕ check2: no tearing during update (2ms)
111+
✓ check3: ability to interrupt render
112+
✕ check4: proper update after interrupt (5067ms)
113+
use-subscription
114+
✓ check1: updated properly (8458ms)
115+
✓ check2: no tearing during update (1ms)
116+
✓ check3: ability to interrupt render
117+
✕ check4: proper update after interrupt (5054ms)
118+
```
119+
120+
</details>
121+
122+
<table>
123+
<tr>
124+
<th></th>
125+
<th>check1: updated properly</th>
126+
<th>check2: no tearing during update</th>
127+
<th>check3: ability to interrupt render</th>
128+
<th>check4: proper update after interrupt</th>
129+
</tr>
130+
131+
<tr>
132+
<th>react-redux</th>
133+
<td>Pass</td>
134+
<td>Fail</td>
135+
<td>Pass</td>
136+
<td>Fail</td>
137+
</tr>
138+
139+
<tr>
140+
<th>reactive-react-redux</th>
141+
<td>Pass</td>
142+
<td>Pass</td>
143+
<td>Pass</td>
144+
<td>Pass</td>
145+
</tr>
146+
147+
</tr>
148+
<th>react-tracked</th>
149+
<td>Pass</td>
150+
<td>Pass</td>
151+
<td>Pass</td>
152+
<td>Pass</td>
153+
</tr>
154+
155+
</tr>
156+
<th>constate</th>
157+
<td>Pass</td>
158+
<td>Pass</td>
159+
<td>Pass</td>
160+
<td>Pass</td>
161+
</tr>
162+
163+
</tr>
164+
<th>unstated-next</th>
165+
<td>Pass</td>
166+
<td>Pass</td>
167+
<td>Pass</td>
168+
<td>Pass</td>
169+
</tr>
170+
171+
</tr>
172+
<th>zustand</th>
173+
<td>Pass</td>
174+
<td>Fail</td>
175+
<td>Pass</td>
176+
<td>Fail</td>
177+
</tr>
178+
179+
</tr>
180+
<th>react-sweet-state</th>
181+
<td>Pass</td>
182+
<td>Fail</td>
183+
<td>Fail? (just a little bit slow)</td>
184+
<td>Fail</td>
185+
</tr>
186+
187+
</tr>
188+
<th>storeon</th>
189+
<td>Pass</td>
190+
<td>Fail</td>
191+
<td>Pass</td>
192+
<td>Fail</td>
193+
</tr>
194+
195+
</tr>
196+
<th>react-hooks-global-state</th>
197+
<td>Pass</td>
198+
<td>Pass</td>
199+
<td>Pass</td>
200+
<td>Pass</td>
201+
</tr>
202+
203+
</tr>
204+
<th>use-context-selector</th>
205+
<td>Pass</td>
206+
<td>Pass</td>
207+
<td>Pass</td>
208+
<td>Pass</td>
209+
</tr>
210+
211+
</tr>
212+
<th>mobx-react-lite</th>
213+
<td>Fail</td>
214+
<td>Fail</td>
215+
<td>Pass</td>
216+
<td>Fail</td>
217+
</tr>
218+
219+
</tr>
220+
<th>use-subscription (w/ redux)</th>
221+
<td>Pass</td>
222+
<td>Pass</td>
223+
<td>Pass</td>
224+
<td>Fail</td>
225+
</tr>
226+
</table>
227+
228+
## Caution
229+
230+
Do no believe the result too much.
231+
The test is done in a very limited way.
232+
Something might be wrong.
233+
234+
## Caution2
235+
236+
We are not yet sure what the final conurrent mode would be.
237+
It's likely that there could be some issues other than "tearing."
238+
239+
## If you are interested
240+
241+
The reason why I created this is to promote my projects!
242+
243+
- [react-tracked](https://github.com/dai-shi/react-tracked)
244+
- [reactive-react-redux](https://github.com/dai-shi/reactive-react-redux)
245+
246+
The feature of these libraries is not only concurrent mode friendly,
247+
but also state usage tracking.

test/cm/__tests__/all_spec.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/* global page, jestPuppeteer */
2+
3+
const port = process.env.PORT || '8080';
4+
5+
const names = [
6+
'react-redux',
7+
];
8+
9+
const sleep = ms => new Promise(r => setTimeout(r, ms));
10+
jest.setTimeout(15 * 1000);
11+
const REPEAT = 10;
12+
const NUM_COMPONENTS = 50; // defined in src/common.js
13+
14+
names.forEach((name) => {
15+
describe(name, () => {
16+
let delays;
17+
18+
beforeAll(async () => {
19+
await page.goto(`http://localhost:${port}/${name}/index.html`);
20+
const title = await page.title();
21+
if (title === 'failed') throw new Error('failed to reset title');
22+
// wait until all counts become zero
23+
await Promise.all([...Array(NUM_COMPONENTS).keys()].map(async (i) => {
24+
await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
25+
text: '0',
26+
timeout: 5 * 1000,
27+
});
28+
}));
29+
});
30+
31+
it('check1: updated properly', async () => {
32+
delays = [];
33+
for (let loop = 0; loop < REPEAT; ++loop) {
34+
const start = Date.now();
35+
// click buttons three times
36+
await Promise.all([
37+
page.click('#remoteIncrement'),
38+
page.click('#remoteIncrement'),
39+
sleep(50).then(() => page.click('#remoteIncrement')), // a bit delayed
40+
]);
41+
delays.push(Date.now() - start);
42+
}
43+
console.log(name, delays);
44+
// check if all counts become 15 = REPEAT * 3
45+
await Promise.all([...Array(NUM_COMPONENTS).keys()].map(async (i) => {
46+
await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
47+
text: `${REPEAT * 3}`,
48+
timeout: 10 * 1000,
49+
});
50+
}));
51+
});
52+
53+
it('check2: no tearing during update', async () => {
54+
// check if there's inconsistency duroing update
55+
// see useCheckTearing() in src/common.js
56+
await expect(page.title()).resolves.not.toBe('failed');
57+
});
58+
59+
it('check3: ability to interrupt render', async () => {
60+
// check delays taken by clicking buttons in check1
61+
// each render takes at least 20ms and there are 50 components,
62+
// it triggers triple clicks, so 300ms on average.
63+
const avg = delays.reduce((a, b) => a + b) / delays.length;
64+
expect(avg).toBeLessThan(300);
65+
});
66+
67+
it('check4: proper update after interrupt', async () => {
68+
// click both buttons to update local count during updating remote count
69+
await Promise.all([
70+
page.click('#remoteIncrement'),
71+
page.click('#remoteIncrement'),
72+
page.click('#localIncrement'),
73+
]);
74+
// check if all counts become 18 = REPEAT * 3 + 3
75+
await Promise.all([...Array(NUM_COMPONENTS).keys()].map(async (i) => {
76+
await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
77+
text: `${REPEAT * 3 + 2}`,
78+
timeout: 5 * 1000,
79+
});
80+
}));
81+
});
82+
83+
afterAll(async () => {
84+
await jestPuppeteer.resetBrowser();
85+
});
86+
});
87+
});

test/cm/jest-puppeteer.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
launch: {
3+
headless: true,
4+
},
5+
};

0 commit comments

Comments
 (0)