Merge pull request #258 from bvaughn/profiler-test-experiments

Added profiler tests
This commit is contained in:
Brian Vaughn
2019-05-06 14:00:45 -07:00
committed by GitHub
19 changed files with 1594 additions and 187 deletions
+81
View File
@@ -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<any>;
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<ReactTestInstance | string>,
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>): any,
};
declare module 'react-test-renderer' {
declare export type ReactTestRenderer = {
toJSON(): null | ReactTestRendererJSON,
toTree(): null | ReactTestRendererTree,
unmount(nextElement?: React$Element<any>): void,
update(nextElement: React$Element<any>): void,
getInstance(): ?ReactComponentInstance,
root: ReactTestInstance,
};
declare type Thenable = {
then(resolve: () => mixed, reject?: () => mixed): mixed,
};
declare function create(
nextElement: React$Element<any>,
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: React$Element<any>>(): E;
getRenderOutput(): React$Element<any>;
render(element: React$Element<any>, context?: any): void;
unmount(): void;
}
}
+5 -4
View File
@@ -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",
@@ -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]
▸ <Parent key="A">
[root]
▸ <Parent key="B">
`;
exports[`Profiler should start and stop profiling, handle root unmounting: 2: profiling started 1`] = `
[root]
▸ <Parent key="A">
[root]
▸ <Parent key="B">
`;
exports[`Profiler should start and stop profiling, handle root unmounting: 3: update 1`] = `
[root]
▸ <Parent key="A">
[root]
▸ <Parent key="B">
`;
exports[`Profiler should start and stop profiling, handle root unmounting: 4: unmount B 1`] = `
[root]
▸ <Parent key="A">
`;
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`] = ``;
@@ -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,
}
`;
@@ -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,
},
],
}
`;
@@ -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,
}
`;
-60
View File
@@ -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) => <Child key={index} />);
const Child = () => <div>Hi!</div>;
const containerA = document.createElement('div');
const containerB = document.createElement('div');
act(() => {
ReactDOM.render(<Parent key="A" count={3} />, containerA);
ReactDOM.render(<Parent key="B" count={2} />, containerB);
});
expect(store).toMatchSnapshot('1: mount');
act(() => {
store.startProfiling();
});
expect(store).toMatchSnapshot('2: profiling started');
act(() => {
ReactDOM.render(<Parent key="A" count={4} />, containerA);
ReactDOM.render(<Parent key="B" count={1} />, 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');
});
});
+293
View File
@@ -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) => <Child key={index} />);
};
const Child = () => {
Scheduler.advanceTime(2);
return null;
};
const container = document.createElement('div');
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
utils.act(() => store.startProfiling());
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
utils.act(() => ReactDOM.render(<Parent count={0} />, 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(
<React.Suspense fallback={null}>
<Suspender rendererID={rendererID} rootID={rootID} />
</React.Suspense>
)
);
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) => <Child key={index} />);
};
const Child = () => {
Scheduler.advanceTime(2);
return null;
};
const container = document.createElement('div');
utils.act(() => store.startProfiling());
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
utils.act(() => ReactDOM.render(<Parent count={0} />, 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(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
);
});
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) => <Child key={index} />);
};
const Child = () => {
Scheduler.advanceTime(2);
return null;
};
const container = document.createElement('div');
utils.act(() => store.startProfiling());
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
utils.act(() => ReactDOM.render(<Parent count={3} />, 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(
<React.Suspense fallback={null}>
<Suspender
fiberID={fiberID}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
);
});
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) => <Child key={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(<Parent count={1} />, container)
)
);
utils.act(() =>
SchedulerTracing.unstable_trace(
'update: two children',
Scheduler.unstable_now(),
() => ReactDOM.render(<Parent count={2} />, 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(
<React.Suspense fallback={null}>
<Suspender rendererID={rendererID} rootID={rootID} />
</React.Suspense>
)
);
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) => <Child key={index} />);
const Child = () => <div>Hi!</div>;
const containerA = document.createElement('div');
const containerB = document.createElement('div');
utils.act(() => {
ReactDOM.render(<Parent key="A" count={3} />, containerA);
ReactDOM.render(<Parent key="B" count={2} />, containerB);
});
utils.act(() => store.startProfiling());
utils.act(() => {
ReactDOM.render(<Parent key="A" count={4} />, containerA);
ReactDOM.render(<Parent key="B" count={1} />, containerB);
});
utils.act(() => ReactDOM.unmountComponentAtNode(containerB));
utils.act(() => ReactDOM.unmountComponentAtNode(containerA));
utils.act(() => store.stopProfiling());
// Assert all maps are empty
store.assertExpectedRootMapSizes();
});
});
+292
View File
@@ -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 (
<React.Fragment>
<Child key="first" />
<Child key="second" />
</React.Fragment>
);
};
// 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(<Parent />, container)
)
);
utils.act(() =>
SchedulerTracing.unstable_trace(
'update',
Scheduler.unstable_now(),
() => ReactDOM.render(<Parent />, 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(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
),
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 (
<React.Fragment>
<Child key="first" />
<Child key="second" />
</React.Fragment>
);
};
// 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(<Parent />, container)
)
);
utils.act(() =>
SchedulerTracing.unstable_trace(
'update',
Scheduler.unstable_now(),
() => ReactDOM.render(<Parent />, 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(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
),
3
);
expect(suspenseResolved).toBe(true);
}
done();
});
});
describe('interactions', () => {
it('should contain valid data', async done => {
const Parent = ({ count }) => {
Scheduler.advanceTime(10);
return (
<React.Fragment>
<Child key="first" />
<Child key="second" />
</React.Fragment>
);
};
// 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(<Parent />, container)
)
);
utils.act(() =>
SchedulerTracing.unstable_trace(
'update',
Scheduler.unstable_now(),
() => ReactDOM.render(<Parent />, 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(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
),
3
);
expect(suspenseResolved).toBe(true);
}
done();
});
});
});
@@ -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) => <Child key={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(<Parent count={1} />, container));
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
utils.act(() => ReactDOM.render(<Parent count={0} />, 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(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
),
3
);
expect(suspenseResolved).toBe(true);
}
done();
});
});
+15 -9
View File
@@ -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();
});
+8 -13
View File
@@ -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(() =>
+2 -9
View File
@@ -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', () => {
+7 -6
View File
@@ -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');
}
+2 -9
View File
@@ -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;
});
@@ -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;
});
+79
View File
@@ -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<void> {
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;
}
}
+41 -18
View File
@@ -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<any, any>, mapName: string) {
if (map.size !== 0) {
assertMapSizeMatchesRootCount(map: Map<any, any>, 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);
+28 -18
View File
@@ -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"