react-native-navigation
Version:
React Native Navigation - truly native navigation for iOS and Android
1,080 lines (987 loc) • 31.3 kB
text/typescript
import { OptionsProcessor } from './OptionsProcessor';
import { UniqueIdProvider } from '../adapters/UniqueIdProvider';
import { Store } from '../components/Store';
import { OptionProcessorsStore } from '../processors/OptionProcessorsStore';
import {
Options,
OptionsModalPresentationStyle,
StackAnimationOptions,
} from '../interfaces/Options';
import { mock, when, instance, anyNumber, verify, anything } from 'ts-mockito';
import { ColorService } from '../adapters/ColorService';
import { AssetService } from '../adapters/AssetResolver';
import { Deprecations } from './Deprecations';
import { CommandName } from '../interfaces/CommandName';
import { OptionsProcessor as Processor } from '../interfaces/Processors';
import { DynamicColorIOS, Platform } from 'react-native';
describe('navigation options', () => {
let uut: OptionsProcessor;
let optionProcessorsRegistry: OptionProcessorsStore;
const mockedStore = mock(Store) as Store;
const store = instance(mockedStore) as Store;
beforeEach(() => {
const mockedAssetService = mock(AssetService) as AssetService;
when(mockedAssetService.resolveFromRequire(anyNumber())).thenReturn({
height: 100,
scale: 1,
uri: 'lol',
width: 100,
});
const assetService = instance(mockedAssetService);
const mockedColorService = mock(ColorService) as ColorService;
when(mockedColorService.toNativeColor('red')).thenReturn(0xffff0000);
when(mockedColorService.toNativeColor('green')).thenReturn(0xff00ff00);
when(mockedColorService.toNativeColor('blue')).thenReturn(0xff0000ff);
const colorService = instance(mockedColorService);
optionProcessorsRegistry = new OptionProcessorsStore();
let uuid = 0;
const mockedUniqueIdProvider: UniqueIdProvider = mock(UniqueIdProvider);
when(mockedUniqueIdProvider.generate(anything())).thenCall((prefix) => {
return `${prefix}${++uuid}`;
});
uut = new OptionsProcessor(
store,
instance(mockedUniqueIdProvider),
optionProcessorsRegistry,
colorService,
assetService,
new Deprecations()
);
});
it('processes old setRoot animation value to new enter exit format on Android', () => {
Platform.OS = 'android';
const options: Options = {
animations: {
setRoot: {
enabled: false,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
},
};
const expectedOptions: Options = {
animations: {
setRoot: {
enter: {
enabled: false,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
},
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual(expectedOptions);
});
describe('Modal Animation Options', () => {
describe('Show Modal', () => {
it('processes old options into new options,backwards compatibility ', () => {
const options: Options = {
animations: {
showModal: {
enabled: false,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
dismissModal: {
enabled: true,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
},
};
const expected: Options = {
animations: {
showModal: {
enter: {
enabled: false,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
},
dismissModal: {
exit: {
enabled: true,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
},
},
};
uut.processOptions(CommandName.ShowModal, options);
expect(options).toEqual(expected);
});
it('processes old enabled options into new options,backwards compatibility ', () => {
const options: Options = {
animations: {
showModal: {
enabled: false,
},
dismissModal: {
enabled: true,
},
},
};
const expected: Options = {
animations: {
showModal: {
enter: {
enabled: false,
},
},
dismissModal: {
exit: {
enabled: true,
},
},
},
};
uut.processOptions(CommandName.ShowModal, options);
expect(options).toEqual(expected);
});
it('should not process new options', () => {
const options: Options = {
animations: {
showModal: {
enter: {
enabled: false,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
},
dismissModal: {
exit: {
enabled: true,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
},
},
};
const expected: Options = { ...options };
uut.processOptions(CommandName.ShowModal, options);
expect(options).toEqual(expected);
});
});
describe('Dismiss Modal', () => {
it('processes old options into new options,backwards compatibility ', () => {
const options: Options = {
animations: {
showModal: {
enabled: false,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
dismissModal: {
enabled: true,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
},
};
const expected: Options = {
animations: {
showModal: {
enter: {
enabled: false,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
},
dismissModal: {
exit: {
enabled: true,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
},
},
};
uut.processOptions(CommandName.DismissModal, options);
expect(options).toEqual(expected);
});
it('processes old enabled options into new options,backwards compatibility ', () => {
const options: Options = {
animations: {
showModal: {
enabled: false,
},
dismissModal: {
enabled: true,
},
},
};
const expected: Options = {
animations: {
showModal: {
enter: {
enabled: false,
},
},
dismissModal: {
exit: {
enabled: true,
},
},
},
};
uut.processOptions(CommandName.DismissModal, options);
expect(options).toEqual(expected);
});
it('should not process new options', () => {
const options: Options = {
animations: {
showModal: {
enter: {
enabled: false,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
},
dismissModal: {
exit: {
enabled: true,
translationY: {
from: 0,
to: 1,
duration: 3,
},
},
},
},
};
const expected: Options = { ...options };
uut.processOptions(CommandName.DismissModal, options);
expect(options).toEqual(expected);
});
});
});
it('keeps original values if values were not processed', () => {
const options: Options = {
blurOnUnmount: false,
popGesture: false,
modalPresentationStyle: OptionsModalPresentationStyle.fullScreen,
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
blurOnUnmount: false,
popGesture: false,
modalPresentationStyle: OptionsModalPresentationStyle.fullScreen,
});
});
it('passes value to registered processor', () => {
const options: Options = {
topBar: {
visible: true,
},
};
optionProcessorsRegistry.addProcessor('topBar.visible', (value: boolean) => {
return !value;
});
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: {
visible: false,
},
});
});
it('process options object with multiple values using registered processor', () => {
const options: Options = {
topBar: {
visible: true,
background: {
translucent: true,
},
},
};
optionProcessorsRegistry.addProcessor('topBar.visible', (value: boolean) => {
return !value;
});
optionProcessorsRegistry.addProcessor('topBar.background.translucent', (value: boolean) => {
return !value;
});
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: {
visible: false,
background: {
translucent: false,
},
},
});
});
it('passes commandName to registered processor', () => {
const options: Options = {
topBar: {
visible: false,
},
};
optionProcessorsRegistry.addProcessor('topBar.visible', (_value, commandName) => {
expect(commandName).toEqual(CommandName.SetRoot);
});
uut.processOptions(CommandName.SetRoot, options);
});
it('passes props to registered processor', () => {
const options: Options = {
topBar: {
visible: false,
},
};
const props = {
prop: 'prop',
};
const processor: Processor<boolean> = (_value, _commandName, passProps) => {
expect(passProps).toEqual(props);
return _value;
};
optionProcessorsRegistry.addProcessor('topBar.visible', processor);
uut.processOptions(CommandName.SetRoot, options, props);
});
it('supports multiple registered processors', () => {
const options: Options = {
topBar: {
visible: true,
},
};
optionProcessorsRegistry.addProcessor('topBar.visible', () => false);
optionProcessorsRegistry.addProcessor('topBar.visible', () => true);
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: {
visible: true,
},
});
});
it('supports multiple registered processors deep props', () => {
const options: Options = {
topBar: {
visible: false,
background: {
translucent: false,
},
},
bottomTabs: {
visible: false,
},
};
optionProcessorsRegistry.addProcessor('topBar.visible', () => true);
optionProcessorsRegistry.addProcessor('bottomTabs.visible', () => true);
optionProcessorsRegistry.addProcessor('topBar.background.translucent', () => true);
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: {
visible: true,
background: {
translucent: true,
},
},
bottomTabs: {
visible: true,
},
});
});
describe('color processor', () => {
describe('Android', () => {
beforeEach(() => {
Platform.OS = 'android';
});
it('should not process undefined color', () => {
const options: Options = {
topBar: { background: { color: undefined } },
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: { background: { color: undefined } },
});
});
it('PlatformColor should be passed to native as is', () => {
const options: Options = {
topBar: {
background: {
color: {
// @ts-ignore
resource_paths: ['@color/textColor'],
},
},
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: { background: { color: { resource_paths: ['@color/textColor'] } } },
});
});
it('processes color keys', () => {
const options: Options = {
statusBar: { backgroundColor: 'red' },
topBar: { background: { color: 'blue' } },
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
statusBar: { backgroundColor: { light: 0xffff0000, dark: 0xffff0000 } },
topBar: { background: { color: { light: 0xff0000ff, dark: 0xff0000ff } } },
});
});
it('processes null color', () => {
const options: Options = {
topBar: { background: { color: null } },
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: { background: { color: { light: 'NoColor', dark: 'NoColor' } } },
});
});
it('processes color keys to ThemeColor', () => {
const options: Options = {
topBar: {
background: { color: { light: 'blue', dark: 'red' } },
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: {
background: { color: { light: 0xff0000ff, dark: 0xffff0000 } },
},
});
});
});
describe('iOS', () => {
beforeEach(() => {
Platform.OS = 'ios';
});
it('processes color keys', () => {
const options: Options = {
statusBar: { backgroundColor: 'red' },
topBar: { background: { color: 'blue' } },
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
statusBar: { backgroundColor: 0xffff0000 },
topBar: { background: { color: 0xff0000ff } },
});
});
it('should not process undefined color', () => {
const options: Options = {
topBar: { background: { color: undefined } },
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: { background: { color: undefined } },
});
});
it('processes null color', () => {
const options: Options = {
topBar: { background: { color: null } },
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: { background: { color: 'NoColor' } },
});
});
it('processes color keys to ThemeColor', () => {
const options: Options = {
statusBar: { backgroundColor: 'red' },
topBar: {
background: { color: { light: 'blue', dark: 'red' } },
title: {
color: undefined,
},
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
statusBar: { backgroundColor: 0xffff0000 },
topBar: {
background: { color: { dynamic: { light: 0xff0000ff, dark: 0xffff0000 } } },
title: {
color: undefined,
},
},
});
});
it('supports DynamicColorIOS', () => {
const options: Options = {
topBar: { background: { color: DynamicColorIOS({ light: 'red', dark: 'blue' }) } },
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: { background: { color: { dynamic: { light: 0xffff0000, dark: 0xff0000ff } } } },
});
});
it('should not process undefined value', () => {
const options: Options = {
topBar: { background: { color: undefined } },
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: { background: { color: undefined } },
});
});
});
});
it('processes image keys', () => {
const options: Options = {
backgroundImage: 123,
rootBackgroundImage: 234,
bottomTab: { icon: 345, selectedIcon: 345 },
};
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
backgroundImage: { height: 100, scale: 1, uri: 'lol', width: 100 },
rootBackgroundImage: { height: 100, scale: 1, uri: 'lol', width: 100 },
bottomTab: {
icon: { height: 100, scale: 1, uri: 'lol', width: 100 },
selectedIcon: { height: 100, scale: 1, uri: 'lol', width: 100 },
},
});
});
it('calls store if component has passProps', () => {
const passProps = { some: 'thing' };
const options = { topBar: { title: { component: { passProps, name: 'a' } } } };
uut.processOptions(CommandName.SetRoot, options);
verify(mockedStore.setPendingProps('CustomComponent1', passProps)).called();
});
it('generates componentId for component id was not passed', () => {
const options = { topBar: { title: { component: { name: 'a' } } } };
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: { title: { component: { name: 'a', componentId: 'CustomComponent1' } } },
});
});
it('copies passed id to componentId key', () => {
const options = { topBar: { title: { component: { name: 'a', id: 'Component1' } } } };
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({
topBar: { title: { component: { name: 'a', id: 'Component1', componentId: 'Component1' } } },
});
});
it('calls store when button has passProps and id', () => {
const passProps = { prop: 'prop' };
const options = { topBar: { rightButtons: [{ passProps, id: '1' }] } };
uut.processOptions(CommandName.SetRoot, options);
verify(mockedStore.setPendingProps('1', passProps)).called();
});
it('do not touch passProps when id for button is missing', () => {
const passProps = { prop: 'prop' };
const options = { topBar: { rightButtons: [{ passProps } as any] } };
uut.processOptions(CommandName.SetRoot, options);
expect(options).toEqual({ topBar: { rightButtons: [{ passProps }] } });
});
it('omits passProps when processing buttons or components', () => {
const options = {
topBar: {
rightButtons: [{ passProps: {}, id: 'btn1' }],
leftButtons: [{ passProps: {}, id: 'btn2' }],
title: { component: { name: 'helloThere1', passProps: {} } },
background: { component: { name: 'helloThere2', passProps: {} } },
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options.topBar.rightButtons[0].passProps).toBeUndefined();
expect(options.topBar.leftButtons[0].passProps).toBeUndefined();
expect(options.topBar.title.component.passProps).toBeUndefined();
expect(options.topBar.background.component.passProps).toBeUndefined();
});
it('Will ensure the store has a chance to lazily load components in options', () => {
const options = {
topBar: {
title: { component: { name: 'helloThere1', passProps: {} } },
background: { component: { name: 'helloThere2', passProps: {} } },
},
};
uut.processOptions(CommandName.SetRoot, options);
verify(mockedStore.ensureClassForName('helloThere1')).called();
verify(mockedStore.ensureClassForName('helloThere2')).called();
});
it('show warning on iOS when toggling bottomTabs visibility through mergeOptions', () => {
jest.spyOn(console, 'warn');
uut.processOptions(CommandName.MergeOptions, { bottomTabs: { visible: false } });
expect(console.warn).toBeCalledWith(
'toggling bottomTabs visibility is deprecated on iOS. For more information see https://github.com/wix/react-native-navigation/issues/6416',
{
bottomTabs: { visible: false },
}
);
});
describe('searchBar', () => {
describe('Android', () => {
beforeEach(() => {
Platform.OS = 'android';
});
it('transform searchBar bool to object', () => {
const options = { topBar: { searchBar: true as any } };
uut.processOptions(CommandName.SetRoot, options);
expect(options.topBar.searchBar).toStrictEqual({
visible: true,
hideOnScroll: false,
hideTopBarOnFocus: false,
obscuresBackgroundDuringPresentation: false,
backgroundColor: undefined,
tintColor: undefined,
placeholder: '',
});
});
it('transform searchBar bool to object and merges in deprecated values', () => {
const options = {
topBar: {
searchBar: true as any,
searchBarHiddenWhenScrolling: true,
hideNavBarOnFocusSearchBar: true,
searchBarBackgroundColor: 'red',
searchBarTintColor: 'green',
searchBarPlaceholder: 'foo',
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options.topBar.searchBar).toStrictEqual({
visible: true,
hideOnScroll: true,
hideTopBarOnFocus: true,
obscuresBackgroundDuringPresentation: false,
backgroundColor: { dark: 0xffff0000, light: 0xffff0000 },
tintColor: { dark: 0xff00ff00, light: 0xff00ff00 },
placeholder: 'foo',
});
});
});
describe('iOS', () => {
beforeEach(() => {
Platform.OS = 'ios';
});
it('transform searchBar bool to object', () => {
const options = { topBar: { searchBar: true as any } };
uut.processOptions(CommandName.SetRoot, options);
expect(options.topBar.searchBar).toStrictEqual({
visible: true,
hideOnScroll: false,
hideTopBarOnFocus: false,
obscuresBackgroundDuringPresentation: false,
backgroundColor: undefined,
tintColor: undefined,
placeholder: '',
});
});
it('transform searchBar bool to object and merges in deprecated values', () => {
const options = {
topBar: {
searchBar: true as any,
searchBarHiddenWhenScrolling: true,
hideNavBarOnFocusSearchBar: true,
searchBarBackgroundColor: 'red',
searchBarTintColor: 'green',
searchBarPlaceholder: 'foo',
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options.topBar.searchBar).toStrictEqual({
visible: true,
hideOnScroll: true,
hideTopBarOnFocus: true,
obscuresBackgroundDuringPresentation: false,
backgroundColor: 0xffff0000,
tintColor: 0xff00ff00,
placeholder: 'foo',
});
});
});
});
describe('process animations options', () => {
const performOnViewsInvolvedInStackAnimation = (action: (view: string) => void) =>
['content', 'topBar', 'bottomTabs'].forEach(action);
describe('push', () => {
it('old *.push api is converted into push.*.enter', () => {
performOnViewsInvolvedInStackAnimation((view: string) => {
const options: Options = {
animations: {
push: {
[view]: {
alpha: {
from: 0,
to: 1,
},
},
},
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options.animations!!.push).toStrictEqual({
[view]: {
enter: {
alpha: {
from: 0,
to: 1,
},
},
},
});
});
});
it('StackAnimationOptions based push api is left as is', () => {
performOnViewsInvolvedInStackAnimation((view: string) => {
const options: Options = {
animations: {
push: {
[view]: {
exit: {
alpha: {
from: 1,
to: 0,
},
},
enter: {
alpha: {
from: 0,
to: 1,
},
},
},
},
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options.animations!!.push).toStrictEqual({
[view]: {
exit: {
alpha: {
from: 1,
to: 0,
},
},
enter: {
alpha: {
from: 0,
to: 1,
},
},
},
});
});
});
it('Options not related to views are left as is', () => {
performOnViewsInvolvedInStackAnimation(() => {
const options: Options = {
animations: {
push: {
enabled: false,
waitForRender: true,
sharedElementTransitions: [],
elementTransitions: [],
},
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options.animations!!.push).toStrictEqual({
enabled: false,
waitForRender: true,
sharedElementTransitions: [],
elementTransitions: [],
});
});
});
});
describe('pop', () => {
it('old pop.content api is converted into pop.content.exit', () => {
performOnViewsInvolvedInStackAnimation((view: string) => {
const options: Options = {
animations: {
pop: {
[view]: {
alpha: {
from: 0,
to: 1,
},
},
},
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options.animations!!.pop).toStrictEqual({
[view]: {
exit: {
alpha: {
from: 0,
to: 1,
},
},
},
});
});
});
it('StackAnimationOptions based pop api is left as is', () => {
performOnViewsInvolvedInStackAnimation((view: string) => {
const options: Options = {
animations: {
pop: {
[view]: {
exit: {
alpha: {
from: 1,
to: 0,
},
},
enter: {
alpha: {
from: 0,
to: 1,
},
},
},
},
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options.animations!!.pop).toStrictEqual({
[view]: {
exit: {
alpha: {
from: 1,
to: 0,
},
},
enter: {
alpha: {
from: 0,
to: 1,
},
},
},
});
});
});
it('Options not related to views are left as is', () => {
performOnViewsInvolvedInStackAnimation(() => {
const options: Options = {
animations: {
pop: {
enabled: false,
waitForRender: true,
sharedElementTransitions: [],
elementTransitions: [],
},
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options.animations!!.pop).toStrictEqual({
enabled: false,
waitForRender: true,
sharedElementTransitions: [],
elementTransitions: [],
});
});
});
});
describe('setStackRoot', () => {
it('ViewAnimationOptions based setStackRoot api is converted to StackAnimationOptions based api', () => {
const options: Options = {
animations: {
setStackRoot: {
alpha: {
from: 0,
to: 1,
},
},
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options.animations!!.setStackRoot).toStrictEqual({
content: {
enter: {
alpha: {
from: 0,
to: 1,
},
},
},
});
});
it('Disabled ViewAnimationOptions based setStackRoot api is converted to StackAnimationOptions based api', () => {
const options: Options = {
animations: {
setStackRoot: {
enabled: false,
waitForRender: true,
},
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options.animations!!.setStackRoot as StackAnimationOptions).toStrictEqual({
enabled: false,
waitForRender: true,
content: {
enter: {
enabled: false,
waitForRender: true,
},
},
});
});
it('StackAnimationOptions based setStackRoot api is left as is', () => {
performOnViewsInvolvedInStackAnimation((view: string) => {
const options: Options = {
animations: {
setStackRoot: {
[view]: {
enter: {
alpha: {
from: 0,
to: 1,
},
},
},
},
},
};
uut.processOptions(CommandName.SetRoot, options);
expect(options.animations!!.setStackRoot).toStrictEqual({
[view]: {
enter: {
alpha: {
from: 0,
to: 1,
},
},
},
});
});
});
});
});
});