1import { renderHook as tlRenderHook } from '@testing-library/react-native';
2import React from 'react';
3import { expectType } from 'tsd';
4
5import { ExpoRoot, router } from '../exports';
6import { useGlobalSearchParams, useLocalSearchParams, usePathname, useSegments } from '../hooks';
7import Stack from '../layouts/Stack';
8import { act, renderRouter } from '../testing-library';
9import { inMemoryContext } from '../testing-library/context-stubs';
10
11/*
12 * Creates an Expo Router context around the hook, where every router renders the hook
13 * This allows you full navigation
14 */
15function renderHook<T>(
16  renderCallback: () => T,
17  routes: string[] = ['index'],
18  { initialUrl = '/' }: { initialUrl?: string } = {}
19) {
20  return tlRenderHook(renderCallback, {
21    wrapper: function Wrapper({ children }) {
22      const context = {};
23      for (const key of routes) {
24        context[key] = () => <>{children}</>;
25      }
26
27      return (
28        <ExpoRoot
29          context={inMemoryContext(context)}
30          location={new URL(initialUrl, 'test://test')}
31        />
32      );
33    },
34  });
35}
36
37function renderHookOnce<T>(
38  renderCallback: () => T,
39  routes?: string[],
40  options?: { initialUrl?: string }
41) {
42  return renderHook<T>(renderCallback, routes, options).result.current;
43}
44
45describe(useSegments, () => {
46  it(`defaults abstract types`, () => {
47    const segments = renderHookOnce(() => useSegments());
48    expectType<string>(segments[0]);
49    expectType<string[]>(segments);
50  });
51  it(`allows abstract types`, () => {
52    const segments = renderHookOnce(() => useSegments<['alpha']>());
53    expectType<'alpha'>(segments[0]);
54  });
55  it(`allows abstract union types`, () => {
56    const segments = renderHookOnce(() => useSegments<['a'] | ['b'] | ['b', 'c']>());
57    expectType<'a' | 'b'>(segments[0]);
58    if (segments[0] === 'b') expectType<'c' | undefined>(segments[1]);
59  });
60});
61
62describe(useGlobalSearchParams, () => {
63  it(`return styles of deeply nested routes`, () => {
64    const { result } = renderHook(() => useGlobalSearchParams(), ['[fruit]/[shape]/[...veg?]'], {
65      initialUrl: '/apple/square',
66    });
67
68    expect(result.current).toEqual({
69      fruit: 'apple',
70      shape: 'square',
71    });
72
73    act(() => router.push('/banana/circle/carrot'));
74
75    expect(result.current).toEqual({
76      fruit: 'banana',
77      shape: 'circle',
78      veg: 'carrot',
79    });
80  });
81
82  it(`defaults abstract types`, () => {
83    const params = renderHookOnce(() => useGlobalSearchParams());
84    expectType<Record<string, string | string[] | undefined>>(params);
85    expectType<string | string[] | undefined>(params.a);
86  });
87  it(`allows abstract types`, () => {
88    const params = renderHookOnce(() => useGlobalSearchParams<{ a: string }>());
89    expectType<{ a?: string }>(params);
90    expectType<string | undefined>(params.a);
91  });
92
93  it(`only renders once per navigation`, () => {
94    const allHookValues: unknown[] = [];
95
96    renderRouter(
97      {
98        '[fruit]/[shape]/[...veg?]': function Test() {
99          allHookValues.push({
100            url: usePathname(),
101            globalParams: useGlobalSearchParams(),
102            params: useLocalSearchParams(),
103          });
104          return null;
105        },
106      },
107      {
108        initialUrl: '/apple/square',
109      }
110    );
111
112    act(() => router.push('/banana/circle/carrot'));
113
114    expect(allHookValues).toEqual([
115      // The initial render
116      {
117        url: '/apple/square',
118        globalParams: {
119          fruit: 'apple',
120          shape: 'square',
121        },
122        params: {
123          fruit: 'apple',
124          shape: 'square',
125        },
126      },
127      // The new screen
128      {
129        url: '/banana/circle/carrot',
130        globalParams: {
131          fruit: 'banana',
132          shape: 'circle',
133          veg: 'carrot',
134        },
135        params: {
136          fruit: 'banana',
137          shape: 'circle',
138          veg: ['carrot'],
139        },
140      },
141    ]);
142  });
143
144  it(`causes stacks in a screen to rerender on change `, () => {
145    const allHookValues: unknown[] = [];
146
147    // When using a navigation that keeps the screens in memory (e.g. Stack)
148    // , any <Screen /> that uses useGlobalSearchParams Should update
149    // when the searchparams change, even if not visible
150    //
151    // This is different to the "only renders once per navigation" which only renders
152    // the current screen
153
154    renderRouter(
155      {
156        _layout: () => <Stack />,
157        '[fruit]/[shape]/[...veg?]': function Test() {
158          allHookValues.push({
159            url: usePathname(),
160            globalParams: useGlobalSearchParams(),
161            params: useLocalSearchParams(),
162          });
163          return null;
164        },
165      },
166      {
167        initialUrl: '/apple/square',
168      }
169    );
170
171    act(() => router.push('/banana/circle/carrot'));
172
173    expect(allHookValues).toEqual([
174      // The initial render
175      {
176        url: '/apple/square',
177        globalParams: {
178          fruit: 'apple',
179          shape: 'square',
180        },
181        params: {
182          fruit: 'apple',
183          shape: 'square',
184        },
185      },
186      // The new screen
187      {
188        url: '/banana/circle/carrot',
189        globalParams: {
190          fruit: 'banana',
191          shape: 'circle',
192          veg: 'carrot',
193        },
194        params: {
195          fruit: 'banana',
196          shape: 'circle',
197          veg: ['carrot'],
198        },
199      },
200      // The is the first page rerendering due to being in a <Stack />
201      {
202        url: '/banana/circle/carrot',
203        globalParams: {
204          fruit: 'banana',
205          shape: 'circle',
206          veg: 'carrot',
207        },
208        params: {
209          fruit: 'apple',
210          shape: 'square',
211        },
212      },
213    ]);
214  });
215});
216
217describe(useLocalSearchParams, () => {
218  it(`return styles of deeply nested routes`, () => {
219    const { result } = renderHook(() => useGlobalSearchParams(), ['[fruit]/[shape]/[...veg?]'], {
220      initialUrl: '/apple/square',
221    });
222
223    expect(result.current).toEqual({
224      fruit: 'apple',
225      shape: 'square',
226    });
227
228    act(() => router.push('/banana/circle/carrot'));
229
230    expect(result.current).toEqual({
231      fruit: 'banana',
232      shape: 'circle',
233      veg: 'carrot',
234    });
235  });
236
237  it(`defaults abstract types`, () => {
238    const params = renderHookOnce(() => useLocalSearchParams());
239    expectType<Record<string, string | string[] | undefined>>(params);
240    expectType<string | string[] | undefined>(params.a);
241  });
242  it(`allows abstract types`, () => {
243    const params = renderHookOnce(() => useLocalSearchParams<{ a: string }>());
244    expectType<{ a?: string }>(params);
245    expectType<string | undefined>(params.a);
246  });
247});
248
249describe(usePathname, () => {
250  it(`return pathname of deeply nested routes`, () => {
251    const { result } = renderHook(() => usePathname(), ['[fruit]/[shape]/[...veg?]'], {
252      initialUrl: '/apple/square',
253    });
254
255    expect(result.current).toEqual('/apple/square');
256
257    act(() => router.push('/banana/circle/carrot'));
258    expect(result.current).toEqual('/banana/circle/carrot');
259
260    act(() => router.push('/banana/circle/carrot/beetroot'));
261    expect(result.current).toEqual('/banana/circle/carrot/beetroot');
262
263    act(() => router.push('/banana/circle/carrot/beetroot/beans'));
264    expect(result.current).toEqual('/banana/circle/carrot/beetroot/beans');
265
266    act(() => router.push('/banana/circle/carrot/beetroot?foo=bar'));
267    expect(result.current).toEqual('/banana/circle/carrot/beetroot');
268  });
269});
270
271describe('hooks rendering', () => {});
272