react-native-navigation
Version:
React Native Navigation - truly native navigation for iOS and Android
739 lines (669 loc) • 25.2 kB
text/typescript
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';
import { LayoutTreeCrawler } from './LayoutTreeCrawler';
import { Store } from '../components/Store';
import { Commands } from './Commands';
import { CommandsObserver } from '../events/CommandsObserver';
import { NativeCommandsSender } from '../adapters/NativeCommandsSender';
import { OptionsProcessor } from './OptionsProcessor';
import { UniqueIdProvider } from '../adapters/UniqueIdProvider';
import { Options } from '../interfaces/Options';
import { LayoutProcessor } from '../processors/LayoutProcessor';
import { LayoutProcessorsStore } from '../processors/LayoutProcessorsStore';
import { CommandName } from '../interfaces/CommandName';
import { OptionsCrawler } from './OptionsCrawler';
import React from 'react';
import { IWrappedComponent } from 'src/components/ComponentWrapper';
describe('Commands', () => {
let uut: Commands;
let mockedNativeCommandsSender: NativeCommandsSender;
let mockedOptionsProcessor: OptionsProcessor;
let mockedStore: Store;
let commandsObserver: CommandsObserver;
let mockedUniqueIdProvider: UniqueIdProvider;
let layoutProcessor: 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) as OptionsProcessor;
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(): 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({} as IWrappedComponent);
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,
} as IWrappedComponent);
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: 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: 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: 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: 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: 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: any;
let mockedLayoutTreeParser: LayoutTreeParser;
let mockedLayoutTreeCrawler: LayoutTreeCrawler;
beforeEach(() => {
cb = jest.fn();
mockedLayoutTreeParser = mock(LayoutTreeParser);
mockedLayoutTreeCrawler = mock(LayoutTreeCrawler);
commandsObserver.register(cb);
const mockedOptionsProcessor = mock(OptionsProcessor) as 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: Record<string, any[]> = {
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: Record<string, object> = {
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]);
});
});
});
});
});