Clean up NavigationStateUtils
Summary: == API Breaking Change == - Add unit tests to ensure that NavigationStateUtils does the right thing. - Remove the logics that lets NavigationStateUtils accept empty value as input and return a new state. - Remove the method `NavigationStateUtils.getParent`, `NavigationStateUtils.set`. These methods are rarely used and they can be replaced by other methods. Reviewed By: ericvicenti Differential Revision: D3374934 fbshipit-source-id: 0fdf538d014d7c5b4aa1f15a0ee8db9dc91e33cd
This commit is contained in:
parent
30e9c40898
commit
67002e8ae3
|
@ -44,13 +44,26 @@ export type UIExplorerNavigationState = {
|
|||
|
||||
const defaultGetReducerForState = (initialState) => (state) => state || initialState;
|
||||
|
||||
function getNavigationState(state: any): ?NavigationState {
|
||||
if (
|
||||
(state instanceof Object) &&
|
||||
(state.routes instanceof Array) &&
|
||||
(state.routes[0] !== undefined) &&
|
||||
(typeof state.index === 'number') &&
|
||||
(state.routes[state.index] !== undefined)
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function StackReducer({initialState, getReducerForState, getPushedReducerForAction}: any): Function {
|
||||
const getReducerForStateWithDefault = getReducerForState || defaultGetReducerForState;
|
||||
return function (lastState: ?NavigationState, action: any): NavigationState {
|
||||
if (!lastState) {
|
||||
return initialState;
|
||||
}
|
||||
const lastParentState = NavigationStateUtils.getParent(lastState);
|
||||
const lastParentState = getNavigationState(lastState);
|
||||
if (!lastParentState) {
|
||||
return lastState;
|
||||
}
|
||||
|
|
|
@ -18,166 +18,180 @@ import type {
|
|||
NavigationState,
|
||||
} from 'NavigationTypeDefinition';
|
||||
|
||||
function getParent(state: NavigationState): ?NavigationState {
|
||||
if (
|
||||
(state instanceof Object) &&
|
||||
(state.routes instanceof Array) &&
|
||||
(state.routes[0] !== undefined) &&
|
||||
(typeof state.index === 'number') &&
|
||||
(state.routes[state.index] !== undefined)
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Utilities to perform atomic operation with navigate state and routes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets a route by key
|
||||
*/
|
||||
function get(state: NavigationState, key: string): ?NavigationRoute {
|
||||
const parentState = getParent(state);
|
||||
if (!parentState) {
|
||||
return null;
|
||||
}
|
||||
const childState = parentState.routes.find(child => child.key === key);
|
||||
return childState || null;
|
||||
return state.routes.find(route => route.key === key) || null;
|
||||
}
|
||||
|
||||
function indexOf(state: NavigationState, key: string): ?number {
|
||||
const parentState = getParent(state);
|
||||
if (!parentState) {
|
||||
return null;
|
||||
}
|
||||
const index = parentState.routes.map(child => child.key).indexOf(key);
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
return index;
|
||||
/**
|
||||
* Returns the first index at which a given route's key can be found in the
|
||||
* routes of the navigation state, or -1 if it is not present.
|
||||
*/
|
||||
function indexOf(state: NavigationState, key: string): number {
|
||||
return state.routes.map(route => route.key).indexOf(key);
|
||||
}
|
||||
|
||||
function push(state: NavigationState, newChildState: NavigationRoute): NavigationState {
|
||||
var lastChildren: Array<NavigationRoute> = state.routes;
|
||||
/**
|
||||
* Returns `true` at which a given route's key can be found in the
|
||||
* routes of the navigation state.
|
||||
*/
|
||||
function has(state: NavigationState, key: string): boolean {
|
||||
return !!state.routes.some(route => route.key === key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a new route into the navigation state.
|
||||
* Note that this moves the index to the positon to where the last route in the
|
||||
* stack is at.
|
||||
*/
|
||||
function push(state: NavigationState, route: NavigationRoute): NavigationState {
|
||||
invariant(
|
||||
indexOf(state, route.key) === -1,
|
||||
'should not push route with duplicated key %s',
|
||||
route.key,
|
||||
);
|
||||
|
||||
const routes = [
|
||||
...state.routes,
|
||||
route,
|
||||
];
|
||||
|
||||
return {
|
||||
...state,
|
||||
routes: [
|
||||
...lastChildren,
|
||||
newChildState,
|
||||
],
|
||||
index: lastChildren.length,
|
||||
index: routes.length - 1,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops out a route from the navigation state.
|
||||
* Note that this moves the index to the positon to where the last route in the
|
||||
* stack is at.
|
||||
*/
|
||||
function pop(state: NavigationState): NavigationState {
|
||||
if (state.index <= 0) {
|
||||
// [Note]: Over-popping does not throw error. Instead, it will be no-op.
|
||||
return state;
|
||||
}
|
||||
const lastChildren = state.routes;
|
||||
const routes = state.routes.slice(0, -1);
|
||||
return {
|
||||
...state,
|
||||
routes: lastChildren.slice(0, lastChildren.length - 1),
|
||||
index: lastChildren.length - 2,
|
||||
};
|
||||
}
|
||||
|
||||
function reset(state: NavigationState, nextChildren: ?Array<NavigationRoute>, nextIndex: ?number): NavigationState {
|
||||
const parentState = getParent(state);
|
||||
if (!parentState) {
|
||||
return state;
|
||||
}
|
||||
const routes = nextChildren || parentState.routes;
|
||||
const index = nextIndex == null ? parentState.index : nextIndex;
|
||||
if (routes === parentState.routes && index === parentState.index) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...parentState,
|
||||
index: routes.length - 1,
|
||||
routes,
|
||||
index,
|
||||
};
|
||||
}
|
||||
|
||||
function set(state: ?NavigationState, key: string, nextChildren: Array<NavigationRoute>, nextIndex: number): NavigationState {
|
||||
if (!state) {
|
||||
return {
|
||||
routes: nextChildren,
|
||||
index: nextIndex,
|
||||
};
|
||||
}
|
||||
const parentState = getParent(state);
|
||||
if (!parentState) {
|
||||
return {
|
||||
routes: nextChildren,
|
||||
index: nextIndex,
|
||||
};
|
||||
}
|
||||
if (nextChildren === parentState.routes && nextIndex === parentState.index) {
|
||||
return parentState;
|
||||
}
|
||||
return {
|
||||
...parentState,
|
||||
routes: nextChildren,
|
||||
index: nextIndex,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the focused route of the navigation state by index.
|
||||
*/
|
||||
function jumpToIndex(state: NavigationState, index: number): NavigationState {
|
||||
const parentState = getParent(state);
|
||||
if (parentState && parentState.index === index) {
|
||||
return parentState;
|
||||
if (index === state.index) {
|
||||
return state;
|
||||
}
|
||||
|
||||
invariant(!!state.routes[index], 'invalid index %s to jump to', index);
|
||||
|
||||
return {
|
||||
...parentState,
|
||||
...state,
|
||||
index,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the focused route of the navigation state by key.
|
||||
*/
|
||||
function jumpTo(state: NavigationState, key: string): NavigationState {
|
||||
const parentState = getParent(state);
|
||||
if (!parentState) {
|
||||
return state;
|
||||
}
|
||||
const index = parentState.routes.indexOf(parentState.routes.find(child => child.key === key));
|
||||
invariant(
|
||||
index !== -1,
|
||||
'Cannot find child with matching key in this NavigationRoute'
|
||||
);
|
||||
return {
|
||||
...parentState,
|
||||
index,
|
||||
};
|
||||
const index = indexOf(state, key);
|
||||
return jumpToIndex(state, index);
|
||||
}
|
||||
|
||||
function replaceAt(state: NavigationState, key: string, newState: NavigationRoute): NavigationState {
|
||||
const parentState = getParent(state);
|
||||
if (!parentState) {
|
||||
/**
|
||||
* Replace a route by a key.
|
||||
* Note that this moves the index to the positon to where the new route in the
|
||||
* stack is at.
|
||||
*/
|
||||
function replaceAt(
|
||||
state: NavigationState,
|
||||
key: string,
|
||||
route: NavigationRoute,
|
||||
): NavigationState {
|
||||
const index = indexOf(state, key);
|
||||
return replaceAtIndex(state, index, route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a route by a index.
|
||||
* Note that this moves the index to the positon to where the new route in the
|
||||
* stack is at.
|
||||
*/
|
||||
function replaceAtIndex(
|
||||
state: NavigationState,
|
||||
index: number,
|
||||
route: NavigationRoute,
|
||||
): NavigationState {
|
||||
invariant(
|
||||
!!state.routes[index],
|
||||
'invalid index %s for replacing route %s',
|
||||
index,
|
||||
route.key,
|
||||
);
|
||||
|
||||
if (state.routes[index] === route) {
|
||||
return state;
|
||||
}
|
||||
const routes = [...parentState.routes];
|
||||
const index = parentState.routes.indexOf(parentState.routes.find(child => child.key === key));
|
||||
invariant(
|
||||
index !== -1,
|
||||
'Cannot find child with matching key in this NavigationRoute'
|
||||
);
|
||||
routes[index] = newState;
|
||||
|
||||
const routes = state.routes.slice();
|
||||
routes[index] = route;
|
||||
|
||||
return {
|
||||
...parentState,
|
||||
...state,
|
||||
index,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
|
||||
function replaceAtIndex(state: NavigationState, index: number, newState: NavigationRoute): NavigationState {
|
||||
const parentState = getParent(state);
|
||||
if (!parentState) {
|
||||
return state;
|
||||
/**
|
||||
* Resets all routes.
|
||||
* Note that this moves the index to the positon to where the last route in the
|
||||
* stack is at if the param `index` isn't provided.
|
||||
*/
|
||||
function reset(
|
||||
state: NavigationState,
|
||||
routes: Array<NavigationRoute>,
|
||||
index?: number,
|
||||
): NavigationState {
|
||||
invariant(
|
||||
routes.length && Array.isArray(routes),
|
||||
'invalid routes to replace',
|
||||
);
|
||||
|
||||
const nextIndex: number = index === undefined ? routes.length - 1 : index;
|
||||
|
||||
if (state.routes.length === routes.length && state.index === nextIndex) {
|
||||
const compare = (route, ii) => routes[ii] === route;
|
||||
if (state.routes.every(compare)) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
const routes = [...parentState.routes];
|
||||
routes[index] = newState;
|
||||
|
||||
invariant(!!routes[nextIndex], 'invalid index %s to reset', nextIndex);
|
||||
|
||||
return {
|
||||
...parentState,
|
||||
...state,
|
||||
index: nextIndex,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
|
||||
const NavigationStateUtils = {
|
||||
get: get,
|
||||
getParent,
|
||||
has,
|
||||
indexOf,
|
||||
jumpTo,
|
||||
jumpToIndex,
|
||||
|
@ -186,7 +200,6 @@ const NavigationStateUtils = {
|
|||
replaceAt,
|
||||
replaceAtIndex,
|
||||
reset,
|
||||
set: set,
|
||||
};
|
||||
|
||||
module.exports = NavigationStateUtils;
|
||||
|
|
|
@ -5,52 +5,138 @@
|
|||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @flow-broken
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest
|
||||
.unmock('NavigationStateUtils');
|
||||
jest.unmock('NavigationStateUtils');
|
||||
|
||||
var NavigationStateUtils = require('NavigationStateUtils');
|
||||
|
||||
var VALID_PARENT_STATES = [
|
||||
{routes: ['a','b'], index: 0},
|
||||
{routes: [{key: 'a'},{key: 'b', foo: 123}], index: 1},
|
||||
{routes: [{key: 'a'},{key: 'b'}], index: 0},
|
||||
{routes: [{key: 'a'},{key: 'b'}], index: 2},
|
||||
];
|
||||
var INVALID_PARENT_STATES = [
|
||||
'foo',
|
||||
{},
|
||||
{routes: [{key: 'a'}], index: 4},
|
||||
{routes: [{key: 'a'}], index: -1},
|
||||
{routes: [{key: 'a'}]},
|
||||
{routes: {key: 'foo'}},
|
||||
12,
|
||||
null,
|
||||
undefined,
|
||||
[],
|
||||
];
|
||||
const NavigationStateUtils = require('NavigationStateUtils');
|
||||
|
||||
describe('NavigationStateUtils', () => {
|
||||
|
||||
it('identifies parents correctly with getParent', () => {
|
||||
for (var i = 0; i <= VALID_PARENT_STATES.length; i++) {
|
||||
var navState = VALID_PARENT_STATES[0];
|
||||
expect(NavigationStateUtils.getParent(navState)).toBe(navState);
|
||||
}
|
||||
for (var i = 0; i <= INVALID_PARENT_STATES.length; i++) {
|
||||
var navState = INVALID_PARENT_STATES[0];
|
||||
expect(NavigationStateUtils.getParent(navState)).toBe(null);
|
||||
}
|
||||
// Getters
|
||||
it('gets route', () => {
|
||||
const state = {index: 0, routes: [{key: 'a'}]};
|
||||
expect(NavigationStateUtils.get(state, 'a')).toEqual({key: 'a'});
|
||||
expect(NavigationStateUtils.get(state, 'b')).toBe(null);
|
||||
});
|
||||
|
||||
it('can get routes', () => {
|
||||
var fooState = {key: 'foo'};
|
||||
var navState = {routes: [{key: 'foobar'}, fooState], index: 0};
|
||||
expect(NavigationStateUtils.get(navState, 'foo')).toBe(fooState);
|
||||
expect(NavigationStateUtils.get(navState, 'missing')).toBe(null);
|
||||
it('gets route index', () => {
|
||||
const state = {index: 1, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
expect(NavigationStateUtils.indexOf(state, 'a')).toBe(0);
|
||||
expect(NavigationStateUtils.indexOf(state, 'b')).toBe(1);
|
||||
expect(NavigationStateUtils.indexOf(state, 'c')).toBe(-1);
|
||||
});
|
||||
|
||||
it('has a route', () => {
|
||||
const state = {index: 0, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
expect(NavigationStateUtils.has(state, 'b')).toBe(true);
|
||||
expect(NavigationStateUtils.has(state, 'c')).toBe(false);
|
||||
});
|
||||
|
||||
// Push
|
||||
it('pushes a route', () => {
|
||||
const state = {index: 0, routes: [{key: 'a'}]};
|
||||
const newState = {index: 1, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
expect(NavigationStateUtils.push(state, {key: 'b'})).toEqual(newState);
|
||||
});
|
||||
|
||||
it('does not push duplicated route', () => {
|
||||
const state = {index: 0, routes: [{key: 'a'}]};
|
||||
expect(() => NavigationStateUtils.push(state, {key: 'a'})).toThrow();
|
||||
});
|
||||
|
||||
// Pop
|
||||
it('pops route', () => {
|
||||
const state = {index: 1, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
const newState = {index: 0, routes: [{key: 'a'}]};
|
||||
expect(NavigationStateUtils.pop(state)).toEqual(newState);
|
||||
});
|
||||
|
||||
it('does not pop route if not applicable', () => {
|
||||
const state = {index: 0, routes: [{key: 'a'}]};
|
||||
expect(NavigationStateUtils.pop(state)).toBe(state);
|
||||
});
|
||||
|
||||
// Jump
|
||||
it('jumps to new index', () => {
|
||||
const state = {index: 0, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
const newState = {index: 1, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
expect(NavigationStateUtils.jumpToIndex(state, 0)).toBe(state);
|
||||
expect(NavigationStateUtils.jumpToIndex(state, 1)).toEqual(newState);
|
||||
});
|
||||
|
||||
it('throws if jumps to invalid index', () => {
|
||||
const state = {index: 0, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
expect(() => NavigationStateUtils.jumpToIndex(state, 2)).toThrow();
|
||||
});
|
||||
|
||||
it('jumps to new key', () => {
|
||||
const state = {index: 0, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
const newState = {index: 1, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
expect(NavigationStateUtils.jumpTo(state, 'a')).toBe(state);
|
||||
expect(NavigationStateUtils.jumpTo(state, 'b')).toEqual(newState);
|
||||
});
|
||||
|
||||
it('throws if jumps to invalid key', () => {
|
||||
const state = {index: 0, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
expect(() => NavigationStateUtils.jumpTo(state, 'c')).toThrow();
|
||||
});
|
||||
|
||||
// Replace
|
||||
it('Replaces by key', () => {
|
||||
const state = {index: 0, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
const newState = {index: 1, routes: [{key: 'a'}, {key: 'c'}]};
|
||||
expect(
|
||||
NavigationStateUtils.replaceAt(
|
||||
state,
|
||||
'b',
|
||||
{key: 'c'},
|
||||
)
|
||||
).toEqual(newState);
|
||||
});
|
||||
|
||||
it('Replaces by index', () => {
|
||||
const state = {index: 0, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
const newState = {index: 1, routes: [{key: 'a'}, {key: 'c'}]};
|
||||
expect(
|
||||
NavigationStateUtils.replaceAtIndex(
|
||||
state,
|
||||
1,
|
||||
{key: 'c'},
|
||||
)
|
||||
).toEqual(newState);
|
||||
});
|
||||
|
||||
// Reset
|
||||
it('Resets routes', () => {
|
||||
const state = {index: 0, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
const newState = {index: 1, routes: [{key: 'x'}, {key: 'y'}]};
|
||||
expect(
|
||||
NavigationStateUtils.reset(
|
||||
state,
|
||||
[{key: 'x'}, {key: 'y'}],
|
||||
)
|
||||
).toEqual(newState);
|
||||
|
||||
expect(() => {
|
||||
NavigationStateUtils.reset(state, []);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('Resets routes with index', () => {
|
||||
const state = {index: 0, routes: [{key: 'a'}, {key: 'b'}]};
|
||||
const newState = {index: 0, routes: [{key: 'x'}, {key: 'y'}]};
|
||||
expect(
|
||||
NavigationStateUtils.reset(
|
||||
state,
|
||||
[{key: 'x'}, {key: 'y'}],
|
||||
0,
|
||||
)
|
||||
).toEqual(newState);
|
||||
|
||||
expect(() => {
|
||||
NavigationStateUtils.reset(state, [{key: 'x'}, {key: 'y'}], 100);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue