1import Constants from 'expo-constants';
2
3import { configFromFs } from '../../utils/mockState';
4import getPathFromState from '../getPathFromState';
5import getStateFromPath, {
6  stripBasePath,
7  getUrlWithReactNavigationConcessions,
8} from '../getStateFromPath';
9
10jest.mock('expo-constants', () => ({
11  __esModule: true,
12  default: {
13    expoConfig: {},
14  },
15}));
16
17afterEach(() => {
18  Constants.expoConfig!.experiments = undefined;
19});
20
21describe(stripBasePath, () => {
22  [
23    [
24      // Input
25      '/',
26      // Base Path
27      '',
28      // Result
29      '/',
30    ],
31    ['/one/two', '/one', '/two'],
32    ['/one/two', '/one/two', ''],
33    ['/one/two/', '/one/two', '/'],
34    ['///one/', '/one', '/'],
35    ['one/', '/one', 'one/'],
36    ['/a/b', '/one', '/a/b'],
37  ].forEach(([path, basePath, result]) => {
38    it(`strips basePath "${path}"`, () => {
39      expect(stripBasePath(path, basePath)).toBe(result);
40    });
41  });
42});
43
44describe('basePath', () => {
45  it('accounts for basePath', () => {
46    // @ts-expect-error
47    Constants.expoConfig = {
48      experiments: {
49        basePath: '/expo/prefix',
50      },
51    };
52    const path = '/expo/prefix/bar';
53    const config = configFromFs(['_layout.tsx', 'bar.tsx', 'index.tsx']);
54
55    expect(getStateFromPath<object>(path, config)).toEqual({
56      routes: [{ name: '', state: { routes: [{ name: 'bar', path: '/bar' }] } }],
57    });
58
59    expect(getPathFromState(getStateFromPath<object>(path, config), config)).toBe(
60      '/expo/prefix/bar'
61    );
62  });
63
64  it('has basePath and state that does not match', () => {
65    // @ts-expect-error
66    Constants.expoConfig = {
67      experiments: {
68        basePath: '/expo',
69      },
70    };
71    const path = '/bar';
72    const config = configFromFs(['_layout.tsx', 'bar.tsx', 'index.tsx']);
73
74    expect(getStateFromPath<object>(path, config)).toEqual({
75      routes: [{ name: '', state: { routes: [{ name: 'bar', path: '/bar' }] } }],
76    });
77    expect(getPathFromState(getStateFromPath<object>(path, config), config)).toBe('/expo/bar');
78  });
79});
80
81describe(getUrlWithReactNavigationConcessions, () => {
82  ['/', 'foo/', 'foo/bar/', 'foo/bar/baz/'].forEach((path) => {
83    it(`returns the pathname for ${path}`, () => {
84      expect(getUrlWithReactNavigationConcessions(path).nonstandardPathname).toBe(path);
85    });
86  });
87
88  [
89    ['', '/'],
90    ['https://acme.com/hello/world?foo=bar#123', 'hello/world/'],
91    ['https://acme.com/hello/world/?foo=bar#123', 'hello/world/'],
92  ].forEach(([url, expected]) => {
93    it(`returns the pathname for ${url}`, () => {
94      expect(getUrlWithReactNavigationConcessions(url).nonstandardPathname).toBe(expected);
95    });
96  });
97
98  [
99    ['/gh-pages/', '/'],
100    ['https://acme.com/gh-pages/hello/world?foo=bar#123', 'hello/world/'],
101    ['https://acme.com/gh-pages/hello/world/?foo=bar#123', 'hello/world/'],
102  ].forEach(([url, expected]) => {
103    it(`returns the pathname for ${url}`, () => {
104      expect(getUrlWithReactNavigationConcessions(url, 'gh-pages').nonstandardPathname).toBe(
105        expected
106      );
107    });
108  });
109
110  [
111    ['', ''],
112    ['https://acme.com/hello/world/?foo=bar#123', 'https://acme.com/hello/world/?foo=bar'],
113    ['/foobar#123', '/foobar'],
114  ].forEach(([url, expected]) => {
115    it(`returns the pathname without hash for ${url}`, () => {
116      expect(getUrlWithReactNavigationConcessions(url).inputPathnameWithoutHash).toBe(expected);
117    });
118  });
119});
120
121it(`strips hashes`, () => {
122  expect(
123    getStateFromPath('/hello#123', {
124      screens: {
125        hello: 'hello',
126      },
127    } as any)
128  ).toEqual({
129    routes: [
130      {
131        name: 'hello',
132        path: '/hello',
133      },
134    ],
135  });
136
137  expect(getStateFromPath('/hello#123', configFromFs(['[hello].js']))).toEqual({
138    routes: [
139      {
140        name: '[hello]',
141        params: {
142          hello: 'hello',
143        },
144        path: '/hello',
145      },
146    ],
147  });
148
149  // TODO: Test rest params
150});
151
152it(`supports spaces`, () => {
153  expect(
154    getStateFromPath('/hello%20world', {
155      screens: {
156        'hello world': 'hello world',
157      },
158    } as any)
159  ).toEqual({
160    routes: [
161      {
162        name: 'hello world',
163        path: '/hello%20world',
164      },
165    ],
166  });
167
168  expect(getStateFromPath('/hello%20world', configFromFs(['[hello world].js']))).toEqual({
169    routes: [
170      {
171        name: '[hello world]',
172        params: {
173          'hello world': 'hello%20world',
174        },
175        path: '/hello%20world',
176      },
177    ],
178  });
179
180  // TODO: Test rest params
181});
182
183it(`matches unmatched existing groups against 404`, () => {
184  expect(
185    getStateFromPath(
186      '/(app)/(explore)',
187      configFromFs([
188        '[...404].js',
189
190        '(app)/_layout.tsx',
191
192        '(app)/(explore)/_layout.tsx',
193        '(app)/(explore)/[user]/index.tsx',
194        '(app)/(explore)/explore.tsx',
195
196        '(app)/([user])/_layout.tsx',
197        '(app)/([user])/[user]/index.tsx',
198        '(app)/([user])/explore.tsx',
199      ])
200    )
201  ).toEqual({
202    routes: [
203      {
204        name: '(app)',
205        params: { user: '(explore)' },
206        state: {
207          routes: [
208            {
209              name: '([user])',
210              params: { user: '(explore)' },
211              state: {
212                routes: [
213                  {
214                    name: '[user]/index',
215                    params: { user: '(explore)' },
216                    path: '',
217                  },
218                ],
219              },
220            },
221          ],
222        },
223      },
224    ],
225  });
226});
227
228it(`adds dynamic route params from all levels of the path`, () => {
229  // A route at `app/[foo]/bar/[baz]/other` should get all of the params from the path.
230  expect(
231    getStateFromPath(
232      '/foo/bar/baz/other',
233
234      configFromFs([
235        '[foo]/_layout.tsx',
236        '[foo]/bar/_layout.tsx',
237        '[foo]/bar/[baz]/_layout.tsx',
238        '[foo]/bar/[baz]/other.tsx',
239      ])
240    )
241  ).toEqual({
242    routes: [
243      {
244        name: '[foo]',
245        params: { baz: 'baz', foo: 'foo' },
246        state: {
247          routes: [
248            {
249              name: 'bar',
250              params: { baz: 'baz', foo: 'foo' },
251              state: {
252                routes: [
253                  {
254                    name: '[baz]',
255                    params: { baz: 'baz', foo: 'foo' },
256                    state: {
257                      routes: [
258                        {
259                          name: 'other',
260                          params: {
261                            baz: 'baz',
262                            foo: 'foo',
263                          },
264                          path: '/foo/bar/baz/other',
265                        },
266                      ],
267                    },
268                  },
269                ],
270              },
271            },
272          ],
273        },
274      },
275    ],
276  });
277});
278