diff --git a/.eslintrc b/.eslintrc index 5065c9aae8..5ee6406982 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,6 +12,7 @@ }, "globals": { "__DEV__": "readonly", - "jasmine": "readonly" + "jasmine": "readonly", + "spyOn": "readonly" } } diff --git a/src/__tests__/__snapshots__/profilerContext-test.js.snap b/src/__tests__/__snapshots__/profilerContext-test.js.snap index c56fd9dee4..927f3180b4 100644 --- a/src/__tests__/__snapshots__/profilerContext-test.js.snap +++ b/src/__tests__/__snapshots__/profilerContext-test.js.snap @@ -1,6 +1,41 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ProfilerContext should gracefully handle an empty profiling session: 1: mount 1`] = ` +exports[`ProfilerContext should auto-select the root ID matching the Components tab selection if it has profiling data: mounted 1`] = ` [root] - + ▾ + +[root] + ▾ + +`; + +exports[`ProfilerContext should maintain root selection between profiling sessions so long as there is data for that root: mounted 1`] = ` +[root] + ▾ + +[root] + ▾ + +`; + +exports[`ProfilerContext should not select the root ID matching the Components tab selection if it has no profiling data: mounted 1`] = ` +[root] + ▾ + +[root] + ▾ + +`; + +exports[`ProfilerContext should sync selected element in the Components tab too, provided the element is a match: mounted 1`] = ` +[root] + ▾ + ▾ + +`; + +exports[`ProfilerContext should sync selected element in the Components tab too, provided the element is a match: updated 1`] = ` +[root] + ▾ + `; diff --git a/src/__tests__/profilerContext-test.js b/src/__tests__/profilerContext-test.js index cbfe1043e3..a6b0a62ff9 100644 --- a/src/__tests__/profilerContext-test.js +++ b/src/__tests__/profilerContext-test.js @@ -1,8 +1,9 @@ // @flow import typeof ReactTestRenderer from 'react-test-renderer'; -import type { Element } from 'src/devtools/views/Components/types'; import type Bridge from 'src/bridge'; +import type { Context } from 'src/devtools/views/Profiler/ProfilerContext'; +import type { DispatcherContext } from 'src/devtools/views/Components/TreeContext'; import type Store from 'src/devtools/store'; describe('ProfilerContext', () => { @@ -18,6 +19,8 @@ describe('ProfilerContext', () => { let ProfilerContextController; let StoreContext; let TreeContextController; + let TreeDispatcherContext; + let TreeStateContext; beforeEach(() => { utils = require('./utils'); @@ -39,13 +42,17 @@ describe('ProfilerContext', () => { StoreContext = require('src/devtools/views/context').StoreContext; TreeContextController = require('src/devtools/views/Components/TreeContext') .TreeContextController; + TreeDispatcherContext = require('src/devtools/views/Components/TreeContext') + .TreeDispatcherContext; + TreeStateContext = require('src/devtools/views/Components/TreeContext') + .TreeStateContext; }); const Contexts = ({ children = null, defaultSelectedElementID = null, defaultSelectedElementIndex = null, - }) => ( + }: any) => ( { ); - it('should gracefully handle an empty profiling session', () => { - const Example = () => { - const [count] = React.useState(1); - return count; - }; + it('should gracefully handle an empty profiling session (with no recorded commits)', async done => { + const Example = () => null; + + utils.act(() => + ReactDOM.render(, document.createElement('div')) + ); + + let context: Context = ((null: any): Context); + + function ContextReader() { + context = React.useContext(ProfilerContext); + return null; + } + + // Profile but don't record any updates. + await utils.actAsync(() => store.profilerStore.startProfiling()); + await utils.actAsync(() => { + TestRenderer.create( + + + + ); + }); + expect(context).not.toBeNull(); + expect(context.didRecordCommits).toBe(false); + expect(context.isProcessingData).toBe(false); + expect(context.isProfiling).toBe(true); + expect(context.profilingData).toBe(null); + await utils.actAsync(() => store.profilerStore.stopProfiling()); + + expect(context).not.toBeNull(); + expect(context.didRecordCommits).toBe(false); + expect(context.isProcessingData).toBe(false); + expect(context.isProfiling).toBe(false); + expect(context.profilingData).not.toBe(null); + + done(); + }); + + it('should auto-select the root ID matching the Components tab selection if it has profiling data', async done => { + const Parent = () => ; + const Child = () => null; + + const containerOne = document.createElement('div'); + const containerTwo = document.createElement('div'); + utils.act(() => ReactDOM.render(, containerOne)); + utils.act(() => ReactDOM.render(, containerTwo)); + expect(store).toMatchSnapshot('mounted'); + + // Profile and record updates to both roots. + await utils.actAsync(() => store.profilerStore.startProfiling()); + await utils.actAsync(() => ReactDOM.render(, containerOne)); + await utils.actAsync(() => ReactDOM.render(, containerTwo)); + await utils.actAsync(() => store.profilerStore.stopProfiling()); + + let context: Context = ((null: any): Context); + function ContextReader() { + context = React.useContext(ProfilerContext); + return null; + } + + // Select an element within the second root. + await utils.actAsync(() => + TestRenderer.create( + + + + ) + ); + + expect(context).not.toBeNull(); + expect(context.rootID).toBe( + store.getRootIDForElement(((store.getElementIDAtIndex(3): any): number)) + ); + + done(); + }); + + it('should not select the root ID matching the Components tab selection if it has no profiling data', async done => { + const Parent = () => ; + const Child = () => null; + + const containerOne = document.createElement('div'); + const containerTwo = document.createElement('div'); + utils.act(() => ReactDOM.render(, containerOne)); + utils.act(() => ReactDOM.render(, containerTwo)); + expect(store).toMatchSnapshot('mounted'); + + // Profile and record updates to only the first root. + await utils.actAsync(() => store.profilerStore.startProfiling()); + await utils.actAsync(() => ReactDOM.render(, containerOne)); + await utils.actAsync(() => store.profilerStore.stopProfiling()); + + let context: Context = ((null: any): Context); + function ContextReader() { + context = React.useContext(ProfilerContext); + return null; + } + + // Select an element within the second root. + await utils.actAsync(() => + TestRenderer.create( + + + + ) + ); + + // Verify the default profiling root is the first one. + expect(context).not.toBeNull(); + expect(context.rootID).toBe( + store.getRootIDForElement(((store.getElementIDAtIndex(0): any): number)) + ); + + done(); + }); + + it('should maintain root selection between profiling sessions so long as there is data for that root', async done => { + const Parent = () => ; + const Child = () => null; + + const containerA = document.createElement('div'); + const containerB = document.createElement('div'); + utils.act(() => ReactDOM.render(, containerA)); + utils.act(() => ReactDOM.render(, containerB)); + expect(store).toMatchSnapshot('mounted'); + + // Profile and record updates. + await utils.actAsync(() => store.profilerStore.startProfiling()); + await utils.actAsync(() => ReactDOM.render(, containerA)); + await utils.actAsync(() => ReactDOM.render(, containerB)); + await utils.actAsync(() => store.profilerStore.stopProfiling()); + + let context: Context = ((null: any): Context); + let dispatch: DispatcherContext = ((null: any): DispatcherContext); + let selectedElementID = null; + function ContextReader() { + context = React.useContext(ProfilerContext); + dispatch = React.useContext(TreeDispatcherContext); + selectedElementID = React.useContext(TreeStateContext).selectedElementID; + return null; + } + + const id = ((store.getElementIDAtIndex(3): any): number); + + // Select an element within the second root. + await utils.actAsync(() => + TestRenderer.create( + + + + ) + ); + + expect(selectedElementID).toBe(id); + + // Profile and record more updates to both roots + await utils.actAsync(() => store.profilerStore.startProfiling()); + await utils.actAsync(() => ReactDOM.render(, containerA)); + await utils.actAsync(() => ReactDOM.render(, containerB)); + await utils.actAsync(() => store.profilerStore.stopProfiling()); + + const otherID = ((store.getElementIDAtIndex(0): any): number); + + // Change the selected element within a the Components tab. + utils.act(() => dispatch({ type: 'SELECT_ELEMENT_AT_INDEX', payload: 0 })); + + // Verify that the initial Profiler root selection is maintained. + expect(selectedElementID).toBe(otherID); + expect(context).not.toBeNull(); + expect(context.rootID).toBe(store.getRootIDForElement(id)); + + done(); + }); + + it('should sync selected element in the Components tab too, provided the element is a match', async done => { + const GrandParent = ({ includeChild }) => ( + + ); + const Parent = ({ includeChild }) => (includeChild ? : null); + const Child = () => null; const container = document.createElement('div'); - utils.act(() => ReactDOM.render(, container)); - expect(store).toMatchSnapshot('1: mount'); + utils.act(() => + ReactDOM.render(, container) + ); + expect(store).toMatchSnapshot('mounted'); - utils.act(() => store.profilerStore.startProfiling()); - utils.act(() => store.profilerStore.stopProfiling()); + const parentID = ((store.getElementIDAtIndex(1): any): number); + const childID = ((store.getElementIDAtIndex(2): any): number); - utils.act(() => { - TestRenderer.create(); - }); + // Profile and record updates. + await utils.actAsync(() => store.profilerStore.startProfiling()); + await utils.actAsync(() => + ReactDOM.render(, container) + ); + await utils.actAsync(() => + ReactDOM.render(, container) + ); + await utils.actAsync(() => store.profilerStore.stopProfiling()); + + expect(store).toMatchSnapshot('updated'); + + let context: Context = ((null: any): Context); + let selectedElementID = null; + function ContextReader() { + context = React.useContext(ProfilerContext); + selectedElementID = React.useContext(TreeStateContext).selectedElementID; + return null; + } + + await utils.actAsync(() => + TestRenderer.create( + + + + ) + ); + expect(selectedElementID).toBeNull(); + + // Select an element in the Profiler tab and verify that the selection is synced to the Components tab. + await utils.actAsync(() => context.selectFiber(parentID, 'Parent')); + expect(selectedElementID).toBe(parentID); + + // We expect a "no element found" warning. + // Let's hide it from the test console though. + spyOn(console, 'warn'); + + // Select an unmounted element and verify no Components tab selection doesn't change. + await utils.actAsync(() => context.selectFiber(childID, 'Child')); + expect(selectedElementID).toBe(parentID); + + expect(console.warn).toHaveBeenCalledWith( + `No element found with id "${childID}"` + ); + + done(); }); }); diff --git a/src/devtools/ProfilerStore.js b/src/devtools/ProfilerStore.js index c59c073be4..30e1934600 100644 --- a/src/devtools/ProfilerStore.js +++ b/src/devtools/ProfilerStore.js @@ -122,7 +122,7 @@ export default class ProfilerStore extends EventEmitter { } // Profiling data has been recorded for at least one root. - get hasProfilingData(): boolean { + get didRecordCommits(): boolean { return ( this._dataFrontend !== null && this._dataFrontend.dataForRoots.size > 0 ); diff --git a/src/devtools/views/Profiler/ClearProfilingDataButton.js b/src/devtools/views/Profiler/ClearProfilingDataButton.js index c99f370fce..d3223eb8de 100644 --- a/src/devtools/views/Profiler/ClearProfilingDataButton.js +++ b/src/devtools/views/Profiler/ClearProfilingDataButton.js @@ -8,14 +8,14 @@ import { StoreContext } from '../context'; export default function ClearProfilingDataButton() { const store = useContext(StoreContext); - const { hasProfilingData, isProfiling } = useContext(ProfilerContext); + const { didRecordCommits, isProfiling } = useContext(ProfilerContext); const { profilerStore } = store; const clear = useCallback(() => profilerStore.clear(), [profilerStore]); return (