react-native-navigation的迁移库

Commands.test.ts 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. import * as _ from 'lodash';
  2. import { LayoutTreeParser } from './LayoutTreeParser';
  3. import { LayoutTreeCrawler } from './LayoutTreeCrawler';
  4. import { Store } from '../components/Store';
  5. import { UniqueIdProvider } from '../adapters/UniqueIdProvider.mock';
  6. import { NativeCommandsSender } from '../adapters/NativeCommandsSender.mock';
  7. import { Commands } from './Commands';
  8. import { CommandsObserver } from '../events/CommandsObserver';
  9. describe('Commands', () => {
  10. let uut: Commands;
  11. let mockCommandsSender;
  12. let store;
  13. let commandsObserver: CommandsObserver;
  14. beforeEach(() => {
  15. mockCommandsSender = new NativeCommandsSender();
  16. store = new Store();
  17. commandsObserver = new CommandsObserver();
  18. uut = new Commands(
  19. mockCommandsSender,
  20. new LayoutTreeParser(),
  21. new LayoutTreeCrawler(new UniqueIdProvider(), store),
  22. commandsObserver,
  23. new UniqueIdProvider()
  24. );
  25. });
  26. describe('setRoot', () => {
  27. it('sends setRoot to native after parsing into a correct layout tree', () => {
  28. uut.setRoot({
  29. root: {
  30. component: {
  31. name: 'com.example.MyScreen'
  32. }
  33. }
  34. });
  35. expect(mockCommandsSender.setRoot).toHaveBeenCalledTimes(1);
  36. expect(mockCommandsSender.setRoot).toHaveBeenCalledWith('setRoot+UNIQUE_ID', {
  37. root: {
  38. type: 'Component',
  39. id: 'Component+UNIQUE_ID',
  40. children: [],
  41. data: {
  42. name: 'com.example.MyScreen',
  43. options: {}
  44. }
  45. },
  46. modals: [],
  47. overlays: []
  48. });
  49. });
  50. it('deep clones input to avoid mutation errors', () => {
  51. const obj = {};
  52. uut.setRoot({ root: { component: { name: 'bla', inner: obj } as any } });
  53. expect(mockCommandsSender.setRoot.mock.calls[0][1].root.data.inner).not.toBe(obj);
  54. });
  55. it('passProps into components', () => {
  56. const passProps = {
  57. fn: () => 'Hello'
  58. };
  59. expect(store.getPropsForId('Component+UNIQUE_ID')).toEqual({});
  60. uut.setRoot({ root: { component: { name: 'asd', passProps } } });
  61. expect(store.getPropsForId('Component+UNIQUE_ID')).toEqual(passProps);
  62. expect(store.getPropsForId('Component+UNIQUE_ID').fn()).toEqual('Hello');
  63. });
  64. it('returns a promise with the resolved layout', async () => {
  65. mockCommandsSender.setRoot.mockReturnValue(Promise.resolve('the resolved layout'));
  66. const result = await uut.setRoot({ root: { component: { name: 'com.example.MyScreen' } } });
  67. expect(result).toEqual('the resolved layout');
  68. });
  69. it('inputs modals and overlays', () => {
  70. uut.setRoot({
  71. root: {
  72. component: {
  73. name: 'com.example.MyScreen'
  74. }
  75. },
  76. modals: [
  77. {
  78. component: {
  79. name: 'com.example.MyModal'
  80. }
  81. }
  82. ],
  83. overlays: [
  84. {
  85. component: {
  86. name: 'com.example.MyOverlay'
  87. }
  88. }
  89. ]
  90. });
  91. expect(mockCommandsSender.setRoot).toHaveBeenCalledTimes(1);
  92. expect(mockCommandsSender.setRoot).toHaveBeenCalledWith('setRoot+UNIQUE_ID', {
  93. root:
  94. {
  95. type: 'Component',
  96. id: 'Component+UNIQUE_ID',
  97. children: [],
  98. data: {
  99. name: 'com.example.MyScreen',
  100. options: {}
  101. }
  102. },
  103. modals: [
  104. {
  105. type: 'Component',
  106. id: 'Component+UNIQUE_ID',
  107. children: [],
  108. data: {
  109. name: 'com.example.MyModal',
  110. options: {}
  111. }
  112. }
  113. ],
  114. overlays: [
  115. {
  116. type: 'Component',
  117. id: 'Component+UNIQUE_ID',
  118. children: [],
  119. data: {
  120. name: 'com.example.MyOverlay',
  121. options: {}
  122. }
  123. }
  124. ]
  125. });
  126. });
  127. });
  128. describe('mergeOptions', () => {
  129. it('deep clones input to avoid mutation errors', () => {
  130. const obj = { title: 'test' };
  131. uut.mergeOptions('theComponentId', obj as any);
  132. expect(mockCommandsSender.mergeOptions.mock.calls[0][1]).not.toBe(obj);
  133. });
  134. it('passes options for component', () => {
  135. uut.mergeOptions('theComponentId', { title: '1' } as any);
  136. expect(mockCommandsSender.mergeOptions).toHaveBeenCalledTimes(1);
  137. expect(mockCommandsSender.mergeOptions).toHaveBeenCalledWith('theComponentId', { title: '1' });
  138. });
  139. });
  140. describe('setDefaultOptions', () => {
  141. it('deep clones input to avoid mutation errors', () => {
  142. const obj = { title: 'test' };
  143. uut.setDefaultOptions(obj as any);
  144. expect(mockCommandsSender.setDefaultOptions.mock.calls[0][0]).not.toBe(obj);
  145. });
  146. });
  147. describe('showModal', () => {
  148. it('sends command to native after parsing into a correct layout tree', () => {
  149. uut.showModal({
  150. component: {
  151. name: 'com.example.MyScreen'
  152. }
  153. });
  154. expect(mockCommandsSender.showModal).toHaveBeenCalledTimes(1);
  155. expect(mockCommandsSender.showModal).toHaveBeenCalledWith('showModal+UNIQUE_ID', {
  156. type: 'Component',
  157. id: 'Component+UNIQUE_ID',
  158. data: {
  159. name: 'com.example.MyScreen',
  160. options: {}
  161. },
  162. children: []
  163. });
  164. });
  165. it('deep clones input to avoid mutation errors', () => {
  166. const obj = {};
  167. uut.showModal({ component: { name: 'name', inner: obj } as any });
  168. expect(mockCommandsSender.showModal.mock.calls[0][1].data.inner).not.toBe(obj);
  169. });
  170. it('passProps into components', () => {
  171. const passProps = {};
  172. expect(store.getPropsForId('Component+UNIQUE_ID')).toEqual({});
  173. uut.showModal({
  174. component: {
  175. name: 'com.example.MyScreen',
  176. passProps
  177. }
  178. });
  179. expect(store.getPropsForId('Component+UNIQUE_ID')).toEqual(passProps);
  180. });
  181. it('returns a promise with the resolved layout', async () => {
  182. mockCommandsSender.showModal.mockReturnValue(Promise.resolve('the resolved layout'));
  183. const result = await uut.showModal({ component: { name: 'com.example.MyScreen' } });
  184. expect(result).toEqual('the resolved layout');
  185. });
  186. });
  187. describe('dismissModal', () => {
  188. it('sends command to native', () => {
  189. uut.dismissModal('myUniqueId');
  190. expect(mockCommandsSender.dismissModal).toHaveBeenCalledTimes(1);
  191. expect(mockCommandsSender.dismissModal).toHaveBeenCalledWith('dismissModal+UNIQUE_ID', 'myUniqueId');
  192. });
  193. it('returns a promise with the id', async () => {
  194. mockCommandsSender.dismissModal.mockReturnValue(Promise.resolve('the id'));
  195. const result = await uut.dismissModal('myUniqueId');
  196. expect(result).toEqual('the id');
  197. });
  198. });
  199. describe('dismissAllModals', () => {
  200. it('sends command to native', () => {
  201. uut.dismissAllModals();
  202. expect(mockCommandsSender.dismissAllModals).toHaveBeenCalledTimes(1);
  203. expect(mockCommandsSender.dismissAllModals).toHaveBeenCalledWith('dismissAllModals+UNIQUE_ID');
  204. });
  205. it('returns a promise with the id', async () => {
  206. mockCommandsSender.dismissAllModals.mockReturnValue(Promise.resolve('the id'));
  207. const result = await uut.dismissAllModals();
  208. expect(result).toEqual('the id');
  209. });
  210. });
  211. describe('push', () => {
  212. it('deep clones input to avoid mutation errors', () => {
  213. const obj = {};
  214. uut.push('theComponentId', { component: { name: 'name', passProps: { foo: obj } } });
  215. expect(mockCommandsSender.push.mock.calls[0][2].data.passProps.foo).not.toBe(obj);
  216. });
  217. it('resolves with the parsed layout', async () => {
  218. mockCommandsSender.push.mockReturnValue(Promise.resolve('the resolved layout'));
  219. const result = await uut.push('theComponentId', { component: { name: 'com.example.MyScreen' } });
  220. expect(result).toEqual('the resolved layout');
  221. });
  222. it('parses into correct layout node and sends to native', () => {
  223. uut.push('theComponentId', { component: { name: 'com.example.MyScreen' } });
  224. expect(mockCommandsSender.push).toHaveBeenCalledTimes(1);
  225. expect(mockCommandsSender.push).toHaveBeenCalledWith('push+UNIQUE_ID', 'theComponentId', {
  226. type: 'Component',
  227. id: 'Component+UNIQUE_ID',
  228. data: {
  229. name: 'com.example.MyScreen',
  230. options: {}
  231. },
  232. children: []
  233. });
  234. });
  235. });
  236. describe('pop', () => {
  237. it('pops a component, passing componentId', () => {
  238. uut.pop('theComponentId', {});
  239. expect(mockCommandsSender.pop).toHaveBeenCalledTimes(1);
  240. expect(mockCommandsSender.pop).toHaveBeenCalledWith('pop+UNIQUE_ID', 'theComponentId', {});
  241. });
  242. it('pops a component, passing componentId and options', () => {
  243. const options = {
  244. customTransition: {
  245. animations: [
  246. { type: 'sharedElement', fromId: 'title2', toId: 'title1', startDelay: 0, springVelocity: 0.2, duration: 0.5 }
  247. ],
  248. duration: 0.8
  249. }
  250. };
  251. uut.pop('theComponentId', options as any);
  252. expect(mockCommandsSender.pop).toHaveBeenCalledTimes(1);
  253. expect(mockCommandsSender.pop).toHaveBeenCalledWith('pop+UNIQUE_ID', 'theComponentId', options);
  254. });
  255. it('pop returns a promise that resolves to componentId', async () => {
  256. mockCommandsSender.pop.mockReturnValue(Promise.resolve('theComponentId'));
  257. const result = await uut.pop('theComponentId', {});
  258. expect(result).toEqual('theComponentId');
  259. });
  260. });
  261. describe('popTo', () => {
  262. it('pops all components until the passed Id is top', () => {
  263. uut.popTo('theComponentId');
  264. expect(mockCommandsSender.popTo).toHaveBeenCalledTimes(1);
  265. expect(mockCommandsSender.popTo).toHaveBeenCalledWith('popTo+UNIQUE_ID', 'theComponentId');
  266. });
  267. it('returns a promise that resolves to targetId', async () => {
  268. mockCommandsSender.popTo.mockReturnValue(Promise.resolve('theComponentId'));
  269. const result = await uut.popTo('theComponentId');
  270. expect(result).toEqual('theComponentId');
  271. });
  272. });
  273. describe('popToRoot', () => {
  274. it('pops all components to root', () => {
  275. uut.popToRoot('theComponentId');
  276. expect(mockCommandsSender.popToRoot).toHaveBeenCalledTimes(1);
  277. expect(mockCommandsSender.popToRoot).toHaveBeenCalledWith('popToRoot+UNIQUE_ID', 'theComponentId');
  278. });
  279. it('returns a promise that resolves to targetId', async () => {
  280. mockCommandsSender.popToRoot.mockReturnValue(Promise.resolve('theComponentId'));
  281. const result = await uut.popToRoot('theComponentId');
  282. expect(result).toEqual('theComponentId');
  283. });
  284. });
  285. describe('setStackRoot', () => {
  286. it('parses into correct layout node and sends to native', () => {
  287. uut.setStackRoot('theComponentId', { component: { name: 'com.example.MyScreen' } });
  288. expect(mockCommandsSender.setStackRoot).toHaveBeenCalledTimes(1);
  289. expect(mockCommandsSender.setStackRoot).toHaveBeenCalledWith('setStackRoot+UNIQUE_ID', 'theComponentId', {
  290. type: 'Component',
  291. id: 'Component+UNIQUE_ID',
  292. data: {
  293. name: 'com.example.MyScreen',
  294. options: {}
  295. },
  296. children: []
  297. });
  298. });
  299. });
  300. describe('showOverlay', () => {
  301. it('sends command to native after parsing into a correct layout tree', () => {
  302. uut.showOverlay({
  303. component: {
  304. name: 'com.example.MyScreen'
  305. }
  306. });
  307. expect(mockCommandsSender.showOverlay).toHaveBeenCalledTimes(1);
  308. expect(mockCommandsSender.showOverlay).toHaveBeenCalledWith('showOverlay+UNIQUE_ID', {
  309. type: 'Component',
  310. id: 'Component+UNIQUE_ID',
  311. data: {
  312. name: 'com.example.MyScreen',
  313. options: {}
  314. },
  315. children: []
  316. });
  317. });
  318. it('deep clones input to avoid mutation errors', () => {
  319. const obj = {};
  320. uut.showOverlay({ component: { name: 'name', inner: obj } as any });
  321. expect(mockCommandsSender.showOverlay.mock.calls[0][1].data.inner).not.toBe(obj);
  322. });
  323. it('resolves with the component id', async () => {
  324. mockCommandsSender.showOverlay.mockReturnValue(Promise.resolve('Component1'));
  325. const result = await uut.showOverlay({ component: { name: 'com.example.MyScreen' } });
  326. expect(result).toEqual('Component1');
  327. });
  328. });
  329. describe('dismissOverlay', () => {
  330. it('check promise returns true', async () => {
  331. mockCommandsSender.dismissOverlay.mockReturnValue(Promise.resolve(true));
  332. const result = await uut.dismissOverlay('Component1');
  333. expect(mockCommandsSender.dismissOverlay).toHaveBeenCalledTimes(1);
  334. expect(result).toEqual(true);
  335. });
  336. it('send command to native with componentId', () => {
  337. uut.dismissOverlay('Component1');
  338. expect(mockCommandsSender.dismissOverlay).toHaveBeenCalledTimes(1);
  339. expect(mockCommandsSender.dismissOverlay).toHaveBeenCalledWith('dismissOverlay+UNIQUE_ID', 'Component1');
  340. });
  341. });
  342. describe('notifies commandsObserver', () => {
  343. let cb;
  344. beforeEach(() => {
  345. cb = jest.fn();
  346. const mockParser = { parse: () => 'parsed' };
  347. const mockCrawler = { crawl: (x) => x, processOptions: (x) => x };
  348. commandsObserver.register(cb);
  349. uut = new Commands(mockCommandsSender, mockParser, mockCrawler, commandsObserver, new UniqueIdProvider());
  350. });
  351. function getAllMethodsOfUut() {
  352. const uutFns = Object.getOwnPropertyNames(Commands.prototype);
  353. const methods = _.filter(uutFns, (fn) => fn !== 'constructor');
  354. expect(methods.length).toBeGreaterThan(1);
  355. return methods;
  356. }
  357. function getAllMethodsOfNativeCommandsSender() {
  358. const nativeCommandsSenderFns = _.functions(mockCommandsSender);
  359. expect(nativeCommandsSenderFns.length).toBeGreaterThan(1);
  360. return nativeCommandsSenderFns;
  361. }
  362. it('always call last, when nativeCommand fails, dont notify listeners', () => {
  363. // throw when calling any native commands sender
  364. _.forEach(getAllMethodsOfNativeCommandsSender(), (fn) => {
  365. mockCommandsSender[fn].mockImplementation(() => {
  366. throw new Error(`throwing from mockNativeCommandsSender`);
  367. });
  368. });
  369. expect(getAllMethodsOfUut().sort()).toEqual(getAllMethodsOfNativeCommandsSender().sort());
  370. // call all commands on uut, all should throw, no commandObservers called
  371. _.forEach(getAllMethodsOfUut(), (m) => {
  372. expect(() => uut[m]()).toThrow();
  373. expect(cb).not.toHaveBeenCalled();
  374. });
  375. });
  376. it('notify on all commands', () => {
  377. _.forEach(getAllMethodsOfUut(), (m) => {
  378. uut[m]({});
  379. });
  380. expect(cb).toHaveBeenCalledTimes(getAllMethodsOfUut().length);
  381. });
  382. describe('passes correct params', () => {
  383. const argsForMethodName = {
  384. setRoot: [{}],
  385. setDefaultOptions: [{}],
  386. mergeOptions: ['id', {}],
  387. showModal: [{}],
  388. dismissModal: ['id'],
  389. dismissAllModals: [],
  390. push: ['id', {}],
  391. pop: ['id', {}],
  392. popTo: ['id'],
  393. popToRoot: ['id'],
  394. setStackRoot: ['id', {}],
  395. showOverlay: [{}],
  396. dismissOverlay: ['id'],
  397. getLaunchArgs: ['id']
  398. };
  399. const paramsForMethodName = {
  400. setRoot: { commandId: 'setRoot+UNIQUE_ID', layout: { root: 'parsed', modals: [], overlays: [] } },
  401. setDefaultOptions: { options: {} },
  402. mergeOptions: { componentId: 'id', options: {} },
  403. showModal: { commandId: 'showModal+UNIQUE_ID', layout: 'parsed' },
  404. dismissModal: { commandId: 'dismissModal+UNIQUE_ID', componentId: 'id' },
  405. dismissAllModals: { commandId: 'dismissAllModals+UNIQUE_ID' },
  406. push: { commandId: 'push+UNIQUE_ID', componentId: 'id', layout: 'parsed' },
  407. pop: { commandId: 'pop+UNIQUE_ID', componentId: 'id', options: {} },
  408. popTo: { commandId: 'popTo+UNIQUE_ID', componentId: 'id' },
  409. popToRoot: { commandId: 'popToRoot+UNIQUE_ID', componentId: 'id' },
  410. setStackRoot: { commandId: 'setStackRoot+UNIQUE_ID', componentId: 'id', layout: 'parsed' },
  411. showOverlay: { commandId: 'showOverlay+UNIQUE_ID', layout: 'parsed' },
  412. dismissOverlay: { commandId: 'dismissOverlay+UNIQUE_ID', componentId: 'id' },
  413. getLaunchArgs: { commandId: 'getLaunchArgs+UNIQUE_ID' },
  414. };
  415. _.forEach(getAllMethodsOfUut(), (m) => {
  416. it(`for ${m}`, () => {
  417. expect(argsForMethodName).toHaveProperty(m);
  418. expect(paramsForMethodName).toHaveProperty(m);
  419. _.invoke(uut, m, ...argsForMethodName[m]);
  420. expect(cb).toHaveBeenCalledTimes(1);
  421. expect(cb).toHaveBeenCalledWith(m, paramsForMethodName[m]);
  422. });
  423. });
  424. });
  425. });
  426. });