1// Ensure all the upstream tests from @react-navigation/core pass.
2
3import type { NavigationState, PartialState } from '@react-navigation/routers';
4
5import getPathFromState from '../getPathFromState';
6import getStateFromPath from '../getStateFromPath';
7
8type State = PartialState<NavigationState>;
9
10[
11  [
12    {
13      stale: false,
14      type: 'stack',
15      key: 'stack-My6VVqpqe-qaR8yuSpqY_',
16      index: 1,
17      routeNames: ['index', '_sitemap', '[...blog]'],
18      routes: [
19        {
20          name: 'index',
21          path: '/',
22          key: 'index-T5frP65vdKFCNT4aJr3r_',
23        },
24        {
25          key: '[...blog]-HCj6P0JDBIoBwswJpNfpF',
26          name: '[...blog]',
27          params: {
28            blog: ['1', '2'],
29          },
30        },
31      ],
32    },
33    {
34      screens: {
35        '[...blog]': '*blog',
36        index: '',
37        _sitemap: '_sitemap',
38      },
39    },
40    '/1/2',
41  ],
42  [
43    {
44      index: 0,
45      routes: [
46        {
47          name: '(app)',
48          params: {
49            user: 'evanbacon',
50            initial: true,
51            screen: '(explore)',
52            params: {
53              user: 'evanbacon',
54              initial: false,
55              screen: '[user]/index',
56              params: {
57                user: 'evanbacon',
58              },
59              path: '/evanbacon',
60            },
61          },
62          state: {
63            index: 0,
64            routes: [
65              {
66                name: '(explore)',
67                params: {
68                  user: 'evanbacon',
69                  initial: false,
70                  screen: '[user]/index',
71                  params: {
72                    user: 'evanbacon',
73                  },
74                  path: '/evanbacon',
75                },
76              },
77            ],
78          },
79          key: '(app)-xxx',
80        },
81      ],
82    },
83    {
84      screens: {
85        '(app)': {
86          path: '(app)',
87          screens: {
88            '(explore)': {
89              path: '(explore)',
90              screens: {
91                '[user]/index': ':user',
92                explore: 'explore',
93              },
94              initialRouteName: 'explore',
95            },
96            '([user])': {
97              path: '([user])',
98              screens: {
99                '[user]/index': ':user',
100                explore: 'explore',
101              },
102              initialRouteName: '[user]/index',
103            },
104          },
105        },
106
107        '[...404]': '*404',
108      },
109    },
110    '/evanbacon',
111  ],
112
113  [
114    {
115      index: 0,
116      routes: [
117        {
118          name: '(app)',
119          params: {
120            initial: true,
121            screen: '(explore)',
122            params: {
123              initial: false,
124              screen: 'compose',
125              path: '/compose',
126            },
127          },
128          state: {
129            index: 0,
130            routes: [
131              {
132                name: '([user])',
133                params: {
134                  user: 'evanbacon',
135                },
136              },
137            ],
138          },
139          key: '(app)-xxx',
140        },
141      ],
142    },
143    {
144      screens: {
145        '(app)': {
146          path: '(app)',
147          screens: {
148            '(feed)': {
149              path: '(feed)',
150              screens: {
151                '[user]/index': ':user',
152                compose: 'compose',
153                explore: 'explore',
154                feed: 'feed',
155              },
156              initialRouteName: 'feed',
157            },
158            '(explore)': {
159              path: '(explore)',
160              screens: {
161                '[user]/index': ':user',
162                compose: 'compose',
163                explore: 'explore',
164                feed: 'feed',
165              },
166              initialRouteName: 'explore',
167            },
168            '([user])': {
169              path: '([user])',
170              screens: {
171                '[user]/index': ':user',
172                compose: 'compose',
173                explore: 'explore',
174                feed: 'feed',
175              },
176              initialRouteName: '[user]/index',
177            },
178          },
179        },
180        '[...404]': '*404',
181      },
182    },
183    '/evanbacon',
184  ],
185].forEach(([state, config, expected], index) => {
186  it(`matches required assumptions: ${index}`, () => {
187    // @ts-expect-error
188    expect(getPathFromState(state, config)).toBe(expected);
189  });
190});
191
192it(`supports resolving nonexistent, nested synthetic states into paths that cannot be resolved`, () => {
193  expect(
194    getPathFromState(
195      {
196        index: 0,
197        routes: [
198          {
199            name: '(root)',
200            state: {
201              index: 0,
202              routes: [
203                {
204                  name: 'modal',
205                  path: '/modal',
206                  key: 'modal-qhPtAt8RdiCEcxrLJtxG1',
207                  state: {
208                    index: 0,
209                    routes: [
210                      {
211                        name: 'index',
212                      },
213                    ],
214                  },
215                },
216              ],
217            },
218            key: '(root)-teRKULujwLUHDOUPQ8g2Z',
219          },
220        ],
221      },
222      {
223        screens: {
224          '(root)': {
225            path: '(root)',
226            screens: {
227              '(tabs)': {
228                path: '(tabs)',
229                screens: {
230                  index: '',
231                  two: 'two',
232                },
233              },
234              '[...missing]': '*missing',
235              modal: 'modal',
236            },
237            initialRouteName: '(tabs)',
238          },
239          _sitemap: '_sitemap',
240        },
241      } as any
242    )
243  ).toEqual('/modal');
244});
245
246it('does not collapse conventions', () => {
247  expect(
248    getPathFromState(
249      {
250        stale: false,
251        type: 'stack',
252        key: 'stack-As9iX2L8B1j6ZjV3xN8aQ',
253        index: 0,
254        routeNames: ['(app)'],
255        routes: [
256          {
257            name: '(app)',
258            params: {
259              user: 'bacon',
260            },
261            state: {
262              stale: false,
263              type: 'stack',
264              key: 'stack-TihHf0Ci6SaO_avdb9IAz',
265              index: 0,
266              routeNames: ['[user]'],
267              routes: [
268                {
269                  name: '[user]',
270                  params: {
271                    user: 'bacon',
272                  },
273                  state: {
274                    stale: false,
275                    type: 'tab',
276                    key: 'tab-n3xlu2kPlKh1VOAQWJbEb',
277                    index: 1,
278                    routeNames: ['index', 'related'],
279                    history: [
280                      {
281                        type: 'route',
282                        key: 'index-z-ZR1oYFE3kHksOXi4L9j',
283                      },
284                      {
285                        type: 'route',
286                        key: 'related-WWyKJe4_3X-PyqW5MIzN4',
287                      },
288                    ],
289                    routes: [
290                      {
291                        name: 'index',
292                        key: 'index-z-ZR1oYFE3kHksOXi4L9j',
293                      },
294                      {
295                        name: 'related',
296                        params: {
297                          user: 'bacon',
298                        },
299                        path: '/bacon/related',
300                        key: 'related-WWyKJe4_3X-PyqW5MIzN4',
301                      },
302                    ],
303                  },
304                  key: '[user]-9qb40LvrbVOw4HArHBQQN',
305                },
306              ],
307            },
308            key: '(app)-eHHi2MUdVaFK_IshK8Y2J',
309          },
310        ],
311      },
312      {
313        screens: {
314          '(app)': {
315            path: '(app)',
316            screens: {
317              '[user]': {
318                path: ':user',
319                screens: {
320                  index: '',
321                  related: 'related',
322                },
323              },
324            },
325          },
326        },
327        preserveDynamicRoutes: true,
328        preserveGroups: true,
329      } as any
330    )
331  ).toBe('/(app)/[user]/related?user=bacon');
332});
333
334it('converts state to path string with config', () => {
335  const path = '/few/bar/sweet/apple/baz/jane?id=x10&valid=true';
336  const config = {
337    screens: {
338      Foo: {
339        path: 'few',
340        screens: {
341          Bar: {
342            path: 'bar/:type/:fruit',
343            screens: {
344              Baz: {
345                path: 'baz/:author',
346                parse: {
347                  author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()),
348                  id: (id: string) => Number(id.replace(/^x/, '')),
349                  valid: Boolean,
350                },
351                stringify: {
352                  author: (author: string) => author.toLowerCase(),
353                  id: (id: number) => `x${id}`,
354                },
355              },
356            },
357          },
358        },
359      },
360    },
361  };
362
363  const state = {
364    routes: [
365      {
366        name: 'Foo',
367        state: {
368          index: 1,
369          routes: [
370            { name: 'boo' },
371            {
372              name: 'Bar',
373              params: { fruit: 'apple', type: 'sweet', avaliable: false },
374              state: {
375                routes: [
376                  {
377                    name: 'Baz',
378                    params: {
379                      author: 'Jane',
380                      id: 10,
381                      valid: true,
382                    },
383                  },
384                ],
385              },
386            },
387          ],
388        },
389      },
390    ],
391  };
392
393  expect(getPathFromState<object>(state, config)).toBe(path);
394  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
395    path
396  );
397});
398
399it('handles state with config with nested screens', () => {
400  const path = '/foo/foe/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
401  const config = {
402    screens: {
403      Foo: {
404        path: 'foo',
405        screens: {
406          Foe: {
407            path: 'foe',
408            screens: {
409              Bar: {
410                path: 'bar/:type/:fruit',
411                screens: {
412                  Baz: {
413                    path: 'baz/:author',
414                    parse: {
415                      author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()),
416                      count: Number,
417                      valid: Boolean,
418                    },
419                    stringify: {
420                      author: (author: string) => author.toLowerCase(),
421                      id: (id: number) => `x${id}`,
422                      unknown: (_: unknown) => 'x',
423                    },
424                  },
425                },
426              },
427            },
428          },
429        },
430      },
431    },
432  };
433
434  const state = {
435    routes: [
436      {
437        name: 'Foo',
438        state: {
439          routes: [
440            {
441              name: 'Foe',
442              state: {
443                routes: [
444                  {
445                    name: 'Bar',
446                    params: { fruit: 'apple', type: 'sweet' },
447                    state: {
448                      routes: [
449                        {
450                          name: 'Baz',
451                          params: {
452                            answer: '42',
453                            author: 'Jane',
454                            count: '10',
455                            valid: true,
456                          },
457                        },
458                      ],
459                    },
460                  },
461                ],
462              },
463            },
464          ],
465        },
466      },
467    ],
468  };
469
470  expect(getPathFromState<object>(state, config)).toBe(path);
471  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
472    path
473  );
474});
475
476it('handles state with config with nested screens and exact', () => {
477  const path = '/foe/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
478  const config = {
479    screens: {
480      Foo: {
481        path: 'foo',
482        screens: {
483          Foe: {
484            path: 'foe',
485            exact: true,
486            screens: {
487              Bar: {
488                path: 'bar/:type/:fruit',
489                screens: {
490                  Baz: {
491                    path: 'baz/:author',
492                    parse: {
493                      author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()),
494                      count: Number,
495                      valid: Boolean,
496                    },
497                    stringify: {
498                      author: (author: string) => author.toLowerCase(),
499                      id: (id: number) => `x${id}`,
500                      unknown: (_: unknown) => 'x',
501                    },
502                  },
503                },
504              },
505            },
506          },
507        },
508      },
509    },
510  };
511
512  const state = {
513    routes: [
514      {
515        name: 'Foo',
516        state: {
517          routes: [
518            {
519              name: 'Foe',
520              state: {
521                routes: [
522                  {
523                    name: 'Bar',
524                    params: { fruit: 'apple', type: 'sweet' },
525                    state: {
526                      routes: [
527                        {
528                          name: 'Baz',
529                          params: {
530                            answer: '42',
531                            author: 'Jane',
532                            count: '10',
533                            valid: true,
534                          },
535                        },
536                      ],
537                    },
538                  },
539                ],
540              },
541            },
542          ],
543        },
544      },
545    ],
546  };
547
548  expect(getPathFromState<object>(state, config)).toBe(path);
549  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
550    path
551  );
552});
553
554it('handles state with config with nested screens and unused configs', () => {
555  const path = '/foo/foe/baz/jane?answer=42&count=10&valid=true';
556  const config = {
557    screens: {
558      Foo: {
559        path: 'foo',
560        screens: {
561          Foe: {
562            path: 'foe',
563            screens: {
564              Baz: {
565                path: 'baz/:author',
566                parse: {
567                  author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()),
568                  count: Number,
569                  valid: Boolean,
570                },
571                stringify: {
572                  author: (author: string) => author.replace(/^\w/, (c) => c.toLowerCase()),
573                  unknown: (_: unknown) => 'x',
574                },
575              },
576            },
577          },
578        },
579      },
580    },
581  };
582
583  const state = {
584    routes: [
585      {
586        name: 'Foo',
587        state: {
588          routes: [
589            {
590              name: 'Foe',
591              state: {
592                routes: [
593                  {
594                    name: 'Baz',
595                    params: {
596                      answer: '42',
597                      author: 'Jane',
598                      count: 10,
599                      valid: true,
600                    },
601                  },
602                ],
603              },
604            },
605          ],
606        },
607      },
608    ],
609  };
610
611  expect(getPathFromState<object>(state, config)).toBe(path);
612  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
613    path
614  );
615});
616
617it('handles state with config with nested screens and unused configs with exact', () => {
618  const path = '/foe/baz/jane?answer=42&count=10&valid=true';
619  const config = {
620    screens: {
621      Foo: {
622        path: 'foo',
623        screens: {
624          Foe: {
625            path: 'foe',
626            exact: true,
627            screens: {
628              Baz: {
629                path: 'baz/:author',
630                parse: {
631                  author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()),
632                  count: Number,
633                  valid: Boolean,
634                },
635                stringify: {
636                  author: (author: string) => author.replace(/^\w/, (c) => c.toLowerCase()),
637                  unknown: (_: unknown) => 'x',
638                },
639              },
640            },
641          },
642        },
643      },
644    },
645  };
646
647  const state = {
648    routes: [
649      {
650        name: 'Foo',
651        state: {
652          routes: [
653            {
654              name: 'Foe',
655              state: {
656                routes: [
657                  {
658                    name: 'Baz',
659                    params: {
660                      answer: '42',
661                      author: 'Jane',
662                      count: 10,
663                      valid: true,
664                    },
665                  },
666                ],
667              },
668            },
669          ],
670        },
671      },
672    ],
673  };
674
675  expect(getPathFromState<object>(state, config)).toBe(path);
676  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
677    path
678  );
679});
680
681it('handles nested object with stringify in it', () => {
682  const path = '/bar/sweet/apple/foo/bis/jane?answer=42&count=10&valid=true';
683  const config = {
684    screens: {
685      Bar: {
686        path: 'bar/:type/:fruit',
687        screens: {
688          Foo: {
689            path: 'foo',
690            screens: {
691              Foe: {
692                path: 'foe',
693              },
694              Baz: {
695                screens: {
696                  Bos: 'bos',
697                  Bis: {
698                    path: 'bis/:author',
699                    stringify: {
700                      author: (author: string) => author.replace(/^\w/, (c) => c.toLowerCase()),
701                    },
702                    parse: {
703                      author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()),
704                      count: Number,
705                      valid: Boolean,
706                    },
707                  },
708                },
709              },
710            },
711          },
712        },
713      },
714    },
715  };
716
717  const state = {
718    routes: [
719      {
720        name: 'Bar',
721        params: { fruit: 'apple', type: 'sweet' },
722        state: {
723          routes: [
724            {
725              name: 'Foo',
726              state: {
727                routes: [
728                  {
729                    name: 'Baz',
730                    state: {
731                      routes: [
732                        {
733                          name: 'Bis',
734                          params: {
735                            answer: '42',
736                            author: 'Jane',
737                            count: 10,
738                            valid: true,
739                          },
740                        },
741                      ],
742                    },
743                  },
744                ],
745              },
746            },
747          ],
748        },
749      },
750    ],
751  };
752
753  expect(getPathFromState<object>(state, config)).toBe(path);
754  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
755    path
756  );
757});
758
759it('handles nested object with stringify in it with exact', () => {
760  const path = '/bis/jane?answer=42&count=10&valid=true';
761  const config = {
762    screens: {
763      Bar: {
764        path: 'bar/:type/:fruit',
765        screens: {
766          Foo: {
767            path: 'foo',
768            screens: {
769              Foe: {
770                path: 'foe',
771              },
772              Baz: {
773                path: 'baz',
774                screens: {
775                  Bos: 'bos',
776                  Bis: {
777                    path: 'bis/:author',
778                    exact: true,
779                    stringify: {
780                      author: (author: string) => author.replace(/^\w/, (c) => c.toLowerCase()),
781                    },
782                    parse: {
783                      author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()),
784                      count: Number,
785                      valid: Boolean,
786                    },
787                  },
788                },
789              },
790            },
791          },
792        },
793      },
794    },
795  };
796
797  const state = {
798    routes: [
799      {
800        name: 'Bar',
801        params: { fruit: 'apple', type: 'sweet' },
802        state: {
803          routes: [
804            {
805              name: 'Foo',
806              state: {
807                routes: [
808                  {
809                    name: 'Baz',
810                    state: {
811                      routes: [
812                        {
813                          name: 'Bis',
814                          params: {
815                            answer: '42',
816                            author: 'Jane',
817                            count: 10,
818                            valid: true,
819                          },
820                        },
821                      ],
822                    },
823                  },
824                ],
825              },
826            },
827          ],
828        },
829      },
830    ],
831  };
832
833  expect(getPathFromState<object>(state, config)).toBe(path);
834  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
835    path
836  );
837});
838
839it('handles nested object for second route depth', () => {
840  const path = '/foo/bar/baz';
841  const config = {
842    screens: {
843      Foo: {
844        path: 'foo',
845        screens: {
846          Foe: 'foe',
847          Bar: {
848            path: 'bar',
849            screens: {
850              Baz: 'baz',
851            },
852          },
853        },
854      },
855    },
856  };
857
858  const state = {
859    routes: [
860      {
861        name: 'Foo',
862        state: {
863          routes: [
864            {
865              name: 'Bar',
866              state: {
867                routes: [{ name: 'Baz' }],
868              },
869            },
870          ],
871        },
872      },
873    ],
874  };
875
876  expect(getPathFromState<object>(state, config)).toBe(path);
877  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
878    path
879  );
880});
881
882it('handles nested object for second route depth with exact', () => {
883  const path = '/baz';
884  const config = {
885    screens: {
886      Foo: {
887        path: 'foo',
888        screens: {
889          Foe: 'foe',
890          Bar: {
891            path: 'bar',
892            screens: {
893              Baz: {
894                path: 'baz',
895                exact: true,
896              },
897            },
898          },
899        },
900      },
901    },
902  };
903
904  const state = {
905    routes: [
906      {
907        name: 'Foo',
908        state: {
909          routes: [
910            {
911              name: 'Bar',
912              state: {
913                routes: [{ name: 'Baz' }],
914              },
915            },
916          ],
917        },
918      },
919    ],
920  };
921
922  expect(getPathFromState<object>(state, config)).toBe(path);
923  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
924    path
925  );
926});
927
928it('handles nested object for second route depth and path and stringify in roots', () => {
929  const path = '/foo/dathomir/bar/42/baz';
930  const config = {
931    screens: {
932      Foo: {
933        path: 'foo/:planet',
934        stringify: {
935          id: (id: number) => `planet=${id}`,
936        },
937        screens: {
938          Foe: 'foe',
939          Bar: {
940            path: 'bar/:id',
941            parse: {
942              id: Number,
943            },
944            screens: {
945              Baz: 'baz',
946            },
947          },
948        },
949      },
950    },
951  };
952
953  const state = {
954    routes: [
955      {
956        name: 'Foo',
957        params: { planet: 'dathomir' },
958        state: {
959          routes: [
960            {
961              name: 'Bar',
962              state: {
963                routes: [{ name: 'Baz', params: { id: 42 } }],
964              },
965            },
966          ],
967        },
968      },
969    ],
970  };
971
972  expect(getPathFromState<object>(state, config)).toBe(path);
973  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
974    path
975  );
976});
977
978it('handles nested object for second route depth and path and stringify in roots with exact', () => {
979  const path = '/baz';
980  const config = {
981    screens: {
982      Foo: {
983        path: 'foo/:id',
984        stringify: {
985          id: (id: number) => `id=${id}`,
986        },
987        screens: {
988          Foe: 'foe',
989          Bar: {
990            path: 'bar/:id',
991            stringify: {
992              id: (id: number) => `id=${id}`,
993            },
994            parse: {
995              id: Number,
996            },
997            screens: {
998              Baz: {
999                path: 'baz',
1000                exact: true,
1001              },
1002            },
1003          },
1004        },
1005      },
1006    },
1007  };
1008
1009  const state = {
1010    routes: [
1011      {
1012        name: 'Foo',
1013        state: {
1014          routes: [
1015            {
1016              name: 'Bar',
1017              state: {
1018                routes: [{ name: 'Baz' }],
1019              },
1020            },
1021          ],
1022        },
1023      },
1024    ],
1025  };
1026
1027  expect(getPathFromState<object>(state, config)).toBe(path);
1028  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1029    path
1030  );
1031});
1032
1033it('ignores empty string paths', () => {
1034  const path = '/bar';
1035  const config = {
1036    screens: {
1037      Foo: {
1038        path: '',
1039        screens: {
1040          Foe: 'foe',
1041        },
1042      },
1043      Bar: 'bar',
1044    },
1045  };
1046
1047  const state = {
1048    routes: [
1049      {
1050        name: 'Foo',
1051        state: {
1052          routes: [{ name: 'Bar' }],
1053        },
1054      },
1055    ],
1056  };
1057
1058  expect(getPathFromState<object>(state, config)).toBe(path);
1059  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1060    path
1061  );
1062});
1063
1064it('keeps query params if path is empty', () => {
1065  const path = '/?foo=42';
1066  const config = {
1067    screens: {
1068      Foo: {
1069        screens: {
1070          Foe: 'foe',
1071          Bar: {
1072            screens: {
1073              Qux: {
1074                path: '',
1075                parse: { foo: Number },
1076              },
1077              Baz: 'baz',
1078            },
1079          },
1080        },
1081      },
1082    },
1083  };
1084
1085  const state = {
1086    routes: [
1087      {
1088        name: 'Foo',
1089        state: {
1090          routes: [
1091            {
1092              name: 'Bar',
1093              state: {
1094                routes: [{ name: 'Qux', params: { foo: 42 } }],
1095              },
1096            },
1097          ],
1098        },
1099      },
1100    ],
1101  };
1102
1103  expect(getPathFromState<object>(state, config)).toBe(path);
1104  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toEqual(
1105    path
1106  );
1107});
1108
1109it('does not use Object.prototype properties as parsing functions', () => {
1110  const path = '/?toString=42';
1111  const config = {
1112    screens: {
1113      Foo: {
1114        screens: {
1115          Foe: 'foe',
1116          Bar: {
1117            screens: {
1118              Qux: {
1119                path: '',
1120                parse: {},
1121              },
1122              Baz: 'baz',
1123            },
1124          },
1125        },
1126      },
1127    },
1128  };
1129
1130  const state = {
1131    routes: [
1132      {
1133        name: 'Foo',
1134        state: {
1135          routes: [
1136            {
1137              name: 'Bar',
1138              state: {
1139                routes: [{ name: 'Qux', params: { toString: 42 } }],
1140              },
1141            },
1142          ],
1143        },
1144      },
1145    ],
1146  };
1147
1148  expect(getPathFromState<object>(state, config)).toBe(path);
1149  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toEqual(
1150    path
1151  );
1152});
1153
1154it('cuts nested configs too', () => {
1155  const path = '/foo/baz';
1156  const config = {
1157    screens: {
1158      Foo: {
1159        path: 'foo',
1160        screens: {
1161          Bar: {
1162            path: '',
1163            screens: {
1164              Baz: {
1165                path: 'baz',
1166              },
1167            },
1168          },
1169        },
1170      },
1171    },
1172  };
1173
1174  const state = {
1175    routes: [
1176      {
1177        name: 'Foo',
1178        state: {
1179          routes: [
1180            {
1181              name: 'Bar',
1182              state: {
1183                routes: [{ name: 'Baz' }],
1184              },
1185            },
1186          ],
1187        },
1188      },
1189    ],
1190  };
1191
1192  expect(getPathFromState<object>(state, config)).toBe(path);
1193  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1194    path
1195  );
1196});
1197
1198it('cuts nested configs too with exact', () => {
1199  const path = '/baz';
1200  const config = {
1201    screens: {
1202      Foo: {
1203        path: 'foo',
1204        screens: {
1205          Bar: {
1206            path: '',
1207            exact: true,
1208            screens: {
1209              Baz: {
1210                path: 'baz',
1211              },
1212            },
1213          },
1214        },
1215      },
1216    },
1217  };
1218
1219  const state = {
1220    routes: [
1221      {
1222        name: 'Foo',
1223        state: {
1224          routes: [
1225            {
1226              name: 'Bar',
1227              state: {
1228                routes: [{ name: 'Baz' }],
1229              },
1230            },
1231          ],
1232        },
1233      },
1234    ],
1235  };
1236
1237  expect(getPathFromState<object>(state, config)).toBe(path);
1238  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1239    path
1240  );
1241});
1242
1243it('handles empty path at the end', () => {
1244  const path = '/foo/bar';
1245  const config = {
1246    screens: {
1247      Foo: {
1248        path: 'foo',
1249        screens: {
1250          Bar: 'bar',
1251        },
1252      },
1253      Baz: { path: '' },
1254    },
1255  };
1256
1257  const state = {
1258    routes: [
1259      {
1260        name: 'Foo',
1261        state: {
1262          routes: [
1263            {
1264              name: 'Bar',
1265              state: {
1266                routes: [{ name: 'Baz' }],
1267              },
1268            },
1269          ],
1270        },
1271      },
1272    ],
1273  };
1274
1275  expect(getPathFromState<object>(state, config)).toBe(path);
1276  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1277    path
1278  );
1279});
1280
1281it('returns "/" for empty path', () => {
1282  const path = '/';
1283
1284  const config = {
1285    screens: {
1286      Foo: {
1287        path: '',
1288        screens: {
1289          Bar: '',
1290        },
1291      },
1292    },
1293  };
1294
1295  const state = {
1296    routes: [
1297      {
1298        name: 'Foo',
1299        state: {
1300          routes: [
1301            {
1302              name: 'Bar',
1303            },
1304          ],
1305        },
1306      },
1307    ],
1308  };
1309
1310  expect(getPathFromState<object>(state, config)).toBe(path);
1311  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1312    path
1313  );
1314});
1315
1316it('parses no path specified', () => {
1317  const path = '/bar';
1318  const config = {
1319    screens: {
1320      Foo: {
1321        screens: {
1322          Foe: {},
1323          Bar: 'bar',
1324        },
1325      },
1326    },
1327  };
1328
1329  const state = {
1330    routes: [
1331      {
1332        name: 'Foo',
1333        state: {
1334          routes: [{ name: 'Bar' }],
1335        },
1336      },
1337    ],
1338  };
1339
1340  expect(getPathFromState<object>(state, config)).toBe(path);
1341  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1342    path
1343  );
1344});
1345
1346it('strips undefined query params', () => {
1347  const path = '/bar/sweet/apple/foo/bis/jane?count=10&valid=true';
1348  const config = {
1349    screens: {
1350      Bar: {
1351        path: 'bar/:type/:fruit',
1352        screens: {
1353          Foo: {
1354            path: 'foo',
1355            screens: {
1356              Foe: {
1357                path: 'foe',
1358              },
1359              Baz: {
1360                screens: {
1361                  Bos: 'bos',
1362                  Bis: {
1363                    path: 'bis/:author',
1364                    stringify: {
1365                      author: (author: string) => author.replace(/^\w/, (c) => c.toLowerCase()),
1366                    },
1367                    parse: {
1368                      author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()),
1369                      count: Number,
1370                      valid: Boolean,
1371                    },
1372                  },
1373                },
1374              },
1375            },
1376          },
1377        },
1378      },
1379    },
1380  };
1381
1382  const state = {
1383    routes: [
1384      {
1385        name: 'Bar',
1386        params: { fruit: 'apple', type: 'sweet' },
1387        state: {
1388          routes: [
1389            {
1390              name: 'Foo',
1391              state: {
1392                routes: [
1393                  {
1394                    name: 'Baz',
1395                    state: {
1396                      routes: [
1397                        {
1398                          name: 'Bis',
1399                          params: {
1400                            author: 'Jane',
1401                            count: 10,
1402                            valid: true,
1403                          },
1404                        },
1405                      ],
1406                    },
1407                  },
1408                ],
1409              },
1410            },
1411          ],
1412        },
1413      },
1414    ],
1415  };
1416
1417  expect(getPathFromState<object>(state, config)).toBe(path);
1418  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1419    path
1420  );
1421});
1422
1423it('strips undefined query params with exact', () => {
1424  const path = '/bis/jane?count=10&valid=true';
1425  const config = {
1426    screens: {
1427      Bar: {
1428        path: 'bar/:type/:fruit',
1429        screens: {
1430          Foo: {
1431            path: 'foo',
1432            screens: {
1433              Foe: {
1434                path: 'foe',
1435              },
1436              Baz: {
1437                screens: {
1438                  Bos: 'bos',
1439                  Bis: {
1440                    path: 'bis/:author',
1441                    exact: true,
1442                    stringify: {
1443                      author: (author: string) => author.replace(/^\w/, (c) => c.toLowerCase()),
1444                    },
1445                    parse: {
1446                      author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()),
1447                      count: Number,
1448                      valid: Boolean,
1449                    },
1450                  },
1451                },
1452              },
1453            },
1454          },
1455        },
1456      },
1457    },
1458  };
1459
1460  const state = {
1461    routes: [
1462      {
1463        name: 'Bar',
1464        params: { fruit: 'apple', type: 'sweet' },
1465        state: {
1466          routes: [
1467            {
1468              name: 'Foo',
1469              state: {
1470                routes: [
1471                  {
1472                    name: 'Baz',
1473                    state: {
1474                      routes: [
1475                        {
1476                          name: 'Bis',
1477                          params: {
1478                            author: 'Jane',
1479                            count: 10,
1480                            valid: true,
1481                          },
1482                        },
1483                      ],
1484                    },
1485                  },
1486                ],
1487              },
1488            },
1489          ],
1490        },
1491      },
1492    ],
1493  };
1494
1495  expect(getPathFromState<object>(state, config)).toBe(path);
1496  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1497    path
1498  );
1499});
1500
1501it('handles stripping all query params', () => {
1502  const path = '/bar/sweet/apple/foo/bis/jane';
1503  const config = {
1504    screens: {
1505      Bar: {
1506        path: 'bar/:type/:fruit',
1507        screens: {
1508          Foo: {
1509            path: 'foo',
1510            screens: {
1511              Foe: {
1512                path: 'foe',
1513              },
1514              Baz: {
1515                screens: {
1516                  Bos: 'bos',
1517                  Bis: {
1518                    path: 'bis/:author',
1519                    stringify: {
1520                      author: (author: string) => author.replace(/^\w/, (c) => c.toLowerCase()),
1521                    },
1522                    parse: {
1523                      author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()),
1524                      count: Number,
1525                      valid: Boolean,
1526                    },
1527                  },
1528                },
1529              },
1530            },
1531          },
1532        },
1533      },
1534    },
1535  };
1536
1537  const state = {
1538    routes: [
1539      {
1540        name: 'Bar',
1541        params: { fruit: 'apple', type: 'sweet' },
1542        state: {
1543          routes: [
1544            {
1545              name: 'Foo',
1546              state: {
1547                routes: [
1548                  {
1549                    name: 'Baz',
1550                    state: {
1551                      routes: [
1552                        {
1553                          name: 'Bis',
1554                          params: {
1555                            author: 'Jane',
1556                          },
1557                        },
1558                      ],
1559                    },
1560                  },
1561                ],
1562              },
1563            },
1564          ],
1565        },
1566      },
1567    ],
1568  };
1569
1570  expect(getPathFromState<object>(state, config)).toBe(path);
1571  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1572    path
1573  );
1574});
1575
1576it('handles stripping all query params with exact', () => {
1577  const path = '/bis/jane';
1578  const config = {
1579    screens: {
1580      Bar: {
1581        path: 'bar/:type/:fruit',
1582        screens: {
1583          Foo: {
1584            path: 'foo',
1585            screens: {
1586              Foe: {
1587                path: 'foe',
1588              },
1589              Baz: {
1590                path: 'baz',
1591                screens: {
1592                  Bos: 'bos',
1593                  Bis: {
1594                    path: 'bis/:author',
1595                    exact: true,
1596                    stringify: {
1597                      author: (author: string) => author.replace(/^\w/, (c) => c.toLowerCase()),
1598                    },
1599                    parse: {
1600                      author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()),
1601                      count: Number,
1602                      valid: Boolean,
1603                    },
1604                  },
1605                },
1606              },
1607            },
1608          },
1609        },
1610      },
1611    },
1612  };
1613
1614  const state = {
1615    routes: [
1616      {
1617        name: 'Bar',
1618        params: { fruit: 'apple', type: 'sweet' },
1619        state: {
1620          routes: [
1621            {
1622              name: 'Foo',
1623              state: {
1624                routes: [
1625                  {
1626                    name: 'Baz',
1627                    state: {
1628                      routes: [
1629                        {
1630                          name: 'Bis',
1631                          params: {
1632                            author: 'Jane',
1633                          },
1634                        },
1635                      ],
1636                    },
1637                  },
1638                ],
1639              },
1640            },
1641          ],
1642        },
1643      },
1644    ],
1645  };
1646
1647  expect(getPathFromState<object>(state, config)).toBe(path);
1648  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1649    path
1650  );
1651});
1652
1653it('replaces undefined query params', () => {
1654  const path = '/bar/undefined/apple';
1655  const config = {
1656    screens: {
1657      Bar: 'bar/:type/:fruit',
1658    },
1659  };
1660
1661  const state = {
1662    routes: [
1663      {
1664        name: 'Bar',
1665        params: { fruit: 'apple' },
1666      },
1667    ],
1668  };
1669
1670  // TODO(EvanBacon): Investigate why getStateFromPath isn't matching
1671  expect(getPathFromState<object>(state, config)).toBe('/bar/apple');
1672  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1673    path
1674  );
1675});
1676
1677it('matches wildcard patterns at root', () => {
1678  const path = '/test/bar/42/whatever';
1679  const config = {
1680    screens: {
1681      404: '*404',
1682      Foo: {
1683        screens: {
1684          Bar: {
1685            path: '/bar/:id/',
1686          },
1687        },
1688      },
1689    },
1690  };
1691
1692  const state = {
1693    routes: [{ name: '404' }],
1694  };
1695
1696  // NOTE(EvanBacon): This is custom behavior for our router
1697  expect(getPathFromState<object>(state, config)).toBe('/');
1698  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1699    '/test/bar/42/whatever'
1700  );
1701});
1702
1703it('matches wildcard patterns at nested level', () => {
1704  const path = '/bar/42/whatever/baz/initt';
1705  const config = {
1706    screens: {
1707      Foo: {
1708        screens: {
1709          Bar: {
1710            path: '/bar/:id/',
1711            screens: {
1712              404: '*404',
1713            },
1714          },
1715        },
1716      },
1717    },
1718  };
1719
1720  const state = {
1721    routes: [
1722      {
1723        name: 'Foo',
1724        state: {
1725          routes: [
1726            {
1727              name: 'Bar',
1728              params: { id: '42' },
1729              state: {
1730                routes: [{ name: '404' }],
1731              },
1732            },
1733          ],
1734        },
1735      },
1736    ],
1737  };
1738
1739  // NOTE(EvanBacon): This is custom behavior for our router
1740  expect(getPathFromState<object>(state, config)).toBe('/bar/42');
1741  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1742    path
1743  );
1744});
1745
1746it('matches wildcard patterns at nested level with exact', () => {
1747  const path = '/whatever';
1748  const config = {
1749    screens: {
1750      Foo: {
1751        screens: {
1752          Bar: {
1753            path: '/bar/:id/',
1754            screens: {
1755              404: {
1756                path: '*404',
1757                exact: true,
1758              },
1759            },
1760          },
1761          Baz: {},
1762        },
1763      },
1764    },
1765  };
1766
1767  const state = {
1768    routes: [
1769      {
1770        name: 'Foo',
1771        state: {
1772          routes: [
1773            {
1774              name: 'Bar',
1775              state: {
1776                routes: [{ name: '404' }],
1777              },
1778            },
1779          ],
1780        },
1781      },
1782    ],
1783  };
1784
1785  // NOTE(EvanBacon): This is custom behavior for our router
1786  expect(getPathFromState<object>(state, config)).toBe('/');
1787  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1788    path
1789  );
1790});
1791
1792it('tries to match wildcard patterns at the end', () => {
1793  const path = '/bar/42/test';
1794  const config = {
1795    screens: {
1796      Foo: {
1797        screens: {
1798          Bar: {
1799            path: '/bar/:id/',
1800            screens: {
1801              404: '*404',
1802              Test: 'test',
1803            },
1804          },
1805        },
1806      },
1807    },
1808  };
1809
1810  const state = {
1811    routes: [
1812      {
1813        name: 'Foo',
1814        state: {
1815          routes: [
1816            {
1817              name: 'Bar',
1818              params: { id: '42' },
1819              state: {
1820                routes: [{ name: 'Test' }],
1821              },
1822            },
1823          ],
1824        },
1825      },
1826    ],
1827  };
1828
1829  expect(getPathFromState<object>(state, config)).toBe(path);
1830  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1831    path
1832  );
1833});
1834
1835it('uses nearest parent wildcard match for unmatched paths', () => {
1836  const path = '/bar/42/baz/test';
1837  const config = {
1838    screens: {
1839      Foo: {
1840        screens: {
1841          Bar: {
1842            path: '/bar/:id/',
1843            screens: {
1844              Baz: 'baz',
1845            },
1846          },
1847          '[...404]': '*404',
1848        },
1849      },
1850    },
1851  };
1852
1853  const state = {
1854    routes: [
1855      {
1856        name: 'Foo',
1857
1858        state: {
1859          routes: [
1860            {
1861              name: '[...404]',
1862            },
1863          ],
1864        },
1865      },
1866    ],
1867  };
1868
1869  // NOTE(EvanBacon): This is custom behavior for our router
1870  expect(getPathFromState<object>(state, config)).toBe('/');
1871  expect(getPathFromState<object>(getStateFromPath<object>(path, config) as State, config)).toBe(
1872    '/bar/42/baz/test'
1873  );
1874});
1875