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