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