react-native-navigation
Version:
React Native Navigation - truly native navigation for iOS and Android
759 lines (758 loc) • 25.2 kB
JavaScript
"use strict";
import forEach from 'lodash/forEach';
import filter from 'lodash/filter';
import invoke from 'lodash/invoke';
import { mock, verify, instance, deepEqual, when, anything, anyString, capture } from 'ts-mockito';
import { LayoutTreeParser } from "./LayoutTreeParser.js";
import { LayoutTreeCrawler } from "./LayoutTreeCrawler.js";
import { Store } from "../components/Store.js";
import { Commands } from "./Commands.js";
import { CommandsObserver } from "../events/CommandsObserver.js";
import { NativeCommandsSender } from "../adapters/NativeCommandsSender.js";
import { OptionsProcessor } from "./OptionsProcessor.js";
import { UniqueIdProvider } from "../adapters/UniqueIdProvider.js";
import { LayoutProcessor } from "../processors/LayoutProcessor.js";
import { LayoutProcessorsStore } from "../processors/LayoutProcessorsStore.js";
import { CommandName } from "../interfaces/CommandName.js";
import { OptionsCrawler } from "./OptionsCrawler.js";
import React from 'react';
describe('Commands', () => {
let uut;
let mockedNativeCommandsSender;
let mockedOptionsProcessor;
let mockedStore;
let commandsObserver;
let mockedUniqueIdProvider;
let layoutProcessor;
beforeEach(() => {
mockedNativeCommandsSender = mock(NativeCommandsSender);
mockedUniqueIdProvider = mock(UniqueIdProvider);
when(mockedUniqueIdProvider.generate(anything())).thenCall(prefix => `${prefix}+UNIQUE_ID`);
const uniqueIdProvider = instance(mockedUniqueIdProvider);
mockedStore = mock(Store);
commandsObserver = new CommandsObserver(uniqueIdProvider);
const layoutProcessorsStore = new LayoutProcessorsStore();
mockedOptionsProcessor = mock(OptionsProcessor);
const optionsProcessor = instance(mockedOptionsProcessor);
layoutProcessor = new LayoutProcessor(layoutProcessorsStore);
jest.spyOn(layoutProcessor, 'process');
uut = new Commands(instance(mockedStore), instance(mockedNativeCommandsSender), new LayoutTreeParser(uniqueIdProvider), new LayoutTreeCrawler(instance(mockedStore), optionsProcessor), commandsObserver, uniqueIdProvider, optionsProcessor, layoutProcessor, new OptionsCrawler(instance(mockedStore), uniqueIdProvider));
});
describe('setRoot', () => {
it('sends setRoot to native after parsing into a correct layout tree', () => {
uut.setRoot({
root: {
component: {
name: 'com.example.MyScreen'
}
}
});
verify(mockedNativeCommandsSender.setRoot('setRoot+UNIQUE_ID', deepEqual({
root: {
type: 'Component',
id: 'Component+UNIQUE_ID',
children: [],
data: {
name: 'com.example.MyScreen',
options: {},
passProps: undefined
}
},
modals: [],
overlays: []
}))).called();
});
it('returns a promise with the resolved layout', async () => {
when(mockedNativeCommandsSender.setRoot(anything(), anything())).thenResolve('the resolved layout');
const result = await uut.setRoot({
root: {
component: {
name: 'com.example.MyScreen'
}
}
});
expect(result).toEqual('the resolved layout');
});
it('inputs modals and overlays', () => {
uut.setRoot({
root: {
component: {
name: 'com.example.MyScreen'
}
},
modals: [{
component: {
name: 'com.example.MyModal'
}
}],
overlays: [{
component: {
name: 'com.example.MyOverlay'
}
}]
});
verify(mockedNativeCommandsSender.setRoot('setRoot+UNIQUE_ID', deepEqual({
root: {
type: 'Component',
id: 'Component+UNIQUE_ID',
children: [],
data: {
name: 'com.example.MyScreen',
options: {},
passProps: undefined
}
},
modals: [{
type: 'Component',
id: 'Component+UNIQUE_ID',
children: [],
data: {
name: 'com.example.MyModal',
options: {},
passProps: undefined
}
}],
overlays: [{
type: 'Component',
id: 'Component+UNIQUE_ID',
children: [],
data: {
name: 'com.example.MyOverlay',
options: {},
passProps: undefined
}
}]
}))).called();
});
it('process layout with layoutProcessor', () => {
uut.setRoot({
root: {
component: {
name: 'com.example.MyScreen'
}
}
});
expect(layoutProcessor.process).toBeCalledWith({
component: {
name: 'com.example.MyScreen',
options: {},
id: 'Component+UNIQUE_ID'
}
}, CommandName.SetRoot);
});
it('pass component static options to layoutProcessor', () => {
when(mockedStore.getComponentClassForName('com.example.MyScreen')).thenReturn(() => class extends React.Component {
static options() {
return {
topBar: {
visible: false
}
};
}
});
uut.setRoot({
root: {
component: {
name: 'com.example.MyScreen'
}
}
});
expect(layoutProcessor.process).toBeCalledWith({
component: {
id: 'Component+UNIQUE_ID',
name: 'com.example.MyScreen',
options: {
topBar: {
visible: false
}
}
}
}, CommandName.SetRoot);
});
it('retains passProps properties identity', () => {
const obj = {
some: 'content'
};
uut.setRoot({
root: {
component: {
name: 'com.example.MyScreen',
passProps: {
obj
}
}
}
});
const args = capture(mockedStore.setPendingProps).last();
expect(args[1].obj).toBe(obj);
});
});
describe('mergeOptions', () => {
it('passes options for component', () => {
uut.mergeOptions('theComponentId', {
blurOnUnmount: true
});
verify(mockedNativeCommandsSender.mergeOptions('theComponentId', deepEqual({
blurOnUnmount: true
}))).called();
});
it('show warning when invoking before componentDidMount', () => {
jest.spyOn(console, 'warn');
when(mockedStore.getComponentInstance('component1')).thenReturn({});
const componentId = 'component1';
uut.mergeOptions(componentId, {
blurOnUnmount: true
});
expect(console.warn).toBeCalledWith(`Navigation.mergeOptions was invoked on component with id: ${componentId} before it is mounted, this can cause UI issues and should be avoided.\n Use static options instead.`);
});
it('should not show warning for mounted component', () => {
jest.spyOn(console, 'warn');
const componentId = 'component1';
when(mockedStore.getComponentInstance('component1')).thenReturn({
isMounted: true
});
uut.mergeOptions('component1', {
blurOnUnmount: true
});
expect(console.warn).not.toBeCalledWith(`Navigation.mergeOptions was invoked on component with id: ${componentId} before it is mounted, this can cause UI issues and should be avoided.\n Use static options instead.`);
});
it('should not show warning for component id that does not exist', () => {
jest.spyOn(console, 'warn');
const componentId = 'component1';
when(mockedStore.getComponentInstance('stackId')).thenReturn(undefined);
uut.mergeOptions('stackId', {
blurOnUnmount: true
});
expect(console.warn).not.toBeCalledWith(`Navigation.mergeOptions was invoked on component with id: ${componentId} before it is mounted, this can cause UI issues and should be avoided.\n Use static options instead.`);
});
it('processes mergeOptions', async () => {
const options = {
animations: {
dismissModal: {
enabled: false
}
}
};
uut.mergeOptions('myUniqueId', options);
verify(mockedOptionsProcessor.processOptions(CommandName.MergeOptions, deepEqual(options), undefined)).called();
});
it('processing mergeOptions should pass component props', async () => {
const options = {
animations: {
dismissModal: {
enabled: false
}
}
};
const passProps = {
prop: '1'
};
when(mockedStore.getPropsForId('myUniqueId')).thenReturn(passProps);
uut.mergeOptions('myUniqueId', options);
verify(mockedOptionsProcessor.processOptions(CommandName.MergeOptions, deepEqual(options), passProps)).called();
});
});
describe('updateProps', () => {
it('delegates to store', () => {
uut.updateProps('theComponentId', {
someProp: 'someValue'
});
verify(mockedStore.updateProps('theComponentId', deepEqual({
someProp: 'someValue'
})));
});
it('notifies commands observer', () => {
uut.updateProps('theComponentId', {
someProp: 'someValue'
});
verify(commandsObserver.notify('updateProps', deepEqual({
componentId: 'theComponentId',
props: {
someProp: 'someValue'
}
})));
});
it('update props with callback', () => {
const callback = jest.fn();
uut.updateProps('theComponentId', {
someProp: 'someValue'
}, callback);
const args = capture(mockedStore.updateProps).last();
expect(args[0]).toEqual('theComponentId');
expect(args[1]).toEqual({
someProp: 'someValue'
});
expect(args[2]).toEqual(callback);
});
});
describe('showModal', () => {
it('sends command to native after parsing into a correct layout tree', () => {
uut.showModal({
component: {
name: 'com.example.MyScreen'
}
});
verify(mockedNativeCommandsSender.showModal('showModal+UNIQUE_ID', deepEqual({
type: 'Component',
id: 'Component+UNIQUE_ID',
data: {
name: 'com.example.MyScreen',
options: {},
passProps: undefined
},
children: []
}))).called();
});
it('returns a promise with the resolved layout', async () => {
when(mockedNativeCommandsSender.showModal(anything(), anything())).thenResolve('the resolved layout');
const result = await uut.showModal({
component: {
name: 'com.example.MyScreen'
}
});
expect(result).toEqual('the resolved layout');
});
it('process layout with layoutProcessor', () => {
uut.showModal({
component: {
name: 'com.example.MyScreen'
}
});
expect(layoutProcessor.process).toBeCalledWith({
component: {
id: 'Component+UNIQUE_ID',
name: 'com.example.MyScreen',
options: {}
}
}, CommandName.ShowModal);
});
it('retains passProps properties identity', () => {
const obj = {
some: 'content'
};
uut.showModal({
component: {
name: 'com.example.MyScreen',
passProps: {
obj
}
}
});
const args = capture(mockedStore.setPendingProps).last();
expect(args[1].obj).toBe(obj);
});
});
describe('dismissModal', () => {
it('sends command to native', () => {
uut.dismissModal('myUniqueId', {});
verify(mockedNativeCommandsSender.dismissModal('dismissModal+UNIQUE_ID', 'myUniqueId', deepEqual({}))).called();
});
it('returns a promise with the id', async () => {
when(mockedNativeCommandsSender.dismissModal(anyString(), anything(), anything())).thenResolve('the id');
const result = await uut.dismissModal('myUniqueId');
expect(result).toEqual('the id');
});
it('processes mergeOptions', async () => {
const options = {
animations: {
dismissModal: {
enabled: false
}
}
};
uut.dismissModal('myUniqueId', options);
verify(mockedOptionsProcessor.processOptions(CommandName.DismissModal, options)).called();
});
});
describe('dismissAllModals', () => {
it('sends command to native', () => {
uut.dismissAllModals({});
verify(mockedNativeCommandsSender.dismissAllModals('dismissAllModals+UNIQUE_ID', deepEqual({}))).called();
});
it('returns a promise with the id', async () => {
when(mockedNativeCommandsSender.dismissAllModals(anyString(), anything())).thenResolve('the id');
const result = await uut.dismissAllModals();
expect(result).toEqual('the id');
});
it('processes mergeOptions', async () => {
const options = {
animations: {
dismissModal: {
enabled: false
}
}
};
uut.dismissAllModals(options);
verify(mockedOptionsProcessor.processOptions(CommandName.DismissAllModals, options)).called();
});
});
describe('push', () => {
it('resolves with the parsed layout', async () => {
when(mockedNativeCommandsSender.push(anyString(), anyString(), anything())).thenResolve('the resolved layout');
const result = await uut.push('theComponentId', {
component: {
name: 'com.example.MyScreen'
}
});
expect(result).toEqual('the resolved layout');
});
it('parses into correct layout node and sends to native', () => {
uut.push('theComponentId', {
component: {
name: 'com.example.MyScreen'
}
});
verify(mockedNativeCommandsSender.push('push+UNIQUE_ID', 'theComponentId', deepEqual({
type: 'Component',
id: 'Component+UNIQUE_ID',
data: {
name: 'com.example.MyScreen',
options: {},
passProps: undefined
},
children: []
}))).called();
});
it('process layout with layoutProcessor', () => {
uut.push('theComponentId', {
component: {
name: 'com.example.MyScreen'
}
});
expect(layoutProcessor.process).toBeCalledWith({
component: {
id: 'Component+UNIQUE_ID',
name: 'com.example.MyScreen',
options: {}
}
}, CommandName.Push);
});
it('retains passProps properties identity', () => {
const obj = {
some: 'content'
};
uut.push('theComponentId', {
component: {
name: 'com.example.MyScreen',
passProps: {
obj
}
}
});
const args = capture(mockedStore.setPendingProps).last();
expect(args[1].obj).toBe(obj);
});
});
describe('pop', () => {
it('pops a component, passing componentId', () => {
uut.pop('theComponentId', {});
verify(mockedNativeCommandsSender.pop('pop+UNIQUE_ID', 'theComponentId', deepEqual({}))).called();
});
it('pops a component, passing componentId and options', () => {
const options = {
popGesture: true
};
uut.pop('theComponentId', options);
verify(mockedNativeCommandsSender.pop('pop+UNIQUE_ID', 'theComponentId', options)).called();
});
it('pop returns a promise that resolves to componentId', async () => {
when(mockedNativeCommandsSender.pop(anyString(), anyString(), anything())).thenResolve('theComponentId');
const result = await uut.pop('theComponentId', {});
expect(result).toEqual('theComponentId');
});
it('processes mergeOptions', async () => {
const options = {
animations: {
pop: {
enabled: false
}
}
};
uut.pop('theComponentId', options);
verify(mockedOptionsProcessor.processOptions(CommandName.Pop, options)).called();
});
});
describe('popTo', () => {
it('pops all components until the passed Id is top', () => {
uut.popTo('theComponentId', {});
verify(mockedNativeCommandsSender.popTo('popTo+UNIQUE_ID', 'theComponentId', deepEqual({}))).called();
});
it('returns a promise that resolves to targetId', async () => {
when(mockedNativeCommandsSender.popTo(anyString(), anyString(), anything())).thenResolve('theComponentId');
const result = await uut.popTo('theComponentId');
expect(result).toEqual('theComponentId');
});
it('processes mergeOptions', async () => {
const options = {
animations: {
pop: {
enabled: false
}
}
};
uut.popTo('theComponentId', options);
verify(mockedOptionsProcessor.processOptions(CommandName.PopTo, options)).called();
});
});
describe('popToRoot', () => {
it('pops all components to root', () => {
uut.popToRoot('theComponentId', {});
verify(mockedNativeCommandsSender.popToRoot('popToRoot+UNIQUE_ID', 'theComponentId', deepEqual({}))).called();
});
it('returns a promise that resolves to targetId', async () => {
when(mockedNativeCommandsSender.popToRoot(anyString(), anyString(), anything())).thenResolve('theComponentId');
const result = await uut.popToRoot('theComponentId');
expect(result).toEqual('theComponentId');
});
it('processes mergeOptions', async () => {
const options = {
animations: {
pop: {
enabled: false
}
}
};
uut.popToRoot('theComponentId', options);
verify(mockedOptionsProcessor.processOptions(CommandName.PopToRoot, options)).called();
});
});
describe('setStackRoot', () => {
it('parses into correct layout node and sends to native', () => {
uut.setStackRoot('theComponentId', [{
component: {
name: 'com.example.MyScreen'
}
}]);
verify(mockedNativeCommandsSender.setStackRoot('setStackRoot+UNIQUE_ID', 'theComponentId', deepEqual([{
type: 'Component',
id: 'Component+UNIQUE_ID',
data: {
name: 'com.example.MyScreen',
options: {},
passProps: undefined
},
children: []
}]))).called();
});
it('process layout with layoutProcessor', () => {
uut.setStackRoot('theComponentId', [{
component: {
name: 'com.example.MyScreen'
}
}]);
expect(layoutProcessor.process).toBeCalledWith({
component: {
id: 'Component+UNIQUE_ID',
name: 'com.example.MyScreen',
options: {}
}
}, CommandName.SetStackRoot);
});
it('retains passProps properties identity', () => {
const obj = {
some: 'content'
};
uut.setStackRoot('theComponentId', [{
component: {
name: 'com.example.MyScreen',
passProps: {
obj
}
}
}]);
const args = capture(mockedStore.setPendingProps).last();
expect(args[1].obj).toBe(obj);
});
});
describe('showOverlay', () => {
it('sends command to native after parsing into a correct layout tree', () => {
uut.showOverlay({
component: {
name: 'com.example.MyScreen'
}
});
verify(mockedNativeCommandsSender.showOverlay('showOverlay+UNIQUE_ID', deepEqual({
type: 'Component',
id: 'Component+UNIQUE_ID',
data: {
name: 'com.example.MyScreen',
options: {},
passProps: undefined
},
children: []
}))).called();
});
it('resolves with the component id', async () => {
when(mockedNativeCommandsSender.showOverlay(anyString(), anything())).thenResolve('Component1');
const result = await uut.showOverlay({
component: {
name: 'com.example.MyScreen'
}
});
expect(result).toEqual('Component1');
});
it('process layout with layoutProcessor', () => {
uut.showOverlay({
component: {
name: 'com.example.MyScreen'
}
});
expect(layoutProcessor.process).toBeCalledWith({
component: {
id: 'Component+UNIQUE_ID',
name: 'com.example.MyScreen',
options: {}
}
}, CommandName.ShowOverlay);
});
it('retains passProps properties identity', () => {
const obj = {
some: 'content'
};
uut.showOverlay({
component: {
name: 'com.example.MyScreen',
passProps: {
obj
}
}
});
const args = capture(mockedStore.setPendingProps).last();
expect(args[1].obj).toBe(obj);
});
});
describe('dismissOverlay', () => {
it('check promise returns true', async () => {
when(mockedNativeCommandsSender.dismissOverlay(anyString(), anyString())).thenResolve('true');
const result = await uut.dismissOverlay('Component1');
verify(mockedNativeCommandsSender.dismissOverlay(anyString(), anyString())).called();
expect(result).toEqual('true');
});
it('send command to native with componentId', () => {
uut.dismissOverlay('Component1');
verify(mockedNativeCommandsSender.dismissOverlay('dismissOverlay+UNIQUE_ID', 'Component1')).called();
});
});
describe('notifies commandsObserver', () => {
let cb;
let mockedLayoutTreeParser;
let mockedLayoutTreeCrawler;
beforeEach(() => {
cb = jest.fn();
mockedLayoutTreeParser = mock(LayoutTreeParser);
mockedLayoutTreeCrawler = mock(LayoutTreeCrawler);
commandsObserver.register(cb);
const mockedOptionsProcessor = mock(OptionsProcessor);
uut = new Commands(mockedStore, mockedNativeCommandsSender, instance(mockedLayoutTreeParser), instance(mockedLayoutTreeCrawler), commandsObserver, instance(mockedUniqueIdProvider), instance(mockedOptionsProcessor), new LayoutProcessor(new LayoutProcessorsStore()), new OptionsCrawler(instance(mockedStore), mockedUniqueIdProvider));
});
function getAllMethodsOfUut() {
const uutFns = Object.getOwnPropertyNames(Commands.prototype);
const methods = filter(uutFns, fn => fn !== 'constructor');
expect(methods.length).toBeGreaterThan(1);
return methods;
}
describe('passes correct params', () => {
const argsForMethodName = {
setRoot: [{}],
setDefaultOptions: [{}],
mergeOptions: ['id', {}],
updateProps: ['id', {}],
showModal: [{}],
dismissModal: ['id', {}],
dismissAllModals: [{}],
push: ['id', {}],
pop: ['id', {}],
popTo: ['id', {}],
popToRoot: ['id', {}],
setStackRoot: ['id', [{}]],
showOverlay: [{}],
dismissOverlay: ['id'],
dismissAllOverlays: [{}],
getLaunchArgs: ['id']
};
const paramsForMethodName = {
setRoot: {
commandId: 'setRoot+UNIQUE_ID',
layout: {
root: null,
modals: [],
overlays: []
}
},
setDefaultOptions: {
options: {}
},
mergeOptions: {
componentId: 'id',
options: {}
},
updateProps: {
componentId: 'id',
props: {}
},
showModal: {
commandId: 'showModal+UNIQUE_ID',
layout: null
},
dismissModal: {
commandId: 'dismissModal+UNIQUE_ID',
componentId: 'id',
mergeOptions: {}
},
dismissAllModals: {
commandId: 'dismissAllModals+UNIQUE_ID',
mergeOptions: {}
},
push: {
commandId: 'push+UNIQUE_ID',
componentId: 'id',
layout: null
},
pop: {
commandId: 'pop+UNIQUE_ID',
componentId: 'id',
mergeOptions: {}
},
popTo: {
commandId: 'popTo+UNIQUE_ID',
componentId: 'id',
mergeOptions: {}
},
popToRoot: {
commandId: 'popToRoot+UNIQUE_ID',
componentId: 'id',
mergeOptions: {}
},
setStackRoot: {
commandId: 'setStackRoot+UNIQUE_ID',
componentId: 'id',
layout: [null]
},
showOverlay: {
commandId: 'showOverlay+UNIQUE_ID',
layout: null
},
dismissOverlay: {
commandId: 'dismissOverlay+UNIQUE_ID',
componentId: 'id'
},
dismissAllOverlays: {
commandId: 'dismissAllOverlays+UNIQUE_ID'
},
getLaunchArgs: {
commandId: 'getLaunchArgs+UNIQUE_ID'
}
};
forEach(getAllMethodsOfUut(), m => {
it(`for ${m}`, () => {
expect(argsForMethodName).toHaveProperty(m);
expect(paramsForMethodName).toHaveProperty(m);
invoke(uut, m, ...argsForMethodName[m]);
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenCalledWith(m, paramsForMethodName[m]);
});
});
});
});
});
//# sourceMappingURL=Commands.test.js.map