Added ProfilerContext tests
Brian Vaughn committed
May 23, 2019 at 08:36 UTC
44fbf3cd1dcf19959e68853f4e7386a1769fbf00
8 files changed
+308
-34
.eslintrc
+2
-1
@@ -12,6 +12,7 @@
12
},
13
"globals": {
14
"__DEV__": "readonly",
15
- "jasmine": "readonly"
15
+ "jasmine": "readonly",
16
+ "spyOn": "readonly"
17
}
18
}
src/__tests__/__snapshots__/profilerContext-test.js.snap
+37
-2
@@ -1,6 +1,41 @@
1
// Jest Snapshot v1, https://goo.gl/fbAQLP
2
3
-exports[`ProfilerContext should gracefully handle an empty profiling session: 1: mount 1`] = `
3
+exports[`ProfilerContext should auto-select the root ID matching the Components tab selection if it has profiling data: mounted 1`] = `
4
[root]
5
- <Example>
5
+ ▾ <Parent>
6
+ <Child>
7
+[root]
8
+ ▾ <Parent>
9
+ <Child>
10
+`;
11
+
12
+exports[`ProfilerContext should maintain root selection between profiling sessions so long as there is data for that root: mounted 1`] = `
13
+[root]
14
+ ▾ <Parent>
15
+ <Child>
16
+[root]
17
+ ▾ <Parent>
18
+ <Child>
19
+`;
20
+
21
+exports[`ProfilerContext should not select the root ID matching the Components tab selection if it has no profiling data: mounted 1`] = `
22
+[root]
23
+ ▾ <Parent>
24
+ <Child>
25
+[root]
26
+ ▾ <Parent>
27
+ <Child>
28
+`;
29
+
30
+exports[`ProfilerContext should sync selected element in the Components tab too, provided the element is a match: mounted 1`] = `
31
+[root]
32
+ ▾ <GrandParent>
33
+ ▾ <Parent>
34
+ <Child>
35
+`;
36
+
37
+exports[`ProfilerContext should sync selected element in the Components tab too, provided the element is a match: updated 1`] = `
38
+[root]
39
+ ▾ <GrandParent>
40
+ <Parent>
41
`;
src/__tests__/profilerContext-test.js
+248
-14
@@ -1,8 +1,9 @@
1
// @flow
2
3
import typeof ReactTestRenderer from 'react-test-renderer';
4
-import type { Element } from 'src/devtools/views/Components/types';
4
import type Bridge from 'src/bridge';
5
+import type { Context } from 'src/devtools/views/Profiler/ProfilerContext';
6
+import type { DispatcherContext } from 'src/devtools/views/Components/TreeContext';
7
import type Store from 'src/devtools/store';
8
9
describe('ProfilerContext', () => {
@@ -18,6 +19,8 @@ describe('ProfilerContext', () => {
19
let ProfilerContextController;
20
let StoreContext;
21
let TreeContextController;
22
+ let TreeDispatcherContext;
23
+ let TreeStateContext;
24
25
beforeEach(() => {
26
utils = require('./utils');
@@ -39,13 +42,17 @@ describe('ProfilerContext', () => {
42
StoreContext = require('src/devtools/views/context').StoreContext;
43
TreeContextController = require('src/devtools/views/Components/TreeContext')
44
.TreeContextController;
45
+ TreeDispatcherContext = require('src/devtools/views/Components/TreeContext')
46
+ .TreeDispatcherContext;
47
+ TreeStateContext = require('src/devtools/views/Components/TreeContext')
48
+ .TreeStateContext;
49
});
50
51
const Contexts = ({
52
children = null,
53
defaultSelectedElementID = null,
54
defaultSelectedElementIndex = null,
48
- }) => (
55
+ }: any) => (
56
<BridgeContext.Provider value={bridge}>
57
<StoreContext.Provider value={store}>
58
<TreeContextController
@@ -58,21 +65,248 @@ describe('ProfilerContext', () => {
65
</BridgeContext.Provider>
66
);
67
61
- it('should gracefully handle an empty profiling session', () => {
62
- const Example = () => {
63
- const [count] = React.useState(1);
64
- return count;
65
- };
68
+ it('should gracefully handle an empty profiling session (with no recorded commits)', async done => {
69
+ const Example = () => null;
70
67
- const container = document.createElement('div');
68
- utils.act(() => ReactDOM.render(<Example foo={1} bar="abc" />, container));
69
- expect(store).toMatchSnapshot('1: mount');
71
+ utils.act(() =>
72
+ ReactDOM.render(<Example />, document.createElement('div'))
73
+ );
74
+
75
+ let context: Context = ((null: any): Context);
76
71
- utils.act(() => store.profilerStore.startProfiling());
72
- utils.act(() => store.profilerStore.stopProfiling());
77
+ function ContextReader() {
78
+ context = React.useContext(ProfilerContext);
79
+ return null;
80
+ }
81
74
- utils.act(() => {
75
- TestRenderer.create(<Contexts />);
82
+ // Profile but don't record any updates.
83
+ await utils.actAsync(() => store.profilerStore.startProfiling());
84
+ await utils.actAsync(() => {
85
+ TestRenderer.create(
86
+ <Contexts>
87
+ <ContextReader />
88
+ </Contexts>
89
+ );
90
});
91
+ expect(context).not.toBeNull();
92
+ expect(context.didRecordCommits).toBe(false);
93
+ expect(context.isProcessingData).toBe(false);
94
+ expect(context.isProfiling).toBe(true);
95
+ expect(context.profilingData).toBe(null);
96
+ await utils.actAsync(() => store.profilerStore.stopProfiling());
97
+
98
+ expect(context).not.toBeNull();
99
+ expect(context.didRecordCommits).toBe(false);
100
+ expect(context.isProcessingData).toBe(false);
101
+ expect(context.isProfiling).toBe(false);
102
+ expect(context.profilingData).not.toBe(null);
103
+
104
+ done();
105
+ });
106
+
107
+ it('should auto-select the root ID matching the Components tab selection if it has profiling data', async done => {
108
+ const Parent = () => <Child />;
109
+ const Child = () => null;
110
+
111
+ const containerOne = document.createElement('div');
112
+ const containerTwo = document.createElement('div');
113
+ utils.act(() => ReactDOM.render(<Parent />, containerOne));
114
+ utils.act(() => ReactDOM.render(<Parent />, containerTwo));
115
+ expect(store).toMatchSnapshot('mounted');
116
+
117
+ // Profile and record updates to both roots.
118
+ await utils.actAsync(() => store.profilerStore.startProfiling());
119
+ await utils.actAsync(() => ReactDOM.render(<Parent />, containerOne));
120
+ await utils.actAsync(() => ReactDOM.render(<Parent />, containerTwo));
121
+ await utils.actAsync(() => store.profilerStore.stopProfiling());
122
+
123
+ let context: Context = ((null: any): Context);
124
+ function ContextReader() {
125
+ context = React.useContext(ProfilerContext);
126
+ return null;
127
+ }
128
+
129
+ // Select an element within the second root.
130
+ await utils.actAsync(() =>
131
+ TestRenderer.create(
132
+ <Contexts
133
+ defaultSelectedElementID={store.getElementIDAtIndex(3)}
134
+ defaultSelectedElementIndex={3}
135
+ >
136
+ <ContextReader />
137
+ </Contexts>
138
+ )
139
+ );
140
+
141
+ expect(context).not.toBeNull();
142
+ expect(context.rootID).toBe(
143
+ store.getRootIDForElement(((store.getElementIDAtIndex(3): any): number))
144
+ );
145
+
146
+ done();
147
+ });
148
+
149
+ it('should not select the root ID matching the Components tab selection if it has no profiling data', async done => {
150
+ const Parent = () => <Child />;
151
+ const Child = () => null;
152
+
153
+ const containerOne = document.createElement('div');
154
+ const containerTwo = document.createElement('div');
155
+ utils.act(() => ReactDOM.render(<Parent />, containerOne));
156
+ utils.act(() => ReactDOM.render(<Parent />, containerTwo));
157
+ expect(store).toMatchSnapshot('mounted');
158
+
159
+ // Profile and record updates to only the first root.
160
+ await utils.actAsync(() => store.profilerStore.startProfiling());
161
+ await utils.actAsync(() => ReactDOM.render(<Parent />, containerOne));
162
+ await utils.actAsync(() => store.profilerStore.stopProfiling());
163
+
164
+ let context: Context = ((null: any): Context);
165
+ function ContextReader() {
166
+ context = React.useContext(ProfilerContext);
167
+ return null;
168
+ }
169
+
170
+ // Select an element within the second root.
171
+ await utils.actAsync(() =>
172
+ TestRenderer.create(
173
+ <Contexts
174
+ defaultSelectedElementID={store.getElementIDAtIndex(3)}
175
+ defaultSelectedElementIndex={3}
176
+ >
177
+ <ContextReader />
178
+ </Contexts>
179
+ )
180
+ );
181
+
182
+ // Verify the default profiling root is the first one.
183
+ expect(context).not.toBeNull();
184
+ expect(context.rootID).toBe(
185
+ store.getRootIDForElement(((store.getElementIDAtIndex(0): any): number))
186
+ );
187
+
188
+ done();
189
+ });
190
+
191
+ it('should maintain root selection between profiling sessions so long as there is data for that root', async done => {
192
+ const Parent = () => <Child />;
193
+ const Child = () => null;
194
+
195
+ const containerA = document.createElement('div');
196
+ const containerB = document.createElement('div');
197
+ utils.act(() => ReactDOM.render(<Parent />, containerA));
198
+ utils.act(() => ReactDOM.render(<Parent />, containerB));
199
+ expect(store).toMatchSnapshot('mounted');
200
+
201
+ // Profile and record updates.
202
+ await utils.actAsync(() => store.profilerStore.startProfiling());
203
+ await utils.actAsync(() => ReactDOM.render(<Parent />, containerA));
204
+ await utils.actAsync(() => ReactDOM.render(<Parent />, containerB));
205
+ await utils.actAsync(() => store.profilerStore.stopProfiling());
206
+
207
+ let context: Context = ((null: any): Context);
208
+ let dispatch: DispatcherContext = ((null: any): DispatcherContext);
209
+ let selectedElementID = null;
210
+ function ContextReader() {
211
+ context = React.useContext(ProfilerContext);
212
+ dispatch = React.useContext(TreeDispatcherContext);
213
+ selectedElementID = React.useContext(TreeStateContext).selectedElementID;
214
+ return null;
215
+ }
216
+
217
+ const id = ((store.getElementIDAtIndex(3): any): number);
218
+
219
+ // Select an element within the second root.
220
+ await utils.actAsync(() =>
221
+ TestRenderer.create(
222
+ <Contexts defaultSelectedElementID={id} defaultSelectedElementIndex={3}>
223
+ <ContextReader />
224
+ </Contexts>
225
+ )
226
+ );
227
+
228
+ expect(selectedElementID).toBe(id);
229
+
230
+ // Profile and record more updates to both roots
231
+ await utils.actAsync(() => store.profilerStore.startProfiling());
232
+ await utils.actAsync(() => ReactDOM.render(<Parent />, containerA));
233
+ await utils.actAsync(() => ReactDOM.render(<Parent />, containerB));
234
+ await utils.actAsync(() => store.profilerStore.stopProfiling());
235
+
236
+ const otherID = ((store.getElementIDAtIndex(0): any): number);
237
+
238
+ // Change the selected element within a the Components tab.
239
+ utils.act(() => dispatch({ type: 'SELECT_ELEMENT_AT_INDEX', payload: 0 }));
240
+
241
+ // Verify that the initial Profiler root selection is maintained.
242
+ expect(selectedElementID).toBe(otherID);
243
+ expect(context).not.toBeNull();
244
+ expect(context.rootID).toBe(store.getRootIDForElement(id));
245
+
246
+ done();
247
+ });
248
+
249
+ it('should sync selected element in the Components tab too, provided the element is a match', async done => {
250
+ const GrandParent = ({ includeChild }) => (
251
+ <Parent includeChild={includeChild} />
252
+ );
253
+ const Parent = ({ includeChild }) => (includeChild ? <Child /> : null);
254
+ const Child = () => null;
255
+
256
+ const container = document.createElement('div');
257
+ utils.act(() =>
258
+ ReactDOM.render(<GrandParent includeChild={true} />, container)
259
+ );
260
+ expect(store).toMatchSnapshot('mounted');
261
+
262
+ const parentID = ((store.getElementIDAtIndex(1): any): number);
263
+ const childID = ((store.getElementIDAtIndex(2): any): number);
264
+
265
+ // Profile and record updates.
266
+ await utils.actAsync(() => store.profilerStore.startProfiling());
267
+ await utils.actAsync(() =>
268
+ ReactDOM.render(<GrandParent includeChild={true} />, container)
269
+ );
270
+ await utils.actAsync(() =>
271
+ ReactDOM.render(<GrandParent includeChild={false} />, container)
272
+ );
273
+ await utils.actAsync(() => store.profilerStore.stopProfiling());
274
+
275
+ expect(store).toMatchSnapshot('updated');
276
+
277
+ let context: Context = ((null: any): Context);
278
+ let selectedElementID = null;
279
+ function ContextReader() {
280
+ context = React.useContext(ProfilerContext);
281
+ selectedElementID = React.useContext(TreeStateContext).selectedElementID;
282
+ return null;
283
+ }
284
+
285
+ await utils.actAsync(() =>
286
+ TestRenderer.create(
287
+ <Contexts>
288
+ <ContextReader />
289
+ </Contexts>
290
+ )
291
+ );
292
+ expect(selectedElementID).toBeNull();
293
+
294
+ // Select an element in the Profiler tab and verify that the selection is synced to the Components tab.
295
+ await utils.actAsync(() => context.selectFiber(parentID, 'Parent'));
296
+ expect(selectedElementID).toBe(parentID);
297
+
298
+ // We expect a "no element found" warning.
299
+ // Let's hide it from the test console though.
300
+ spyOn(console, 'warn');
301
+
302
+ // Select an unmounted element and verify no Components tab selection doesn't change.
303
+ await utils.actAsync(() => context.selectFiber(childID, 'Child'));
304
+ expect(selectedElementID).toBe(parentID);
305
+
306
+ expect(console.warn).toHaveBeenCalledWith(
307
+ `No element found with id "${childID}"`
308
+ );
309
+
310
+ done();
311
});
312
});
src/devtools/ProfilerStore.js
+1
-1
@@ -122,7 +122,7 @@ export default class ProfilerStore extends EventEmitter {
122
}
123
124
// Profiling data has been recorded for at least one root.
125
- get hasProfilingData(): boolean {
125
+ get didRecordCommits(): boolean {
126
return (
127
this._dataFrontend !== null && this._dataFrontend.dataForRoots.size > 0
128
);
src/devtools/views/Profiler/ClearProfilingDataButton.js
+2
-2
@@ -8,14 +8,14 @@ import { StoreContext } from '../context';
8
9
export default function ClearProfilingDataButton() {
10
const store = useContext(StoreContext);
11
- const { hasProfilingData, isProfiling } = useContext(ProfilerContext);
11
+ const { didRecordCommits, isProfiling } = useContext(ProfilerContext);
12
const { profilerStore } = store;
13
14
const clear = useCallback(() => profilerStore.clear(), [profilerStore]);
15
16
return (
17
<Button
18
- disabled={isProfiling || !hasProfilingData}
18
+ disabled={isProfiling || !didRecordCommits}
19
onClick={clear}
20
title="Clear profiling data"
21
>
src/devtools/views/Profiler/Profiler.js
+4
-4
@@ -29,7 +29,7 @@ export type Props = {|
29
30
function Profiler({ supportsProfiling }: Props) {
31
const {
32
- hasProfilingData,
32
+ didRecordCommits,
33
isProcessingData,
34
isProfiling,
35
selectedFiberID,
@@ -38,7 +38,7 @@ function Profiler({ supportsProfiling }: Props) {
38
} = useContext(ProfilerContext);
39
40
let view = null;
41
- if (hasProfilingData) {
41
+ if (didRecordCommits) {
42
switch (selectedTabID) {
43
case 'flame-chart':
44
view = <CommitFlamegraph />;
@@ -63,7 +63,7 @@ function Profiler({ supportsProfiling }: Props) {
63
}
64
65
let sidebar = null;
66
- if (!isProfiling && !isProcessingData && hasProfilingData) {
66
+ if (!isProfiling && !isProcessingData && didRecordCommits) {
67
switch (selectedTabID) {
68
case 'interactions':
69
sidebar = <SidebarInteractions />;
@@ -102,7 +102,7 @@ function Profiler({ supportsProfiling }: Props) {
102
<div className={styles.Spacer} />
103
<ToggleCommitFilterModalButton />
104
<div className={styles.VRule} />
105
- {hasProfilingData && <SnapshotSelector />}
105
+ {didRecordCommits && <SnapshotSelector />}
106
</div>
107
<div className={styles.Content}>
108
{view}
src/devtools/views/Profiler/ProfilerContext.js
+13
-9
@@ -20,7 +20,7 @@ import type { ProfilingDataFrontend } from './types';
20
21
export type TabID = 'flame-chart' | 'ranked-chart' | 'interactions';
22
23
-type Context = {|
23
+export type Context = {|
24
// Which tab is selexted in the Profiler UI?
25
selectedTabID: TabID,
26
selectTab(id: TabID): void,
@@ -30,7 +30,7 @@ type Context = {|
30
// This value may be modified by the record button in the Profiler toolbar,
31
// or from the backend itself (after a reload-and-profile action).
32
// It is synced between the backend and frontend via a Store subscription.
33
- hasProfilingData: boolean,
33
+ didRecordCommits: boolean,
34
isProcessingData: boolean,
35
isProfiling: boolean,
36
profilingData: ProfilingDataFrontend | null,
@@ -73,7 +73,7 @@ const ProfilerContext = createContext<Context>(((null: any): Context));
73
ProfilerContext.displayName = 'ProfilerContext';
74
75
type StoreProfilingState = {|
76
- hasProfilingData: boolean,
76
+ didRecordCommits: boolean,
77
isProcessingData: boolean,
78
isProfiling: boolean,
79
profilingData: ProfilingDataFrontend | null,
@@ -93,7 +93,7 @@ function ProfilerContextController({ children }: Props) {
93
const subscription = useMemo(
94
() => ({
95
getCurrentValue: () => ({
96
- hasProfilingData: profilerStore.hasProfilingData,
96
+ didRecordCommits: profilerStore.didRecordCommits,
97
isProcessingData: profilerStore.isProcessingData,
98
isProfiling: profilerStore.isProfiling,
99
profilingData: profilerStore.profilingData,
@@ -112,7 +112,7 @@ function ProfilerContextController({ children }: Props) {
112
[profilerStore]
113
);
114
const {
115
- hasProfilingData,
115
+ didRecordCommits,
116
isProcessingData,
117
isProfiling,
118
profilingData,
@@ -177,10 +177,14 @@ function ProfilerContextController({ children }: Props) {
177
(id: number | null, name: string | null) => {
178
selectFiberID(id);
179
selectFiberName(name);
180
+
181
+ // Sync selection to the Components tab for convenience.
182
if (id !== null) {
181
- // If this element is still in the store, then select it in the Components tab as well.
183
const element = store.getElementByID(id);
183
- if (element !== null) {
184
+
185
+ // Keep in mind that profiling data may be from a previous session.
186
+ // In that case, IDs may match up arbitrarily; to be safe, compare both ID and display name.
187
+ if (element !== null && element.displayName === name) {
188
dispatch({
189
type: 'SELECT_ELEMENT_BY_ID',
190
payload: id,
@@ -211,7 +215,7 @@ function ProfilerContextController({ children }: Props) {
215
selectedTabID,
216
selectTab,
217
214
- hasProfilingData,
218
+ didRecordCommits,
219
isProcessingData,
220
isProfiling,
221
profilingData,
@@ -240,7 +244,7 @@ function ProfilerContextController({ children }: Props) {
244
selectedTabID,
245
selectTab,
246
243
- hasProfilingData,
247
+ didRecordCommits,
248
isProcessingData,
249
isProfiling,
250
profilingData,
src/devtools/views/Profiler/ProfilingImportExportButtons.js
+1
-1
@@ -109,7 +109,7 @@ export default function ProfilingImportExportButtons() {
109
<ButtonIcon type="import" />
110
</Button>
111
<Button
112
- disabled={isProfiling || !profilerStore.hasProfilingData}
112
+ disabled={isProfiling || !profilerStore.didRecordCommits}
113
onClick={downloadData}
114
title="Save profile..."
115
>