@pyreon/router 0.11.5 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- import { hydrateLoaderData, prefetchLoaderData, serializeLoaderData } from "../loader"
2
- import { createRouter, setActiveRouter, useIsActive, useSearchParams } from "../router"
3
- import type { RouteRecord, RouterInstance } from "../types"
1
+ import { hydrateLoaderData, prefetchLoaderData, serializeLoaderData } from '../loader'
2
+ import { createRouter, setActiveRouter, useIsActive, useSearchParams } from '../router'
3
+ import type { RouteRecord, RouterInstance } from '../types'
4
4
 
5
5
  const Home = () => null
6
6
  const About = () => null
@@ -8,44 +8,44 @@ const User = () => null
8
8
 
9
9
  // ─── serializeLoaderData / hydrateLoaderData round-trip edge cases ────────────
10
10
 
11
- describe("loader data serialization — edge cases", () => {
12
- test("serializes multiple route loaders", async () => {
11
+ describe('loader data serialization — edge cases', () => {
12
+ test('serializes multiple route loaders', async () => {
13
13
  const routes: RouteRecord[] = [
14
14
  {
15
- path: "/admin",
15
+ path: '/admin',
16
16
  component: Home,
17
- loader: async () => "admin-data",
17
+ loader: async () => 'admin-data',
18
18
  children: [
19
19
  {
20
- path: "users",
20
+ path: 'users',
21
21
  component: About,
22
- loader: async () => "users-data",
22
+ loader: async () => 'users-data',
23
23
  },
24
24
  ],
25
25
  },
26
26
  ]
27
- const router = createRouter({ routes, url: "/admin/users" }) as RouterInstance
28
- await prefetchLoaderData(router, "/admin/users")
27
+ const router = createRouter({ routes, url: '/admin/users' }) as RouterInstance
28
+ await prefetchLoaderData(router, '/admin/users')
29
29
 
30
30
  const serialized = serializeLoaderData(router)
31
- expect(serialized["/admin"]).toBe("admin-data")
32
- expect(serialized.users).toBe("users-data")
31
+ expect(serialized['/admin']).toBe('admin-data')
32
+ expect(serialized.users).toBe('users-data')
33
33
  })
34
34
 
35
- test("hydrate ignores paths not in current route matched", () => {
35
+ test('hydrate ignores paths not in current route matched', () => {
36
36
  const routes: RouteRecord[] = [
37
- { path: "/", component: Home },
38
- { path: "/page", component: About, loader: async () => [] },
37
+ { path: '/', component: Home },
38
+ { path: '/page', component: About, loader: async () => [] },
39
39
  ]
40
- const router = createRouter({ routes, url: "/" }) as RouterInstance
40
+ const router = createRouter({ routes, url: '/' }) as RouterInstance
41
41
  // Hydrate with data for a path that is NOT currently matched
42
- hydrateLoaderData(router, { "/page": "should-be-ignored" })
42
+ hydrateLoaderData(router, { '/page': 'should-be-ignored' })
43
43
  expect(router._loaderData.size).toBe(0)
44
44
  })
45
45
 
46
- test("hydrate with non-object values is no-op", () => {
47
- const routes: RouteRecord[] = [{ path: "/", component: Home }]
48
- const router = createRouter({ routes, url: "/" }) as RouterInstance
46
+ test('hydrate with non-object values is no-op', () => {
47
+ const routes: RouteRecord[] = [{ path: '/', component: Home }]
48
+ const router = createRouter({ routes, url: '/' }) as RouterInstance
49
49
 
50
50
  // These should not throw
51
51
  hydrateLoaderData(router, null as unknown as Record<string, unknown>)
@@ -54,313 +54,313 @@ describe("loader data serialization — edge cases", () => {
54
54
  expect(router._loaderData.size).toBe(0)
55
55
  })
56
56
 
57
- test("round-trip with complex data types", async () => {
57
+ test('round-trip with complex data types', async () => {
58
58
  const complexData = {
59
59
  items: [
60
- { id: 1, name: "Item 1" },
61
- { id: 2, name: "Item 2" },
60
+ { id: 1, name: 'Item 1' },
61
+ { id: 2, name: 'Item 2' },
62
62
  ],
63
63
  meta: { total: 2, page: 1 },
64
64
  nested: { deep: { value: true } },
65
65
  }
66
66
  const routes: RouteRecord[] = [
67
- { path: "/data", component: Home, loader: async () => complexData },
67
+ { path: '/data', component: Home, loader: async () => complexData },
68
68
  ]
69
- const ssrRouter = createRouter({ routes, url: "/data" }) as RouterInstance
70
- await prefetchLoaderData(ssrRouter, "/data")
69
+ const ssrRouter = createRouter({ routes, url: '/data' }) as RouterInstance
70
+ await prefetchLoaderData(ssrRouter, '/data')
71
71
 
72
72
  const serialized = serializeLoaderData(ssrRouter)
73
- const clientRouter = createRouter({ routes, url: "/data" }) as RouterInstance
73
+ const clientRouter = createRouter({ routes, url: '/data' }) as RouterInstance
74
74
  hydrateLoaderData(clientRouter, serialized)
75
75
 
76
76
  const values = Array.from(clientRouter._loaderData.values())
77
77
  expect(values[0]).toEqual(complexData)
78
78
  })
79
79
 
80
- test("prefetchLoaderData passes AbortSignal to loaders", async () => {
80
+ test('prefetchLoaderData passes AbortSignal to loaders', async () => {
81
81
  let receivedSignal: AbortSignal | undefined
82
82
  const routes: RouteRecord[] = [
83
83
  {
84
- path: "/data",
84
+ path: '/data',
85
85
  component: Home,
86
86
  loader: async ({ signal }) => {
87
87
  receivedSignal = signal
88
- return "ok"
88
+ return 'ok'
89
89
  },
90
90
  },
91
91
  ]
92
- const router = createRouter({ routes, url: "/" }) as RouterInstance
93
- await prefetchLoaderData(router, "/data")
92
+ const router = createRouter({ routes, url: '/' }) as RouterInstance
93
+ await prefetchLoaderData(router, '/data')
94
94
  expect(receivedSignal).toBeDefined()
95
95
  expect(receivedSignal).toBeInstanceOf(AbortSignal)
96
96
  })
97
97
 
98
- test("prefetchLoaderData skips routes without loaders", async () => {
98
+ test('prefetchLoaderData skips routes without loaders', async () => {
99
99
  const routes: RouteRecord[] = [
100
100
  {
101
- path: "/admin",
101
+ path: '/admin',
102
102
  component: Home,
103
103
  children: [
104
- { path: "users", component: About }, // no loader
104
+ { path: 'users', component: About }, // no loader
105
105
  ],
106
106
  },
107
107
  ]
108
- const router = createRouter({ routes, url: "/" }) as RouterInstance
109
- await prefetchLoaderData(router, "/admin/users")
108
+ const router = createRouter({ routes, url: '/' }) as RouterInstance
109
+ await prefetchLoaderData(router, '/admin/users')
110
110
  expect(router._loaderData.size).toBe(0)
111
111
  })
112
112
  })
113
113
 
114
114
  // ─── useIsActive — edge cases ────────────────────────────────────────────────
115
115
 
116
- describe("useIsActive — edge cases", () => {
116
+ describe('useIsActive — edge cases', () => {
117
117
  beforeEach(() => {
118
118
  setActiveRouter(null)
119
119
  })
120
120
 
121
- test("throws when no router installed", () => {
122
- expect(() => useIsActive("/")).toThrow("[pyreon-router] No router installed")
121
+ test('throws when no router installed', () => {
122
+ expect(() => useIsActive('/')).toThrow('[pyreon-router] No router installed')
123
123
  })
124
124
 
125
- test("exact match for root path", () => {
126
- const router = createRouter({ routes: [{ path: "/", component: Home }], url: "/" })
125
+ test('exact match for root path', () => {
126
+ const router = createRouter({ routes: [{ path: '/', component: Home }], url: '/' })
127
127
  setActiveRouter(router as RouterInstance)
128
- const isActive = useIsActive("/", true)
128
+ const isActive = useIsActive('/', true)
129
129
  expect(isActive()).toBe(true)
130
130
  })
131
131
 
132
- test("partial match: /admin matches /admin/users", () => {
132
+ test('partial match: /admin matches /admin/users', () => {
133
133
  const routes: RouteRecord[] = [
134
134
  {
135
- path: "/admin",
135
+ path: '/admin',
136
136
  component: Home,
137
- children: [{ path: "users", component: About }],
137
+ children: [{ path: 'users', component: About }],
138
138
  },
139
139
  ]
140
- const router = createRouter({ routes, url: "/admin/users" })
140
+ const router = createRouter({ routes, url: '/admin/users' })
141
141
  setActiveRouter(router as RouterInstance)
142
- const isActive = useIsActive("/admin")
142
+ const isActive = useIsActive('/admin')
143
143
  expect(isActive()).toBe(true)
144
144
  })
145
145
 
146
- test("partial match: /admin does NOT match /admin-panel", async () => {
146
+ test('partial match: /admin does NOT match /admin-panel', async () => {
147
147
  const routes: RouteRecord[] = [
148
- { path: "/admin", component: Home },
149
- { path: "/admin-panel", component: About },
148
+ { path: '/admin', component: Home },
149
+ { path: '/admin-panel', component: About },
150
150
  ]
151
- const router = createRouter({ routes, url: "/admin-panel" })
151
+ const router = createRouter({ routes, url: '/admin-panel' })
152
152
  setActiveRouter(router as RouterInstance)
153
- const isActive = useIsActive("/admin")
153
+ const isActive = useIsActive('/admin')
154
154
  expect(isActive()).toBe(false)
155
155
  })
156
156
 
157
- test("exact match: /admin does NOT match /admin/users", () => {
157
+ test('exact match: /admin does NOT match /admin/users', () => {
158
158
  const routes: RouteRecord[] = [
159
159
  {
160
- path: "/admin",
160
+ path: '/admin',
161
161
  component: Home,
162
- children: [{ path: "users", component: About }],
162
+ children: [{ path: 'users', component: About }],
163
163
  },
164
164
  ]
165
- const router = createRouter({ routes, url: "/admin/users" })
165
+ const router = createRouter({ routes, url: '/admin/users' })
166
166
  setActiveRouter(router as RouterInstance)
167
- const isActive = useIsActive("/admin", true)
167
+ const isActive = useIsActive('/admin', true)
168
168
  expect(isActive()).toBe(false)
169
169
  })
170
170
 
171
- test("root path partial match only matches /", () => {
171
+ test('root path partial match only matches /', () => {
172
172
  const routes: RouteRecord[] = [
173
- { path: "/", component: Home },
174
- { path: "/about", component: About },
173
+ { path: '/', component: Home },
174
+ { path: '/about', component: About },
175
175
  ]
176
- const router = createRouter({ routes, url: "/about" })
176
+ const router = createRouter({ routes, url: '/about' })
177
177
  setActiveRouter(router as RouterInstance)
178
- const isActive = useIsActive("/")
178
+ const isActive = useIsActive('/')
179
179
  // Root path in partial mode should only match "/"
180
180
  expect(isActive()).toBe(false)
181
181
  })
182
182
 
183
- test("param pattern: /user/:id matches /user/42 in exact mode", () => {
184
- const routes: RouteRecord[] = [{ path: "/user/:id", component: User }]
185
- const router = createRouter({ routes, url: "/user/42" })
183
+ test('param pattern: /user/:id matches /user/42 in exact mode', () => {
184
+ const routes: RouteRecord[] = [{ path: '/user/:id', component: User }]
185
+ const router = createRouter({ routes, url: '/user/42' })
186
186
  setActiveRouter(router as RouterInstance)
187
- const isActive = useIsActive("/user/:id", true)
187
+ const isActive = useIsActive('/user/:id', true)
188
188
  expect(isActive()).toBe(true)
189
189
  })
190
190
 
191
- test("param pattern: /user/:id matches /user/42 in partial mode", () => {
191
+ test('param pattern: /user/:id matches /user/42 in partial mode', () => {
192
192
  const routes: RouteRecord[] = [
193
193
  {
194
- path: "/user/:id",
194
+ path: '/user/:id',
195
195
  component: User,
196
- children: [{ path: "posts", component: About }],
196
+ children: [{ path: 'posts', component: About }],
197
197
  },
198
198
  ]
199
- const router = createRouter({ routes, url: "/user/42/posts" })
199
+ const router = createRouter({ routes, url: '/user/42/posts' })
200
200
  setActiveRouter(router as RouterInstance)
201
- const isActive = useIsActive("/user/:id")
201
+ const isActive = useIsActive('/user/:id')
202
202
  expect(isActive()).toBe(true)
203
203
  })
204
204
 
205
- test("exact match with wrong segment count returns false", () => {
206
- const routes: RouteRecord[] = [{ path: "/a/b/c", component: Home }]
207
- const router = createRouter({ routes, url: "/a/b/c" })
205
+ test('exact match with wrong segment count returns false', () => {
206
+ const routes: RouteRecord[] = [{ path: '/a/b/c', component: Home }]
207
+ const router = createRouter({ routes, url: '/a/b/c' })
208
208
  setActiveRouter(router as RouterInstance)
209
- const isActive = useIsActive("/a/b", true)
209
+ const isActive = useIsActive('/a/b', true)
210
210
  expect(isActive()).toBe(false)
211
211
  })
212
212
 
213
- test("partial match with more pattern segments than current returns false", () => {
214
- const routes: RouteRecord[] = [{ path: "/a", component: Home }]
215
- const router = createRouter({ routes, url: "/a" })
213
+ test('partial match with more pattern segments than current returns false', () => {
214
+ const routes: RouteRecord[] = [{ path: '/a', component: Home }]
215
+ const router = createRouter({ routes, url: '/a' })
216
216
  setActiveRouter(router as RouterInstance)
217
- const isActive = useIsActive("/a/b/c")
217
+ const isActive = useIsActive('/a/b/c')
218
218
  expect(isActive()).toBe(false)
219
219
  })
220
220
  })
221
221
 
222
222
  // ─── useSearchParams — edge cases ────────────────────────────────────────────
223
223
 
224
- describe("useSearchParams — edge cases", () => {
224
+ describe('useSearchParams — edge cases', () => {
225
225
  beforeEach(() => {
226
226
  setActiveRouter(null)
227
227
  })
228
228
 
229
- test("throws when no router installed", () => {
230
- expect(() => useSearchParams()).toThrow("[pyreon-router] No router installed")
229
+ test('throws when no router installed', () => {
230
+ expect(() => useSearchParams()).toThrow('[pyreon-router] No router installed')
231
231
  })
232
232
 
233
- test("returns query params from current route", () => {
234
- const routes: RouteRecord[] = [{ path: "/search", component: Home }]
235
- const router = createRouter({ routes, url: "/search?q=hello&page=1" })
233
+ test('returns query params from current route', () => {
234
+ const routes: RouteRecord[] = [{ path: '/search', component: Home }]
235
+ const router = createRouter({ routes, url: '/search?q=hello&page=1' })
236
236
  setActiveRouter(router as RouterInstance)
237
237
  const [get] = useSearchParams()
238
- expect(get().q).toBe("hello")
239
- expect(get().page).toBe("1")
238
+ expect(get().q).toBe('hello')
239
+ expect(get().page).toBe('1')
240
240
  })
241
241
 
242
- test("merges defaults with route query", () => {
243
- const routes: RouteRecord[] = [{ path: "/search", component: Home }]
244
- const router = createRouter({ routes, url: "/search?q=hello" })
242
+ test('merges defaults with route query', () => {
243
+ const routes: RouteRecord[] = [{ path: '/search', component: Home }]
244
+ const router = createRouter({ routes, url: '/search?q=hello' })
245
245
  setActiveRouter(router as RouterInstance)
246
- const [get] = useSearchParams({ q: "", page: "1", sort: "name" })
247
- expect(get().q).toBe("hello") // from URL, overrides default
248
- expect(get().page).toBe("1") // from default
249
- expect(get().sort).toBe("name") // from default
246
+ const [get] = useSearchParams({ q: '', page: '1', sort: 'name' })
247
+ expect(get().q).toBe('hello') // from URL, overrides default
248
+ expect(get().page).toBe('1') // from default
249
+ expect(get().sort).toBe('name') // from default
250
250
  })
251
251
 
252
- test("set navigates with merged query", async () => {
253
- const routes: RouteRecord[] = [{ path: "/search", component: Home }]
254
- const router = createRouter({ routes, url: "/search?q=hello" })
252
+ test('set navigates with merged query', async () => {
253
+ const routes: RouteRecord[] = [{ path: '/search', component: Home }]
254
+ const router = createRouter({ routes, url: '/search?q=hello' })
255
255
  setActiveRouter(router as RouterInstance)
256
- const [, set] = useSearchParams({ q: "", page: "1" })
256
+ const [, set] = useSearchParams({ q: '', page: '1' })
257
257
 
258
- await set({ page: "2" })
258
+ await set({ page: '2' })
259
259
  // Router should navigate — check that the route updated
260
260
  const route = router.currentRoute()
261
- expect(route.query.page).toBe("2")
262
- expect(route.query.q).toBe("hello")
261
+ expect(route.query.page).toBe('2')
262
+ expect(route.query.q).toBe('hello')
263
263
  })
264
264
  })
265
265
 
266
266
  // ─── Router — trailing slash normalization ───────────────────────────────────
267
267
 
268
- describe("router — trailing slash normalization", () => {
268
+ describe('router — trailing slash normalization', () => {
269
269
  const routes: RouteRecord[] = [
270
- { path: "/", component: Home },
271
- { path: "/about", component: About },
270
+ { path: '/', component: Home },
271
+ { path: '/about', component: About },
272
272
  ]
273
273
 
274
- test("strip mode (default) removes trailing slashes", () => {
275
- const router = createRouter({ routes, url: "/about/" })
276
- expect(router.currentRoute().path).toBe("/about")
274
+ test('strip mode (default) removes trailing slashes', () => {
275
+ const router = createRouter({ routes, url: '/about/' })
276
+ expect(router.currentRoute().path).toBe('/about')
277
277
  })
278
278
 
279
- test("add mode ensures trailing slashes", () => {
280
- const router = createRouter({ routes, url: "/about", trailingSlash: "add" })
281
- expect(router.currentRoute().path).toBe("/about/")
279
+ test('add mode ensures trailing slashes', () => {
280
+ const router = createRouter({ routes, url: '/about', trailingSlash: 'add' })
281
+ expect(router.currentRoute().path).toBe('/about/')
282
282
  })
283
283
 
284
- test("ignore mode does not modify path", () => {
285
- const router = createRouter({ routes, url: "/about/", trailingSlash: "ignore" })
286
- expect(router.currentRoute().path).toBe("/about/")
284
+ test('ignore mode does not modify path', () => {
285
+ const router = createRouter({ routes, url: '/about/', trailingSlash: 'ignore' })
286
+ expect(router.currentRoute().path).toBe('/about/')
287
287
  })
288
288
 
289
- test("root path is not modified by strip mode", () => {
290
- const router = createRouter({ routes, url: "/", trailingSlash: "strip" })
291
- expect(router.currentRoute().path).toBe("/")
289
+ test('root path is not modified by strip mode', () => {
290
+ const router = createRouter({ routes, url: '/', trailingSlash: 'strip' })
291
+ expect(router.currentRoute().path).toBe('/')
292
292
  })
293
293
 
294
- test("strip mode handles path with query string", async () => {
295
- const router = createRouter({ routes, url: "/" })
296
- await router.push("/about/?q=1")
297
- expect(router.currentRoute().path).toBe("/about")
298
- expect(router.currentRoute().query.q).toBe("1")
294
+ test('strip mode handles path with query string', async () => {
295
+ const router = createRouter({ routes, url: '/' })
296
+ await router.push('/about/?q=1')
297
+ expect(router.currentRoute().path).toBe('/about')
298
+ expect(router.currentRoute().query.q).toBe('1')
299
299
  })
300
300
  })
301
301
 
302
302
  // ─── Router — onError handler ────────────────────────────────────────────────
303
303
 
304
- describe("router — onError handler", () => {
305
- test("onError receives error from failed loader", async () => {
304
+ describe('router — onError handler', () => {
305
+ test('onError receives error from failed loader', async () => {
306
306
  const errors: unknown[] = []
307
307
  const routes: RouteRecord[] = [
308
- { path: "/", component: Home },
308
+ { path: '/', component: Home },
309
309
  {
310
- path: "/fail",
310
+ path: '/fail',
311
311
  component: About,
312
312
  loader: async () => {
313
- throw new Error("loader-error")
313
+ throw new Error('loader-error')
314
314
  },
315
315
  },
316
316
  ]
317
317
  const router = createRouter({
318
318
  routes,
319
- url: "/",
319
+ url: '/',
320
320
  onError: (err) => {
321
321
  errors.push(err)
322
322
  return undefined
323
323
  },
324
324
  })
325
325
 
326
- await router.push("/fail")
326
+ await router.push('/fail')
327
327
  expect(errors.length).toBe(1)
328
- expect((errors[0] as Error).message).toBe("loader-error")
329
- expect(router.currentRoute().path).toBe("/fail")
328
+ expect((errors[0] as Error).message).toBe('loader-error')
329
+ expect(router.currentRoute().path).toBe('/fail')
330
330
  })
331
331
 
332
- test("onError returning false cancels navigation", async () => {
332
+ test('onError returning false cancels navigation', async () => {
333
333
  const routes: RouteRecord[] = [
334
- { path: "/", component: Home },
334
+ { path: '/', component: Home },
335
335
  {
336
- path: "/fail",
336
+ path: '/fail',
337
337
  component: About,
338
338
  loader: async () => {
339
- throw new Error("fail")
339
+ throw new Error('fail')
340
340
  },
341
341
  },
342
342
  ]
343
343
  const router = createRouter({
344
344
  routes,
345
- url: "/",
345
+ url: '/',
346
346
  onError: () => false,
347
347
  })
348
348
 
349
- await router.push("/fail")
350
- expect(router.currentRoute().path).toBe("/")
349
+ await router.push('/fail')
350
+ expect(router.currentRoute().path).toBe('/')
351
351
  })
352
352
  })
353
353
 
354
354
  // ─── Router — destroy ────────────────────────────────────────────────────────
355
355
 
356
- describe("router — destroy", () => {
357
- test("destroy clears guards, hooks, caches, and blockers", () => {
358
- const routes: RouteRecord[] = [{ path: "/", component: Home }]
359
- const router = createRouter({ routes, url: "/" }) as RouterInstance
356
+ describe('router — destroy', () => {
357
+ test('destroy clears guards, hooks, caches, and blockers', () => {
358
+ const routes: RouteRecord[] = [{ path: '/', component: Home }]
359
+ const router = createRouter({ routes, url: '/' }) as RouterInstance
360
360
  router.beforeEach(() => true)
361
361
  router.afterEach(() => {})
362
362
  router._blockers.add(() => false)
363
- router._loaderData.set(routes[0] as RouteRecord, "data")
363
+ router._loaderData.set(routes[0] as RouteRecord, 'data')
364
364
 
365
365
  router.destroy()
366
366
 
@@ -369,9 +369,9 @@ describe("router — destroy", () => {
369
369
  expect(router._abortController).toBeNull()
370
370
  })
371
371
 
372
- test("destroy is idempotent (safe to call twice)", () => {
373
- const routes: RouteRecord[] = [{ path: "/", component: Home }]
374
- const router = createRouter({ routes, url: "/" })
372
+ test('destroy is idempotent (safe to call twice)', () => {
373
+ const routes: RouteRecord[] = [{ path: '/', component: Home }]
374
+ const router = createRouter({ routes, url: '/' })
375
375
  expect(() => {
376
376
  router.destroy()
377
377
  router.destroy()
@@ -381,114 +381,114 @@ describe("router — destroy", () => {
381
381
 
382
382
  // ─── Router — isReady ────────────────────────────────────────────────────────
383
383
 
384
- describe("router — isReady", () => {
385
- test("isReady resolves after initial navigation", async () => {
386
- const routes: RouteRecord[] = [{ path: "/", component: Home }]
387
- const router = createRouter({ routes, url: "/" })
384
+ describe('router — isReady', () => {
385
+ test('isReady resolves after initial navigation', async () => {
386
+ const routes: RouteRecord[] = [{ path: '/', component: Home }]
387
+ const router = createRouter({ routes, url: '/' })
388
388
  await router.isReady()
389
389
  // Should not hang
390
- expect(router.currentRoute().path).toBe("/")
390
+ expect(router.currentRoute().path).toBe('/')
391
391
  })
392
392
  })
393
393
 
394
394
  // ─── Router — relative path navigation ───────────────────────────────────────
395
395
 
396
- describe("router — relative path navigation", () => {
396
+ describe('router — relative path navigation', () => {
397
397
  const routes: RouteRecord[] = [
398
- { path: "/", component: Home },
399
- { path: "/user/:id", component: User },
398
+ { path: '/', component: Home },
399
+ { path: '/user/:id', component: User },
400
400
  {
401
- path: "/admin",
401
+ path: '/admin',
402
402
  component: Home,
403
403
  children: [
404
- { path: "users", component: About },
405
- { path: "settings", component: User },
404
+ { path: 'users', component: About },
405
+ { path: 'settings', component: User },
406
406
  ],
407
407
  },
408
408
  ]
409
409
 
410
- test("relative path ./sibling navigates correctly", async () => {
411
- const router = createRouter({ routes, url: "/admin/users" })
412
- await router.push("./settings")
413
- expect(router.currentRoute().path).toBe("/admin/settings")
410
+ test('relative path ./sibling navigates correctly', async () => {
411
+ const router = createRouter({ routes, url: '/admin/users' })
412
+ await router.push('./settings')
413
+ expect(router.currentRoute().path).toBe('/admin/settings')
414
414
  })
415
415
 
416
- test("relative path ../up navigates correctly", async () => {
417
- const router = createRouter({ routes, url: "/admin/users" })
418
- await router.push("../")
419
- expect(router.currentRoute().path).toBe("/")
416
+ test('relative path ../up navigates correctly', async () => {
417
+ const router = createRouter({ routes, url: '/admin/users' })
418
+ await router.push('../')
419
+ expect(router.currentRoute().path).toBe('/')
420
420
  })
421
421
 
422
- test("absolute path is not modified", async () => {
423
- const router = createRouter({ routes, url: "/admin/users" })
424
- await router.push("/user/42")
425
- expect(router.currentRoute().path).toBe("/user/42")
422
+ test('absolute path is not modified', async () => {
423
+ const router = createRouter({ routes, url: '/admin/users' })
424
+ await router.push('/user/42')
425
+ expect(router.currentRoute().path).toBe('/user/42')
426
426
  })
427
427
  })
428
428
 
429
429
  // ─── Router — replace with named route ───────────────────────────────────────
430
430
 
431
- describe("router — replace with named route", () => {
432
- test("replace with named route navigates correctly", async () => {
431
+ describe('router — replace with named route', () => {
432
+ test('replace with named route navigates correctly', async () => {
433
433
  const routes: RouteRecord[] = [
434
- { path: "/", component: Home, name: "home" },
435
- { path: "/user/:id", component: User, name: "user" },
434
+ { path: '/', component: Home, name: 'home' },
435
+ { path: '/user/:id', component: User, name: 'user' },
436
436
  ]
437
- const router = createRouter({ routes, url: "/" })
438
- await router.replace({ name: "user", params: { id: "42" } })
439
- expect(router.currentRoute().path).toBe("/user/42")
437
+ const router = createRouter({ routes, url: '/' })
438
+ await router.replace({ name: 'user', params: { id: '42' } })
439
+ expect(router.currentRoute().path).toBe('/user/42')
440
440
  })
441
441
 
442
- test("replace with unknown named route falls back to /", async () => {
443
- const routes: RouteRecord[] = [{ path: "/", component: Home }]
444
- const router = createRouter({ routes, url: "/" })
445
- await router.replace({ name: "nonexistent" })
446
- expect(router.currentRoute().path).toBe("/")
442
+ test('replace with unknown named route falls back to /', async () => {
443
+ const routes: RouteRecord[] = [{ path: '/', component: Home }]
444
+ const router = createRouter({ routes, url: '/' })
445
+ await router.replace({ name: 'nonexistent' })
446
+ expect(router.currentRoute().path).toBe('/')
447
447
  })
448
448
  })
449
449
 
450
450
  // ─── Router — sanitize unsafe URLs ───────────────────────────────────────────
451
451
 
452
- describe("router — URL sanitization", () => {
452
+ describe('router — URL sanitization', () => {
453
453
  const routes: RouteRecord[] = [
454
- { path: "/", component: Home },
455
- { path: "/about", component: About },
454
+ { path: '/', component: Home },
455
+ { path: '/about', component: About },
456
456
  ]
457
457
 
458
- test("blocks vbscript: URI", async () => {
459
- const router = createRouter({ routes, url: "/" })
460
- await router.push("vbscript:alert(1)")
461
- expect(router.currentRoute().path).toBe("/")
458
+ test('blocks vbscript: URI', async () => {
459
+ const router = createRouter({ routes, url: '/' })
460
+ await router.push('vbscript:alert(1)')
461
+ expect(router.currentRoute().path).toBe('/')
462
462
  })
463
463
 
464
- test("blocks absolute URLs (http)", async () => {
465
- const router = createRouter({ routes, url: "/" })
466
- await router.push("http://evil.com")
467
- expect(router.currentRoute().path).toBe("/")
464
+ test('blocks absolute URLs (http)', async () => {
465
+ const router = createRouter({ routes, url: '/' })
466
+ await router.push('http://evil.com')
467
+ expect(router.currentRoute().path).toBe('/')
468
468
  })
469
469
 
470
- test("blocks absolute URLs (https)", async () => {
471
- const router = createRouter({ routes, url: "/" })
472
- await router.push("https://evil.com")
473
- expect(router.currentRoute().path).toBe("/")
470
+ test('blocks absolute URLs (https)', async () => {
471
+ const router = createRouter({ routes, url: '/' })
472
+ await router.push('https://evil.com')
473
+ expect(router.currentRoute().path).toBe('/')
474
474
  })
475
475
 
476
- test("blocks protocol-relative URLs", async () => {
477
- const router = createRouter({ routes, url: "/" })
478
- await router.push("//evil.com")
479
- expect(router.currentRoute().path).toBe("/")
476
+ test('blocks protocol-relative URLs', async () => {
477
+ const router = createRouter({ routes, url: '/' })
478
+ await router.push('//evil.com')
479
+ expect(router.currentRoute().path).toBe('/')
480
480
  })
481
481
  })
482
482
 
483
483
  // ─── Router — staleWhileRevalidate ───────────────────────────────────────────
484
484
 
485
- describe("router — staleWhileRevalidate", () => {
486
- test("serves stale data immediately, revalidates in background", async () => {
485
+ describe('router — staleWhileRevalidate', () => {
486
+ test('serves stale data immediately, revalidates in background', async () => {
487
487
  let loaderCallCount = 0
488
488
  const routes: RouteRecord[] = [
489
- { path: "/", component: Home },
489
+ { path: '/', component: Home },
490
490
  {
491
- path: "/data",
491
+ path: '/data',
492
492
  component: About,
493
493
  staleWhileRevalidate: true,
494
494
  loader: async () => {
@@ -497,16 +497,16 @@ describe("router — staleWhileRevalidate", () => {
497
497
  },
498
498
  },
499
499
  ]
500
- const router = createRouter({ routes, url: "/" }) as RouterInstance
500
+ const router = createRouter({ routes, url: '/' }) as RouterInstance
501
501
 
502
502
  // First navigation — loader runs as blocking
503
- await router.push("/data")
503
+ await router.push('/data')
504
504
  expect(loaderCallCount).toBe(1)
505
- expect(router._loaderData.get(routes[1] as RouteRecord)).toBe("data-v1")
505
+ expect(router._loaderData.get(routes[1] as RouteRecord)).toBe('data-v1')
506
506
 
507
507
  // Navigate away and back — should show stale data and revalidate
508
- await router.push("/")
509
- await router.push("/data")
508
+ await router.push('/')
509
+ await router.push('/data')
510
510
 
511
511
  // Give background revalidation time
512
512
  await new Promise<void>((r) => setTimeout(r, 50))