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