diff --git a/src/__tests__/createAppContainer-test.js b/src/__tests__/createAppContainer-test.js index a3dd106..2b6864d 100644 --- a/src/__tests__/createAppContainer-test.js +++ b/src/__tests__/createAppContainer-test.js @@ -1,8 +1,7 @@ import React from 'react'; -import { View } from 'react-native'; - -import renderer from 'react-test-renderer'; - +import { View, Linking } from 'react-native'; +import TestRenderer from 'react-test-renderer'; +import flushPromises from '../utils/flushPromises'; import createAppContainer, { _TESTING_ONLY_reset_container_count, } from '../createAppContainer'; @@ -59,10 +58,16 @@ describe('NavigationContainer', () => { const NavigationContainer = createAppContainer(Stack); describe('state.nav', () => { - it("should be preloaded with the router's initial state", () => { - const navigationContainer = renderer - .create() - .getInstance(); + it("should be preloaded with the router's initial state", async () => { + const testRenderer = TestRenderer.create(); + const navigationContainer = testRenderer.getInstance(); + + // the state only actually gets set asynchronously on componentDidMount + // thus on the first render the component returns null (or the result of renderLoadingExperimental) + expect(testRenderer.toJSON()).toEqual(null); + // wait for the state to be set + await flushPromises(); + expect(navigationContainer.state.nav).toMatchObject({ index: 0 }); expect(navigationContainer.state.nav.routes).toBeInstanceOf(Array); expect(navigationContainer.state.nav.routes.length).toBe(1); @@ -70,14 +75,38 @@ describe('NavigationContainer', () => { routeName: 'foo', }); }); + it('should be preloaded with the state corresponding to the URL', async () => { + const standardGetInitialURL = Linking.getInitialURL; + Linking.getInitialURL = () => Promise.resolve('host://elk'); + const testRenderer = TestRenderer.create(); + const navigationContainer = testRenderer.getInstance(); + + // the state only actually gets set asynchronously on componentDidMount + // wait for the state to be set + await flushPromises(); + + expect(navigationContainer.state.nav).toMatchObject({ index: 0 }); + expect(navigationContainer.state.nav.routes).toBeInstanceOf(Array); + expect(navigationContainer.state.nav.routes.length).toBe(1); + expect(navigationContainer.state.nav.routes[0]).toMatchObject({ + routeName: 'elk', + }); + + Linking.getInitialURL = standardGetInitialURL; + }); }); describe('dispatch', () => { - it('returns true when given a valid action', () => { - const navigationContainer = renderer - .create() - .getInstance(); + it('returns true when given a valid action', async () => { + const testRenderer = TestRenderer.create(); + const navigationContainer = testRenderer.getInstance(); + + // the state only actually gets set asynchronously on componentDidMount + // wait for the state to be set + await flushPromises(); + jest.runOnlyPendingTimers(); + expect( navigationContainer.dispatch( NavigationActions.navigate({ routeName: 'bar' }) @@ -85,20 +114,28 @@ describe('NavigationContainer', () => { ).toEqual(true); }); - it('returns false when given an invalid action', () => { - const navigationContainer = renderer - .create() - .getInstance(); + it('returns false when given an invalid action', async () => { + const testRenderer = TestRenderer.create(); + const navigationContainer = testRenderer.getInstance(); + + // the state only actually gets set asynchronously on componentDidMount + // wait for the state to be set + await flushPromises(); + jest.runOnlyPendingTimers(); + expect(navigationContainer.dispatch(NavigationActions.back())).toEqual( false ); }); - it('updates state.nav with an action by the next tick', () => { - const navigationContainer = renderer - .create() - .getInstance(); + it('updates state.nav with an action by the next tick', async () => { + const testRenderer = TestRenderer.create(); + const navigationContainer = testRenderer.getInstance(); + + // the state only actually gets set asynchronously on componentDidMount + // wait for the state to be set + await flushPromises(); expect( navigationContainer.dispatch( @@ -115,10 +152,14 @@ describe('NavigationContainer', () => { }); }); - it('does not discard actions when called twice in one tick', () => { - const navigationContainer = renderer - .create() - .getInstance(); + it('does not discard actions when called twice in one tick', async () => { + const testRenderer = TestRenderer.create(); + const navigationContainer = testRenderer.getInstance(); + + // the state only actually gets set asynchronously on componentDidMount + // wait for the state to be set + await flushPromises(); + const initialState = JSON.parse( JSON.stringify(navigationContainer.state.nav) ); @@ -153,10 +194,14 @@ describe('NavigationContainer', () => { }); }); - it('does not discard actions when called more than 2 times in one tick', () => { - const navigationContainer = renderer - .create() - .getInstance(); + it('does not discard actions when called more than 2 times in one tick', async () => { + const testRenderer = TestRenderer.create(); + const navigationContainer = testRenderer.getInstance(); + + // the state only actually gets set asynchronously on componentDidMount + // wait for the state to be set + await flushPromises(); + const initialState = JSON.parse( JSON.stringify(navigationContainer.state.nav) ); @@ -238,7 +283,7 @@ describe('NavigationContainer', () => { let spy = spyConsole(); - it('warns when you render more than one container explicitly', () => { + it('warns when you render more than one container explicitly', async () => { class BlankScreen extends React.Component { render() { return ; @@ -267,7 +312,12 @@ describe('NavigationContainer', () => { }) ); - renderer.create().toJSON(); + TestRenderer.create(); + + // the state only actually gets set asynchronously on componentDidMount + // wait for the state to be set + await flushPromises(); + expect(spy).toMatchSnapshot(); }); }); diff --git a/src/createAppContainer.js b/src/createAppContainer.js index 0c0daf5..56840b5 100644 --- a/src/createAppContainer.js +++ b/src/createAppContainer.js @@ -97,7 +97,9 @@ export default function createNavigationContainer(Component) { this.state = { nav: - this._isStateful() && !props.persistenceKey + this._isStateful() && + !props.persistenceKey && + props.enableURLHandling === false ? Component.router.getStateForAction(this._initialAction) : null, }; @@ -220,23 +222,8 @@ export default function createNavigationContainer(Component) { // Initialize state. This must be done *after* any async code // so we don't end up with a different value for this.state.nav // due to changes while async function was resolving - let action = this._initialAction; + let action = null; let startupState = this.state.nav; - if (!startupState) { - !!process.env.REACT_NAV_LOGGING && - console.log('Init new Navigation State'); - startupState = Component.router.getStateForAction(action); - } - - // Pull persisted state from AsyncStorage - if (startupStateJSON) { - try { - startupState = JSON.parse(startupStateJSON); - _reactNavigationIsHydratingState = true; - } catch (e) { - /* do nothing */ - } - } // Pull state out of URL if (parsedUrl) { @@ -252,13 +239,29 @@ export default function createNavigationContainer(Component) { parsedUrl ); action = urlAction; - startupState = Component.router.getStateForAction( - urlAction, - startupState - ); + // This is an **initial** URL, hence the null state as parameter + startupState = Component.router.getStateForAction(urlAction, null); } } + // Pull persisted state from AsyncStorage + if (startupStateJSON) { + try { + startupState = JSON.parse(startupStateJSON); + _reactNavigationIsHydratingState = true; + } catch (e) { + /* do nothing */ + } + } + + // Initialize state if there was no valid initial URL nor valid persisted state + if (!startupState) { + !!process.env.REACT_NAV_LOGGING && + console.log('Init new Navigation State'); + action = this._initialAction; + startupState = Component.router.getStateForAction(action); + } + const dispatchActions = () => this._actionEventSubscribers.forEach(subscriber => subscriber({ diff --git a/src/utils/flushPromises.js b/src/utils/flushPromises.js new file mode 100644 index 0000000..ab7436b --- /dev/null +++ b/src/utils/flushPromises.js @@ -0,0 +1,4 @@ +// see https://github.com/facebook/jest/issues/2157#issuecomment-279171856 +export default function flushPromises() { + return new Promise(resolve => setImmediate(resolve)); +}