diff --git a/flow-typed/npm/react-test-renderer_v16.x.x.js b/flow-typed/npm/react-test-renderer_v16.x.x.js new file mode 100644 index 0000000000..87a149a1d3 --- /dev/null +++ b/flow-typed/npm/react-test-renderer_v16.x.x.js @@ -0,0 +1,81 @@ +// flow-typed signature: b6bb53397d83d2d821e258cc73818d1b +// flow-typed version: 9c71eca8ef/react-test-renderer_v16.x.x/flow_>=v0.47.x + +// Type definitions for react-test-renderer 16.x.x +// Ported from: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-test-renderer + +type ReactComponentInstance = React$Component; + +type ReactTestRendererJSON = { + type: string, + props: { [propName: string]: any }, + children: null | ReactTestRendererJSON[], +}; + +type ReactTestRendererTree = ReactTestRendererJSON & { + nodeType: 'component' | 'host', + instance: ?ReactComponentInstance, + rendered: null | ReactTestRendererTree, +}; + +type ReactTestInstance = { + instance: ?ReactComponentInstance, + type: string, + props: { [propName: string]: any }, + parent: null | ReactTestInstance, + children: Array, + + find(predicate: (node: ReactTestInstance) => boolean): ReactTestInstance, + findByType(type: React$ElementType): ReactTestInstance, + findByProps(props: { [propName: string]: any }): ReactTestInstance, + + findAll( + predicate: (node: ReactTestInstance) => boolean, + options?: { deep: boolean } + ): ReactTestInstance[], + findAllByType( + type: React$ElementType, + options?: { deep: boolean } + ): ReactTestInstance[], + findAllByProps( + props: { [propName: string]: any }, + options?: { deep: boolean } + ): ReactTestInstance[], +}; + +type TestRendererOptions = { + createNodeMock(element: React$Element): any, +}; + +declare module 'react-test-renderer' { + declare export type ReactTestRenderer = { + toJSON(): null | ReactTestRendererJSON, + toTree(): null | ReactTestRendererTree, + unmount(nextElement?: React$Element): void, + update(nextElement: React$Element): void, + getInstance(): ?ReactComponentInstance, + root: ReactTestInstance, + }; + + declare type Thenable = { + then(resolve: () => mixed, reject?: () => mixed): mixed, + }; + + declare function create( + nextElement: React$Element, + options?: TestRendererOptions + ): ReactTestRenderer; + + declare function act(callback: () => void): Thenable; +} + +declare module 'react-test-renderer/shallow' { + declare export default class ShallowRenderer { + static createRenderer(): ShallowRenderer; + getMountedInstance(): ReactTestInstance; + getRenderOutput>(): E; + getRenderOutput(): React$Element; + render(element: React$Element, context?: any): void; + unmount(): void; + } +} diff --git a/package.json b/package.json index 479e760241..a86f97bd33 100644 --- a/package.json +++ b/package.json @@ -130,14 +130,15 @@ "opener": "^1.5.1", "prettier": "^1.16.4", "prop-types": "^15.6.2", - "react": "0.0.0-fb28e9048", + "react": "^0.0.0-6da04b5d8", "react-color": "^2.11.7", - "react-dom": "0.0.0-fb28e9048", - "react-is": "0.0.0-fb28e9048", + "react-dom": "^0.0.0-6da04b5d8", + "react-is": "^0.0.0-6da04b5d8", + "react-test-renderer": "^0.0.0-6da04b5d8", "react-virtualized-auto-sizer": "^1.0.2", "react-window": "^1.8.0", "request-promise": "^4.2.4", - "scheduler": "0.0.0-fb28e9048", + "scheduler": "^0.0.0-6da04b5d8", "semver": "^5.5.1", "style-loader": "^0.23.1", "web-ext": "^3.0.0", diff --git a/src/__tests__/__snapshots__/profiler-test.js.snap b/src/__tests__/__snapshots__/profiler-test.js.snap deleted file mode 100644 index dade7370b5..0000000000 --- a/src/__tests__/__snapshots__/profiler-test.js.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Profiler should start and stop profiling, handle root unmounting: 1: mount 1`] = ` -[root] - ▸ -[root] - ▸ -`; - -exports[`Profiler should start and stop profiling, handle root unmounting: 2: profiling started 1`] = ` -[root] - ▸ -[root] - ▸ -`; - -exports[`Profiler should start and stop profiling, handle root unmounting: 3: update 1`] = ` -[root] - ▸ -[root] - ▸ -`; - -exports[`Profiler should start and stop profiling, handle root unmounting: 4: unmount B 1`] = ` -[root] - ▸ -`; - -exports[`Profiler should start and stop profiling, handle root unmounting: 5: unmount A 1`] = ``; - -exports[`Profiler should start and stop profiling, handle root unmounting: 6: profiling stopped 1`] = ``; diff --git a/src/__tests__/__snapshots__/profiling-test.js.snap b/src/__tests__/__snapshots__/profiling-test.js.snap new file mode 100644 index 0000000000..c0dfd222e0 --- /dev/null +++ b/src/__tests__/__snapshots__/profiling-test.js.snap @@ -0,0 +1,155 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`profiling CommitDetails should be collected for each commit: CommitDetails commitIndex: 0 1`] = ` +Object { + "actualDurations": Map { + 1 => 14, + 2 => 14, + 3 => 2, + 4 => 2, + }, + "commitIndex": 0, + "interactions": Array [], + "rootID": 1, +} +`; + +exports[`profiling CommitDetails should be collected for each commit: CommitDetails commitIndex: 1 1`] = ` +Object { + "actualDurations": Map { + 3 => 2, + 4 => 2, + 5 => 2, + 2 => 16, + 1 => 16, + }, + "commitIndex": 1, + "interactions": Array [], + "rootID": 1, +} +`; + +exports[`profiling CommitDetails should be collected for each commit: CommitDetails commitIndex: 2 1`] = ` +Object { + "actualDurations": Map { + 3 => 2, + 2 => 12, + 1 => 12, + }, + "commitIndex": 2, + "interactions": Array [], + "rootID": 1, +} +`; + +exports[`profiling CommitDetails should be collected for each commit: CommitDetails commitIndex: 3 1`] = ` +Object { + "actualDurations": Map { + 2 => 10, + 1 => 10, + }, + "commitIndex": 3, + "interactions": Array [], + "rootID": 1, +} +`; + +exports[`profiling FiberCommits should be collected for each rendered fiber: FiberCommits: element 2 1`] = ` +Object { + "commitDurations": Array [ + 0, + 12, + 1, + 14, + 2, + 16, + ], + "fiberID": 2, + "rootID": 1, +} +`; + +exports[`profiling FiberCommits should be collected for each rendered fiber: FiberCommits: element 3 1`] = ` +Object { + "commitDurations": Array [ + 0, + 2, + 1, + 2, + 2, + 2, + ], + "fiberID": 3, + "rootID": 1, +} +`; + +exports[`profiling FiberCommits should be collected for each rendered fiber: FiberCommits: element 4 1`] = ` +Object { + "commitDurations": Array [ + 1, + 2, + 2, + 2, + ], + "fiberID": 4, + "rootID": 1, +} +`; + +exports[`profiling FiberCommits should be collected for each rendered fiber: FiberCommits: element 5 1`] = ` +Object { + "commitDurations": Array [ + 2, + 2, + ], + "fiberID": 5, + "rootID": 1, +} +`; + +exports[`profiling Interactions should be collected for every traced interaction: Interactions 1`] = ` +Array [ + Object { + "__count": 1, + "commits": Array [ + 0, + ], + "id": 0, + "name": "mount: one child", + "timestamp": 0, + }, + Object { + "__count": 0, + "commits": Array [ + 1, + ], + "id": 1, + "name": "update: two children", + "timestamp": 12, + }, +] +`; + +exports[`profiling ProfilingSummary should be collected for each commit: ProfilingSummary 1`] = ` +Object { + "commitDurations": Array [ + 16, + 12, + 10, + ], + "commitTimes": Array [ + 16, + 28, + 38, + ], + "initialTreeBaseDurations": Map { + 1 => 14, + 2 => 14, + 3 => 2, + 4 => 2, + }, + "interactionCount": 0, + "rootID": 1, +} +`; diff --git a/src/__tests__/__snapshots__/profilingCharts-test.js.snap b/src/__tests__/__snapshots__/profilingCharts-test.js.snap new file mode 100644 index 0000000000..36e4c8cf90 --- /dev/null +++ b/src/__tests__/__snapshots__/profilingCharts-test.js.snap @@ -0,0 +1,333 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`profiling charts flamegraph chart should contain valid data: 0: CommitTree 1`] = ` +Object { + "nodes": Map { + 1 => Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "parentID": 0, + "treeBaseDuration": 14, + }, + 2 => Object { + "children": Array [ + 3, + 4, + ], + "displayName": "Parent", + "id": 2, + "key": null, + "parentID": 1, + "treeBaseDuration": 14, + }, + 3 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 3, + "key": "first", + "parentID": 2, + "treeBaseDuration": 2, + }, + 4 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 4, + "key": "second", + "parentID": 2, + "treeBaseDuration": 2, + }, + }, + "rootID": 1, +} +`; + +exports[`profiling charts flamegraph chart should contain valid data: 0: FlamegraphChartData 1`] = ` +Object { + "baseDuration": 14, + "depth": 2, + "idToDepthMap": Map { + 2 => 1, + 4 => 2, + 3 => 2, + }, + "maxSelfDuration": 10, + "rows": Array [ + Array [ + Object { + "actualDuration": 14, + "didRender": true, + "id": 2, + "label": "Parent (10.0ms of 14.0ms)", + "name": "Parent", + "offset": 0, + "selfDuration": 10, + "treeBaseDuration": 14, + }, + ], + Array [ + Object { + "actualDuration": 2, + "didRender": true, + "id": 4, + "label": "Memo(Child) key=\\"second\\" (2.0ms of 2.0ms)", + "name": "Memo(Child)", + "offset": 12, + "selfDuration": 2, + "treeBaseDuration": 2, + }, + Object { + "actualDuration": 2, + "didRender": true, + "id": 3, + "label": "Memo(Child) key=\\"first\\" (2.0ms of 2.0ms)", + "name": "Memo(Child)", + "offset": 10, + "selfDuration": 2, + "treeBaseDuration": 2, + }, + ], + ], +} +`; + +exports[`profiling charts flamegraph chart should contain valid data: 1: CommitTree 1`] = ` +Object { + "nodes": Map { + 1 => Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "parentID": 0, + "treeBaseDuration": 14, + }, + 2 => Object { + "children": Array [ + 3, + 4, + ], + "displayName": "Parent", + "id": 2, + "key": null, + "parentID": 1, + "treeBaseDuration": 14, + }, + 3 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 3, + "key": "first", + "parentID": 2, + "treeBaseDuration": 2, + }, + 4 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 4, + "key": "second", + "parentID": 2, + "treeBaseDuration": 2, + }, + }, + "rootID": 1, +} +`; + +exports[`profiling charts flamegraph chart should contain valid data: 1: FlamegraphChartData 1`] = ` +Object { + "baseDuration": 14, + "depth": 2, + "idToDepthMap": Map { + 2 => 1, + 4 => 2, + 3 => 2, + }, + "maxSelfDuration": 10, + "rows": Array [ + Array [ + Object { + "actualDuration": 10, + "didRender": true, + "id": 2, + "label": "Parent (10.0ms of 10.0ms)", + "name": "Parent", + "offset": 0, + "selfDuration": 10, + "treeBaseDuration": 14, + }, + ], + Array [ + Object { + "actualDuration": 0, + "didRender": false, + "id": 4, + "label": "Memo(Child) key=\\"second\\"", + "name": "Memo(Child)", + "offset": 12, + "selfDuration": 0, + "treeBaseDuration": 2, + }, + Object { + "actualDuration": 0, + "didRender": false, + "id": 3, + "label": "Memo(Child) key=\\"first\\"", + "name": "Memo(Child)", + "offset": 10, + "selfDuration": 0, + "treeBaseDuration": 2, + }, + ], + ], +} +`; + +exports[`profiling charts interactions should contain valid data: Interactions 1`] = ` +Object { + "lastInteractionTime": 24, + "maxCommitDuration": 14, +} +`; + +exports[`profiling charts interactions should contain valid data: Interactions 2`] = ` +Object { + "lastInteractionTime": 24, + "maxCommitDuration": 14, +} +`; + +exports[`profiling charts ranked chart should contain valid data: 0: CommitTree 1`] = ` +Object { + "nodes": Map { + 1 => Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "parentID": 0, + "treeBaseDuration": 14, + }, + 2 => Object { + "children": Array [ + 3, + 4, + ], + "displayName": "Parent", + "id": 2, + "key": null, + "parentID": 1, + "treeBaseDuration": 14, + }, + 3 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 3, + "key": "first", + "parentID": 2, + "treeBaseDuration": 2, + }, + 4 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 4, + "key": "second", + "parentID": 2, + "treeBaseDuration": 2, + }, + }, + "rootID": 1, +} +`; + +exports[`profiling charts ranked chart should contain valid data: 0: RankedChartData 1`] = ` +Object { + "maxValue": 10, + "nodes": Array [ + Object { + "id": 2, + "label": "Parent (10.0ms)", + "name": "Parent", + "value": 10, + }, + Object { + "id": 3, + "label": "Memo(Child) key=\\"first\\" (2.0ms)", + "name": "Memo(Child)", + "value": 2, + }, + Object { + "id": 4, + "label": "Memo(Child) key=\\"second\\" (2.0ms)", + "name": "Memo(Child)", + "value": 2, + }, + ], +} +`; + +exports[`profiling charts ranked chart should contain valid data: 1: CommitTree 1`] = ` +Object { + "nodes": Map { + 1 => Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "parentID": 0, + "treeBaseDuration": 14, + }, + 2 => Object { + "children": Array [ + 3, + 4, + ], + "displayName": "Parent", + "id": 2, + "key": null, + "parentID": 1, + "treeBaseDuration": 14, + }, + 3 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 3, + "key": "first", + "parentID": 2, + "treeBaseDuration": 2, + }, + 4 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 4, + "key": "second", + "parentID": 2, + "treeBaseDuration": 2, + }, + }, + "rootID": 1, +} +`; + +exports[`profiling charts ranked chart should contain valid data: 1: RankedChartData 1`] = ` +Object { + "maxValue": 10, + "nodes": Array [ + Object { + "id": 2, + "label": "Parent (10.0ms)", + "name": "Parent", + "value": 10, + }, + ], +} +`; diff --git a/src/__tests__/__snapshots__/profilingCommitTreeBuilder-test.js.snap b/src/__tests__/__snapshots__/profilingCommitTreeBuilder-test.js.snap new file mode 100644 index 0000000000..b483cd5efc --- /dev/null +++ b/src/__tests__/__snapshots__/profilingCommitTreeBuilder-test.js.snap @@ -0,0 +1,162 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`commit tree should be able to rebuild the store tree for each commit: 0: CommitTree 1`] = ` +Object { + "nodes": Map { + 1 => Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "parentID": 0, + "treeBaseDuration": 12, + }, + 2 => Object { + "children": Array [ + 3, + ], + "displayName": "Parent", + "id": 2, + "key": null, + "parentID": 1, + "treeBaseDuration": 12, + }, + 3 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 3, + "key": "0", + "parentID": 2, + "treeBaseDuration": 2, + }, + }, + "rootID": 1, +} +`; + +exports[`commit tree should be able to rebuild the store tree for each commit: 1: CommitTree 1`] = ` +Object { + "nodes": Map { + 1 => Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "parentID": 0, + "treeBaseDuration": 16, + }, + 2 => Object { + "children": Array [ + 3, + 4, + 5, + ], + "displayName": "Parent", + "id": 2, + "key": null, + "parentID": 1, + "treeBaseDuration": 16, + }, + 3 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 3, + "key": "0", + "parentID": 2, + "treeBaseDuration": 2, + }, + 4 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 4, + "key": "1", + "parentID": 2, + "treeBaseDuration": 2, + }, + 5 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 5, + "key": "2", + "parentID": 2, + "treeBaseDuration": 2, + }, + }, + "rootID": 1, +} +`; + +exports[`commit tree should be able to rebuild the store tree for each commit: 2: CommitTree 1`] = ` +Object { + "nodes": Map { + 1 => Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "parentID": 0, + "treeBaseDuration": 14, + }, + 2 => Object { + "children": Array [ + 3, + 4, + ], + "displayName": "Parent", + "id": 2, + "key": null, + "parentID": 1, + "treeBaseDuration": 14, + }, + 3 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 3, + "key": "0", + "parentID": 2, + "treeBaseDuration": 2, + }, + 4 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 4, + "key": "1", + "parentID": 2, + "treeBaseDuration": 2, + }, + }, + "rootID": 1, +} +`; + +exports[`commit tree should be able to rebuild the store tree for each commit: 3: CommitTree 1`] = ` +Object { + "nodes": Map { + 1 => Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "parentID": 0, + "treeBaseDuration": 10, + }, + 2 => Object { + "children": Array [], + "displayName": "Parent", + "id": 2, + "key": null, + "parentID": 1, + "treeBaseDuration": 10, + }, + }, + "rootID": 1, +} +`; diff --git a/src/__tests__/profiler-test.js b/src/__tests__/profiler-test.js deleted file mode 100644 index 28be905d05..0000000000 --- a/src/__tests__/profiler-test.js +++ /dev/null @@ -1,60 +0,0 @@ -// @flow - -describe('Profiler', () => { - let React; - let ReactDOM; - let TestUtils; - let store; - - const act = (callback: Function) => { - TestUtils.act(() => { - callback(); - }); - jest.runAllTimers(); // Flush Bridge operations - }; - - beforeEach(() => { - store = global.store; - - React = require('react'); - ReactDOM = require('react-dom'); - TestUtils = require('react-dom/test-utils'); - }); - - it('should start and stop profiling, handle root unmounting', async () => { - const Parent = ({ count }) => - new Array(count).fill(true).map((_, index) => ); - const Child = () =>
Hi!
; - - const containerA = document.createElement('div'); - const containerB = document.createElement('div'); - - act(() => { - ReactDOM.render(, containerA); - ReactDOM.render(, containerB); - }); - expect(store).toMatchSnapshot('1: mount'); - - act(() => { - store.startProfiling(); - }); - expect(store).toMatchSnapshot('2: profiling started'); - - act(() => { - ReactDOM.render(, containerA); - ReactDOM.render(, containerB); - }); - expect(store).toMatchSnapshot('3: update'); - - act(() => ReactDOM.unmountComponentAtNode(containerB)); - expect(store).toMatchSnapshot('4: unmount B'); - - act(() => ReactDOM.unmountComponentAtNode(containerA)); - expect(store).toMatchSnapshot('5: unmount A'); - - act(() => { - store.stopProfiling(); - }); - expect(store).toMatchSnapshot('6: profiling stopped'); - }); -}); diff --git a/src/__tests__/profiling-test.js b/src/__tests__/profiling-test.js new file mode 100644 index 0000000000..39dd595e49 --- /dev/null +++ b/src/__tests__/profiling-test.js @@ -0,0 +1,293 @@ +// @flow + +import typeof ReactTestRenderer from 'react-test-renderer'; +import type Store from 'src/devtools/store'; + +describe('profiling', () => { + let React; + let ReactDOM; + let Scheduler; + let SchedulerTracing; + let TestRenderer: ReactTestRenderer; + let store: Store; + let utils; + + beforeEach(() => { + utils = require('./utils'); + utils.beforeEachProfiling(); + + store = global.store; + store.collapseNodesByDefault = false; + + React = require('react'); + ReactDOM = require('react-dom'); + Scheduler = require('scheduler'); + SchedulerTracing = require('scheduler/tracing'); + TestRenderer = utils.requireTestRenderer(); + }); + + describe('ProfilingSummary', () => { + it('should be collected for each commit', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + return new Array(count) + .fill(true) + .map((_, index) => ); + }; + const Child = () => { + Scheduler.advanceTime(2); + return null; + }; + + const container = document.createElement('div'); + + utils.act(() => ReactDOM.render(, container)); + utils.act(() => store.startProfiling()); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => store.stopProfiling()); + + let suspenseResolved = false; + + function Suspender({ rendererID, rootID }) { + const profilingSummary = store.profilingCache.ProfilingSummary.read({ + rendererID, + rootID, + }); + suspenseResolved = true; + expect(profilingSummary).toMatchSnapshot('ProfilingSummary'); + return null; + } + + const rendererID = utils.getRendererID(); + const rootID = store.roots[0]; + + await utils.actSuspense(() => + TestRenderer.create( + + + + ) + ); + + expect(suspenseResolved).toBe(true); + + done(); + }); + }); + + describe('CommitDetails', () => { + it('should be collected for each commit', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + return new Array(count) + .fill(true) + .map((_, index) => ); + }; + const Child = () => { + Scheduler.advanceTime(2); + return null; + }; + + const container = document.createElement('div'); + + utils.act(() => store.startProfiling()); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => store.stopProfiling()); + + let suspenseResolved = false; + + function Suspender({ commitIndex, rendererID, rootID }) { + const commitDetails = store.profilingCache.CommitDetails.read({ + commitIndex, + rendererID, + rootID, + }); + suspenseResolved = true; + expect(commitDetails).toMatchSnapshot( + `CommitDetails commitIndex: ${commitIndex}` + ); + return null; + } + + const rendererID = utils.getRendererID(); + const rootID = store.roots[0]; + + for (let commitIndex = 0; commitIndex <= 3; commitIndex++) { + suspenseResolved = false; + await utils.actSuspense(() => { + TestRenderer.create( + + + + ); + }); + expect(suspenseResolved).toBe(true); + } + + done(); + }); + }); + + describe('FiberCommits', () => { + it('should be collected for each rendered fiber', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + return new Array(count) + .fill(true) + .map((_, index) => ); + }; + const Child = () => { + Scheduler.advanceTime(2); + return null; + }; + + const container = document.createElement('div'); + + utils.act(() => store.startProfiling()); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => store.stopProfiling()); + + let suspenseResolved = false; + + function Suspender({ fiberID, rendererID, rootID }) { + const fiberCommits = store.profilingCache.FiberCommits.read({ + fiberID, + rendererID, + rootID, + }); + suspenseResolved = true; + expect(fiberCommits).toMatchSnapshot( + `FiberCommits: element ${fiberID}` + ); + return null; + } + + const rendererID = utils.getRendererID(); + const rootID = store.roots[0]; + + for (let index = 0; index < store.numElements; index++) { + suspenseResolved = false; + await utils.actSuspense(() => { + const fiberID = store.getElementIDAtIndex(index); + if (fiberID == null) { + throw Error(`Unexpected null ID for element at index ${index}`); + } + TestRenderer.create( + + + + ); + }); + expect(suspenseResolved).toBe(true); + } + + done(); + }); + }); + + describe('Interactions', () => { + it('should be collected for every traced interaction', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + return new Array(count) + .fill(true) + .map((_, index) => ); + }; + const Child = () => { + Scheduler.advanceTime(2); + return null; + }; + + const container = document.createElement('div'); + + utils.act(() => store.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.stopProfiling()); + + let suspenseResolved = false; + + function Suspender({ rendererID, rootID }) { + const interactions = store.profilingCache.Interactions.read({ + rendererID, + rootID, + }); + suspenseResolved = true; + expect(interactions).toMatchSnapshot('Interactions'); + return null; + } + + const rendererID = utils.getRendererID(); + const rootID = store.roots[0]; + + await utils.actSuspense(() => + TestRenderer.create( + + + + ) + ); + + expect(suspenseResolved).toBe(true); + + done(); + }); + }); + + it('should remove profiling data when roots are unmounted', async () => { + const Parent = ({ count }) => + new Array(count).fill(true).map((_, index) => ); + const Child = () =>
Hi!
; + + const containerA = document.createElement('div'); + const containerB = document.createElement('div'); + + utils.act(() => { + ReactDOM.render(, containerA); + ReactDOM.render(, containerB); + }); + + utils.act(() => store.startProfiling()); + + utils.act(() => { + ReactDOM.render(, containerA); + ReactDOM.render(, containerB); + }); + + utils.act(() => ReactDOM.unmountComponentAtNode(containerB)); + + utils.act(() => ReactDOM.unmountComponentAtNode(containerA)); + + utils.act(() => store.stopProfiling()); + + // Assert all maps are empty + store.assertExpectedRootMapSizes(); + }); +}); diff --git a/src/__tests__/profilingCharts-test.js b/src/__tests__/profilingCharts-test.js new file mode 100644 index 0000000000..3e2613b5af --- /dev/null +++ b/src/__tests__/profilingCharts-test.js @@ -0,0 +1,292 @@ +// @flow + +import typeof TestRendererType from 'react-test-renderer'; +import type Store from 'src/devtools/store'; + +describe('profiling charts', () => { + let React; + let ReactDOM; + let Scheduler; + let SchedulerTracing; + let TestRenderer: TestRendererType; + let store: Store; + let utils; + + beforeEach(() => { + utils = require('./utils'); + utils.beforeEachProfiling(); + + store = global.store; + store.collapseNodesByDefault = false; + + React = require('react'); + ReactDOM = require('react-dom'); + Scheduler = require('scheduler'); + SchedulerTracing = require('scheduler/tracing'); + TestRenderer = utils.requireTestRenderer(); + }); + + describe('flamegraph chart', () => { + it('should contain valid data', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + return ( + + + + + ); + }; + + // Memoize children to verify that chart doesn't include in the update. + const Child = React.memo(function Child() { + Scheduler.advanceTime(2); + return null; + }); + + const container = document.createElement('div'); + + utils.act(() => store.startProfiling()); + utils.act(() => + SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => + ReactDOM.render(, container) + ) + ); + utils.act(() => + SchedulerTracing.unstable_trace( + 'update', + Scheduler.unstable_now(), + () => ReactDOM.render(, container) + ) + ); + utils.act(() => store.stopProfiling()); + + let suspenseResolved = false; + + function Suspender({ commitIndex, rendererID, rootID }) { + const profilingSummary = store.profilingCache.ProfilingSummary.read({ + rendererID, + rootID, + }); + const commitDetails = store.profilingCache.CommitDetails.read({ + commitIndex, + rendererID, + rootID, + }); + suspenseResolved = true; + const commitTree = store.profilingCache.getCommitTree({ + commitIndex, + profilingSummary, + }); + const chartData = store.profilingCache.getFlamegraphChartData({ + commitDetails, + commitIndex, + commitTree, + }); + expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`); + expect(chartData).toMatchSnapshot( + `${commitIndex}: FlamegraphChartData` + ); + return null; + } + + const rendererID = utils.getRendererID(); + const rootID = store.roots[0]; + + for (let commitIndex = 0; commitIndex < 2; commitIndex++) { + suspenseResolved = false; + + await utils.actSuspense( + () => + TestRenderer.create( + + + + ), + 3 + ); + + expect(suspenseResolved).toBe(true); + } + + expect(suspenseResolved).toBe(true); + + done(); + }); + }); + + describe('ranked chart', () => { + it('should contain valid data', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + return ( + + + + + ); + }; + + // Memoize children to verify that chart doesn't include in the update. + const Child = React.memo(function Child() { + Scheduler.advanceTime(2); + return null; + }); + + const container = document.createElement('div'); + + utils.act(() => store.startProfiling()); + utils.act(() => + SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => + ReactDOM.render(, container) + ) + ); + utils.act(() => + SchedulerTracing.unstable_trace( + 'update', + Scheduler.unstable_now(), + () => ReactDOM.render(, container) + ) + ); + utils.act(() => store.stopProfiling()); + + let suspenseResolved = false; + + function Suspender({ commitIndex, rendererID, rootID }) { + const profilingSummary = store.profilingCache.ProfilingSummary.read({ + rendererID, + rootID, + }); + const commitDetails = store.profilingCache.CommitDetails.read({ + commitIndex, + rendererID, + rootID, + }); + suspenseResolved = true; + const commitTree = store.profilingCache.getCommitTree({ + commitIndex, + profilingSummary, + }); + const chartData = store.profilingCache.getRankedChartData({ + commitDetails, + commitIndex, + commitTree, + }); + expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`); + expect(chartData).toMatchSnapshot(`${commitIndex}: RankedChartData`); + return null; + } + + const rendererID = utils.getRendererID(); + const rootID = store.roots[0]; + + for (let commitIndex = 0; commitIndex < 2; commitIndex++) { + suspenseResolved = false; + + await utils.actSuspense( + () => + TestRenderer.create( + + + + ), + 3 + ); + + expect(suspenseResolved).toBe(true); + } + + done(); + }); + }); + + describe('interactions', () => { + it('should contain valid data', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + return ( + + + + + ); + }; + + // Memoize children to verify that chart doesn't include in the update. + const Child = React.memo(function Child() { + Scheduler.advanceTime(2); + return null; + }); + + const container = document.createElement('div'); + + utils.act(() => store.startProfiling()); + utils.act(() => + SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => + ReactDOM.render(, container) + ) + ); + utils.act(() => + SchedulerTracing.unstable_trace( + 'update', + Scheduler.unstable_now(), + () => ReactDOM.render(, container) + ) + ); + utils.act(() => store.stopProfiling()); + + let suspenseResolved = false; + + function Suspender({ commitIndex, rendererID, rootID }) { + const profilingSummary = store.profilingCache.ProfilingSummary.read({ + rendererID, + rootID, + }); + const interactions = store.profilingCache.Interactions.read({ + rendererID, + rootID, + }); + suspenseResolved = true; + const chartData = store.profilingCache.getInteractionsChartData({ + interactions, + profilingSummary, + }); + expect(chartData).toMatchSnapshot('Interactions'); + return null; + } + + const rendererID = utils.getRendererID(); + const rootID = store.roots[0]; + + for (let commitIndex = 0; commitIndex < 2; commitIndex++) { + suspenseResolved = false; + + await utils.actSuspense( + () => + TestRenderer.create( + + + + ), + 3 + ); + + expect(suspenseResolved).toBe(true); + } + + done(); + }); + }); +}); diff --git a/src/__tests__/profilingCommitTreeBuilder-test.js b/src/__tests__/profilingCommitTreeBuilder-test.js new file mode 100644 index 0000000000..1d037e778f --- /dev/null +++ b/src/__tests__/profilingCommitTreeBuilder-test.js @@ -0,0 +1,89 @@ +// @flow + +import typeof TestRendererType from 'react-test-renderer'; +import type Store from 'src/devtools/store'; + +describe('commit tree', () => { + let React; + let ReactDOM; + let Scheduler; + let TestRenderer: TestRendererType; + let store: Store; + let utils; + + beforeEach(() => { + utils = require('./utils'); + utils.beforeEachProfiling(); + + store = global.store; + store.collapseNodesByDefault = false; + + React = require('react'); + ReactDOM = require('react-dom'); + Scheduler = require('scheduler'); + TestRenderer = utils.requireTestRenderer(); + }); + + it('should be able to rebuild the store tree for each commit', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + return new Array(count) + .fill(true) + .map((_, index) => ); + }; + const Child = React.memo(function Child() { + Scheduler.advanceTime(2); + return null; + }); + + const container = document.createElement('div'); + + utils.act(() => store.startProfiling()); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => store.stopProfiling()); + + let suspenseResolved = false; + + function Suspender({ commitIndex, rendererID, rootID }) { + const profilingSummary = store.profilingCache.ProfilingSummary.read({ + rendererID, + rootID, + }); + suspenseResolved = true; + const commitTree = store.profilingCache.getCommitTree({ + commitIndex, + profilingSummary, + }); + expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`); + return null; + } + + const rendererID = utils.getRendererID(); + const rootID = store.roots[0]; + + for (let commitIndex = 0; commitIndex < 4; commitIndex++) { + suspenseResolved = false; + + await utils.actSuspense( + () => + TestRenderer.create( + + + + ), + 3 + ); + + expect(suspenseResolved).toBe(true); + } + + done(); + }); +}); diff --git a/src/__tests__/setupTests.js b/src/__tests__/setupTests.js index ce96ed2b48..2aec331a95 100644 --- a/src/__tests__/setupTests.js +++ b/src/__tests__/setupTests.js @@ -1,16 +1,16 @@ // @flow -import Agent from 'src/backend/agent'; -import { initBackend } from 'src/backend'; -import Bridge from 'src/bridge'; -import Store from 'src/devtools/store'; -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(); + // These files should be required (and re-reuired) before each test, + // rather than imported at the head of the module. + // That's because we reset modules between tests, + // which disconnects the DevTool's cache from the current dispatcher ref. + const Agent = require('src/backend/agent').default; + const { initBackend } = require('src/backend'); + const Bridge = require('src/bridge').default; + const Store = require('src/devtools/store').default; + const { installHook } = require('src/hook'); // Fake timers let us flush Bridge operations between setup and assertions. jest.useFakeTimers(); @@ -55,4 +55,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/src/__tests__/store-test.js b/src/__tests__/store-test.js index 5942574a77..38c5b33eab 100644 --- a/src/__tests__/store-test.js +++ b/src/__tests__/store-test.js @@ -3,24 +3,21 @@ describe('Store', () => { let React; let ReactDOM; - let TestUtils; let agent; + let act; + let getRendererID; let store; - const act = (callback: Function) => { - TestUtils.act(() => { - callback(); - }); - jest.runAllTimers(); // Flush Bridge operations - }; - beforeEach(() => { agent = global.agent; store = global.store; React = require('react'); ReactDOM = require('react-dom'); - TestUtils = require('react-dom/test-utils'); + + const utils = require('./utils'); + act = utils.act; + getRendererID = utils.getRendererID; }); it('should not allow a root node to be collapsed', () => { @@ -281,8 +278,7 @@ describe('Store', () => { ); expect(store).toMatchSnapshot('7: only third child is suspended'); - // HACK There's only one renderer for this test - const rendererID = Object.keys(agent._rendererInterfaces)[0]; + const rendererID = getRendererID(); act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(4), @@ -673,8 +669,7 @@ describe('Store', () => { act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false)); expect(store).toMatchSnapshot('2: expand tree'); - // HACK There's only one renderer for this test - const rendererID = Object.keys(agent._rendererInterfaces)[0]; + const rendererID = getRendererID(); const suspenseID = store.getElementIDAtIndex(1); act(() => diff --git a/src/__tests__/storeOwners-test.js b/src/__tests__/storeOwners-test.js index f4853b2fe0..9ed0e53fdc 100644 --- a/src/__tests__/storeOwners-test.js +++ b/src/__tests__/storeOwners-test.js @@ -5,23 +5,16 @@ const { printOwnersList } = require('./storeSerializer'); describe('Store owners list', () => { let React; let ReactDOM; - let TestUtils; + let act; let store; - const act = (callback: Function) => { - TestUtils.act(() => { - callback(); - }); - jest.runAllTimers(); // Flush Bridge operations - }; - beforeEach(() => { store = global.store; store.collapseNodesByDefault = false; React = require('react'); ReactDOM = require('react-dom'); - TestUtils = require('react-dom/test-utils'); + act = require('./utils').act; }); it('should drill through intermediate components', () => { diff --git a/src/__tests__/storeSerializer.js b/src/__tests__/storeSerializer.js index f41cb0fb84..6e6400aaf5 100644 --- a/src/__tests__/storeSerializer.js +++ b/src/__tests__/storeSerializer.js @@ -1,8 +1,9 @@ -import Store from 'src/devtools/store'; - // test() is part of Jest's serializer API export function test(maybeStore) { - return maybeStore instanceof Store; + // It's important to lazy-require the Store rather than imported at the head of the module. + // Because we reset modules between tests, different Store implementations will be used for each test. + // Unfortunately Jest does not reset its own serializer modules. + return maybeStore instanceof require('src/devtools/store').default; } // print() is part of Jest's serializer API @@ -70,9 +71,9 @@ export function printStore(store, includeWeight = false) { ); } - if (store.roots.length === 0) { - store.assertEmptyMaps(); - } + // If roots have been unmounted, verify that they've been removed from maps. + // This helps ensure the Store doesn't leak memory. + store.assertExpectedRootMapSizes(); return snapshotLines.join('\n'); } diff --git a/src/__tests__/storeStressSync-test.js b/src/__tests__/storeStressSync-test.js index 54d60ef13e..8877d5d999 100644 --- a/src/__tests__/storeStressSync-test.js +++ b/src/__tests__/storeStressSync-test.js @@ -3,18 +3,11 @@ describe('StoreStress (Sync Mode)', () => { let React; let ReactDOM; - let TestUtils; + let act; let bridge; let store; let print; - const act = (callback: Function) => { - TestUtils.act(() => { - callback(); - }); - jest.runAllTimers(); // Flush Bridge operations - }; - beforeEach(() => { bridge = global.bridge; store = global.store; @@ -22,7 +15,7 @@ describe('StoreStress (Sync Mode)', () => { React = require('react'); ReactDOM = require('react-dom'); - TestUtils = require('react-dom/test-utils'); + act = require('./utils').act; print = require('./storeSerializer').print; }); diff --git a/src/__tests__/storeStressTestConcurrent-test.js b/src/__tests__/storeStressTestConcurrent-test.js index 546db55066..d2a834ca98 100644 --- a/src/__tests__/storeStressTestConcurrent-test.js +++ b/src/__tests__/storeStressTestConcurrent-test.js @@ -3,19 +3,11 @@ describe('StoreStressConcurrent', () => { let React; let ReactDOM; - let TestUtils; + let act; let bridge; let store; let print; - const act = (callback: Function) => { - TestUtils.act(() => { - callback(); - }); - jest.advanceTimersByTime(1000); // Flush rendering and Suspense - jest.runAllTimers(); // Flush Bridge operations - }; - beforeEach(() => { bridge = global.bridge; store = global.store; @@ -23,7 +15,7 @@ describe('StoreStressConcurrent', () => { React = require('react'); ReactDOM = require('react-dom'); - TestUtils = require('react-dom/test-utils'); + act = require('./utils').act; print = require('./storeSerializer').print; }); diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js new file mode 100644 index 0000000000..89c7ffd6b1 --- /dev/null +++ b/src/__tests__/utils.js @@ -0,0 +1,79 @@ +// @flow + +import typeof ReactTestRenderer from 'react-test-renderer'; + +export function act(callback: Function): void { + const TestUtils = require('react-dom/test-utils'); + TestUtils.act(() => { + callback(); + }); + + // Flush Bridge operations + jest.runAllTimers(); +} + +export async function actSuspense( + callback: Function, + numTimesToFlush: number = 1 +): Promise { + const TestUtils = require('react-dom/test-utils'); + const Scheduler = require('scheduler'); + + // $FlowFixMe Flow doens't know about "await act()" yet + await TestUtils.act(async () => { + callback(); + + // Resolve pending suspense promises + jest.runAllTimers(); + }); + + // Run cascading microtasks and flush scheduled React work. + // Components that suspend multiple times will need to do this once per suspend operation. + // HACK Ideally the mock scheduler would provide an API to ask if there was outstanding work. + while (--numTimesToFlush >= 0) { + // $FlowFixMe Flow doens't know about "await act()" yet + await TestUtils.act(async () => { + jest.runAllTimers(); + Scheduler.flushAll(); + }); + } +} + +export function beforeEachProfiling(): void { + // Mock React's timing information so that test runs are predictable. + jest.mock('scheduler', () => + // $FlowFixMe Flow does not konw about requireActual + require.requireActual('scheduler/unstable_mock') + ); + + // DevTools itself uses performance.now() to offset commit times + // so they appear relative to when profiling was started in the UI. + jest.spyOn(performance, 'now').mockImplementation( + // $FlowFixMe Flow does not konw about requireActual + require.requireActual('scheduler/unstable_mock').unstable_now + ); +} + +export function getRendererID(): number { + if (global.agent == null) { + throw Error('Agent unavailable.'); + } + const ids = Object.keys(global.agent._rendererInterfaces); + if (ids.length !== 1) { + throw Error('Multiple renderers attached.'); + } + return parseInt(ids[0], 10); +} + +export function requireTestRenderer(): ReactTestRenderer { + let hook; + try { + // Hide the hook before requiring TestRenderer, so we don't end up with a loop. + hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__; + delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__; + + return require('react-test-renderer'); + } finally { + global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook; + } +} diff --git a/src/devtools/store.js b/src/devtools/store.js index 8321702717..7a84b33647 100644 --- a/src/devtools/store.js +++ b/src/devtools/store.js @@ -192,30 +192,49 @@ export default class Store extends EventEmitter { } // This is only used in tests to avoid memory leaks. - assertEmptyMaps() { - this.assertEmptyMap(this._idToElement, '_idToElement'); - this.assertEmptyMap(this._ownersMap, '_ownersMap'); - this.assertEmptyMap( - this._profilingOperationsByRootID, - '_profilingOperationsByRootID' + assertExpectedRootMapSizes() { + if (this.roots.length === 0) { + // The only safe time to assert these maps are empty is when the store is empty. + this.assertMapSizeMatchesRootCount(this._idToElement, '_idToElement'); + this.assertMapSizeMatchesRootCount(this._ownersMap, '_ownersMap'); + + // These maps will be empty unless profiling mode has been started. + // After this, their size should always match the number of roots, + // but unless we want to track additional metadata about profiling history, + // the only safe time to assert this is when the store is empty. + this.assertMapSizeMatchesRootCount( + this._profilingOperationsByRootID, + '_profilingOperationsByRootID' + ); + this.assertMapSizeMatchesRootCount( + this._profilingScreenshotsByRootID, + '_profilingScreenshotsByRootID' + ); + this.assertMapSizeMatchesRootCount( + this._profilingSnapshotsByRootID, + '_profilingSnapshotsByRootID' + ); + } + + // These maps should always be the same size as the number of roots + this.assertMapSizeMatchesRootCount( + this._rootIDToCapabilities, + '_rootIDToCapabilities' ); - this.assertEmptyMap( - this._profilingScreenshotsByRootID, - '_profilingScreenshotsByRootID' + this.assertMapSizeMatchesRootCount( + this._rootIDToRendererID, + '_rootIDToRendererID' ); - this.assertEmptyMap( - this._profilingSnapshotsByRootID, - '_profilingSnapshotsByRootID' - ); - this.assertEmptyMap(this._rootIDToCapabilities, '_rootIDToCapabilities'); - this.assertEmptyMap(this._rootIDToRendererID, '_rootIDToRendererID'); } // This is only used in tests to avoid memory leaks. - assertEmptyMap(map: Map, mapName: string) { - if (map.size !== 0) { + assertMapSizeMatchesRootCount(map: Map, mapName: string) { + const expectedSize = this.roots.length; + if (map.size !== expectedSize) { throw new Error( - `Expected ${mapName} to be empty, got ${map.size}: ${inspect(map, { + `Expected ${mapName} to contain ${expectedSize} items, but it contains ${ + map.size + } items\n\n${inspect(map, { depth: 20, })}` ); @@ -833,6 +852,10 @@ export default class Store extends EventEmitter { weight: 0, }); + if (this._isProfiling) { + this._profilingSnapshotsByRootID.set(id, new Map()); + } + haveRootsChanged = true; } else { parentID = ((operations[i]: any): number); diff --git a/yarn.lock b/yarn.lock index 6ed41c6740..4b65dd25f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9807,20 +9807,20 @@ react-color@^2.11.7: reactcss "^1.2.0" tinycolor2 "^1.4.1" -react-dom@0.0.0-fb28e9048: - version "0.0.0-fb28e9048" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-fb28e9048.tgz#d4276f11a16f74b2cef0700bb5a8eefaee3761d2" - integrity sha512-C4bdIWINwV4pF1P0oN7fDAq+vTHOW2rxQLvGE5rDslGrK/DJ7bM5PrD81AcPkdgUnK3O/JvIkeGIvRcRgYemSw== +react-dom@^0.0.0-6da04b5d8: + version "0.0.0-6da04b5d8" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-6da04b5d8.tgz#ee78e45a40771560c756b8fc7f1fa213f2179ebe" + integrity sha512-6oyfkucrweqCB5XyLsfEnPSWhvkFnttutkU9uUQovLAljuazgpAjvBy6MBGSewKptqch2OTNwopqvR3QUMD8AQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "0.0.0-fb28e9048" + scheduler "0.0.0-6da04b5d8" -react-is@0.0.0-fb28e9048: - version "0.0.0-fb28e9048" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-0.0.0-fb28e9048.tgz#f2bdccfea1005ea5234719de192f25504ff4244e" - integrity sha512-McCC7GvLXMcY29GIiJ56bvuXFVTIwBr7wcz93TcGvUN23a7l0h9KjCwUjGQfEpBRG3pbeBtMxDc6GiXuVbK6Lg== +react-is@0.0.0-6da04b5d8, react-is@^0.0.0-6da04b5d8: + version "0.0.0-6da04b5d8" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-0.0.0-6da04b5d8.tgz#ded1f02e9f1e2b8456812d0d45c341d70b7bf7db" + integrity sha512-+Df3meqx+XUir+3dCqiHNAHruwmOAgXVp3TYmlUvgtPNBu+LF0OdchTMZ/xMUok1gYVoe4l/xhfs9CTEPkWt3g== react-is@^16.8.1: version "16.8.3" @@ -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-6da04b5d8: + version "0.0.0-6da04b5d8" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-0.0.0-6da04b5d8.tgz#01bed04c5a4cf22339f0ae3b23f89bb45e9a8f2a" + integrity sha512-yDt5RPDXLZXTqlWS0jXMJ1IyS1e/UZXSr0L8bG0UCsna4T7A3HIYR2zChlydGXsxjGMZntKqEfm27hUhFgC06Q== + dependencies: + object-assign "^4.1.1" + prop-types "^15.6.2" + react-is "0.0.0-6da04b5d8" + scheduler "0.0.0-6da04b5d8" + 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" @@ -9850,15 +9860,15 @@ react-window@^1.8.0: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" -react@0.0.0-fb28e9048: - version "0.0.0-fb28e9048" - resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-fb28e9048.tgz#f3488c3e5cac772b2731d6d4bb3484410821254c" - integrity sha512-6zsjgsy9hcKC41T6X6SMbyf3fJ2VadmUxDgEXUVAJ2qV9q4SP0mnK4ttDfOwDvlFNbisqtS5HbS0hnWO/8iKxQ== +react@^0.0.0-6da04b5d8: + version "0.0.0-6da04b5d8" + resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-6da04b5d8.tgz#583d81f73b26771da41170a5042a5ab0bdcfe37a" + integrity sha512-8hXBHwDCKxSVFqj5Kb4OskZz7//2fx2IpUnYyukYV8qyAHlXr0qUl3GxwuryhdPzJHlzi776WjN0YEmEEANhYA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "0.0.0-fb28e9048" + scheduler "0.0.0-6da04b5d8" reactcss@^1.2.0: version "1.2.3" @@ -10514,10 +10524,10 @@ sax@>=0.6.0, sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -scheduler@0.0.0-fb28e9048: - version "0.0.0-fb28e9048" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-fb28e9048.tgz#5e7e37a44eb69204936a3ae67f187947980438a8" - integrity sha512-2s3rauGawjL8fGNLWHv5NJkUn8y1IOwAtptE6en0IRUcz33IZQyKlBlFpwdAKyarGScRL6p9FpcLYorXZCaDuw== +scheduler@0.0.0-6da04b5d8, scheduler@^0.0.0-6da04b5d8: + version "0.0.0-6da04b5d8" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-6da04b5d8.tgz#5e0ec65c2b0a7f05ffdc5522fc3a6d95b693e5c9" + integrity sha512-upTrWBZvk4IjMsC/AcRpgCwjnSQl8i78+07KmcndqWOnWp7s4wauowWXhyswP9vucLtZaN5ussFdM3d7dXcTkw== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1"