Cleaned up tests a bit. Profiling test uses mock timers now.
Brian Vaughn committed
May 4, 2019 at 09:36 UTC
dd96b3314cf0ec0ad50d38d9348b591068c492c5
12 files changed
+286
-299
src/__tests__/__snapshots__/profiler-test.js.snap
deleted
-110
@@ -1,110 +0,0 @@
1
-// Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
-exports[`Profiler should clean up after a root has been unmounted: 1: mount 1`] = `
4
-[root]
5
- ▾ <Parent key="A">
6
- <Child key="0">
7
- <Child key="1">
8
- <Child key="2">
9
-[root]
10
- ▾ <Parent key="B">
11
- <Child key="0">
12
- <Child key="1">
13
-`;
14
-
15
-exports[`Profiler should clean up after a root has been unmounted: 2: profiling started 1`] = `
16
-[root]
17
- ▾ <Parent key="A">
18
- <Child key="0">
19
- <Child key="1">
20
- <Child key="2">
21
-[root]
22
- ▾ <Parent key="B">
23
- <Child key="0">
24
- <Child key="1">
25
-`;
26
-
27
-exports[`Profiler should clean up after a root has been unmounted: 3: update 1`] = `
28
-[root]
29
- ▾ <Parent key="A">
30
- <Child key="0">
31
- <Child key="1">
32
- <Child key="2">
33
- <Child key="3">
34
-[root]
35
- ▾ <Parent key="B">
36
- <Child key="0">
37
-`;
38
-
39
-exports[`Profiler should clean up after a root has been unmounted: 4: unmount B 1`] = `
40
-[root]
41
- ▾ <Parent key="A">
42
- <Child key="0">
43
- <Child key="1">
44
- <Child key="2">
45
- <Child key="3">
46
-`;
47
-
48
-exports[`Profiler should clean up after a root has been unmounted: 5: unmount A 1`] = ``;
49
-
50
-exports[`Profiler should clean up after a root has been unmounted: 6: profiling stopped 1`] = ``;
51
-
52
-exports[`Profiler should collect basic profiling metrics: 1: mount 1`] = `
53
-[root]
54
- ▾ <Parent key="A">
55
- <Child key="0">
56
- <Child key="1">
57
-`;
58
-
59
-exports[`Profiler should collect basic profiling metrics: 2: add child 1`] = `
60
-[root]
61
- ▾ <Parent key="A">
62
- <Child key="0">
63
- <Child key="1">
64
- <Child key="2">
65
-`;
66
-
67
-exports[`Profiler should collect basic profiling metrics: 3: remove children 1`] = `
68
-[root]
69
- ▾ <Parent key="A">
70
- <Child key="0">
71
-`;
72
-
73
-exports[`Profiler should collect basic profiling metrics: 4: profiling stopped 1`] = `
74
-[root]
75
- ▾ <Parent key="A">
76
- <Child key="0">
77
-`;
78
-
79
-exports[`Profiler should collect basic profiling metrics: ProfilingSummary 1`] = `
80
-{
81
- "rootID": 1,
82
- "commitDurations": [
83
- 0,
84
- 1
85
- ],
86
- "commitTimes": [
87
- 0,
88
- 1
89
- ],
90
- "initialTreeBaseDurations": [
91
- [
92
- 1,
93
- 0
94
- ],
95
- [
96
- 2,
97
- 1
98
- ],
99
- [
100
- 3,
101
- 2
102
- ],
103
- [
104
- 4,
105
- 3
106
- ]
107
- ],
108
- "interactionCount": 0
109
-}
110
-`;
src/__tests__/__snapshots__/profiling-test.js.snap
new
+61
@@ -0,0 +1,61 @@
1
+// Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+exports[`profiling profilingSummary should be collected for each commit: 1: mount 1`] = `
4
+[root]
5
+ ▾ <Parent key="A">
6
+ <Child key="0">
7
+ <Child key="1">
8
+`;
9
+
10
+exports[`profiling profilingSummary should be collected for each commit: 2: add child 1`] = `
11
+[root]
12
+ ▾ <Parent key="A">
13
+ <Child key="0">
14
+ <Child key="1">
15
+ <Child key="2">
16
+`;
17
+
18
+exports[`profiling profilingSummary should be collected for each commit: 3: remove children 1`] = `
19
+[root]
20
+ ▾ <Parent key="A">
21
+ <Child key="0">
22
+`;
23
+
24
+exports[`profiling profilingSummary should be collected for each commit: 4: profiling stopped 1`] = `
25
+[root]
26
+ ▾ <Parent key="A">
27
+ <Child key="0">
28
+`;
29
+
30
+exports[`profiling profilingSummary should be collected for each commit: ProfilingSummary 1`] = `
31
+{
32
+ "rootID": 1,
33
+ "commitDurations": [
34
+ 16,
35
+ 12
36
+ ],
37
+ "commitTimes": [
38
+ 16,
39
+ 28
40
+ ],
41
+ "initialTreeBaseDurations": [
42
+ [
43
+ 1,
44
+ 14
45
+ ],
46
+ [
47
+ 2,
48
+ 14
49
+ ],
50
+ [
51
+ 3,
52
+ 2
53
+ ],
54
+ [
55
+ 4,
56
+ 2
57
+ ]
58
+ ],
59
+ "interactionCount": 0
60
+}
61
+`;
src/__tests__/profiler-test.js
deleted
-131
@@ -1,131 +0,0 @@
1
-// @flow
2
-
3
-describe('Profiler', () => {
4
- let React;
5
- let ReactDOM;
6
- let TestRenderer;
7
- let TestUtils;
8
- let agent;
9
- let store;
10
-
11
- const act = (callback: Function) => {
12
- TestUtils.act(() => {
13
- callback();
14
- });
15
- jest.runAllTimers(); // Flush Bridge operations
16
- };
17
-
18
- const renderAndResolve = async (root, element) => {
19
- // $FlowFixMe Flow doens't know about "await act()" yet
20
- await TestUtils.act(async () => {
21
- root.update(element);
22
-
23
- // Resolve pending suspense promises
24
- jest.runAllTimers();
25
- });
26
-
27
- // Re-render after resolved promises
28
- jest.runAllTimers();
29
- };
30
-
31
- beforeEach(() => {
32
- agent = global.agent;
33
- store = global.store;
34
- store.collapseNodesByDefault = false;
35
-
36
- React = require('react');
37
- ReactDOM = require('react-dom');
38
- TestUtils = require('react-dom/test-utils');
39
-
40
- // Hide the hook before requiring TestRenderer, so we don't end up with a loop.
41
- const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
42
- delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
43
- TestRenderer = require('react-test-renderer');
44
- global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
45
- });
46
-
47
- it('should collect basic profiling metrics', async done => {
48
- const Parent = ({ count }) =>
49
- new Array(count).fill(true).map((_, index) => <Child key={index} />);
50
- const Child = () => {
51
- jest.advanceTimersByTime(1);
52
- return null;
53
- };
54
-
55
- const container = document.createElement('div');
56
-
57
- act(() => ReactDOM.render(<Parent key="A" count={2} />, container));
58
- expect(store).toMatchSnapshot('1: mount');
59
-
60
- act(() => store.startProfiling());
61
-
62
- act(() => ReactDOM.render(<Parent key="A" count={3} />, container));
63
- expect(store).toMatchSnapshot('2: add child');
64
-
65
- act(() => ReactDOM.render(<Parent key="A" count={1} />, container));
66
- expect(store).toMatchSnapshot('3: remove children');
67
-
68
- act(() => store.stopProfiling());
69
- expect(store).toMatchSnapshot('4: profiling stopped');
70
-
71
- let profilingSummary;
72
- function Suspender({ rendererID, rootID }) {
73
- profilingSummary = store.profilingCache.ProfilingSummary.read({
74
- rendererID,
75
- rootID,
76
- });
77
- return null;
78
- }
79
-
80
- // HACK There's only one renderer for this test
81
- const rendererID = Object.keys(agent._rendererInterfaces)[0];
82
- const rootID = store.roots[0];
83
-
84
- let root = TestRenderer.create();
85
- await renderAndResolve(
86
- root,
87
- <React.Suspense fallback={null}>
88
- <Suspender rendererID={rendererID} rootID={rootID} />
89
- </React.Suspense>
90
- );
91
-
92
- // HACK root.toTree() doesn't handle Suspense yet
93
- // but Jest serializer wouldn't work with a JSON string
94
- expect(profilingSummary).toMatchSnapshot('ProfilingSummary');
95
-
96
- done();
97
- });
98
-
99
- it('should clean up after a root has been unmounted', async () => {
100
- const Parent = ({ count }) =>
101
- new Array(count).fill(true).map((_, index) => <Child key={index} />);
102
- const Child = () => <div>Hi!</div>;
103
-
104
- const containerA = document.createElement('div');
105
- const containerB = document.createElement('div');
106
-
107
- act(() => {
108
- ReactDOM.render(<Parent key="A" count={3} />, containerA);
109
- ReactDOM.render(<Parent key="B" count={2} />, containerB);
110
- });
111
- expect(store).toMatchSnapshot('1: mount');
112
-
113
- act(() => store.startProfiling());
114
- expect(store).toMatchSnapshot('2: profiling started');
115
-
116
- act(() => {
117
- ReactDOM.render(<Parent key="A" count={4} />, containerA);
118
- ReactDOM.render(<Parent key="B" count={1} />, containerB);
119
- });
120
- expect(store).toMatchSnapshot('3: update');
121
-
122
- act(() => ReactDOM.unmountComponentAtNode(containerB));
123
- expect(store).toMatchSnapshot('4: unmount B');
124
-
125
- act(() => ReactDOM.unmountComponentAtNode(containerA));
126
- expect(store).toMatchSnapshot('5: unmount A');
127
-
128
- act(() => store.stopProfiling());
129
- expect(store).toMatchSnapshot('6: profiling stopped');
130
- });
131
-});
src/__tests__/profiling-test.js
new
+108
@@ -0,0 +1,108 @@
1
+// @flow
2
+
3
+describe('profiling', () => {
4
+ let React;
5
+ let ReactDOM;
6
+ let Scheduler;
7
+ let TestRenderer;
8
+ let store;
9
+ let utils;
10
+
11
+ beforeEach(() => {
12
+ utils = require('./utils');
13
+ utils.beforeEachProfiling();
14
+
15
+ store = global.store;
16
+ store.collapseNodesByDefault = false;
17
+
18
+ React = require('react');
19
+ ReactDOM = require('react-dom');
20
+ Scheduler = require('scheduler');
21
+ TestRenderer = utils.requireTestRenderer();
22
+ });
23
+
24
+ describe('profilingSummary', () => {
25
+ it('should be collected for each commit', async done => {
26
+ const Parent = ({ count }) => {
27
+ Scheduler.advanceTime(10);
28
+ return new Array(count)
29
+ .fill(true)
30
+ .map((_, index) => <Child key={index} />);
31
+ };
32
+ const Child = () => {
33
+ Scheduler.advanceTime(2);
34
+ return null;
35
+ };
36
+
37
+ const container = document.createElement('div');
38
+
39
+ utils.act(() => ReactDOM.render(<Parent key="A" count={2} />, container));
40
+ expect(store).toMatchSnapshot('1: mount');
41
+
42
+ utils.act(() => store.startProfiling());
43
+
44
+ utils.act(() => ReactDOM.render(<Parent key="A" count={3} />, container));
45
+ expect(store).toMatchSnapshot('2: add child');
46
+
47
+ utils.act(() => ReactDOM.render(<Parent key="A" count={1} />, container));
48
+ expect(store).toMatchSnapshot('3: remove children');
49
+
50
+ utils.act(() => store.stopProfiling());
51
+ expect(store).toMatchSnapshot('4: profiling stopped');
52
+
53
+ let profilingSummary;
54
+ function Suspender({ rendererID, rootID }) {
55
+ profilingSummary = store.profilingCache.ProfilingSummary.read({
56
+ rendererID,
57
+ rootID,
58
+ });
59
+ return null;
60
+ }
61
+
62
+ const rendererID = utils.getRendererID();
63
+ const rootID = store.roots[0];
64
+
65
+ await utils.actSuspense(() =>
66
+ TestRenderer.create(
67
+ <React.Suspense fallback={null}>
68
+ <Suspender rendererID={rendererID} rootID={rootID} />
69
+ </React.Suspense>
70
+ )
71
+ );
72
+
73
+ expect(profilingSummary).toMatchSnapshot('ProfilingSummary');
74
+
75
+ done();
76
+ });
77
+ });
78
+
79
+ it('should remove profiling data when roots are unmounted', async () => {
80
+ const Parent = ({ count }) =>
81
+ new Array(count).fill(true).map((_, index) => <Child key={index} />);
82
+ const Child = () => <div>Hi!</div>;
83
+
84
+ const containerA = document.createElement('div');
85
+ const containerB = document.createElement('div');
86
+
87
+ utils.act(() => {
88
+ ReactDOM.render(<Parent key="A" count={3} />, containerA);
89
+ ReactDOM.render(<Parent key="B" count={2} />, containerB);
90
+ });
91
+
92
+ utils.act(() => store.startProfiling());
93
+
94
+ utils.act(() => {
95
+ ReactDOM.render(<Parent key="A" count={4} />, containerA);
96
+ ReactDOM.render(<Parent key="B" count={1} />, containerB);
97
+ });
98
+
99
+ utils.act(() => ReactDOM.unmountComponentAtNode(containerB));
100
+
101
+ utils.act(() => ReactDOM.unmountComponentAtNode(containerA));
102
+
103
+ utils.act(() => store.stopProfiling());
104
+
105
+ // Assert all maps are empty
106
+ store.assertExpectedRootMapSizes();
107
+ });
108
+});
src/__tests__/profilingSummarySerializer.js
+1
-5
@@ -17,11 +17,7 @@ export function print(profilingSummary, serialize, indent) {
17
return JSON.stringify(
18
{
19
...profilingSummary,
20
- commitDurations: profilingSummary.commitDurations.map((_, i) => i),
21
- commitTimes: profilingSummary.commitTimes.map((_, i) => i),
22
- initialTreeBaseDurations: [
23
- ...profilingSummary.initialTreeBaseDurations,
24
- ].map(([id, _], index) => [id, index]),
20
+ initialTreeBaseDurations: [...profilingSummary.initialTreeBaseDurations],
21
},
22
null,
23
2
src/__tests__/store-test.js
+4
-4
@@ -6,6 +6,7 @@ describe('Store', () => {
6
let TestUtils;
7
let agent;
8
let store;
9
+ let utils;
10
11
const act = (callback: Function) => {
12
TestUtils.act(() => {
@@ -21,6 +22,7 @@ describe('Store', () => {
22
React = require('react');
23
ReactDOM = require('react-dom');
24
TestUtils = require('react-dom/test-utils');
25
+ utils = require('./utils');
26
});
27
28
it('should not allow a root node to be collapsed', () => {
@@ -281,8 +283,7 @@ describe('Store', () => {
283
);
284
expect(store).toMatchSnapshot('7: only third child is suspended');
285
284
- // HACK There's only one renderer for this test
285
- const rendererID = Object.keys(agent._rendererInterfaces)[0];
286
+ const rendererID = utils.getRendererID();
287
act(() =>
288
agent.overrideSuspense({
289
id: store.getElementIDAtIndex(4),
@@ -673,8 +674,7 @@ describe('Store', () => {
674
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false));
675
expect(store).toMatchSnapshot('2: expand tree');
676
676
- // HACK There's only one renderer for this test
677
- const rendererID = Object.keys(agent._rendererInterfaces)[0];
677
+ const rendererID = utils.getRendererID();
678
const suspenseID = store.getElementIDAtIndex(1);
679
680
act(() =>
src/__tests__/storeOwners-test.js
+2
-9
@@ -5,23 +5,16 @@ const { printOwnersList } = require('./storeSerializer');
5
describe('Store owners list', () => {
6
let React;
7
let ReactDOM;
8
- let TestUtils;
8
+ let act;
9
let store;
10
11
- const act = (callback: Function) => {
12
- TestUtils.act(() => {
13
- callback();
14
- });
15
- jest.runAllTimers(); // Flush Bridge operations
16
- };
17
-
11
beforeEach(() => {
12
store = global.store;
13
store.collapseNodesByDefault = false;
14
15
React = require('react');
16
ReactDOM = require('react-dom');
24
- TestUtils = require('react-dom/test-utils');
17
+ act = require('./utils').act;
18
});
19
20
it('should drill through intermediate components', () => {
src/__tests__/storeSerializer.js
+3
-3
@@ -70,9 +70,9 @@ export function printStore(store, includeWeight = false) {
70
);
71
}
72
73
- if (store.roots.length === 0) {
74
- store.assertEmptyMaps();
75
- }
73
+ // If roots have been unmounted, verify that they've been removed from maps.
74
+ // This helps ensure the Store doesn't leak memory.
75
+ store.assertExpectedRootMapSizes();
76
77
return snapshotLines.join('\n');
78
}
src/__tests__/storeStressSync-test.js
+2
-9
@@ -3,18 +3,11 @@
3
describe('StoreStress (Sync Mode)', () => {
4
let React;
5
let ReactDOM;
6
- let TestUtils;
6
+ let act;
7
let bridge;
8
let store;
9
let print;
10
11
- const act = (callback: Function) => {
12
- TestUtils.act(() => {
13
- callback();
14
- });
15
- jest.runAllTimers(); // Flush Bridge operations
16
- };
17
-
11
beforeEach(() => {
12
bridge = global.bridge;
13
store = global.store;
@@ -22,7 +15,7 @@ describe('StoreStress (Sync Mode)', () => {
15
16
React = require('react');
17
ReactDOM = require('react-dom');
25
- TestUtils = require('react-dom/test-utils');
18
+ act = require('./utils').act;
19
20
print = require('./storeSerializer').print;
21
});
src/__tests__/storeStressTestConcurrent-test.js
+2
-10
@@ -3,19 +3,11 @@
3
describe('StoreStressConcurrent', () => {
4
let React;
5
let ReactDOM;
6
- let TestUtils;
6
+ let act;
7
let bridge;
8
let store;
9
let print;
10
11
- const act = (callback: Function) => {
12
- TestUtils.act(() => {
13
- callback();
14
- });
15
- jest.advanceTimersByTime(1000); // Flush rendering and Suspense
16
- jest.runAllTimers(); // Flush Bridge operations
17
- };
18
-
11
beforeEach(() => {
12
bridge = global.bridge;
13
store = global.store;
@@ -23,7 +15,7 @@ describe('StoreStressConcurrent', () => {
15
16
React = require('react');
17
ReactDOM = require('react-dom');
26
- TestUtils = require('react-dom/test-utils');
18
+ act = require('./utils').act;
19
20
print = require('./storeSerializer').print;
21
});
src/__tests__/utils.js
new
+66
@@ -0,0 +1,66 @@
1
+// @flow
2
+
3
+export function act(callback: Function): void {
4
+ const TestUtils = require('react-dom/test-utils');
5
+ TestUtils.act(() => {
6
+ callback();
7
+ });
8
+
9
+ // Flush Bridge operations
10
+ jest.runAllTimers();
11
+}
12
+
13
+export async function actSuspense(callback: Function) {
14
+ const TestUtils = require('react-dom/test-utils');
15
+ const Scheduler = require('scheduler');
16
+
17
+ // $FlowFixMe Flow doens't know about "await act()" yet
18
+ await TestUtils.act(async () => {
19
+ callback();
20
+
21
+ // Resolve pending suspense promises
22
+ jest.runAllTimers();
23
+ });
24
+
25
+ // Re-render after resolved promises
26
+ Scheduler.flushAll();
27
+}
28
+
29
+export function beforeEachProfiling() {
30
+ // Mock React's timing information so that test runs are predictable.
31
+ jest.mock('scheduler', () =>
32
+ // $FlowFixMe Flow does not konw about requireActual
33
+ require.requireActual('scheduler/unstable_mock')
34
+ );
35
+
36
+ // DevTools itself uses performance.now() to offset commit times
37
+ // so they appear relative to when profiling was started in the UI.
38
+ jest.spyOn(performance, 'now').mockImplementation(
39
+ // $FlowFixMe Flow does not konw about requireActual
40
+ require.requireActual('scheduler/unstable_mock').unstable_now
41
+ );
42
+}
43
+
44
+export function getRendererID() {
45
+ if (global.agent == null) {
46
+ throw Error('Agent unavailable.');
47
+ }
48
+ const ids = Object.keys(global.agent._rendererInterfaces);
49
+ if (ids.length !== 1) {
50
+ throw Error('Multiple renderers attached.');
51
+ }
52
+ return ids[0];
53
+}
54
+
55
+export function requireTestRenderer() {
56
+ let hook;
57
+ try {
58
+ // Hide the hook before requiring TestRenderer, so we don't end up with a loop.
59
+ hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
60
+ delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
61
+
62
+ return require('react-test-renderer');
63
+ } finally {
64
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
65
+ }
66
+}
src/devtools/store.js
+37
-18
@@ -192,30 +192,49 @@ export default class Store extends EventEmitter {
192
}
193
194
// This is only used in tests to avoid memory leaks.
195
- assertEmptyMaps() {
196
- this.assertEmptyMap(this._idToElement, '_idToElement');
197
- this.assertEmptyMap(this._ownersMap, '_ownersMap');
198
- this.assertEmptyMap(
199
- this._profilingOperationsByRootID,
200
- '_profilingOperationsByRootID'
201
- );
202
- this.assertEmptyMap(
203
- this._profilingScreenshotsByRootID,
204
- '_profilingScreenshotsByRootID'
195
+ assertExpectedRootMapSizes() {
196
+ if (this.roots.length === 0) {
197
+ // The only safe time to assert these maps are empty is when the store is empty.
198
+ this.assertMapSizeMatchesRootCount(this._idToElement, '_idToElement');
199
+ this.assertMapSizeMatchesRootCount(this._ownersMap, '_ownersMap');
200
+
201
+ // These maps will be empty unless profiling mode has been started.
202
+ // After this, their size should always match the number of roots,
203
+ // but unless we want to track additional metadata about profiling history,
204
+ // the only safe time to assert this is when the store is empty.
205
+ this.assertMapSizeMatchesRootCount(
206
+ this._profilingOperationsByRootID,
207
+ '_profilingOperationsByRootID'
208
+ );
209
+ this.assertMapSizeMatchesRootCount(
210
+ this._profilingScreenshotsByRootID,
211
+ '_profilingScreenshotsByRootID'
212
+ );
213
+ this.assertMapSizeMatchesRootCount(
214
+ this._profilingSnapshotsByRootID,
215
+ '_profilingSnapshotsByRootID'
216
+ );
217
+ }
218
+
219
+ // These maps should always be the same size as the number of roots
220
+ this.assertMapSizeMatchesRootCount(
221
+ this._rootIDToCapabilities,
222
+ '_rootIDToCapabilities'
223
);
206
- this.assertEmptyMap(
207
- this._profilingSnapshotsByRootID,
208
- '_profilingSnapshotsByRootID'
224
+ this.assertMapSizeMatchesRootCount(
225
+ this._rootIDToRendererID,
226
+ '_rootIDToRendererID'
227
);
210
- this.assertEmptyMap(this._rootIDToCapabilities, '_rootIDToCapabilities');
211
- this.assertEmptyMap(this._rootIDToRendererID, '_rootIDToRendererID');
228
}
229
230
// This is only used in tests to avoid memory leaks.
215
- assertEmptyMap(map: Map<any, any>, mapName: string) {
216
- if (map.size !== 0) {
231
+ assertMapSizeMatchesRootCount(map: Map<any, any>, mapName: string) {
232
+ const expectedSize = this.roots.length;
233
+ if (map.size !== expectedSize) {
234
throw new Error(
218
- `Expected ${mapName} to be empty, got ${map.size}: ${inspect(map, {
235
+ `Expected ${mapName} to contain ${expectedSize} items, but it contains ${
236
+ map.size
237
+ } items\n\n${inspect(map, {
238
depth: 20,
239
})}`
240
);