diff --git a/Examples/UIExplorer/UIExplorerNavigationReducer.js b/Examples/UIExplorer/UIExplorerNavigationReducer.js index 741c6abb7..cc2016914 100644 --- a/Examples/UIExplorer/UIExplorerNavigationReducer.js +++ b/Examples/UIExplorer/UIExplorerNavigationReducer.js @@ -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; } diff --git a/Libraries/NavigationExperimental/NavigationStateUtils.js b/Libraries/NavigationExperimental/NavigationStateUtils.js index 8ff97e844..14d5166a2 100644 --- a/Libraries/NavigationExperimental/NavigationStateUtils.js +++ b/Libraries/NavigationExperimental/NavigationStateUtils.js @@ -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 = 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, 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, 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, + 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; diff --git a/Libraries/NavigationExperimental/__tests__/NavigationStateUtils-test.js b/Libraries/NavigationExperimental/__tests__/NavigationStateUtils-test.js index cccd07fb4..7b037fff8 100644 --- a/Libraries/NavigationExperimental/__tests__/NavigationStateUtils-test.js +++ b/Libraries/NavigationExperimental/__tests__/NavigationStateUtils-test.js @@ -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(); }); });