Skip to content
This repository was archived by the owner on Feb 25, 2020. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 80 additions & 30 deletions src/__tests__/createAppContainer-test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -59,46 +58,84 @@ describe('NavigationContainer', () => {
const NavigationContainer = createAppContainer(Stack);

describe('state.nav', () => {
it("should be preloaded with the router's initial state", () => {
const navigationContainer = renderer
.create(<NavigationContainer />)
.getInstance();
it("should be preloaded with the router's initial state", async () => {
const testRenderer = TestRenderer.create(<NavigationContainer />);
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);
expect(navigationContainer.state.nav.routes[0]).toMatchObject({
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(<NavigationContainer />);
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(<NavigationContainer />)
.getInstance();
it('returns true when given a valid action', async () => {
const testRenderer = TestRenderer.create(<NavigationContainer />);
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' })
)
).toEqual(true);
});

it('returns false when given an invalid action', () => {
const navigationContainer = renderer
.create(<NavigationContainer />)
.getInstance();
it('returns false when given an invalid action', async () => {
const testRenderer = TestRenderer.create(<NavigationContainer />);
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(<NavigationContainer />)
.getInstance();
it('updates state.nav with an action by the next tick', async () => {
const testRenderer = TestRenderer.create(<NavigationContainer />);
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(
Expand All @@ -115,10 +152,14 @@ describe('NavigationContainer', () => {
});
});

it('does not discard actions when called twice in one tick', () => {
const navigationContainer = renderer
.create(<NavigationContainer />)
.getInstance();
it('does not discard actions when called twice in one tick', async () => {
const testRenderer = TestRenderer.create(<NavigationContainer />);
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)
);
Expand Down Expand Up @@ -153,10 +194,14 @@ describe('NavigationContainer', () => {
});
});

it('does not discard actions when called more than 2 times in one tick', () => {
const navigationContainer = renderer
.create(<NavigationContainer />)
.getInstance();
it('does not discard actions when called more than 2 times in one tick', async () => {
const testRenderer = TestRenderer.create(<NavigationContainer />);
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)
);
Expand Down Expand Up @@ -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 <View />;
Expand Down Expand Up @@ -267,7 +312,12 @@ describe('NavigationContainer', () => {
})
);

renderer.create(<RootStack />).toJSON();
TestRenderer.create(<RootStack />);

// the state only actually gets set asynchronously on componentDidMount
// wait for the state to be set
await flushPromises();

expect(spy).toMatchSnapshot();
});
});
Expand Down
45 changes: 24 additions & 21 deletions src/createAppContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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) {
Expand All @@ -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({
Expand Down
4 changes: 4 additions & 0 deletions src/utils/flushPromises.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// see https://github.com/facebook/jest/issues/2157#issuecomment-279171856
export default function flushPromises() {
return new Promise(resolve => setImmediate(resolve));
}