Added ProfilerContext tests
This commit is contained in:
parent
5719454377
commit
44fbf3cd1d
|
@ -12,6 +12,7 @@
|
|||
},
|
||||
"globals": {
|
||||
"__DEV__": "readonly",
|
||||
"jasmine": "readonly"
|
||||
"jasmine": "readonly",
|
||||
"spyOn": "readonly"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,41 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ProfilerContext should gracefully handle an empty profiling session: 1: mount 1`] = `
|
||||
exports[`ProfilerContext should auto-select the root ID matching the Components tab selection if it has profiling data: mounted 1`] = `
|
||||
[root]
|
||||
<Example>
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
`;
|
||||
|
||||
exports[`ProfilerContext should maintain root selection between profiling sessions so long as there is data for that root: mounted 1`] = `
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
`;
|
||||
|
||||
exports[`ProfilerContext should not select the root ID matching the Components tab selection if it has no profiling data: mounted 1`] = `
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
`;
|
||||
|
||||
exports[`ProfilerContext should sync selected element in the Components tab too, provided the element is a match: mounted 1`] = `
|
||||
[root]
|
||||
▾ <GrandParent>
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
`;
|
||||
|
||||
exports[`ProfilerContext should sync selected element in the Components tab too, provided the element is a match: updated 1`] = `
|
||||
[root]
|
||||
▾ <GrandParent>
|
||||
<Parent>
|
||||
`;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
// @flow
|
||||
|
||||
import typeof ReactTestRenderer from 'react-test-renderer';
|
||||
import type { Element } from 'src/devtools/views/Components/types';
|
||||
import type Bridge from 'src/bridge';
|
||||
import type { Context } from 'src/devtools/views/Profiler/ProfilerContext';
|
||||
import type { DispatcherContext } from 'src/devtools/views/Components/TreeContext';
|
||||
import type Store from 'src/devtools/store';
|
||||
|
||||
describe('ProfilerContext', () => {
|
||||
|
@ -18,6 +19,8 @@ describe('ProfilerContext', () => {
|
|||
let ProfilerContextController;
|
||||
let StoreContext;
|
||||
let TreeContextController;
|
||||
let TreeDispatcherContext;
|
||||
let TreeStateContext;
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
|
@ -39,13 +42,17 @@ describe('ProfilerContext', () => {
|
|||
StoreContext = require('src/devtools/views/context').StoreContext;
|
||||
TreeContextController = require('src/devtools/views/Components/TreeContext')
|
||||
.TreeContextController;
|
||||
TreeDispatcherContext = require('src/devtools/views/Components/TreeContext')
|
||||
.TreeDispatcherContext;
|
||||
TreeStateContext = require('src/devtools/views/Components/TreeContext')
|
||||
.TreeStateContext;
|
||||
});
|
||||
|
||||
const Contexts = ({
|
||||
children = null,
|
||||
defaultSelectedElementID = null,
|
||||
defaultSelectedElementIndex = null,
|
||||
}) => (
|
||||
}: any) => (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
<TreeContextController
|
||||
|
@ -58,21 +65,248 @@ describe('ProfilerContext', () => {
|
|||
</BridgeContext.Provider>
|
||||
);
|
||||
|
||||
it('should gracefully handle an empty profiling session', () => {
|
||||
const Example = () => {
|
||||
const [count] = React.useState(1);
|
||||
return count;
|
||||
};
|
||||
it('should gracefully handle an empty profiling session (with no recorded commits)', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Example />, document.createElement('div'))
|
||||
);
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Profile but don't record any updates.
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
await utils.actAsync(() => {
|
||||
TestRenderer.create(
|
||||
<Contexts>
|
||||
<ContextReader />
|
||||
</Contexts>
|
||||
);
|
||||
});
|
||||
expect(context).not.toBeNull();
|
||||
expect(context.didRecordCommits).toBe(false);
|
||||
expect(context.isProcessingData).toBe(false);
|
||||
expect(context.isProfiling).toBe(true);
|
||||
expect(context.profilingData).toBe(null);
|
||||
await utils.actAsync(() => store.profilerStore.stopProfiling());
|
||||
|
||||
expect(context).not.toBeNull();
|
||||
expect(context.didRecordCommits).toBe(false);
|
||||
expect(context.isProcessingData).toBe(false);
|
||||
expect(context.isProfiling).toBe(false);
|
||||
expect(context.profilingData).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should auto-select the root ID matching the Components tab selection if it has profiling data', async done => {
|
||||
const Parent = () => <Child />;
|
||||
const Child = () => null;
|
||||
|
||||
const containerOne = document.createElement('div');
|
||||
const containerTwo = document.createElement('div');
|
||||
utils.act(() => ReactDOM.render(<Parent />, containerOne));
|
||||
utils.act(() => ReactDOM.render(<Parent />, containerTwo));
|
||||
expect(store).toMatchSnapshot('mounted');
|
||||
|
||||
// Profile and record updates to both roots.
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerOne));
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerTwo));
|
||||
await utils.actAsync(() => store.profilerStore.stopProfiling());
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Select an element within the second root.
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={store.getElementIDAtIndex(3)}
|
||||
defaultSelectedElementIndex={3}
|
||||
>
|
||||
<ContextReader />
|
||||
</Contexts>
|
||||
)
|
||||
);
|
||||
|
||||
expect(context).not.toBeNull();
|
||||
expect(context.rootID).toBe(
|
||||
store.getRootIDForElement(((store.getElementIDAtIndex(3): any): number))
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not select the root ID matching the Components tab selection if it has no profiling data', async done => {
|
||||
const Parent = () => <Child />;
|
||||
const Child = () => null;
|
||||
|
||||
const containerOne = document.createElement('div');
|
||||
const containerTwo = document.createElement('div');
|
||||
utils.act(() => ReactDOM.render(<Parent />, containerOne));
|
||||
utils.act(() => ReactDOM.render(<Parent />, containerTwo));
|
||||
expect(store).toMatchSnapshot('mounted');
|
||||
|
||||
// Profile and record updates to only the first root.
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerOne));
|
||||
await utils.actAsync(() => store.profilerStore.stopProfiling());
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Select an element within the second root.
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={store.getElementIDAtIndex(3)}
|
||||
defaultSelectedElementIndex={3}
|
||||
>
|
||||
<ContextReader />
|
||||
</Contexts>
|
||||
)
|
||||
);
|
||||
|
||||
// Verify the default profiling root is the first one.
|
||||
expect(context).not.toBeNull();
|
||||
expect(context.rootID).toBe(
|
||||
store.getRootIDForElement(((store.getElementIDAtIndex(0): any): number))
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should maintain root selection between profiling sessions so long as there is data for that root', async done => {
|
||||
const Parent = () => <Child />;
|
||||
const Child = () => null;
|
||||
|
||||
const containerA = document.createElement('div');
|
||||
const containerB = document.createElement('div');
|
||||
utils.act(() => ReactDOM.render(<Parent />, containerA));
|
||||
utils.act(() => ReactDOM.render(<Parent />, containerB));
|
||||
expect(store).toMatchSnapshot('mounted');
|
||||
|
||||
// Profile and record updates.
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerA));
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerB));
|
||||
await utils.actAsync(() => store.profilerStore.stopProfiling());
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
let dispatch: DispatcherContext = ((null: any): DispatcherContext);
|
||||
let selectedElementID = null;
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
dispatch = React.useContext(TreeDispatcherContext);
|
||||
selectedElementID = React.useContext(TreeStateContext).selectedElementID;
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = ((store.getElementIDAtIndex(3): any): number);
|
||||
|
||||
// Select an element within the second root.
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts defaultSelectedElementID={id} defaultSelectedElementIndex={3}>
|
||||
<ContextReader />
|
||||
</Contexts>
|
||||
)
|
||||
);
|
||||
|
||||
expect(selectedElementID).toBe(id);
|
||||
|
||||
// Profile and record more updates to both roots
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerA));
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerB));
|
||||
await utils.actAsync(() => store.profilerStore.stopProfiling());
|
||||
|
||||
const otherID = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
// Change the selected element within a the Components tab.
|
||||
utils.act(() => dispatch({ type: 'SELECT_ELEMENT_AT_INDEX', payload: 0 }));
|
||||
|
||||
// Verify that the initial Profiler root selection is maintained.
|
||||
expect(selectedElementID).toBe(otherID);
|
||||
expect(context).not.toBeNull();
|
||||
expect(context.rootID).toBe(store.getRootIDForElement(id));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should sync selected element in the Components tab too, provided the element is a match', async done => {
|
||||
const GrandParent = ({ includeChild }) => (
|
||||
<Parent includeChild={includeChild} />
|
||||
);
|
||||
const Parent = ({ includeChild }) => (includeChild ? <Child /> : null);
|
||||
const Child = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
utils.act(() => ReactDOM.render(<Example foo={1} bar="abc" />, container));
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<GrandParent includeChild={true} />, container)
|
||||
);
|
||||
expect(store).toMatchSnapshot('mounted');
|
||||
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
const parentID = ((store.getElementIDAtIndex(1): any): number);
|
||||
const childID = ((store.getElementIDAtIndex(2): any): number);
|
||||
|
||||
utils.act(() => {
|
||||
TestRenderer.create(<Contexts />);
|
||||
});
|
||||
// Profile and record updates.
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<GrandParent includeChild={true} />, container)
|
||||
);
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<GrandParent includeChild={false} />, container)
|
||||
);
|
||||
await utils.actAsync(() => store.profilerStore.stopProfiling());
|
||||
|
||||
expect(store).toMatchSnapshot('updated');
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
let selectedElementID = null;
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
selectedElementID = React.useContext(TreeStateContext).selectedElementID;
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts>
|
||||
<ContextReader />
|
||||
</Contexts>
|
||||
)
|
||||
);
|
||||
expect(selectedElementID).toBeNull();
|
||||
|
||||
// Select an element in the Profiler tab and verify that the selection is synced to the Components tab.
|
||||
await utils.actAsync(() => context.selectFiber(parentID, 'Parent'));
|
||||
expect(selectedElementID).toBe(parentID);
|
||||
|
||||
// We expect a "no element found" warning.
|
||||
// Let's hide it from the test console though.
|
||||
spyOn(console, 'warn');
|
||||
|
||||
// Select an unmounted element and verify no Components tab selection doesn't change.
|
||||
await utils.actAsync(() => context.selectFiber(childID, 'Child'));
|
||||
expect(selectedElementID).toBe(parentID);
|
||||
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
`No element found with id "${childID}"`
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -122,7 +122,7 @@ export default class ProfilerStore extends EventEmitter {
|
|||
}
|
||||
|
||||
// Profiling data has been recorded for at least one root.
|
||||
get hasProfilingData(): boolean {
|
||||
get didRecordCommits(): boolean {
|
||||
return (
|
||||
this._dataFrontend !== null && this._dataFrontend.dataForRoots.size > 0
|
||||
);
|
||||
|
|
|
@ -8,14 +8,14 @@ import { StoreContext } from '../context';
|
|||
|
||||
export default function ClearProfilingDataButton() {
|
||||
const store = useContext(StoreContext);
|
||||
const { hasProfilingData, isProfiling } = useContext(ProfilerContext);
|
||||
const { didRecordCommits, isProfiling } = useContext(ProfilerContext);
|
||||
const { profilerStore } = store;
|
||||
|
||||
const clear = useCallback(() => profilerStore.clear(), [profilerStore]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={isProfiling || !hasProfilingData}
|
||||
disabled={isProfiling || !didRecordCommits}
|
||||
onClick={clear}
|
||||
title="Clear profiling data"
|
||||
>
|
||||
|
|
|
@ -29,7 +29,7 @@ export type Props = {|
|
|||
|
||||
function Profiler({ supportsProfiling }: Props) {
|
||||
const {
|
||||
hasProfilingData,
|
||||
didRecordCommits,
|
||||
isProcessingData,
|
||||
isProfiling,
|
||||
selectedFiberID,
|
||||
|
@ -38,7 +38,7 @@ function Profiler({ supportsProfiling }: Props) {
|
|||
} = useContext(ProfilerContext);
|
||||
|
||||
let view = null;
|
||||
if (hasProfilingData) {
|
||||
if (didRecordCommits) {
|
||||
switch (selectedTabID) {
|
||||
case 'flame-chart':
|
||||
view = <CommitFlamegraph />;
|
||||
|
@ -63,7 +63,7 @@ function Profiler({ supportsProfiling }: Props) {
|
|||
}
|
||||
|
||||
let sidebar = null;
|
||||
if (!isProfiling && !isProcessingData && hasProfilingData) {
|
||||
if (!isProfiling && !isProcessingData && didRecordCommits) {
|
||||
switch (selectedTabID) {
|
||||
case 'interactions':
|
||||
sidebar = <SidebarInteractions />;
|
||||
|
@ -102,7 +102,7 @@ function Profiler({ supportsProfiling }: Props) {
|
|||
<div className={styles.Spacer} />
|
||||
<ToggleCommitFilterModalButton />
|
||||
<div className={styles.VRule} />
|
||||
{hasProfilingData && <SnapshotSelector />}
|
||||
{didRecordCommits && <SnapshotSelector />}
|
||||
</div>
|
||||
<div className={styles.Content}>
|
||||
{view}
|
||||
|
|
|
@ -20,7 +20,7 @@ import type { ProfilingDataFrontend } from './types';
|
|||
|
||||
export type TabID = 'flame-chart' | 'ranked-chart' | 'interactions';
|
||||
|
||||
type Context = {|
|
||||
export type Context = {|
|
||||
// Which tab is selexted in the Profiler UI?
|
||||
selectedTabID: TabID,
|
||||
selectTab(id: TabID): void,
|
||||
|
@ -30,7 +30,7 @@ type Context = {|
|
|||
// This value may be modified by the record button in the Profiler toolbar,
|
||||
// or from the backend itself (after a reload-and-profile action).
|
||||
// It is synced between the backend and frontend via a Store subscription.
|
||||
hasProfilingData: boolean,
|
||||
didRecordCommits: boolean,
|
||||
isProcessingData: boolean,
|
||||
isProfiling: boolean,
|
||||
profilingData: ProfilingDataFrontend | null,
|
||||
|
@ -73,7 +73,7 @@ const ProfilerContext = createContext<Context>(((null: any): Context));
|
|||
ProfilerContext.displayName = 'ProfilerContext';
|
||||
|
||||
type StoreProfilingState = {|
|
||||
hasProfilingData: boolean,
|
||||
didRecordCommits: boolean,
|
||||
isProcessingData: boolean,
|
||||
isProfiling: boolean,
|
||||
profilingData: ProfilingDataFrontend | null,
|
||||
|
@ -93,7 +93,7 @@ function ProfilerContextController({ children }: Props) {
|
|||
const subscription = useMemo(
|
||||
() => ({
|
||||
getCurrentValue: () => ({
|
||||
hasProfilingData: profilerStore.hasProfilingData,
|
||||
didRecordCommits: profilerStore.didRecordCommits,
|
||||
isProcessingData: profilerStore.isProcessingData,
|
||||
isProfiling: profilerStore.isProfiling,
|
||||
profilingData: profilerStore.profilingData,
|
||||
|
@ -112,7 +112,7 @@ function ProfilerContextController({ children }: Props) {
|
|||
[profilerStore]
|
||||
);
|
||||
const {
|
||||
hasProfilingData,
|
||||
didRecordCommits,
|
||||
isProcessingData,
|
||||
isProfiling,
|
||||
profilingData,
|
||||
|
@ -177,10 +177,14 @@ function ProfilerContextController({ children }: Props) {
|
|||
(id: number | null, name: string | null) => {
|
||||
selectFiberID(id);
|
||||
selectFiberName(name);
|
||||
|
||||
// Sync selection to the Components tab for convenience.
|
||||
if (id !== null) {
|
||||
// If this element is still in the store, then select it in the Components tab as well.
|
||||
const element = store.getElementByID(id);
|
||||
if (element !== null) {
|
||||
|
||||
// Keep in mind that profiling data may be from a previous session.
|
||||
// In that case, IDs may match up arbitrarily; to be safe, compare both ID and display name.
|
||||
if (element !== null && element.displayName === name) {
|
||||
dispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: id,
|
||||
|
@ -211,7 +215,7 @@ function ProfilerContextController({ children }: Props) {
|
|||
selectedTabID,
|
||||
selectTab,
|
||||
|
||||
hasProfilingData,
|
||||
didRecordCommits,
|
||||
isProcessingData,
|
||||
isProfiling,
|
||||
profilingData,
|
||||
|
@ -240,7 +244,7 @@ function ProfilerContextController({ children }: Props) {
|
|||
selectedTabID,
|
||||
selectTab,
|
||||
|
||||
hasProfilingData,
|
||||
didRecordCommits,
|
||||
isProcessingData,
|
||||
isProfiling,
|
||||
profilingData,
|
||||
|
|
|
@ -109,7 +109,7 @@ export default function ProfilingImportExportButtons() {
|
|||
<ButtonIcon type="import" />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isProfiling || !profilerStore.hasProfilingData}
|
||||
disabled={isProfiling || !profilerStore.didRecordCommits}
|
||||
onClick={downloadData}
|
||||
title="Save profile..."
|
||||
>
|
||||
|
|
Loading…
Reference in New Issue