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