@samitouri / QOS-React-2 / commits / 44fbf3cd1d

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 >