import { findFocusedRoute } from '@react-navigation/native'; import type { InitialState } from '@react-navigation/routers'; import produce from 'immer'; import { configFromFs } from '../../utils/mockState'; import getPathFromState from '../getPathFromState'; import getStateFromPath from '../getStateFromPath'; const changePath = (state: T, path: string): T => produce(state, (draftState) => { const route = findFocusedRoute(draftState); // @ts-expect-error: immer won't mutate this route.path = path; }); it('returns undefined for invalid path', () => { expect(getStateFromPath('//', { screens: {} })).toBe(undefined); }); it('converts path string to initial state with config', () => { const path = '/foo/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true'; const config = { screens: { Foo: { path: 'foo', screens: { Bar: { path: 'bar/:type/:fruit', screens: { Baz: { path: 'baz/:author', parse: { author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()), count: Number, valid: Boolean, }, stringify: { author: (author: string) => author.toLowerCase(), }, }, }, }, }, }, }, }; const state = { routes: [ { name: 'Foo', params: { author: 'Jane', fruit: 'apple', type: 'sweet', }, state: { routes: [ { name: 'Bar', params: { author: 'Jane', fruit: 'apple', type: 'sweet' }, state: { routes: [ { name: 'Baz', params: { author: 'Jane', count: 10, fruit: 'apple', type: 'sweet', answer: '42', valid: true, }, path, }, ], }, }, ], }, }, ], }; testConversions(path, config, state); }); it('handles leading slash when converting', () => { const path = '/foo/bar/?count=42'; expect( getStateFromPath(path, { screens: { foo: { path: 'foo', screens: { bar: { path: 'bar', }, }, }, }, }) ).toEqual({ routes: [ { name: 'foo', state: { routes: [ { name: 'bar', params: { count: '42' }, path, }, ], }, }, ], }); }); it('handles ending slash when converting', () => { const path = 'foo/bar/?count=42'; expect( getStateFromPath(path, { screens: { foo: { path: 'foo', screens: { bar: { path: 'bar', }, }, }, }, }) ).toEqual({ routes: [ { name: 'foo', state: { routes: [ { name: 'bar', params: { count: '42' }, path, }, ], }, }, ], }); }); it('converts path string to initial state with config with nested screens', () => { const path = '/foe/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true'; const config = { screens: { Foo: { path: 'foo', screens: { Foe: { path: 'foe', exact: true, screens: { Bar: { path: 'bar/:type/:fruit', screens: { Baz: { path: 'baz/:author', parse: { author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()), count: Number, valid: Boolean, }, stringify: { author: (author: string) => author.toLowerCase(), }, }, }, }, }, }, }, }, }, }; const state = { routes: [ { name: 'Foo', params: { fruit: 'apple', type: 'sweet', author: 'Jane', }, state: { routes: [ { name: 'Foe', params: { fruit: 'apple', type: 'sweet', author: 'Jane', }, state: { routes: [ { name: 'Bar', params: { fruit: 'apple', type: 'sweet', author: 'Jane', }, state: { routes: [ { name: 'Baz', params: { fruit: 'apple', type: 'sweet', author: 'Jane', count: 10, answer: '42', valid: true, }, path, }, ], }, }, ], }, }, ], }, }, ], }; testConversions(path, config, state); }); it('converts path string to initial state with config with nested screens and unused parse functions', () => { const path = '/foe/baz/jane?count=10&answer=42&valid=true'; const config = { screens: { Foo: { path: 'foo', screens: { Foe: { path: 'foe', exact: true, screens: { Baz: { path: 'baz/:author', parse: { author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()), count: Number, valid: Boolean, id: Boolean, }, }, }, }, }, }, }, }; const state = { routes: [ { name: 'Foo', params: { author: 'Jane', }, state: { routes: [ { name: 'Foe', params: { author: 'Jane', }, state: { routes: [ { name: 'Baz', params: { author: 'Jane', count: 10, answer: '42', valid: true, }, path, }, ], }, }, ], }, }, ], }; expect(getStateFromPath(path, config)).toEqual(state); expect(getStateFromPath(getPathFromState(state, config), config)).toEqual( changePath(state, '/foe/baz/Jane?count=10&answer=42&valid=true') ); }); it('handles nested object with unused configs and with parse in it', () => { const path = '/bar/sweet/apple/foe/bis/jane?count=10&answer=42&valid=true'; const config = { screens: { Bar: { path: 'bar/:type/:fruit', screens: { Foo: { screens: { Foe: { path: 'foe', screens: { Baz: { screens: { Bos: { path: 'bos', exact: true, }, Bis: { path: 'bis/:author', stringify: { author: (author: string) => author.replace(/^\w/, (c) => c.toLowerCase()), }, parse: { author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()), count: Number, valid: Boolean, }, }, }, }, }, }, }, }, }, }, }, }; const state = { routes: [ { name: 'Bar', params: { fruit: 'apple', type: 'sweet', author: 'Jane' }, state: { routes: [ { name: 'Foo', params: { author: 'Jane', fruit: 'apple', type: 'sweet', }, state: { routes: [ { name: 'Foe', params: { author: 'Jane', fruit: 'apple', type: 'sweet', }, state: { routes: [ { name: 'Baz', params: { author: 'Jane', fruit: 'apple', type: 'sweet', }, state: { routes: [ { name: 'Bis', params: { author: 'Jane', count: 10, answer: '42', valid: true, fruit: 'apple', type: 'sweet', }, path, }, ], }, }, ], }, }, ], }, }, ], }, }, ], }; testConversions(path, config, state); }); it('handles parse in nested object for second route depth', () => { const path = '/baz'; const config = { screens: { Foo: { path: 'foo', screens: { Foe: { path: 'foe', exact: true, }, Bar: { path: 'bar', exact: true, screens: { Baz: { path: 'baz', exact: true, }, }, }, }, }, }, }; const state = { routes: [ { name: 'Foo', state: { routes: [ { name: 'Bar', state: { routes: [{ name: 'Baz', path }], }, }, ], }, }, ], }; testConversions(path, config, state); }); it('handles parse in nested object for second route depth and and path and parse in roots', () => { const path = '/baz'; const config = { screens: { Foo: { path: 'foo/:id', parse: { id: Number, }, stringify: { id: (id: number) => `id=${id}`, }, screens: { Foe: 'foe', Bar: { screens: { Baz: { path: 'baz', exact: true, }, }, }, }, }, }, }; const state = { routes: [ { name: 'Foo', state: { routes: [ { name: 'Bar', state: { routes: [{ name: 'Baz', path }], }, }, ], }, }, ], }; testConversions(path, config, state); }); it('handles initialRouteName inside a screen', () => { const path = '/baz'; const config = { screens: { Foo: { initialRouteName: 'Foe', screens: { Foe: 'foe', Bar: { screens: { Baz: 'baz', }, }, }, }, }, }; const state = { routes: [ { name: 'Foo', state: { index: 1, routes: [ { name: 'Foe', }, { name: 'Bar', state: { routes: [{ name: 'Baz', path }], }, }, ], }, }, ], }; testConversions(path, config, state); }); it('handles initialRouteName included in path', () => { const path = '/baz'; const config = { screens: { Foo: { initialRouteName: 'Foe', screens: { Foe: { screens: { Baz: 'baz', }, }, Bar: 'bar', }, }, }, }; const state = { routes: [ { name: 'Foo', state: { routes: [ { name: 'Foe', state: { routes: [{ name: 'Baz', path }], }, }, ], }, }, ], }; testConversions(path, config, state); }); it('handles two initialRouteNames', () => { const path = '/bar/sweet/apple/foe/bis/jane?answer=42&count=10&valid=true'; const config = { screens: { Bar: { path: 'bar/:type/:fruit', screens: { Foo: { screens: { Foe: { path: 'foe', screens: { Baz: { initialRouteName: 'Bos', screens: { Bos: { path: 'bos', exact: true, }, Bis: { path: 'bis/:author', stringify: { author: (author: string) => author.replace(/^\w/, (c) => c.toLowerCase()), }, parse: { author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()), count: Number, valid: Boolean, }, }, }, }, }, }, }, }, }, }, }, }; const state = { routes: [ { name: 'Bar', params: { author: 'Jane', fruit: 'apple', type: 'sweet' }, state: { routes: [ { name: 'Foo', params: { author: 'Jane', fruit: 'apple', type: 'sweet' }, state: { routes: [ { name: 'Foe', params: { author: 'Jane', fruit: 'apple', type: 'sweet' }, state: { routes: [ { name: 'Baz', params: { author: 'Jane', fruit: 'apple', type: 'sweet', }, state: { index: 1, routes: [ { name: 'Bos' }, { name: 'Bis', params: { answer: '42', author: 'Jane', count: 10, valid: true, fruit: 'apple', type: 'sweet', }, path, }, ], }, }, ], }, }, ], }, }, ], }, }, ], }; testConversions(path, config, state); }); it('accepts initialRouteName without config for it', () => { const path = '/bar/sweet/apple/foe/bis/jane?answer=42&count=10&valid=true'; const config = { screens: { Bar: { path: 'bar/:type/:fruit', screens: { Foo: { screens: { Foe: { path: 'foe', screens: { Baz: { initialRouteName: 'Bas', screens: { Bos: { path: 'bos', exact: true, }, Bis: { path: 'bis/:author', stringify: { author: (author: string) => author.replace(/^\w/, (c) => c.toLowerCase()), }, parse: { author: (author: string) => author.replace(/^\w/, (c) => c.toUpperCase()), count: Number, valid: Boolean, }, }, }, }, }, }, }, }, }, }, }, }; const state = { routes: [ { name: 'Bar', params: { author: 'Jane', fruit: 'apple', type: 'sweet' }, state: { routes: [ { name: 'Foo', params: { author: 'Jane', fruit: 'apple', type: 'sweet' }, state: { routes: [ { name: 'Foe', params: { author: 'Jane', fruit: 'apple', type: 'sweet' }, state: { routes: [ { name: 'Baz', params: { author: 'Jane', fruit: 'apple', type: 'sweet', }, state: { index: 1, routes: [ { name: 'Bas', }, { name: 'Bis', params: { answer: '42', author: 'Jane', count: 10, fruit: 'apple', type: 'sweet', valid: true, }, path: '/bar/sweet/apple/foe/bis/jane?answer=42&count=10&valid=true', }, ], }, }, ], }, }, ], }, }, ], }, }, ], }; testConversions(path, config, state); }); it('returns undefined if path is empty and no matching screen is present', () => { const config = { screens: { Foo: { screens: { Foe: 'foe', Bar: { screens: { Baz: 'baz', }, }, }, }, }, }; const path = ''; expect(getStateFromPath(path, config)).toEqual(undefined); }); it('returns matching screen if path is empty', () => { const path = ''; const config = { screens: { Foo: { screens: { Foe: 'foe', Bar: { screens: { Qux: '', Baz: 'baz', }, }, }, }, }, }; const state = { routes: [ { name: 'Foo', state: { routes: [ { name: 'Bar', state: { routes: [{ name: 'Qux', path }], }, }, ], }, }, ], }; expect(getStateFromPath(path, config)).toEqual(state); expect(getStateFromPath(getPathFromState(state, config), config)).toEqual( changePath(state, '/') ); }); it('returns matching screen if path is only slash', () => { const path = '/'; const config = { screens: { Foo: { screens: { Foe: 'foe', Bar: { screens: { Qux: '', Baz: 'baz', }, }, }, }, }, }; const state = { routes: [ { name: 'Foo', state: { routes: [ { name: 'Bar', state: { routes: [{ name: 'Qux', path }], }, }, ], }, }, ], }; expect(getStateFromPath(path, config)).toEqual(state); expect(getStateFromPath(getPathFromState(state, config), config)).toEqual( changePath(state, '/') ); }); // Test `app/(app)/index.js` matching `/` it('returns matching screen if path is only slash and root is a group', () => { expect( getStateFromPath('/', { screens: { '(app)/index': '(app)', '[id]': ':id', '[...404]': '*404', }, }) ).toEqual({ routes: [{ name: '(app)/index', path: '/' }] }); }); // Test `app/(one)/(two)/index.js` matching `/` it('returns matching screen if path is only slash and root is a nested group', () => { expect( getStateFromPath('/', { screens: { '(one)/(two)/index': '(one)/(two)', '[id]': ':id', '[...404]': '*404', }, }) ).toEqual({ routes: [{ name: '(one)/(two)/index', path: '/' }] }); }); it('returns matching screen with params if path is empty', () => { const path = '?foo=42'; const config = { screens: { Foo: { screens: { Foe: 'foe', Bar: { screens: { Qux: { path: '', parse: { foo: Number }, }, Baz: 'baz', }, }, }, }, }, }; const state = { routes: [ { name: 'Foo', state: { routes: [ { name: 'Bar', state: { routes: [{ name: 'Qux', params: { foo: 42 }, path }], }, }, ], }, }, ], }; expect(getStateFromPath(path, config)).toEqual(state); expect(getStateFromPath(getPathFromState(state, config), config)).toEqual( changePath(state, '/?foo=42') ); }); it("doesn't match nested screen if path is empty", () => { const config = { screens: { Foo: { screens: { Foe: 'foe', Bar: { path: 'bar', screens: { Qux: { path: '', parse: { foo: Number }, }, }, }, }, }, }, }; const path = ''; expect(getStateFromPath(path, config)).toEqual(undefined); }); it('chooses more exhaustive pattern', () => { const path = '/foo/5'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foo/:id', parse: { id: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 5, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bis', params: { id: 5 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handles same paths beginnings', () => { const path = '/foos'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foos', }, }, }, }, }; const state = { routes: [ { name: 'Foe', state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bis', path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handles same paths beginnings with params', () => { const path = '/foos/5'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foos/:id', parse: { id: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 5, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bis', params: { id: 5 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handles not taking path with too many segments', () => { const path = '/foos/5'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foos/:id', parse: { id: Number, }, }, Bas: { path: 'foos/:id/:nip', parse: { id: Number, pwd: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 5, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bis', params: { id: 5 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handles differently ordered params v1', () => { const path = '/foos/5/res/20'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foos/:id', parse: { id: Number, }, }, Bas: { path: 'foos/:id/res/:pwd', parse: { id: Number, pwd: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 5, pwd: 20, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { id: 5, pwd: 20 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handles differently ordered params v2', () => { const path = '/5/20/foos/res'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foos/:id', parse: { id: Number, }, }, Bas: { path: ':id/:pwd/foos/res', parse: { id: Number, pwd: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 5, pwd: 20, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { id: 5, pwd: 20 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handles differently ordered params v3', () => { const path = '/foos/5/20/res'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foos/:id', parse: { id: Number, }, }, Bas: { path: 'foos/:id/:pwd/res', parse: { id: Number, pwd: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 5, pwd: 20, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { id: 5, pwd: 20 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handles differently ordered params v4', () => { const path = '5/foos/res/20'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foos/:id', parse: { id: Number, }, }, Bas: { path: ':id/foos/res/:pwd', parse: { id: Number, pwd: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 5, pwd: 20, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { id: 5, pwd: 20 }, path, }, ], }, }, ], }; expect(getStateFromPath(path, config)).toEqual(state); expect(getStateFromPath(getPathFromState(state, config), config)).toEqual( changePath(state, '/5/foos/res/20') ); }); it('handles simple optional params', () => { const path = '/foos/5'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foo/:id', parse: { id: Number, }, }, Bas: { path: 'foos/:id/:nip?', parse: { id: Number, nip: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 5, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { id: 5 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handle 2 optional params at the end v1', () => { const path = '/foos/5'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foo/:id', parse: { id: Number, }, }, Bas: { path: 'foos/:id/:nip?/:pwd?', parse: { id: Number, nip: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 5, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { id: 5 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handle 2 optional params at the end v2', () => { const path = '/foos/5/10'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foo/:id', parse: { id: Number, }, }, Bas: { path: 'foos/:id/:nip?/:pwd?', parse: { id: Number, nip: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 5, nip: 10, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { id: 5, nip: 10 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handle 2 optional params at the end v3', () => { const path = '/foos/5/10/15'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foo/:id', parse: { id: Number, }, }, Bas: { path: 'foos/:id/:nip?/:pwd?', parse: { id: Number, nip: Number, pwd: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { nip: 10, pwd: 15, id: 5, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { id: 5, nip: 10, pwd: 15 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handle optional params in the middle v1', () => { const path = '/foos/5/10'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foo/:id', parse: { id: Number, }, }, Bas: { path: 'foos/:id/:nip?/:pwd', parse: { id: Number, nip: Number, pwd: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 5, pwd: 10, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { id: 5, pwd: 10 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handle optional params in the middle v2', () => { const path = '/foos/5/10/15'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foo/:id', parse: { id: Number, }, }, Bas: { path: 'foos/:id/:nip?/:pwd', parse: { id: Number, nip: Number, pwd: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 5, nip: 10, pwd: 15, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { id: 5, nip: 10, pwd: 15 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handle optional params in the middle v3', () => { const path = '/foos/5/10/15'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foo/:id', parse: { id: Number, }, }, Bas: { path: 'foos/:id/:nip?/:pwd/:smh', parse: { id: Number, nip: Number, pwd: Number, smh: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 5, pwd: 10, smh: 15, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { id: 5, pwd: 10, smh: 15 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handle optional params in the middle v4', () => { const path = '/foos/5/10'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foo/:id', parse: { id: Number, }, }, Bas: { path: 'foos/:nip?/:pwd/:smh?/:id', parse: { id: Number, nip: Number, pwd: Number, smh: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 10, pwd: 5, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { pwd: 5, id: 10 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handle optional params in the middle v5', () => { const path = '/foos/5/10/15'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foo/:id', parse: { id: Number, }, }, Bas: { path: 'foos/:nip?/:pwd/:smh?/:id', parse: { id: Number, nip: Number, pwd: Number, smh: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 15, nip: 5, pwd: 10, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { nip: 5, pwd: 10, id: 15 }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('handle optional params in the beginning v1', () => { const path = '5/10/foos/15'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foo/:id', parse: { id: Number, }, }, Bas: { path: ':nip?/:pwd/foos/:smh?/:id', parse: { id: Number, nip: Number, pwd: Number, smh: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 15, nip: 5, pwd: 10, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { nip: 5, pwd: 10, id: 15 }, path, }, ], }, }, ], }; expect(getStateFromPath(path, config)).toEqual(state); expect(getStateFromPath(getPathFromState(state, config), config)).toEqual( changePath(state, '/5/10/foos/15') ); }); it('handle optional params in the beginning v2', () => { const path = '5/10/foos/15'; const config = { screens: { Foe: { path: '/', initialRouteName: 'Foo', screens: { Foo: 'foo', Bis: { path: 'foo/:id', parse: { id: Number, }, }, Bas: { path: ':nip?/:smh?/:pwd/foos/:id', parse: { id: Number, nip: Number, pwd: Number, smh: Number, }, }, }, }, }, }; const state = { routes: [ { name: 'Foe', params: { id: 15, nip: 5, pwd: 10, }, state: { index: 1, routes: [ { name: 'Foo', }, { name: 'Bas', params: { nip: 5, pwd: 10, id: 15 }, path, }, ], }, }, ], }; expect(getStateFromPath(path, config)).toEqual(state); expect(getStateFromPath(getPathFromState(state, config), config)).toEqual( changePath(state, '/5/10/foos/15') ); }); it('merges parent patterns if needed', () => { const path = 'foo/42/baz/babel'; const config = { screens: { Foo: { path: 'foo/:bar', parse: { bar: Number, }, screens: { Baz: 'baz/:qux', }, }, }, }; const state = { routes: [ { name: 'Foo', params: { bar: 42, qux: 'babel' }, state: { routes: [ { name: 'Baz', params: { bar: 42, qux: 'babel' }, path, }, ], }, }, ], }; expect(getStateFromPath(path, config)).toEqual(state); expect(getStateFromPath(getPathFromState(state, config), config)).toEqual( changePath(state, '/foo/42/baz/babel') ); }); it('ignores extra slashes in the pattern', () => { const path = '/bar/42'; const config = { screens: { Foo: { screens: { Bar: { path: '/bar//:id/', }, }, }, }, }; const state = { routes: [ { name: 'Foo', params: { id: '42', }, state: { routes: [ { name: 'Bar', params: { id: '42' }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('matches wildcard patterns at root', () => { const path = '/test/bar/42/whatever'; const config = configFromFs(['[...404].js', 'foo/bar.js', 'index.js']); const state = { routes: [ { params: { '404': ['test', 'bar', '42', 'whatever'], }, name: '[...404]', path, }, ], }; testConversions(path, config, state); }); it('matches wildcard patterns at nested level', () => { const path = '/bar/42/whatever/baz/initt'; const config = configFromFs([ '(foo)/_layout.tsx', '(foo)/bar/_layout.tsx', '(foo)/bar/[id].tsx', '(foo)/bar/[...rest].tsx', ]); const state = { routes: [ { name: '(foo)', params: { rest: ['42', 'whatever', 'baz', 'initt'] }, state: { routes: [ { name: 'bar', params: { rest: ['42', 'whatever', 'baz', 'initt'] }, state: { routes: [ { name: '[...rest]', params: { rest: ['42', 'whatever', 'baz', 'initt'], }, path: '/bar/42/whatever/baz/initt', }, ], }, }, ], }, }, ], }; testConversions(path, config, state); }); xit('matches wildcard patterns at nested level with exact', () => { const path = '/whatever'; const config = { screens: { Foo: { screens: { Bar: { path: '/bar/:id/', screens: { 404: { path: '*404', exact: true, }, }, }, Baz: {}, }, }, }, }; const state = { routes: [ { name: 'Foo', params: { '404': ['whatever'], }, state: { routes: [ { name: 'Bar', params: { '404': ['whatever'], }, state: { routes: [ { name: '404', params: { '404': ['whatever'], }, path, }, ], }, }, ], }, }, ], }; expect(getStateFromPath(path, config)).toEqual(state); expect(getStateFromPath(getPathFromState(state, config), config)).toEqual( changePath(state, path) ); }); it('tries to match wildcard patterns at the end', () => { const path = '/bar/42/test'; const config = { screens: { bar: { path: 'bar', screens: { '[...404]': '*404', '[userSlug]': ':userSlug', ':id': { path: ':id', screens: { test: 'test', }, }, }, }, }, }; const state = { routes: [ { name: 'bar', params: { id: '42' }, state: { routes: [ { name: ':id', params: { id: '42' }, state: { routes: [ { name: 'test', params: { id: '42' }, path: '/bar/42/test', }, ], }, }, ], }, }, ], }; testConversions(path, config, state); }); it('uses nearest parent wildcard match for unmatched paths', () => { const path = '/bar/42/baz/test'; const config = { screens: { Foo: { screens: { Bar: { path: '/bar/:id/', screens: { Baz: 'baz', }, }, '[...404]': '*404', }, }, }, }; const state = { routes: [ { name: 'Foo', params: { '404': ['bar', '42', 'baz', 'test'], }, state: { routes: [ { name: '[...404]', params: { '404': ['bar', '42', 'baz', 'test'], }, path, }, ], }, }, ], }; testConversions(path, config, state); }); it('throws if two screens map to the same pattern', () => { const path = '/bar/42/baz/test'; expect(() => getStateFromPath(path, { screens: { Foo: { screens: { Bar: { path: '/bar/:id/', screens: { Baz: 'baz', }, }, Bax: '/bar/:id/baz', }, }, }, }) ).toThrow( "The route pattern 'bar/:id/baz' resolves to both 'Foo//bar/:id/baz' and 'Foo/Bar/baz'. Patterns must be unique and cannot resolve to more than one route." ); expect(() => getStateFromPath(path, { screens: { Foo: { screens: { Bar: { path: '/bar/:id/', screens: { Baz: '', }, }, }, }, }, }) ).not.toThrow(); }); it('correctly applies initialRouteName for config with similar route names', () => { const path = '/weekly-earnings'; const config = { screens: { RootTabs: { screens: { HomeTab: { screens: { Home: '', WeeklyEarnings: 'weekly-earnings', EventDetails: 'event-details/:eventId', }, }, EarningsTab: { initialRouteName: 'Earnings', path: 'earnings', screens: { Earnings: '', WeeklyEarnings: 'weekly-earnings', }, }, }, }, }, }; const state = { routes: [ { name: 'RootTabs', state: { routes: [ { name: 'HomeTab', state: { routes: [ { name: 'WeeklyEarnings', path, }, ], }, }, ], }, }, ], }; testConversions(path, config, state); }); it('correctly applies initialRouteName for config with similar route names v2', () => { const path = '/earnings/weekly-earnings'; const config = { screens: { RootTabs: { screens: { HomeTab: { initialRouteName: 'Home', screens: { Home: '', WeeklyEarnings: 'weekly-earnings', }, }, EarningsTab: { initialRouteName: 'Earnings', path: 'earnings', screens: { Earnings: '', WeeklyEarnings: 'weekly-earnings', }, }, }, }, }, }; const state = { routes: [ { name: 'RootTabs', state: { routes: [ { name: 'EarningsTab', state: { index: 1, routes: [ { name: 'Earnings', }, { name: 'WeeklyEarnings', path, }, ], }, }, ], }, }, ], }; testConversions(path, config, state); }); it('throws when invalid properties are specified in the config', () => { expect(() => getStateFromPath('', { Foo: 'foo', Bar: { path: 'bar', }, } as any) ).toThrowErrorMatchingInlineSnapshot(` "Found invalid properties in the configuration: - Foo - Bar Did you forget to specify them under a 'screens' property? You can only specify the following properties: - initialRouteName - screens - _route See https://reactnavigation.org/docs/configuring-links for more details on how to specify a linking configuration." `); expect(() => getStateFromPath('', { screens: { Foo: 'foo', Bar: { path: 'bar', }, Baz: { Qux: { path: 'qux', }, }, }, } as any) ).toThrowErrorMatchingInlineSnapshot(` "Found invalid properties in the configuration: - Qux Did you forget to specify them under a 'screens' property? You can only specify the following properties: - initialRouteName - screens - _route - path - exact - stringify - parse See https://reactnavigation.org/docs/configuring-links for more details on how to specify a linking configuration." `); }); function testConversions(path: string, config: any, state: any) { expect(getStateFromPath(path, config)).toEqual(state); expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(state); }