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