Skip to content

Commit e10a3f1

Browse files
add regression test
1 parent b4142b5 commit e10a3f1

File tree

1 file changed

+95
-1
lines changed

1 file changed

+95
-1
lines changed

packages/router-core/tests/callbacks.test.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it, test, vi } from 'vitest'
22
import { createMemoryHistory } from '@tanstack/history'
3-
import { BaseRootRoute, BaseRoute } from '../src'
3+
import { BaseRootRoute, BaseRoute, createControlledPromise } from '../src'
44
import { createTestRouter } from './routerTestUtils'
55

66
describe('callbacks', () => {
@@ -157,6 +157,100 @@ describe('callbacks', () => {
157157
expect(onLeave).toHaveBeenCalledTimes(0)
158158
expect(onStay).toHaveBeenCalledTimes(2)
159159
})
160+
161+
it('treats a navigation that changes the match instance and match id for the same routeId as stay, not leave plus enter', async () => {
162+
const onEnter = vi.fn()
163+
const onLeave = vi.fn()
164+
const onStay = vi.fn()
165+
const loaderPromises: Array<ReturnType<typeof createControlledPromise<void>>> =
166+
[]
167+
168+
const rootRoute = new BaseRootRoute({})
169+
const fooRoute = new BaseRoute({
170+
getParentRoute: () => rootRoute,
171+
path: '/foo',
172+
loaderDeps: ({ search }: { search: Record<string, unknown> }) => ({
173+
page: search['page'],
174+
}),
175+
onEnter,
176+
onLeave,
177+
onStay,
178+
loader: () => {
179+
const promise = createControlledPromise<void>()
180+
loaderPromises.push(promise)
181+
return promise
182+
},
183+
})
184+
185+
const router = createTestRouter({
186+
routeTree: rootRoute.addChildren([fooRoute]),
187+
history: createMemoryHistory(),
188+
})
189+
190+
const initialNavigation = router.navigate({
191+
to: '/foo',
192+
search: { page: '1' },
193+
})
194+
195+
expect(loaderPromises).toHaveLength(1)
196+
loaderPromises[0]!.resolve()
197+
await initialNavigation
198+
199+
onEnter.mockClear()
200+
onLeave.mockClear()
201+
onStay.mockClear()
202+
203+
const currentMatches = router.stores.activeMatchesSnapshot.state
204+
const currentFooMatch = currentMatches.find(
205+
(match) => match.routeId === fooRoute.id,
206+
)
207+
208+
expect(currentFooMatch).toBeDefined()
209+
expect(router.looseRoutesById[fooRoute.id]!.options.onStay).toBe(onStay)
210+
expect(router.looseRoutesById[fooRoute.id]!.options.onLeave).toBe(onLeave)
211+
expect(router.looseRoutesById[fooRoute.id]!.options.onEnter).toBe(onEnter)
212+
213+
const secondNavigation = router.navigate({
214+
to: '/foo',
215+
search: { page: '2' },
216+
})
217+
218+
expect(loaderPromises).toHaveLength(2)
219+
220+
let pendingMatches = router.stores.pendingMatchesSnapshot.state
221+
for (let index = 0; index < 10 && pendingMatches.length === 0; index++) {
222+
await Promise.resolve()
223+
pendingMatches = router.stores.pendingMatchesSnapshot.state
224+
}
225+
226+
const pendingFooMatch = pendingMatches.find(
227+
(match) => match.routeId === fooRoute.id,
228+
)
229+
230+
expect(pendingFooMatch).toBeDefined()
231+
expect(pendingFooMatch).not.toBe(currentFooMatch)
232+
expect(pendingFooMatch!.id).not.toBe(currentFooMatch!.id)
233+
expect(pendingFooMatch!.routeId).toBe(currentFooMatch!.routeId)
234+
235+
const pendingRouteIds = new Set(pendingMatches.map((match) => match.routeId))
236+
const activeRouteIds = new Set(currentMatches.map((match) => match.routeId))
237+
238+
expect(pendingRouteIds.has(fooRoute.id)).toBe(true)
239+
expect(activeRouteIds.has(fooRoute.id)).toBe(true)
240+
241+
loaderPromises[1]!.resolve()
242+
await secondNavigation
243+
244+
expect(onStay).toHaveBeenCalledTimes(1)
245+
expect(onStay).toHaveBeenCalledWith(
246+
expect.objectContaining({
247+
id: pendingFooMatch!.id,
248+
routeId: fooRoute.id,
249+
}),
250+
)
251+
expect(onLeave).toHaveBeenCalledTimes(0)
252+
expect(onEnter).toHaveBeenCalledTimes(0)
253+
})
160254
})
161255

162256
// Regression tests: switching lifecycle hooks to use routeId must NOT break

0 commit comments

Comments
 (0)