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