// @flow import typeof ReactTestRenderer from 'react-test-renderer'; import type { FrontendBridge } from 'src/bridge'; import type Store from 'src/devtools/store'; describe('ProfilingCache', () => { let PropTypes; let React; let ReactDOM; let Scheduler; let SchedulerTracing; let TestRenderer: ReactTestRenderer; let bridge: FrontendBridge; let store: Store; let utils; beforeEach(() => { utils = require('./utils'); utils.beforeEachProfiling(); bridge = global.bridge; store = global.store; store.collapseNodesByDefault = false; store.recordChangeDescriptions = true; PropTypes = require('prop-types'); React = require('react'); ReactDOM = require('react-dom'); Scheduler = require('scheduler'); SchedulerTracing = require('scheduler/tracing'); TestRenderer = utils.requireTestRenderer(); }); it('should collect data for each root (including ones added or mounted after profiling started)', () => { const Parent = ({ count }) => { Scheduler.unstable_advanceTime(10); const children = new Array(count) .fill(true) .map((_, index) => ); return ( {children} ); }; const Child = ({ duration }) => { Scheduler.unstable_advanceTime(duration); return null; }; const MemoizedChild = React.memo(Child); const containerA = document.createElement('div'); const containerB = document.createElement('div'); const containerC = document.createElement('div'); utils.act(() => ReactDOM.render(, containerA)); utils.act(() => ReactDOM.render(, containerB)); utils.act(() => store.profilerStore.startProfiling()); utils.act(() => ReactDOM.render(, containerA)); utils.act(() => ReactDOM.render(, containerC)); utils.act(() => ReactDOM.render(, containerA)); utils.act(() => ReactDOM.unmountComponentAtNode(containerB)); utils.act(() => ReactDOM.render(, containerA)); utils.act(() => store.profilerStore.stopProfiling()); let allProfilingDataForRoots = []; function Validator({ previousProfilingDataForRoot, rootID }) { const profilingDataForRoot = store.profilerStore.getDataForRoot(rootID); if (previousProfilingDataForRoot != null) { expect(profilingDataForRoot).toEqual(previousProfilingDataForRoot); } else { expect(profilingDataForRoot).toMatchSnapshot( `Data for root ${profilingDataForRoot.displayName}` ); } allProfilingDataForRoots.push(profilingDataForRoot); return null; } const dataForRoots = store.profilerStore.profilingData !== null ? store.profilerStore.profilingData.dataForRoots : null; expect(dataForRoots).not.toBeNull(); if (dataForRoots !== null) { dataForRoots.forEach(dataForRoot => { utils.act(() => TestRenderer.create( ) ); }); } expect(allProfilingDataForRoots).toHaveLength(3); utils.exportImportHelper(bridge, store); allProfilingDataForRoots.forEach(profilingDataForRoot => { utils.act(() => TestRenderer.create( ) ); }); }); it('should collect data for each commit', () => { const Parent = ({ count }) => { Scheduler.unstable_advanceTime(10); const children = new Array(count) .fill(true) .map((_, index) => ); return ( {children} ); }; const Child = ({ duration }) => { Scheduler.unstable_advanceTime(duration); return null; }; const MemoizedChild = React.memo(Child); const container = document.createElement('div'); utils.act(() => store.profilerStore.startProfiling()); utils.act(() => ReactDOM.render(, container)); utils.act(() => ReactDOM.render(, container)); utils.act(() => ReactDOM.render(, container)); utils.act(() => ReactDOM.render(, container)); utils.act(() => store.profilerStore.stopProfiling()); const allCommitData = []; function Validator({ commitIndex, previousCommitDetails, rootID }) { const commitData = store.profilerStore.getCommitData(rootID, commitIndex); if (previousCommitDetails != null) { expect(commitData).toEqual(previousCommitDetails); } else { allCommitData.push(commitData); expect(commitData).toMatchSnapshot( `CommitDetails commitIndex: ${commitIndex}` ); } return null; } const rootID = store.roots[0]; for (let commitIndex = 0; commitIndex < 4; commitIndex++) { utils.act(() => { TestRenderer.create( ); }); } expect(allCommitData).toHaveLength(4); utils.exportImportHelper(bridge, store); for (let commitIndex = 0; commitIndex < 4; commitIndex++) { utils.act(() => { TestRenderer.create( ); }); } }); it('should record changed props/state/context/hooks', () => { let instance = null; const ModernContext = React.createContext(0); class LegacyContextProvider extends React.Component< any, {| count: number |} > { static childContextTypes = { count: PropTypes.number, }; state = { count: 0 }; getChildContext() { return this.state; } render() { instance = this; return ( ); } } const FunctionComponentWithHooks = ({ count }) => { React.useMemo(() => count, [count]); return null; }; class ModernContextConsumer extends React.Component { static contextType = ModernContext; render() { return ; } } class LegacyContextConsumer extends React.Component { static contextTypes = { count: PropTypes.number, }; render() { return ; } } const container = document.createElement('div'); utils.act(() => store.profilerStore.startProfiling()); utils.act(() => ReactDOM.render(, container)); expect(instance).not.toBeNull(); utils.act(() => (instance: any).setState({ count: 1 })); utils.act(() => ReactDOM.render(, container) ); utils.act(() => ReactDOM.render(, container) ); utils.act(() => ReactDOM.render(, container)); utils.act(() => store.profilerStore.stopProfiling()); const allCommitData = []; function Validator({ commitIndex, previousCommitDetails, rootID }) { const commitData = store.profilerStore.getCommitData(rootID, commitIndex); if (previousCommitDetails != null) { expect(commitData).toEqual(previousCommitDetails); } else { allCommitData.push(commitData); expect(commitData).toMatchSnapshot( `CommitDetails commitIndex: ${commitIndex}` ); } return null; } const rootID = store.roots[0]; for (let commitIndex = 0; commitIndex < 5; commitIndex++) { utils.act(() => { TestRenderer.create( ); }); } expect(allCommitData).toHaveLength(5); utils.exportImportHelper(bridge, store); for (let commitIndex = 0; commitIndex < 5; commitIndex++) { utils.act(() => { TestRenderer.create( ); }); } }); it('should calculate a self duration based on actual children (not filtered children)', () => { store.componentFilters = [utils.createDisplayNameFilter('^Parent$')]; const Grandparent = () => { Scheduler.unstable_advanceTime(10); return ( ); }; const Parent = () => { Scheduler.unstable_advanceTime(2); return ; }; const Child = () => { Scheduler.unstable_advanceTime(1); return null; }; utils.act(() => store.profilerStore.startProfiling()); utils.act(() => ReactDOM.render(, document.createElement('div')) ); utils.act(() => store.profilerStore.stopProfiling()); let commitData = null; function Validator({ commitIndex, rootID }) { commitData = store.profilerStore.getCommitData(rootID, commitIndex); expect(commitData).toMatchSnapshot( `CommitDetails with filtered self durations` ); return null; } const rootID = store.roots[0]; utils.act(() => { TestRenderer.create(); }); expect(commitData).not.toBeNull(); }); it('should calculate self duration correctly for suspended views', async done => { let data; const getData = () => { if (data) { return data; } else { throw new Promise(resolve => { data = 'abc'; resolve(data); }); } }; const Parent = () => { Scheduler.unstable_advanceTime(10); return ( }> ); }; const Fallback = () => { Scheduler.unstable_advanceTime(2); return 'Fallback...'; }; const Async = () => { Scheduler.unstable_advanceTime(3); const data = getData(); return data; }; utils.act(() => store.profilerStore.startProfiling()); await utils.actAsync(() => ReactDOM.render(, document.createElement('div')) ); utils.act(() => store.profilerStore.stopProfiling()); const allCommitData = []; function Validator({ commitIndex, rootID }) { const commitData = store.profilerStore.getCommitData(rootID, commitIndex); allCommitData.push(commitData); expect(commitData).toMatchSnapshot( `CommitDetails with filtered self durations` ); return null; } const rootID = store.roots[0]; for (let commitIndex = 0; commitIndex < 2; commitIndex++) { utils.act(() => { TestRenderer.create( ); }); } expect(allCommitData).toHaveLength(2); done(); }); it('should collect data for each rendered fiber', () => { const Parent = ({ count }) => { Scheduler.unstable_advanceTime(10); const children = new Array(count) .fill(true) .map((_, index) => ); return ( {children} ); }; const Child = ({ duration }) => { Scheduler.unstable_advanceTime(duration); return null; }; const MemoizedChild = React.memo(Child); const container = document.createElement('div'); utils.act(() => store.profilerStore.startProfiling()); utils.act(() => ReactDOM.render(, container)); utils.act(() => ReactDOM.render(, container)); utils.act(() => ReactDOM.render(, container)); utils.act(() => store.profilerStore.stopProfiling()); const allFiberCommits = []; function Validator({ fiberID, previousFiberCommits, rootID }) { const fiberCommits = store.profilerStore.profilingCache.getFiberCommits({ fiberID, rootID, }); if (previousFiberCommits != null) { expect(fiberCommits).toEqual(previousFiberCommits); } else { allFiberCommits.push(fiberCommits); expect(fiberCommits).toMatchSnapshot( `FiberCommits: element ${fiberID}` ); } return null; } const rootID = store.roots[0]; for (let index = 0; index < store.numElements; index++) { utils.act(() => { const fiberID = store.getElementIDAtIndex(index); if (fiberID == null) { throw Error(`Unexpected null ID for element at index ${index}`); } TestRenderer.create( ); }); } expect(allFiberCommits).toHaveLength(store.numElements); utils.exportImportHelper(bridge, store); for (let index = 0; index < store.numElements; index++) { utils.act(() => { const fiberID = store.getElementIDAtIndex(index); if (fiberID == null) { throw Error(`Unexpected null ID for element at index ${index}`); } TestRenderer.create( ); }); } }); it('should report every traced interaction', () => { const Parent = ({ count }) => { Scheduler.unstable_advanceTime(10); const children = new Array(count) .fill(true) .map((_, index) => ); return ( {children} ); }; const Child = ({ duration }) => { Scheduler.unstable_advanceTime(duration); return null; }; const MemoizedChild = React.memo(Child); const container = document.createElement('div'); utils.act(() => store.profilerStore.startProfiling()); utils.act(() => SchedulerTracing.unstable_trace( 'mount: one child', Scheduler.unstable_now(), () => ReactDOM.render(, container) ) ); utils.act(() => SchedulerTracing.unstable_trace( 'update: two children', Scheduler.unstable_now(), () => ReactDOM.render(, container) ) ); utils.act(() => store.profilerStore.stopProfiling()); let interactions = null; function Validator({ previousInteractions, rootID }) { interactions = store.profilerStore.profilingCache.getInteractionsChartData( { rootID, } ).interactions; if (previousInteractions != null) { expect(interactions).toEqual(previousInteractions); } else { expect(interactions).toMatchSnapshot('Interactions'); } return null; } const rootID = store.roots[0]; utils.act(() => TestRenderer.create( ) ); expect(interactions).not.toBeNull(); utils.exportImportHelper(bridge, store); utils.act(() => TestRenderer.create( ) ); }); });