From 55477ffd67e031cc9fed0fdabbcfe830894fba25 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Wed, 16 Mar 2016 17:19:59 -0700 Subject: [PATCH] Make NavigationLegacyNavigator more testable. Summary:- Move the logics that manage the routes stack into `NavigationLegacyNavigatorRouteStack` - Add more unit tests for NavigationLegacyNavigatorRouteStack. - Keep NavigationLegacyNavigator as a pure view as possible as we could. Reviewed By: fkgozali Differential Revision: D3060459 fb-gh-sync-id: 2c6802115c3f6ca5e396903f0d314ff54129524c shipit-source-id: 2c6802115c3f6ca5e396903f0d314ff54129524c --- .../NavigationLegacyNavigator.js | 60 ++--------- .../NavigationLegacyNavigatorRouteStack.js | 100 +++++++++++++++--- ...avigationLegacyNavigatorRouteStack-test.js | 87 ++++++++++++--- 3 files changed, 168 insertions(+), 79 deletions(-) diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigator.js b/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigator.js index bbb0221a8d2095..07886df152d43c 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigator.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigator.js @@ -45,7 +45,6 @@ const NavigatorSceneConfigs = require('NavigatorSceneConfigs'); const React = require('react-native'); const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); -const invariant = require('fbjs/lib/invariant'); const guid = require('guid'); import type { @@ -129,20 +128,15 @@ class NavigationLegacyNavigator extends React.Component { } jumpTo(route: any): void { - const index = this._stack.indexOf(route); - invariant( - index > -1, - 'Cannot jump to route that is not in the route stack' - ); - this._jumpToIndex(index); + this._applyStack(this._stack.jumpTo(route)); } jumpForward(): void { - this._jumpToIndex(this._stack.index + 1); + this._applyStack(this._stack.jumpForward()); } jumpBack(): void { - this._jumpToIndex(this._stack.index - 1); + this._applyStack(this._stack.jumpBack()); } push(route: any): void { @@ -150,25 +144,11 @@ class NavigationLegacyNavigator extends React.Component { } pop(): void { - const stack = this._stack; - if (stack.size > 1) { - this._applyStack(stack.pop()); - } + this._applyStack(this._stack.pop()); } replaceAtIndex(route: any, index: number): void { - const stack = this._stack; - - if (index < 0) { - index += stack.size; - } - - if (index >= stack.size) { - // Nothing to replace. - return; - } - - this._applyStack(stack.replaceAtIndex(index, route)); + this._applyStack(this._stack.replaceAtIndex(index, route)); } replace(route: any): void { @@ -184,49 +164,27 @@ class NavigationLegacyNavigator extends React.Component { } popToRoute(route: any): void { - const stack = this._stack; - const nextIndex = stack.indexOf(route); - invariant( - nextIndex > -1, - 'Calling popToRoute for a route that doesn\'t exist!' - ); - this._applyStack(stack.slice(0, nextIndex + 1)); + this._applyStack(this._stack.popToRoute(route)); } replacePreviousAndPop(route: any): void { - const stack = this._stack; - const nextIndex = stack.index - 1; - if (nextIndex < 0) { - return; - } - this._applyStack(stack.replaceAtIndex(nextIndex, route).pop()); + this._applyStack(this._stack.replacePreviousAndPop(route)); } resetTo(route: any): void { - invariant(!!route, 'Must supply route'); - this._applyStack(this._stack.slice(0).replaceAtIndex(0, route)); + this._applyStack(this._stack.resetTo(route)); } immediatelyResetRouteStack(routes: Array): void { - const index = routes.length - 1; - const stack = new RouteStack(index, routes); // Immediately blow away all current scenes with a new key. this._key = guid(); - this._applyStack(stack); + this._applyStack(this._stack.resetRoutes(routes)); } getCurrentRoutes(): Array { return this._stack.toArray(); } - _jumpToIndex(index: number): void { - const stack = this._stack; - if (index < 0 || index >= stack.size) { - return; - } - this._applyStack(stack.jumpToIndex(index)); - } - // Lyfe cycle and private methods below. shouldComponentUpdate(nextProps: Object, nextState: Object): boolean { diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigatorRouteStack.js b/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigatorRouteStack.js index 302678f3e0f5a7..9661a6c0e70fe1 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigatorRouteStack.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigatorRouteStack.js @@ -1,5 +1,10 @@ /** - * Copyright 2004-present Facebook. All Rights Reserved. + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * 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. * * @providesModule NavigationLegacyNavigatorRouteStack * @flow @@ -85,8 +90,7 @@ class RouteNode { } toNavigationState(): NavigationState { - const state: NavigationState = this; - return state; + return this; } } @@ -114,7 +118,7 @@ class RouteStack { invariant( index > -1 && index <= routes.length - 1, - 'index out of bound' + 'RouteStack: index out of bound' ); @@ -187,7 +191,7 @@ class RouteStack { } for (let ii = 0, jj = this._routeNodes.length; ii < jj; ii++) { - let node = this._routeNodes[ii]; + const node = this._routeNodes[ii]; if (node.route === route) { return ii; } @@ -230,16 +234,29 @@ class RouteStack { * excluding the last index in this stack. */ pop(): RouteStack { - invariant( - this._routeNodes.length > 1, - 'should not pop routeNodes stack to empty' - ); + if (this._routeNodes.length <= 1) { + return this; + } // When popping, removes the rest of the routes past the current index. const routeNodes = this._routeNodes.slice(0, this._index); return this._update(routeNodes.length - 1, routeNodes); } + popToRoute(route: any): RouteStack { + const index = this.indexOf(route); + invariant( + index > -1, + 'Calling popToRoute for a route that doesn\'t exist!' + ); + return this.slice(0, index + 1); + } + + jumpTo(route: any): RouteStack { + const index = this.indexOf(route); + return this.jumpToIndex(index); + } + jumpToIndex(index: number): RouteStack { invariant( index > -1 && index < this._routeNodes.length, @@ -249,6 +266,22 @@ class RouteStack { return this._update(index, this._routeNodes); } + jumpForward(): RouteStack { + const index = this._index + 1; + if (index >= this._routeNodes.length) { + return this; + } + return this._update(index, this._routeNodes); + } + + jumpBack(): RouteStack { + const index = this._index - 1; + if (index < 0) { + return this; + } + return this._update(index, this._routeNodes); + } + /** * Replace a route in the navigation stack. * @@ -267,20 +300,59 @@ class RouteStack { invariant(this.indexOf(route) === -1, 'route must be unique'); + const size = this._routeNodes.length; if (index < 0) { - index += this._routeNodes.length; + index += size; } - invariant( - index > -1 && index < this._routeNodes.length, - 'replaceAtIndex: index out of bound' - ); + if (index < 0 || index >= size) { + return this; + } const routeNodes = this._routeNodes.slice(0); routeNodes[index] = new RouteNode(route); return this._update(index, routeNodes); } + replacePreviousAndPop(route: any): RouteStack { + if (this._index < 1) { + // stack is too small. + return this; + } + + const index = this.indexOf(route); + invariant( + index === -1 || index === this._index - 1, + 'route already exists in the stack' + ); + + return this.replaceAtIndex(this._index - 1, route).popToRoute(route); + } + + // Reset + + /** + * Replace the current active route with a new route, and pops out + * the rest routes after it. + */ + resetTo(route: any): RouteStack { + invariant(!isRouteEmpty(route), 'Must supply route'); + const index = this.indexOf(route); + if (index === this._index) { + // Already has this active route. + return this; + } + invariant(index === -1, 'route already exists in the stack'); + const routeNodes = this._routeNodes.slice(0, this._index); + routeNodes.push(new RouteNode(route)); + return this._update(routeNodes.length - 1, routeNodes); + } + + resetRoutes(routes: Array): RouteStack { + const index = routes.length - 1; + return new RouteStack(index, routes); + } + // Iterations forEach(callback: IterationCallback, context: ?Object): void { this._routeNodes.forEach((node, index) => { diff --git a/Libraries/CustomComponents/NavigationExperimental/__tests__/NavigationLegacyNavigatorRouteStack-test.js b/Libraries/CustomComponents/NavigationExperimental/__tests__/NavigationLegacyNavigatorRouteStack-test.js index 30a965d1cc3175..9063b49dc8f8a4 100644 --- a/Libraries/CustomComponents/NavigationExperimental/__tests__/NavigationLegacyNavigatorRouteStack-test.js +++ b/Libraries/CustomComponents/NavigationExperimental/__tests__/NavigationLegacyNavigatorRouteStack-test.js @@ -1,5 +1,10 @@ /** - * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * 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. * * Facebook, Inc. ("Facebook") owns all right, title and interest, including * all intellectual property and other proprietary rights, in and to the React @@ -24,9 +29,7 @@ */ 'use strict'; -jest - .autoMockOff() - .mock('ErrorUtils'); +jest.dontMock('NavigationLegacyNavigatorRouteStack'); const NavigationLegacyNavigatorRouteStack = require('NavigationLegacyNavigatorRouteStack'); @@ -263,11 +266,20 @@ describe('NavigationLegacyNavigatorRouteStack:', () => { expect(stack2.index).toBe(0); }); - it('throws when popping to empty stack', () => { - expect(() => { - const stack = new NavigationLegacyNavigatorRouteStack(0, ['a']); - stack.pop(); - }).toThrow(); + it('does nothing while popping to empty', () => { + const stack = new NavigationLegacyNavigatorRouteStack(0, ['a']); + expect(stack.pop()).toBe(stack); + expect(stack.pop().pop()).toBe(stack); + }); + + it('pops to route', () => { + const stack = new NavigationLegacyNavigatorRouteStack(1, ['a', 'b', 'c']); + expect(stack.popToRoute('b').toArray()).toEqual(['a', 'b']); + expect(stack.popToRoute('b').index).toBe(1); + expect(stack.popToRoute('a').toArray()).toEqual(['a']); + expect(stack.popToRoute('a').index).toBe(0); + + expect(() => {stack.popToRoute('x');}).toThrow(); }); // Jump @@ -291,6 +303,24 @@ describe('NavigationLegacyNavigatorRouteStack:', () => { }).toThrow(); }); + it('jumps to route', () => { + const stack = new NavigationLegacyNavigatorRouteStack(0, ['a', 'b']); + expect(stack.jumpTo('b').index).toBe(1); + }); + + it('jumps backward', () => { + const stack = new NavigationLegacyNavigatorRouteStack(1, ['a', 'b']); + expect(stack.jumpBack().index).toBe(0); + expect(stack.jumpBack().jumpBack().jumpBack().index).toBe(0); + }); + + it('jumps forward', () => { + const stack = new NavigationLegacyNavigatorRouteStack(0, ['a', 'b']); + expect(stack.jumpForward().index).toBe(1); + expect(stack.jumpForward().jumpForward().jumpForward().index).toBe(1); + }); + + // Replace it('replaces route at index', () => { const stack1 = new NavigationLegacyNavigatorRouteStack(1, ['a', 'b']); @@ -317,11 +347,40 @@ describe('NavigationLegacyNavigatorRouteStack:', () => { }).toThrow(); }); - it('throws when replacing at index out of bound', () => { - expect(() => { - const stack = new NavigationLegacyNavigatorRouteStack(1, ['a', 'b']); - stack.replaceAtIndex(100, 'x'); - }).toThrow(); + it('does nothing when replacing at index out of bound', () => { + const stack = new NavigationLegacyNavigatorRouteStack(1, ['a', 'b']); + expect(stack.replaceAtIndex(100, 'x')).toBe(stack); + expect(stack.replaceAtIndex(-100, 'x')).toBe(stack); + }); + + it('replaces previous and pop route', () => { + const stack = new NavigationLegacyNavigatorRouteStack(1, ['a', 'b', 'c']); + expect(stack.replacePreviousAndPop('x').toArray()).toEqual(['x']); + expect(stack.replacePreviousAndPop('x').index).toBe(0); + }); + + it('does nothing when there is nothing to replace', () => { + const stack = new NavigationLegacyNavigatorRouteStack(0, ['a', 'b', 'c']); + expect(stack.replacePreviousAndPop('x')).toBe(stack); + }); + + // Reset + + it('resets route', () => { + const stack = new NavigationLegacyNavigatorRouteStack(1, ['a', 'b', 'c']); + expect(stack.resetTo('b')).toBe(stack); + + expect(stack.resetTo('x').toArray()).toEqual(['a', 'x']); + expect(stack.resetTo('x').index).toBe(1); + + expect(() => {stack.resetTo(null);}).toThrow(); + expect(() => {stack.resetTo('a');}).toThrow(); + }); + + it('resets routes', () => { + const stack = new NavigationLegacyNavigatorRouteStack(0, ['a']); + expect(stack.resetRoutes(['x', 'y']).toArray()).toEqual(['x', 'y']); + expect(stack.resetRoutes(['x', 'y']).index).toBe(1); }); // Iteration