|
1 | 1 | import { describe, expect, it, test, vi } from 'vitest' |
2 | 2 | import { createMemoryHistory } from '@tanstack/history' |
3 | | -import { BaseRootRoute, BaseRoute } from '../src' |
| 3 | +import { BaseRootRoute, BaseRoute, createControlledPromise } from '../src' |
4 | 4 | import { createTestRouter } from './routerTestUtils' |
5 | 5 |
|
6 | 6 | describe('callbacks', () => { |
@@ -157,6 +157,100 @@ describe('callbacks', () => { |
157 | 157 | expect(onLeave).toHaveBeenCalledTimes(0) |
158 | 158 | expect(onStay).toHaveBeenCalledTimes(2) |
159 | 159 | }) |
| 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 | + }) |
160 | 254 | }) |
161 | 255 |
|
162 | 256 | // Regression tests: switching lifecycle hooks to use routeId must NOT break |
|
0 commit comments