Add tests for redux store (#131)
This adds some baseline tests for the redux store, and also fixes some bugs I found along the way. Bugs fixed: - Changing selection after something has been selected within the sandwich view now works - Table sort state is now preserved when switching between profiles Fixes #124
This commit is contained in:
@@ -56,10 +56,10 @@ export type Dispatch = redux.Dispatch<Action<any>>
|
||||
// We make this into a single function invocation instead of the connect(map, map)(Component)
|
||||
// syntax to make better use of type inference.
|
||||
//
|
||||
// NOTE: The way this works right now is going to regenerate new setters on every
|
||||
// store update. To make this not the case, we'd have to have mapStateToProps actually
|
||||
// specified. This may be a performance issue in the future, but we're going to eat
|
||||
// this cost for now in exchange for simpler type inference.
|
||||
// NOTE: To avoid this returning objects which do not compare shallow equal, it's the
|
||||
// responsibility of the caller to ensure that the props returned by map compare shallow
|
||||
// equal. This most importantly mean memoizing functions which wrap dispatch to avoid
|
||||
// all callback props from being regenerated on every call.
|
||||
export function createContainer<OwnProps, State, ComponentProps, ComponentType>(
|
||||
component: {
|
||||
new (props: ComponentProps): ComponentType
|
||||
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
formatPercent,
|
||||
KeyedSet,
|
||||
binarySearch,
|
||||
memoizeByReference,
|
||||
memoizeByShallowEquality,
|
||||
objectsHaveShallowEquality,
|
||||
} from './utils'
|
||||
|
||||
test('sortBy', () => {
|
||||
@@ -109,3 +112,64 @@ test('binarySearch', () => {
|
||||
expect(lo).toBeLessThan(Math.E)
|
||||
expect(hi).toBeGreaterThan(Math.E)
|
||||
})
|
||||
|
||||
test('memoizeByReference', () => {
|
||||
let hitCount = 0
|
||||
const identity = memoizeByReference((arg: number) => {
|
||||
hitCount++
|
||||
return arg
|
||||
})
|
||||
|
||||
expect(identity(3)).toBe(3)
|
||||
expect(hitCount).toBe(1)
|
||||
expect(identity(3)).toBe(3)
|
||||
expect(hitCount).toBe(1)
|
||||
|
||||
expect(identity(5)).toBe(5)
|
||||
expect(hitCount).toBe(2)
|
||||
|
||||
expect(identity(3)).toBe(3)
|
||||
expect(hitCount).toBe(3)
|
||||
})
|
||||
|
||||
test('memoizeByShallowEquality', () => {
|
||||
let hitCount = 0
|
||||
const identity = memoizeByShallowEquality((arg: {a: number; b: number}) => {
|
||||
hitCount++
|
||||
return arg
|
||||
})
|
||||
|
||||
expect(identity({a: 1, b: 2})).toEqual({a: 1, b: 2})
|
||||
expect(hitCount).toBe(1)
|
||||
expect(identity({a: 1, b: 2})).toEqual({a: 1, b: 2})
|
||||
expect(hitCount).toBe(1)
|
||||
|
||||
expect(identity({a: 2, b: 2})).toEqual({a: 2, b: 2})
|
||||
expect(hitCount).toBe(2)
|
||||
expect(identity({a: 2, b: 2})).toEqual({a: 2, b: 2})
|
||||
expect(hitCount).toBe(2)
|
||||
|
||||
expect(identity({a: 2, b: 1})).toEqual({a: 2, b: 1})
|
||||
expect(hitCount).toBe(3)
|
||||
expect(identity({a: 2, b: 1})).toEqual({a: 2, b: 1})
|
||||
expect(hitCount).toBe(3)
|
||||
|
||||
expect(identity({a: 1, b: 2})).toEqual({a: 1, b: 2})
|
||||
expect(hitCount).toBe(4)
|
||||
})
|
||||
|
||||
test('objectsHaveShallowEquality', () => {
|
||||
expect(objectsHaveShallowEquality({}, {})).toBe(true)
|
||||
expect(objectsHaveShallowEquality({a: 1, b: 2}, {a: 1, b: 2})).toBe(true)
|
||||
|
||||
expect(objectsHaveShallowEquality({a: 1, b: 2}, {a: 1, b: 3})).toBe(false)
|
||||
expect(objectsHaveShallowEquality({a: 1, b: 2}, {a: 2, b: 2})).toBe(false)
|
||||
expect(objectsHaveShallowEquality({a: 1}, {a: 1, b: 2})).toBe(false)
|
||||
expect(objectsHaveShallowEquality({a: 1, b: 2}, {b: 2})).toBe(false)
|
||||
|
||||
expect(objectsHaveShallowEquality([], [])).toBe(true)
|
||||
expect(objectsHaveShallowEquality([1, 2], [1, 2])).toBe(true)
|
||||
|
||||
expect(objectsHaveShallowEquality([1], [1, 2])).toBe(false)
|
||||
expect(objectsHaveShallowEquality([1, 2], [1])).toBe(false)
|
||||
})
|
||||
|
||||
@@ -122,7 +122,7 @@ export function binarySearch(
|
||||
|
||||
export function noop(...args: any[]) {}
|
||||
|
||||
function shallowEquals<T extends object>(a: T, b: T): boolean {
|
||||
export function objectsHaveShallowEquality<T extends object>(a: T, b: T): boolean {
|
||||
for (let key in a) {
|
||||
if (a[key] !== b[key]) return false
|
||||
}
|
||||
@@ -132,7 +132,6 @@ function shallowEquals<T extends object>(a: T, b: T): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO(jlfwong): Write tests for this
|
||||
export function memoizeByShallowEquality<T extends object, U>(cb: (t: T) => U): (t: T) => U {
|
||||
let last: {args: T; result: U} | null = null
|
||||
return (args: T) => {
|
||||
@@ -141,7 +140,7 @@ export function memoizeByShallowEquality<T extends object, U>(cb: (t: T) => U):
|
||||
result = cb(args)
|
||||
last = {args, result}
|
||||
return result
|
||||
} else if (shallowEquals(last.args, args)) {
|
||||
} else if (objectsHaveShallowEquality(last.args, args)) {
|
||||
return last.result
|
||||
} else {
|
||||
last.args = args
|
||||
|
||||
@@ -38,9 +38,7 @@ export namespace actions {
|
||||
|
||||
export namespace sandwichView {
|
||||
// Set the table sorting method used for the sandwich view.
|
||||
export const setTableSortMethod = actionCreatorWithIndex<SortMethod>(
|
||||
'sandwichView.setTableSortMethod',
|
||||
)
|
||||
export const setTableSortMethod = actionCreator<SortMethod>('sandwichView.setTableSortMethod')
|
||||
|
||||
export const setSelectedFrame = actionCreatorWithIndex<Frame | null>(
|
||||
'sandwichView.setSelectedFarmr',
|
||||
|
||||
105
src/store/flamechart-view-state.test.ts
Normal file
105
src/store/flamechart-view-state.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import {storeTest, profileGroupTwoSampled} from './store-test-utils'
|
||||
import {actions} from './actions'
|
||||
import {Rect, Vec2} from '../lib/math'
|
||||
import {FlamechartID} from './flamechart-view-state'
|
||||
import {ViewMode} from '.'
|
||||
|
||||
describe('flamechart view state', () => {
|
||||
storeTest('setHoveredNode', ({getState, dispatch}) => {
|
||||
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
|
||||
expect(getState().profileGroup!.profiles[0].chronoViewState.hover).toEqual(null)
|
||||
const {profileGroup} = getState()
|
||||
const hover = {
|
||||
node: profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0],
|
||||
event: {} as MouseEvent,
|
||||
}
|
||||
dispatch(
|
||||
actions.flamechart.setHoveredNode({
|
||||
profileIndex: 0,
|
||||
args: {
|
||||
id: FlamechartID.CHRONO,
|
||||
hover,
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(getState().profileGroup!.profiles[0].chronoViewState.hover).toEqual(hover)
|
||||
expect(getState().profileGroup!.profiles[0].leftHeavyViewState.hover).toEqual(null)
|
||||
expect(getState().profileGroup!.profiles[1].chronoViewState.hover).toEqual(null)
|
||||
|
||||
// Changing view mode should clear the hover state
|
||||
dispatch(actions.setViewMode(ViewMode.LEFT_HEAVY_FLAME_GRAPH))
|
||||
expect(getState().profileGroup!.profiles[0].chronoViewState.hover).toEqual(null)
|
||||
})
|
||||
|
||||
storeTest('setSelectedNode', ({getState, dispatch}) => {
|
||||
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
|
||||
expect(getState().profileGroup!.profiles[0].chronoViewState.selectedNode).toEqual(null)
|
||||
const {profileGroup} = getState()
|
||||
const selectedNode = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0]
|
||||
dispatch(
|
||||
actions.flamechart.setSelectedNode({
|
||||
profileIndex: 0,
|
||||
args: {
|
||||
id: FlamechartID.CHRONO,
|
||||
selectedNode,
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(getState().profileGroup!.profiles[0].chronoViewState.selectedNode).toEqual(selectedNode)
|
||||
expect(getState().profileGroup!.profiles[0].leftHeavyViewState.selectedNode).toEqual(null)
|
||||
expect(getState().profileGroup!.profiles[1].chronoViewState.selectedNode).toEqual(null)
|
||||
})
|
||||
|
||||
storeTest('setLogicalSpaceViewportSize', ({getState, dispatch}) => {
|
||||
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
|
||||
expect(getState().profileGroup!.profiles[0].chronoViewState.logicalSpaceViewportSize).toEqual(
|
||||
Vec2.zero,
|
||||
)
|
||||
const logicalSpaceViewportSize = new Vec2(3, 5)
|
||||
dispatch(
|
||||
actions.flamechart.setLogicalSpaceViewportSize({
|
||||
profileIndex: 0,
|
||||
args: {
|
||||
id: FlamechartID.CHRONO,
|
||||
logicalSpaceViewportSize,
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(getState().profileGroup!.profiles[0].chronoViewState.logicalSpaceViewportSize).toEqual(
|
||||
logicalSpaceViewportSize,
|
||||
)
|
||||
|
||||
expect(
|
||||
getState().profileGroup!.profiles[0].leftHeavyViewState.logicalSpaceViewportSize,
|
||||
).toEqual(Vec2.zero)
|
||||
expect(getState().profileGroup!.profiles[1].chronoViewState.logicalSpaceViewportSize).toEqual(
|
||||
Vec2.zero,
|
||||
)
|
||||
})
|
||||
|
||||
storeTest('setConfigSpaceViewportRect', ({getState, dispatch}) => {
|
||||
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
|
||||
expect(getState().profileGroup!.profiles[0].chronoViewState.configSpaceViewportRect).toEqual(
|
||||
Rect.empty,
|
||||
)
|
||||
dispatch(
|
||||
actions.flamechart.setConfigSpaceViewportRect({
|
||||
profileIndex: 0,
|
||||
args: {
|
||||
id: FlamechartID.CHRONO,
|
||||
configSpaceViewportRect: new Rect(Vec2.zero, Vec2.unit),
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(getState().profileGroup!.profiles[0].chronoViewState.configSpaceViewportRect).toEqual(
|
||||
new Rect(Vec2.zero, Vec2.unit),
|
||||
)
|
||||
|
||||
expect(getState().profileGroup!.profiles[0].leftHeavyViewState.configSpaceViewportRect).toEqual(
|
||||
Rect.empty,
|
||||
)
|
||||
expect(getState().profileGroup!.profiles[1].chronoViewState.configSpaceViewportRect).toEqual(
|
||||
Rect.empty,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -49,8 +49,6 @@ export const getFrameToColorBucket = memoizeByReference((profile: Profile): Map<
|
||||
string | number,
|
||||
number
|
||||
> => {
|
||||
document.title = `${profile.getName()} - speedscope`
|
||||
|
||||
const frames: Frame[] = []
|
||||
profile.forEachFrame(f => frames.push(f))
|
||||
function key(f: Frame) {
|
||||
|
||||
51
src/store/index.test.ts
Normal file
51
src/store/index.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import {ViewMode} from '.'
|
||||
import {actions} from './actions'
|
||||
import {storeTest} from './store-test-utils'
|
||||
import {SortField, SortDirection} from '../views/profile-table-view'
|
||||
|
||||
storeTest('flattenRecursion', ({getState, dispatch}) => {
|
||||
expect(getState().flattenRecursion).toBe(false)
|
||||
dispatch(actions.setFlattenRecursion(true))
|
||||
expect(getState().flattenRecursion).toBe(true)
|
||||
})
|
||||
|
||||
storeTest('viewMode', ({getState, dispatch}) => {
|
||||
expect(getState().viewMode).toBe(ViewMode.CHRONO_FLAME_CHART)
|
||||
dispatch(actions.setViewMode(ViewMode.SANDWICH_VIEW))
|
||||
expect(getState().viewMode).toBe(ViewMode.SANDWICH_VIEW)
|
||||
})
|
||||
|
||||
storeTest('dragActive', ({getState, dispatch}) => {
|
||||
expect(getState().dragActive).toBe(false)
|
||||
dispatch(actions.setDragActive(true))
|
||||
expect(getState().dragActive).toBe(true)
|
||||
})
|
||||
|
||||
storeTest('loading', ({getState, dispatch}) => {
|
||||
expect(getState().loading).toBe(false)
|
||||
dispatch(actions.setLoading(true))
|
||||
expect(getState().loading).toBe(true)
|
||||
})
|
||||
|
||||
storeTest('error', ({getState, dispatch}) => {
|
||||
expect(getState().error).toBe(false)
|
||||
dispatch(actions.setError(true))
|
||||
expect(getState().error).toBe(true)
|
||||
})
|
||||
|
||||
storeTest('setTableSortMethod', ({getState, dispatch}) => {
|
||||
expect(getState().tableSortMethod).toEqual({
|
||||
field: SortField.SELF,
|
||||
direction: SortDirection.DESCENDING,
|
||||
})
|
||||
dispatch(
|
||||
actions.sandwichView.setTableSortMethod({
|
||||
field: SortField.SYMBOL_NAME,
|
||||
direction: SortDirection.ASCENDING,
|
||||
}),
|
||||
)
|
||||
expect(getState().tableSortMethod).toEqual({
|
||||
field: SortField.SYMBOL_NAME,
|
||||
direction: SortDirection.ASCENDING,
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,7 @@ import * as redux from 'redux'
|
||||
import {setter, Reducer} from '../lib/typed-redux'
|
||||
import {HashParams, getHashParams} from '../lib/hash-params'
|
||||
import {ProfileGroupState, profileGroup} from './profiles-state'
|
||||
import {SortMethod, SortField, SortDirection} from '../views/profile-table-view'
|
||||
|
||||
export const enum ViewMode {
|
||||
CHRONO_FLAME_CHART,
|
||||
@@ -28,6 +29,8 @@ export interface ApplicationState {
|
||||
loading: boolean
|
||||
error: boolean
|
||||
|
||||
tableSortMethod: SortMethod
|
||||
|
||||
profileGroup: ProfileGroupState
|
||||
}
|
||||
|
||||
@@ -59,6 +62,11 @@ export function createApplicationStore(
|
||||
dragActive: setter<boolean>(actions.setDragActive, false),
|
||||
loading: setter<boolean>(actions.setLoading, loading),
|
||||
error: setter<boolean>(actions.setError, false),
|
||||
|
||||
tableSortMethod: setter<SortMethod>(actions.sandwichView.setTableSortMethod, {
|
||||
field: SortField.SELF,
|
||||
direction: SortDirection.DESCENDING,
|
||||
}),
|
||||
})
|
||||
|
||||
return redux.createStore(reducer, initialState)
|
||||
|
||||
36
src/store/profiles-state.test.ts
Normal file
36
src/store/profiles-state.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {storeTest, profileGroupTwoSampled} from './store-test-utils'
|
||||
import {actions} from './actions'
|
||||
|
||||
describe('profileGroup', () => {
|
||||
storeTest('setProfileGroup', ({getState, dispatch}) => {
|
||||
expect(getState().profileGroup).toBe(null)
|
||||
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
|
||||
const {profileGroup} = getState()
|
||||
expect(profileGroup!.name).toBe(profileGroupTwoSampled.name)
|
||||
expect(profileGroup!.indexToView).toBe(profileGroupTwoSampled.indexToView)
|
||||
|
||||
for (let i = 0; i < profileGroupTwoSampled.profiles.length; i++) {
|
||||
expect(profileGroup!.profiles[i].profile).toEqual(profileGroupTwoSampled.profiles[i])
|
||||
}
|
||||
})
|
||||
|
||||
storeTest('setProfileIndexToView', ({getState, dispatch}) => {
|
||||
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
|
||||
expect(getState().profileGroup!.indexToView).toBe(1)
|
||||
dispatch(actions.setProfileIndexToView(0))
|
||||
expect(getState()!.profileGroup!.indexToView).toBe(0)
|
||||
dispatch(actions.setProfileIndexToView(-1))
|
||||
expect(getState()!.profileGroup!.indexToView).toBe(0)
|
||||
dispatch(actions.setProfileIndexToView(2))
|
||||
expect(getState().profileGroup!.indexToView).toBe(1)
|
||||
})
|
||||
|
||||
storeTest('preserve state', ({getState, dispatch}) => {
|
||||
// When an unrelated action occurs, we should not change the identiy of the profileGroup
|
||||
// property
|
||||
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
|
||||
const profileGroup = getState().profileGroup
|
||||
dispatch(actions.setDragActive(true))
|
||||
expect(getState().profileGroup).toBe(profileGroup)
|
||||
})
|
||||
})
|
||||
@@ -5,9 +5,10 @@ import {
|
||||
FlamechartID,
|
||||
} from './flamechart-view-state'
|
||||
import {SandwichViewState, createSandwichView} from './sandwich-view-state'
|
||||
import {Reducer, Action, actionCreator, setter} from '../lib/typed-redux'
|
||||
import {Reducer, actionCreator, setter} from '../lib/typed-redux'
|
||||
import {actions} from './actions'
|
||||
import {clamp} from '../lib/math'
|
||||
import {objectsHaveShallowEquality} from '../lib/utils'
|
||||
|
||||
export type ProfileGroupState = {
|
||||
name: string
|
||||
@@ -31,12 +32,33 @@ export function actionCreatorWithIndex<T>(name: string) {
|
||||
return actionCreator<{profileIndex: number; args: T}>(name)
|
||||
}
|
||||
|
||||
export function actionProfileIndex(action: Action<any>): number | null {
|
||||
const {payload} = action
|
||||
if (payload != null && typeof payload === 'object' && 'profileIndex' in payload) {
|
||||
return parseInt(payload.profileIndex, 0)
|
||||
} else {
|
||||
return null
|
||||
function createProfileReducer(profile: Profile, index: number): Reducer<ProfileState> {
|
||||
const chronoViewStateReducer = createFlamechartViewStateReducer(FlamechartID.CHRONO, index)
|
||||
const leftHeavyViewStateReducer = createFlamechartViewStateReducer(FlamechartID.LEFT_HEAVY, index)
|
||||
const sandwichViewStateReducer = createSandwichView(index)
|
||||
|
||||
return (state = undefined, action) => {
|
||||
if (state === undefined) {
|
||||
return {
|
||||
profile,
|
||||
chronoViewState: chronoViewStateReducer(undefined, action),
|
||||
leftHeavyViewState: leftHeavyViewStateReducer(undefined, action),
|
||||
sandwichViewState: sandwichViewStateReducer(undefined, action),
|
||||
}
|
||||
}
|
||||
|
||||
const nextState = {
|
||||
profile,
|
||||
chronoViewState: chronoViewStateReducer(state.chronoViewState, action),
|
||||
leftHeavyViewState: leftHeavyViewStateReducer(state.leftHeavyViewState, action),
|
||||
sandwichViewState: sandwichViewStateReducer(state.sandwichViewState, action),
|
||||
}
|
||||
|
||||
if (objectsHaveShallowEquality(state, nextState)) {
|
||||
return state
|
||||
}
|
||||
|
||||
return nextState
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,19 +69,7 @@ export const profileGroup: Reducer<ProfileGroupState> = (state = null, action) =
|
||||
indexToView,
|
||||
name,
|
||||
profiles: profiles.map((p, i) => {
|
||||
return {
|
||||
profile: p,
|
||||
frameToColorBucket: new Map(),
|
||||
chronoViewState: createFlamechartViewStateReducer(FlamechartID.CHRONO, i)(
|
||||
undefined,
|
||||
action,
|
||||
),
|
||||
leftHeavyViewState: createFlamechartViewStateReducer(FlamechartID.LEFT_HEAVY, i)(
|
||||
undefined,
|
||||
action,
|
||||
),
|
||||
sandwichViewState: createSandwichView(i)(undefined, action),
|
||||
}
|
||||
return createProfileReducer(p, i)(undefined, action)
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -72,35 +82,18 @@ export const profileGroup: Reducer<ProfileGroupState> = (state = null, action) =
|
||||
0,
|
||||
profiles.length - 1,
|
||||
)
|
||||
let nextProfiles = profiles
|
||||
const nextProfiles = profiles.map((profileState, profileIndex) => {
|
||||
return createProfileReducer(profileState.profile, profileIndex)(profileState, action)
|
||||
})
|
||||
|
||||
const profileIndexFromAction = actionProfileIndex(action)
|
||||
if (profileIndexFromAction != null) {
|
||||
nextProfiles = profiles.map((profileState, profileIndex) => {
|
||||
return {
|
||||
profile: profileState.profile,
|
||||
chronoViewState: createFlamechartViewStateReducer(FlamechartID.CHRONO, profileIndex)(
|
||||
profileState.chronoViewState,
|
||||
action,
|
||||
),
|
||||
leftHeavyViewState: createFlamechartViewStateReducer(
|
||||
FlamechartID.LEFT_HEAVY,
|
||||
profileIndex,
|
||||
)(profileState.leftHeavyViewState, action),
|
||||
sandwichViewState: createSandwichView(profileIndex)(
|
||||
profileState.sandwichViewState,
|
||||
action,
|
||||
),
|
||||
}
|
||||
})
|
||||
if (indexToView === nextIndexToView && objectsHaveShallowEquality(profiles, nextProfiles)) {
|
||||
return state
|
||||
}
|
||||
|
||||
if (indexToView !== nextIndexToView || profiles !== nextProfiles) {
|
||||
return {
|
||||
...state,
|
||||
indexToView: nextIndexToView,
|
||||
profiles: nextProfiles,
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
indexToView: nextIndexToView,
|
||||
profiles: nextProfiles,
|
||||
}
|
||||
}
|
||||
return state
|
||||
|
||||
70
src/store/sandwich-view-state.test.ts
Normal file
70
src/store/sandwich-view-state.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {storeTest, profileGroupTwoSampled} from './store-test-utils'
|
||||
import {actions} from './actions'
|
||||
import {Vec2} from '../lib/math'
|
||||
import {FlamechartID} from './flamechart-view-state'
|
||||
|
||||
describe('sandwich view state', () => {
|
||||
storeTest('setSelectedFrame', ({getState, dispatch}) => {
|
||||
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
|
||||
expect(getState().profileGroup!.profiles[0].sandwichViewState.callerCallee).toBe(null)
|
||||
const {profileGroup} = getState()
|
||||
const selectedFrame = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0]
|
||||
.frame
|
||||
dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: selectedFrame}))
|
||||
|
||||
expect(getState().profileGroup!.profiles[0].sandwichViewState.callerCallee!.selectedFrame).toBe(
|
||||
selectedFrame,
|
||||
)
|
||||
|
||||
// Other profiles selection state should not change
|
||||
expect(getState().profileGroup!.profiles[1].sandwichViewState.callerCallee).toBe(null)
|
||||
|
||||
// Changing selection when something is already selected should work
|
||||
const selectedFrame2 = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot()
|
||||
.children[0].children[0].frame
|
||||
dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: selectedFrame2}))
|
||||
expect(getState().profileGroup!.profiles[0].sandwichViewState.callerCallee!.selectedFrame).toBe(
|
||||
selectedFrame2,
|
||||
)
|
||||
|
||||
// Clear selection
|
||||
dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: null}))
|
||||
expect(getState().profileGroup!.profiles[0].sandwichViewState.callerCallee).toBe(null)
|
||||
})
|
||||
|
||||
storeTest('contained flamegraphs receive updates', ({getState, dispatch}) => {
|
||||
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
|
||||
const {profileGroup} = getState()
|
||||
const selectedFrame = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0]
|
||||
.frame
|
||||
dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: selectedFrame}))
|
||||
|
||||
expect(
|
||||
getState().profileGroup!.profiles[0].sandwichViewState.callerCallee!.calleeFlamegraph
|
||||
.logicalSpaceViewportSize,
|
||||
).toEqual(Vec2.zero)
|
||||
dispatch(
|
||||
actions.flamechart.setLogicalSpaceViewportSize({
|
||||
profileIndex: 0,
|
||||
args: {id: FlamechartID.SANDWICH_CALLEES, logicalSpaceViewportSize: Vec2.unit},
|
||||
}),
|
||||
)
|
||||
expect(
|
||||
getState().profileGroup!.profiles[0].sandwichViewState.callerCallee!.calleeFlamegraph
|
||||
.logicalSpaceViewportSize,
|
||||
).toEqual(Vec2.unit)
|
||||
})
|
||||
|
||||
storeTest('preserve identity', ({getState, dispatch}) => {
|
||||
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
|
||||
const {profileGroup} = getState()
|
||||
const selectedFrame = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0]
|
||||
.frame
|
||||
dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: selectedFrame}))
|
||||
|
||||
// When an unrelated action is triggered, it should not change the identity of the sandwichViewState
|
||||
const sandwichViewState = getState().profileGroup!.profiles[0].sandwichViewState
|
||||
dispatch(actions.setLoading(true))
|
||||
expect(getState().profileGroup!.profiles[0].sandwichViewState).toBe(sandwichViewState)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
import {SortMethod, SortField, SortDirection} from '../views/profile-table-view'
|
||||
import {Frame} from '../lib/profile'
|
||||
import {
|
||||
FlamechartViewState,
|
||||
@@ -9,7 +8,6 @@ import {Reducer} from '../lib/typed-redux'
|
||||
import {actions} from './actions'
|
||||
|
||||
export interface SandwichViewState {
|
||||
tableSortMethod: SortMethod
|
||||
callerCallee: CallerCalleeState | null
|
||||
}
|
||||
|
||||
@@ -19,11 +17,6 @@ export interface CallerCalleeState {
|
||||
calleeFlamegraph: FlamechartViewState
|
||||
}
|
||||
|
||||
const defaultSortMethod = {
|
||||
field: SortField.SELF,
|
||||
direction: SortDirection.DESCENDING,
|
||||
}
|
||||
|
||||
export function createSandwichView(profileIndex: number): Reducer<SandwichViewState> {
|
||||
const calleesReducer = createFlamechartViewStateReducer(
|
||||
FlamechartID.SANDWICH_CALLEES,
|
||||
@@ -38,32 +31,7 @@ export function createSandwichView(profileIndex: number): Reducer<SandwichViewSt
|
||||
return payload.profileIndex === profileIndex
|
||||
}
|
||||
|
||||
return (state = {tableSortMethod: defaultSortMethod, callerCallee: null}, action) => {
|
||||
const {callerCallee} = state
|
||||
if (callerCallee) {
|
||||
const {calleeFlamegraph, invertedCallerFlamegraph} = callerCallee
|
||||
const nextCalleeFlamegraph = calleesReducer(calleeFlamegraph, action)
|
||||
const nextInvertedCallerFlamegraph = invertedCallersReducer(invertedCallerFlamegraph, action)
|
||||
|
||||
if (
|
||||
nextCalleeFlamegraph !== calleeFlamegraph ||
|
||||
nextInvertedCallerFlamegraph !== invertedCallerFlamegraph
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
callerCallee: {
|
||||
...callerCallee,
|
||||
calleeFlamegraph: nextCalleeFlamegraph,
|
||||
invertedCallerFlamegraph: nextInvertedCallerFlamegraph,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.sandwichView.setTableSortMethod.matches(action) && applies(action)) {
|
||||
return {...state, tableSortMethod: action.payload.args}
|
||||
}
|
||||
|
||||
return (state = {callerCallee: null}, action) => {
|
||||
if (actions.sandwichView.setSelectedFrame.matches(action) && applies(action)) {
|
||||
if (action.payload.args == null) {
|
||||
return {
|
||||
@@ -82,6 +50,29 @@ export function createSandwichView(profileIndex: number): Reducer<SandwichViewSt
|
||||
}
|
||||
}
|
||||
|
||||
const {callerCallee} = state
|
||||
if (callerCallee) {
|
||||
const {calleeFlamegraph, invertedCallerFlamegraph} = callerCallee
|
||||
const nextCalleeFlamegraph = calleesReducer(calleeFlamegraph, action)
|
||||
const nextInvertedCallerFlamegraph = invertedCallersReducer(invertedCallerFlamegraph, action)
|
||||
|
||||
if (
|
||||
nextCalleeFlamegraph === calleeFlamegraph &&
|
||||
nextInvertedCallerFlamegraph === invertedCallerFlamegraph
|
||||
) {
|
||||
return state
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
callerCallee: {
|
||||
...callerCallee,
|
||||
calleeFlamegraph: nextCalleeFlamegraph,
|
||||
invertedCallerFlamegraph: nextInvertedCallerFlamegraph,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
16
src/store/store-test-utils.ts
Normal file
16
src/store/store-test-utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as fs from 'fs'
|
||||
|
||||
import {Store, AnyAction} from '../../node_modules/redux'
|
||||
import {ApplicationState, createApplicationStore} from '.'
|
||||
import {importSpeedscopeProfiles} from '../lib/file-format'
|
||||
|
||||
export function storeTest(name: string, cb: (store: Store<ApplicationState, AnyAction>) => void) {
|
||||
const store = createApplicationStore({})
|
||||
test(name, () => {
|
||||
cb(store)
|
||||
})
|
||||
}
|
||||
|
||||
const filepath = './sample/profiles/speedscope/0.6.0/two-sampled.speedscope.json'
|
||||
const input = fs.readFileSync(filepath, 'utf8')
|
||||
export const profileGroupTwoSampled = importSpeedscopeProfiles(JSON.parse(input))
|
||||
@@ -262,12 +262,19 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.props.hashParams.title) {
|
||||
profileGroup = {
|
||||
name: this.props.hashParams.title,
|
||||
...profileGroup,
|
||||
}
|
||||
}
|
||||
document.title = `${profileGroup.name} - speedscope`
|
||||
|
||||
for (let profile of profileGroup.profiles) {
|
||||
await profile.demangle()
|
||||
}
|
||||
|
||||
for (let profile of profileGroup.profiles) {
|
||||
// TODO(jlfwong): Profile names vs. profile group names needs some thought
|
||||
const title = this.props.hashParams.title || profile.getName()
|
||||
profile.setName(title)
|
||||
}
|
||||
|
||||
@@ -339,7 +339,8 @@ export const ProfileTableViewContainer = createContainer(
|
||||
const {activeProfileState} = ownProps
|
||||
const {profile, sandwichViewState, index} = activeProfileState
|
||||
if (!profile) throw new Error('profile missing')
|
||||
const {tableSortMethod, callerCallee} = sandwichViewState
|
||||
const {tableSortMethod} = state
|
||||
const {callerCallee} = sandwichViewState
|
||||
const selectedFrame = callerCallee ? callerCallee.selectedFrame : null
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
@@ -349,7 +350,7 @@ export const ProfileTableViewContainer = createContainer(
|
||||
}
|
||||
|
||||
const setSortMethod = (sortMethod: SortMethod) => {
|
||||
dispatch(actions.sandwichView.setTableSortMethod({profileIndex: index, args: sortMethod}))
|
||||
dispatch(actions.sandwichView.setTableSortMethod(sortMethod))
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user