@samitouri / QOS-React-2 / commits / dd96b3314c

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