diff --git a/package.json b/package.json index 479e760241..0c42947e5c 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "/src/__tests__/setupTests" ], "snapshotSerializers": [ + "/src/__tests__/profilingSummarySerializer", "/src/__tests__/storeSerializer" ], "testMatch": [ @@ -134,6 +135,7 @@ "react-color": "^2.11.7", "react-dom": "0.0.0-fb28e9048", "react-is": "0.0.0-fb28e9048", + "react-test-renderer": "0.0.0-fb28e9048", "react-virtualized-auto-sizer": "^1.0.2", "react-window": "^1.8.0", "request-promise": "^4.2.4", diff --git a/src/__tests__/__snapshots__/profiler-test.js.snap b/src/__tests__/__snapshots__/profiler-test.js.snap index dade7370b5..3c68497cad 100644 --- a/src/__tests__/__snapshots__/profiler-test.js.snap +++ b/src/__tests__/__snapshots__/profiler-test.js.snap @@ -1,31 +1,110 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Profiler should start and stop profiling, handle root unmounting: 1: mount 1`] = ` +exports[`Profiler should clean up after a root has been unmounted: 1: mount 1`] = ` [root] - ▸ + ▾ + + + [root] - ▸ + ▾ + + `; -exports[`Profiler should start and stop profiling, handle root unmounting: 2: profiling started 1`] = ` +exports[`Profiler should clean up after a root has been unmounted: 2: profiling started 1`] = ` [root] - ▸ + ▾ + + + [root] - ▸ + ▾ + + `; -exports[`Profiler should start and stop profiling, handle root unmounting: 3: update 1`] = ` +exports[`Profiler should clean up after a root has been unmounted: 3: update 1`] = ` [root] - ▸ + ▾ + + + + [root] - ▸ + ▾ + `; -exports[`Profiler should start and stop profiling, handle root unmounting: 4: unmount B 1`] = ` +exports[`Profiler should clean up after a root has been unmounted: 4: unmount B 1`] = ` [root] - ▸ + ▾ + + + + `; -exports[`Profiler should start and stop profiling, handle root unmounting: 5: unmount A 1`] = ``; +exports[`Profiler should clean up after a root has been unmounted: 5: unmount A 1`] = ``; -exports[`Profiler should start and stop profiling, handle root unmounting: 6: profiling stopped 1`] = ``; +exports[`Profiler should clean up after a root has been unmounted: 6: profiling stopped 1`] = ``; + +exports[`Profiler should collect basic profiling metrics: 1: mount 1`] = ` +[root] + ▾ + + +`; + +exports[`Profiler should collect basic profiling metrics: 2: add child 1`] = ` +[root] + ▾ + + + +`; + +exports[`Profiler should collect basic profiling metrics: 3: remove children 1`] = ` +[root] + ▾ + +`; + +exports[`Profiler should collect basic profiling metrics: 4: profiling stopped 1`] = ` +[root] + ▾ + +`; + +exports[`Profiler should collect basic profiling metrics: ProfilingSummary 1`] = ` +{ + "rootID": 1, + "commitDurations": [ + 0, + 1 + ], + "commitTimes": [ + 0, + 1 + ], + "initialTreeBaseDurations": [ + [ + 1, + 0 + ], + [ + 2, + 1 + ], + [ + 3, + 2 + ], + [ + 4, + 3 + ] + ], + "interactionCount": 0 +} +`; diff --git a/src/__tests__/profiler-test.js b/src/__tests__/profiler-test.js index 28be905d05..98c1172ae3 100644 --- a/src/__tests__/profiler-test.js +++ b/src/__tests__/profiler-test.js @@ -3,7 +3,9 @@ describe('Profiler', () => { let React; let ReactDOM; + let TestRenderer; let TestUtils; + let agent; let store; const act = (callback: Function) => { @@ -13,15 +15,88 @@ describe('Profiler', () => { jest.runAllTimers(); // Flush Bridge operations }; + const renderAndResolve = async (root, element) => { + // $FlowFixMe Flow doens't know about "await act()" yet + await TestUtils.act(async () => { + root.update(element); + + // Resolve pending suspense promises + jest.runAllTimers(); + }); + + // Re-render after resolved promises + jest.runAllTimers(); + }; + beforeEach(() => { + agent = global.agent; store = global.store; + store.collapseNodesByDefault = false; React = require('react'); ReactDOM = require('react-dom'); TestUtils = require('react-dom/test-utils'); + + // Hide the hook before requiring TestRenderer, so we don't end up with a loop. + const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__; + delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__; + TestRenderer = require('react-test-renderer'); + global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook; }); - it('should start and stop profiling, handle root unmounting', async () => { + it('should collect basic profiling metrics', async done => { + const Parent = ({ count }) => + new Array(count).fill(true).map((_, index) => ); + const Child = () => { + jest.advanceTimersByTime(1); + return null; + }; + + const container = document.createElement('div'); + + act(() => ReactDOM.render(, container)); + expect(store).toMatchSnapshot('1: mount'); + + act(() => store.startProfiling()); + + act(() => ReactDOM.render(, container)); + expect(store).toMatchSnapshot('2: add child'); + + act(() => ReactDOM.render(, container)); + expect(store).toMatchSnapshot('3: remove children'); + + act(() => store.stopProfiling()); + expect(store).toMatchSnapshot('4: profiling stopped'); + + let profilingSummary; + function Suspender({ rendererID, rootID }) { + profilingSummary = store.profilingCache.ProfilingSummary.read({ + rendererID, + rootID, + }); + return null; + } + + // HACK There's only one renderer for this test + const rendererID = Object.keys(agent._rendererInterfaces)[0]; + const rootID = store.roots[0]; + + let root = TestRenderer.create(); + await renderAndResolve( + root, + + + + ); + + // HACK root.toTree() doesn't handle Suspense yet + // but Jest serializer wouldn't work with a JSON string + expect(profilingSummary).toMatchSnapshot('ProfilingSummary'); + + done(); + }); + + it('should clean up after a root has been unmounted', async () => { const Parent = ({ count }) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; @@ -35,9 +110,7 @@ describe('Profiler', () => { }); expect(store).toMatchSnapshot('1: mount'); - act(() => { - store.startProfiling(); - }); + act(() => store.startProfiling()); expect(store).toMatchSnapshot('2: profiling started'); act(() => { @@ -52,9 +125,7 @@ describe('Profiler', () => { act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store).toMatchSnapshot('5: unmount A'); - act(() => { - store.stopProfiling(); - }); + act(() => store.stopProfiling()); expect(store).toMatchSnapshot('6: profiling stopped'); }); }); diff --git a/src/__tests__/profilingSummarySerializer.js b/src/__tests__/profilingSummarySerializer.js new file mode 100644 index 0000000000..45e8477dfd --- /dev/null +++ b/src/__tests__/profilingSummarySerializer.js @@ -0,0 +1,29 @@ +// test() is part of Jest's serializer API +export function test(maybeProfilingSummary) { + return ( + typeof maybeProfilingSummary === 'object' && + maybeProfilingSummary !== null && + typeof maybeProfilingSummary.rootID === 'number' && + Array.isArray(maybeProfilingSummary.commitDurations) && + Array.isArray(maybeProfilingSummary.commitTimes) && + typeof maybeProfilingSummary.initialTreeBaseDurations === 'object' && + maybeProfilingSummary.initialTreeBaseDurations !== null && + typeof maybeProfilingSummary.interactionCount === 'number' + ); +} + +// print() is part of Jest's serializer API +export function print(profilingSummary, serialize, indent) { + return JSON.stringify( + { + ...profilingSummary, + commitDurations: profilingSummary.commitDurations.map((_, i) => i), + commitTimes: profilingSummary.commitTimes.map((_, i) => i), + initialTreeBaseDurations: [ + ...profilingSummary.initialTreeBaseDurations, + ].map(([id, _], index) => [id, index]), + }, + null, + 2 + ); +} diff --git a/src/__tests__/setupTests.js b/src/__tests__/setupTests.js index ce96ed2b48..b498a014fd 100644 --- a/src/__tests__/setupTests.js +++ b/src/__tests__/setupTests.js @@ -8,10 +8,6 @@ import { installHook } from 'src/hook'; const env = jasmine.getEnv(); env.beforeEach(() => { - // It's important to reset modules between test runs; - // Without this, ReactDOM won't re-inject itself into the new hook. - jest.resetModules(); - // Fake timers let us flush Bridge operations between setup and assertions. jest.useFakeTimers(); @@ -55,4 +51,10 @@ env.beforeEach(() => { }); env.afterEach(() => { delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__; + + // It's important to reset modules between test runs; + // Without this, ReactDOM won't re-inject itself into the new hook. + // It's also important to reset after tests, rather than before, + // so that we don't disconnect the ReactCurrentDispatcher ref. + jest.resetModules(); }); diff --git a/yarn.lock b/yarn.lock index 6ed41c6740..32a0156b37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9837,6 +9837,16 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-test-renderer@0.0.0-fb28e9048: + version "0.0.0-fb28e9048" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-0.0.0-fb28e9048.tgz#1a94c8d19cbb1ac98ab37c66c0b294e5be280c52" + integrity sha512-WK/wQOh0v6+8Gbkurgb3he9hKoOKWueqQY+RFs2vM3u3vn7PMyYhzm/KkU75VvTG/GVciojNQBHBpekuvU5dYw== + dependencies: + object-assign "^4.1.1" + prop-types "^15.6.2" + react-is "0.0.0-fb28e9048" + scheduler "0.0.0-fb28e9048" + react-virtualized-auto-sizer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"