metaowl 0.1.3 → 0.2.0
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.
- package/README.md +852 -3
- package/bin/metaowl-create.js +425 -0
- package/index.js +155 -1
- package/modules/app-mounter.js +7 -0
- package/modules/auto-import.js +225 -0
- package/modules/cache.js +2 -0
- package/modules/composables.js +600 -0
- package/modules/error-boundary.js +228 -0
- package/modules/fetch.js +7 -0
- package/modules/file-router.js +425 -19
- package/modules/forms.js +353 -0
- package/modules/i18n.js +333 -0
- package/modules/layouts.js +433 -0
- package/modules/odoo-rpc.js +511 -0
- package/modules/pwa.js +515 -0
- package/modules/router.js +593 -29
- package/modules/seo.js +501 -0
- package/modules/store.js +409 -0
- package/modules/templates-manager.js +5 -0
- package/modules/test-utils.js +532 -0
- package/package.json +1 -1
- package/test/auto-import.test.js +110 -0
- package/test/composables.test.js +103 -0
- package/test/dynamic-routes.test.js +520 -0
- package/test/error-boundary.test.js +126 -0
- package/test/forms.test.js +203 -0
- package/test/i18n.test.js +188 -0
- package/test/layouts.test.js +395 -0
- package/test/odoo-rpc.test.js +547 -0
- package/test/pwa.test.js +154 -0
- package/test/router-guards.test.js +617 -0
- package/test/seo.test.js +353 -0
- package/test/store.test.js +476 -0
- package/test/test-utils.test.js +314 -0
- package/vite/plugin.js +43 -5
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
processRoutes,
|
|
4
|
+
beforeEach as beforeEachGuard,
|
|
5
|
+
afterEach as afterEachHook,
|
|
6
|
+
getCurrentRoute,
|
|
7
|
+
getPreviousRoute,
|
|
8
|
+
isNavigating,
|
|
9
|
+
cancelNavigation,
|
|
10
|
+
navigate,
|
|
11
|
+
push,
|
|
12
|
+
replace,
|
|
13
|
+
back,
|
|
14
|
+
forward,
|
|
15
|
+
go,
|
|
16
|
+
router
|
|
17
|
+
} from '../modules/router.js'
|
|
18
|
+
|
|
19
|
+
// Mock document.location
|
|
20
|
+
describe('Router Guards', () => {
|
|
21
|
+
let originalLocation
|
|
22
|
+
let mockRoutes
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
// Save original location
|
|
26
|
+
originalLocation = window.location
|
|
27
|
+
|
|
28
|
+
// Mock location
|
|
29
|
+
delete window.location
|
|
30
|
+
window.location = {
|
|
31
|
+
pathname: '/',
|
|
32
|
+
search: '',
|
|
33
|
+
href: 'http://localhost/',
|
|
34
|
+
replace: vi.fn(),
|
|
35
|
+
assign: vi.fn()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Mock history
|
|
39
|
+
window.history = {
|
|
40
|
+
back: vi.fn(),
|
|
41
|
+
forward: vi.fn(),
|
|
42
|
+
go: vi.fn()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Reset router state
|
|
46
|
+
vi.clearAllMocks()
|
|
47
|
+
|
|
48
|
+
// Define test routes
|
|
49
|
+
mockRoutes = [
|
|
50
|
+
{ name: 'index', path: ['/'], component: class Index {} },
|
|
51
|
+
{ name: 'about', path: ['/about'], component: class About {} },
|
|
52
|
+
{ name: 'user', path: ['/user/:id'], component: class User {} },
|
|
53
|
+
{
|
|
54
|
+
name: 'admin',
|
|
55
|
+
path: ['/admin'],
|
|
56
|
+
component: class Admin {},
|
|
57
|
+
meta: { requiresAuth: true },
|
|
58
|
+
beforeEnter: null
|
|
59
|
+
},
|
|
60
|
+
{ name: 'login', path: ['/login'], component: class Login {} }
|
|
61
|
+
]
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
// Restore original location
|
|
66
|
+
window.location = originalLocation
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('beforeEach guards', () => {
|
|
70
|
+
it('registers and calls global beforeEach guard', async () => {
|
|
71
|
+
const guard = vi.fn((to, from, next) => next())
|
|
72
|
+
|
|
73
|
+
beforeEachGuard(guard)
|
|
74
|
+
window.location.pathname = '/about'
|
|
75
|
+
|
|
76
|
+
await processRoutes(mockRoutes)
|
|
77
|
+
|
|
78
|
+
expect(guard).toHaveBeenCalled()
|
|
79
|
+
expect(guard.mock.calls[0][0].name).toBe('about')
|
|
80
|
+
expect(guard.mock.calls[0][0].fullPath).toBe('/about')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('provides to, from, and next to guard', async () => {
|
|
84
|
+
const guard = vi.fn((to, from, next) => {
|
|
85
|
+
expect(to).toHaveProperty('name')
|
|
86
|
+
expect(to).toHaveProperty('path')
|
|
87
|
+
expect(to).toHaveProperty('fullPath')
|
|
88
|
+
expect(to).toHaveProperty('meta')
|
|
89
|
+
expect(typeof next).toBe('function')
|
|
90
|
+
next()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
beforeEachGuard(guard)
|
|
94
|
+
window.location.pathname = '/about'
|
|
95
|
+
|
|
96
|
+
await processRoutes(mockRoutes)
|
|
97
|
+
|
|
98
|
+
expect(guard).toHaveBeenCalled()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('allows navigation with next()', async () => {
|
|
102
|
+
const guard = vi.fn((to, from, next) => next())
|
|
103
|
+
|
|
104
|
+
beforeEachGuard(guard)
|
|
105
|
+
window.location.pathname = '/about'
|
|
106
|
+
|
|
107
|
+
const result = await processRoutes(mockRoutes)
|
|
108
|
+
|
|
109
|
+
expect(result).toBeDefined()
|
|
110
|
+
expect(result[0].name).toBe('about')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('blocks navigation with next(false)', async () => {
|
|
114
|
+
const guard = vi.fn((to, from, next) => next(false))
|
|
115
|
+
|
|
116
|
+
beforeEachGuard(guard)
|
|
117
|
+
window.location.pathname = '/about'
|
|
118
|
+
|
|
119
|
+
await expect(processRoutes(mockRoutes)).rejects.toThrow('Navigation cancelled')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('redirects with next(path)', async () => {
|
|
123
|
+
const guard = vi.fn((to, from, next) => {
|
|
124
|
+
if (to.meta.requiresAuth) {
|
|
125
|
+
next('/login')
|
|
126
|
+
} else {
|
|
127
|
+
next()
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
beforeEachGuard(guard)
|
|
132
|
+
|
|
133
|
+
// Mock window.location.href setter
|
|
134
|
+
const hrefSetter = vi.fn()
|
|
135
|
+
Object.defineProperty(window, 'location', {
|
|
136
|
+
value: {
|
|
137
|
+
...window.location,
|
|
138
|
+
pathname: '/admin',
|
|
139
|
+
href: '',
|
|
140
|
+
get href() { return '' },
|
|
141
|
+
set href(val) { hrefSetter(val) }
|
|
142
|
+
},
|
|
143
|
+
writable: true
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
mockRoutes[3].beforeEnter = guard
|
|
147
|
+
await expect(processRoutes(mockRoutes)).rejects.toThrow('Navigation redirect')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('allows returning path directly from guard', async () => {
|
|
151
|
+
const guard = vi.fn((to, from, next) => {
|
|
152
|
+
return '/login'
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
beforeEachGuard(guard)
|
|
156
|
+
window.location.pathname = '/admin'
|
|
157
|
+
mockRoutes[3].meta = { requiresAuth: true }
|
|
158
|
+
|
|
159
|
+
await expect(processRoutes(mockRoutes)).rejects.toThrow('Navigation redirect')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('allows returning false directly from guard', async () => {
|
|
163
|
+
const guard = vi.fn((to, from, next) => {
|
|
164
|
+
return false
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
beforeEachGuard(guard)
|
|
168
|
+
window.location.pathname = '/about'
|
|
169
|
+
|
|
170
|
+
await expect(processRoutes(mockRoutes)).rejects.toThrow('Navigation cancelled')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('supports async guards', async () => {
|
|
174
|
+
const guard = vi.fn(async (to, from, next) => {
|
|
175
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
176
|
+
next()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
beforeEachGuard(guard)
|
|
180
|
+
window.location.pathname = '/about'
|
|
181
|
+
|
|
182
|
+
const result = await processRoutes(mockRoutes)
|
|
183
|
+
|
|
184
|
+
expect(guard).toHaveBeenCalled()
|
|
185
|
+
expect(result[0].name).toBe('about')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('handles errors in guards', async () => {
|
|
189
|
+
const guard = vi.fn((to, from, next) => {
|
|
190
|
+
throw new Error('Guard error')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
beforeEachGuard(guard)
|
|
194
|
+
window.location.pathname = '/about'
|
|
195
|
+
|
|
196
|
+
await expect(processRoutes(mockRoutes)).rejects.toThrow('Guard error')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('removes guard when unsubscribe is called', async () => {
|
|
200
|
+
const guard = vi.fn((to, from, next) => next())
|
|
201
|
+
|
|
202
|
+
const unsubscribe = beforeEachGuard(guard)
|
|
203
|
+
unsubscribe()
|
|
204
|
+
|
|
205
|
+
window.location.pathname = '/about'
|
|
206
|
+
await processRoutes(mockRoutes)
|
|
207
|
+
|
|
208
|
+
// Guard was called only once before unsubscribe
|
|
209
|
+
expect(guard).not.toHaveBeenCalled()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('calls multiple guards in order', async () => {
|
|
213
|
+
const order = []
|
|
214
|
+
const guard1 = vi.fn((to, from, next) => { order.push(1); next() })
|
|
215
|
+
const guard2 = vi.fn((to, from, next) => { order.push(2); next() })
|
|
216
|
+
const guard3 = vi.fn((to, from, next) => { order.push(3); next() })
|
|
217
|
+
|
|
218
|
+
beforeEachGuard(guard1)
|
|
219
|
+
beforeEachGuard(guard2)
|
|
220
|
+
beforeEachGuard(guard3)
|
|
221
|
+
|
|
222
|
+
window.location.pathname = '/about'
|
|
223
|
+
await processRoutes(mockRoutes)
|
|
224
|
+
|
|
225
|
+
expect(order).toEqual([1, 2, 3])
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
describe('afterEach hooks', () => {
|
|
230
|
+
it('calls afterEach hooks after navigation', async () => {
|
|
231
|
+
const hook = vi.fn()
|
|
232
|
+
|
|
233
|
+
afterEachHook(hook)
|
|
234
|
+
window.location.pathname = '/about'
|
|
235
|
+
|
|
236
|
+
await processRoutes(mockRoutes)
|
|
237
|
+
|
|
238
|
+
expect(hook).toHaveBeenCalled()
|
|
239
|
+
expect(hook.mock.calls[0][0].name).toBe('about')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('provides to and from to hook', async () => {
|
|
243
|
+
const hook = vi.fn((to, from) => {
|
|
244
|
+
expect(to).toHaveProperty('name')
|
|
245
|
+
expect(to).toHaveProperty('path')
|
|
246
|
+
expect(from).toBeNull() // No previous route on initial
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
afterEachHook(hook)
|
|
250
|
+
window.location.pathname = '/about'
|
|
251
|
+
|
|
252
|
+
await processRoutes(mockRoutes)
|
|
253
|
+
|
|
254
|
+
expect(hook).toHaveBeenCalled()
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('provides previous route on second navigation', async () => {
|
|
258
|
+
let capturedFrom = null
|
|
259
|
+
|
|
260
|
+
afterEachHook((to, from) => {
|
|
261
|
+
capturedFrom = from
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
// First navigation
|
|
265
|
+
window.location.pathname = '/'
|
|
266
|
+
await processRoutes(mockRoutes)
|
|
267
|
+
|
|
268
|
+
// Second navigation
|
|
269
|
+
window.location.pathname = '/about'
|
|
270
|
+
await processRoutes(mockRoutes)
|
|
271
|
+
|
|
272
|
+
expect(capturedFrom).not.toBeNull()
|
|
273
|
+
expect(capturedFrom.name).toBe('index')
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('removes hook when unsubscribe is called', async () => {
|
|
277
|
+
const hook = vi.fn()
|
|
278
|
+
|
|
279
|
+
const unsubscribe = afterEachHook(hook)
|
|
280
|
+
unsubscribe()
|
|
281
|
+
|
|
282
|
+
window.location.pathname = '/about'
|
|
283
|
+
await processRoutes(mockRoutes)
|
|
284
|
+
|
|
285
|
+
expect(hook).not.toHaveBeenCalled()
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
describe('per-route beforeEnter', () => {
|
|
290
|
+
it('calls beforeEnter when defined on route', async () => {
|
|
291
|
+
const beforeEnter = vi.fn((to, from, next) => next())
|
|
292
|
+
|
|
293
|
+
mockRoutes[3].beforeEnter = beforeEnter
|
|
294
|
+
window.location.pathname = '/admin'
|
|
295
|
+
|
|
296
|
+
await processRoutes(mockRoutes)
|
|
297
|
+
|
|
298
|
+
expect(beforeEnter).toHaveBeenCalled()
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('runs per-route guard after global guards', async () => {
|
|
302
|
+
const order = []
|
|
303
|
+
const globalGuard = vi.fn((to, from, next) => { order.push('global'); next() })
|
|
304
|
+
const routeGuard = vi.fn((to, from, next) => { order.push('route'); next() })
|
|
305
|
+
|
|
306
|
+
beforeEachGuard(globalGuard)
|
|
307
|
+
mockRoutes[3].beforeEnter = routeGuard
|
|
308
|
+
|
|
309
|
+
window.location.pathname = '/admin'
|
|
310
|
+
await processRoutes(mockRoutes)
|
|
311
|
+
|
|
312
|
+
expect(order).toEqual(['global', 'route'])
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('blocks navigation in beforeEnter', async () => {
|
|
316
|
+
const beforeEnter = vi.fn((to, from, next) => next(false))
|
|
317
|
+
|
|
318
|
+
mockRoutes[3].beforeEnter = beforeEnter
|
|
319
|
+
window.location.pathname = '/admin'
|
|
320
|
+
|
|
321
|
+
await expect(processRoutes(mockRoutes)).rejects.toThrow('Navigation cancelled')
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
describe('route metadata', () => {
|
|
326
|
+
it('provides meta object on route', async () => {
|
|
327
|
+
const guard = vi.fn((to, from, next) => {
|
|
328
|
+
expect(to.meta).toEqual({ requiresAuth: true })
|
|
329
|
+
next()
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
beforeEachGuard(guard)
|
|
333
|
+
|
|
334
|
+
window.location.pathname = '/admin'
|
|
335
|
+
mockRoutes[3].meta = { requiresAuth: true }
|
|
336
|
+
|
|
337
|
+
await processRoutes(mockRoutes)
|
|
338
|
+
|
|
339
|
+
expect(guard).toHaveBeenCalled()
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('meta is empty object when not defined', async () => {
|
|
343
|
+
const guard = vi.fn((to, from, next) => {
|
|
344
|
+
expect(to.meta).toEqual({})
|
|
345
|
+
next()
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
beforeEachGuard(guard)
|
|
349
|
+
window.location.pathname = '/about'
|
|
350
|
+
|
|
351
|
+
await processRoutes(mockRoutes)
|
|
352
|
+
|
|
353
|
+
expect(guard).toHaveBeenCalled()
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe('query string parsing', () => {
|
|
358
|
+
it('parses query parameters into route', async () => {
|
|
359
|
+
const guard = vi.fn((to, from, next) => {
|
|
360
|
+
expect(to.query).toEqual({ foo: 'bar', baz: 'qux' })
|
|
361
|
+
next()
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
beforeEachGuard(guard)
|
|
365
|
+
|
|
366
|
+
window.location.pathname = '/about'
|
|
367
|
+
window.location.search = '?foo=bar&baz=qux'
|
|
368
|
+
|
|
369
|
+
await processRoutes(mockRoutes)
|
|
370
|
+
|
|
371
|
+
expect(guard).toHaveBeenCalled()
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('handles empty query string', async () => {
|
|
375
|
+
const guard = vi.fn((to, from, next) => {
|
|
376
|
+
expect(to.query).toEqual({})
|
|
377
|
+
next()
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
beforeEachGuard(guard)
|
|
381
|
+
|
|
382
|
+
window.location.pathname = '/about'
|
|
383
|
+
window.location.search = ''
|
|
384
|
+
|
|
385
|
+
await processRoutes(mockRoutes)
|
|
386
|
+
|
|
387
|
+
expect(guard).toHaveBeenCalled()
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('handles repeated query parameters as array', async () => {
|
|
391
|
+
const guard = vi.fn((to, from, next) => {
|
|
392
|
+
expect(to.query.tag).toEqual(['foo', 'bar'])
|
|
393
|
+
next()
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
beforeEachGuard(guard)
|
|
397
|
+
|
|
398
|
+
window.location.pathname = '/about'
|
|
399
|
+
window.location.search = '?tag=foo&tag=bar'
|
|
400
|
+
|
|
401
|
+
await processRoutes(mockRoutes)
|
|
402
|
+
|
|
403
|
+
expect(guard).toHaveBeenCalled()
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
describe('dynamic route parameters', () => {
|
|
408
|
+
it('extracts params from dynamic routes', async () => {
|
|
409
|
+
const guard = vi.fn((to, from, next) => {
|
|
410
|
+
expect(to.params).toEqual({ id: '123' })
|
|
411
|
+
next()
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
beforeEachGuard(guard)
|
|
415
|
+
|
|
416
|
+
window.location.pathname = '/user/123'
|
|
417
|
+
|
|
418
|
+
await processRoutes(mockRoutes)
|
|
419
|
+
|
|
420
|
+
expect(guard).toHaveBeenCalled()
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('params is empty object for static routes', async () => {
|
|
424
|
+
const guard = vi.fn((to, from, next) => {
|
|
425
|
+
expect(to.params).toEqual({})
|
|
426
|
+
next()
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
beforeEachGuard(guard)
|
|
430
|
+
|
|
431
|
+
window.location.pathname = '/about'
|
|
432
|
+
|
|
433
|
+
await processRoutes(mockRoutes)
|
|
434
|
+
|
|
435
|
+
expect(guard).toHaveBeenCalled()
|
|
436
|
+
})
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
describe('route state tracking', () => {
|
|
440
|
+
it('tracks current route', async () => {
|
|
441
|
+
window.location.pathname = '/about'
|
|
442
|
+
|
|
443
|
+
await processRoutes(mockRoutes)
|
|
444
|
+
|
|
445
|
+
expect(getCurrentRoute().name).toBe('about')
|
|
446
|
+
expect(getCurrentRoute().fullPath).toBe('/about')
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('tracks previous route after second navigation', async () => {
|
|
450
|
+
window.location.pathname = '/'
|
|
451
|
+
await processRoutes(mockRoutes)
|
|
452
|
+
|
|
453
|
+
window.location.pathname = '/about'
|
|
454
|
+
await processRoutes(mockRoutes)
|
|
455
|
+
|
|
456
|
+
expect(getPreviousRoute().name).toBe('index')
|
|
457
|
+
expect(getCurrentRoute().name).toBe('about')
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
describe('navigation state', () => {
|
|
462
|
+
it('tracks navigation in progress', async () => {
|
|
463
|
+
let wasNavigating = false
|
|
464
|
+
|
|
465
|
+
beforeEachGuard((to, from, next) => {
|
|
466
|
+
wasNavigating = isNavigating()
|
|
467
|
+
next()
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
window.location.pathname = '/about'
|
|
471
|
+
await processRoutes(mockRoutes)
|
|
472
|
+
|
|
473
|
+
expect(wasNavigating).toBe(true)
|
|
474
|
+
expect(isNavigating()).toBe(false)
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it('can cancel navigation', async () => {
|
|
478
|
+
const guard = vi.fn((to, from, next) => {
|
|
479
|
+
cancelNavigation()
|
|
480
|
+
next()
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
beforeEachGuard(guard)
|
|
484
|
+
window.location.pathname = '/about'
|
|
485
|
+
|
|
486
|
+
// Should still complete because cancel just sets flag
|
|
487
|
+
const result = await processRoutes(mockRoutes)
|
|
488
|
+
expect(result).toBeDefined()
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
describe('router singleton', () => {
|
|
493
|
+
it('exposes beforeEach method', () => {
|
|
494
|
+
expect(typeof router.beforeEach).toBe('function')
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
it('exposes afterEach method', () => {
|
|
498
|
+
expect(typeof router.afterEach).toBe('function')
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it('exposes currentRoute getter', () => {
|
|
502
|
+
expect(router.currentRoute).toBeNull()
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it('exposes previousRoute getter', () => {
|
|
506
|
+
expect(router.previousRoute).toBeNull()
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
it('exposes isNavigating getter', () => {
|
|
510
|
+
expect(router.isNavigating).toBe(false)
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it('exposes navigation methods', () => {
|
|
514
|
+
expect(typeof router.push).toBe('function')
|
|
515
|
+
expect(typeof router.replace).toBe('function')
|
|
516
|
+
expect(typeof router.back).toBe('function')
|
|
517
|
+
expect(typeof router.forward).toBe('function')
|
|
518
|
+
expect(typeof router.go).toBe('function')
|
|
519
|
+
expect(typeof router.cancel).toBe('function')
|
|
520
|
+
})
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
describe('navigation helpers', () => {
|
|
524
|
+
it('push navigates to path', () => {
|
|
525
|
+
push('/new-path')
|
|
526
|
+
|
|
527
|
+
expect(window.location.href).toBe('/new-path')
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it('replace replaces current history entry', () => {
|
|
531
|
+
replace('/new-path')
|
|
532
|
+
|
|
533
|
+
expect(window.location.replace).toHaveBeenCalledWith('/new-path')
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
it('back calls history.back', () => {
|
|
537
|
+
back()
|
|
538
|
+
expect(window.history.back).toHaveBeenCalled()
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
it('forward calls history.forward', () => {
|
|
542
|
+
forward()
|
|
543
|
+
expect(window.history.forward).toHaveBeenCalled()
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
it('go calls history.go', () => {
|
|
547
|
+
go(-2)
|
|
548
|
+
expect(window.history.go).toHaveBeenCalledWith(-2)
|
|
549
|
+
})
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
describe('auth guard pattern', () => {
|
|
553
|
+
it('implements typical auth guard pattern', async () => {
|
|
554
|
+
// Mock auth state
|
|
555
|
+
const auth = { isLoggedIn: false }
|
|
556
|
+
|
|
557
|
+
const authGuard = vi.fn((to, from, next) => {
|
|
558
|
+
if (to.meta.requiresAuth && !auth.isLoggedIn) {
|
|
559
|
+
next('/login')
|
|
560
|
+
} else {
|
|
561
|
+
next()
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
beforeEachGuard(authGuard)
|
|
566
|
+
|
|
567
|
+
// Try to access protected route while not logged in
|
|
568
|
+
window.location.pathname = '/admin'
|
|
569
|
+
mockRoutes[3].meta = { requiresAuth: true }
|
|
570
|
+
|
|
571
|
+
await expect(processRoutes(mockRoutes)).rejects.toThrow('Navigation redirect')
|
|
572
|
+
|
|
573
|
+
// Now login and try again
|
|
574
|
+
auth.isLoggedIn = true
|
|
575
|
+
|
|
576
|
+
// Reset the mock to allow navigation
|
|
577
|
+
authGuard.mockImplementation((to, from, next) => {
|
|
578
|
+
if (to.meta.requiresAuth && !auth.isLoggedIn) {
|
|
579
|
+
next('/login')
|
|
580
|
+
} else {
|
|
581
|
+
next()
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
const result = await processRoutes(mockRoutes)
|
|
586
|
+
expect(result[0].name).toBe('admin')
|
|
587
|
+
})
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
describe('role-based guard pattern', () => {
|
|
591
|
+
it('implements role-based access control', async () => {
|
|
592
|
+
const auth = { user: { role: 'user' } }
|
|
593
|
+
|
|
594
|
+
const roleGuard = vi.fn((to, from, next) => {
|
|
595
|
+
if (to.meta.requiredRole && to.meta.requiredRole !== auth.user.role) {
|
|
596
|
+
next('/unauthorized')
|
|
597
|
+
} else {
|
|
598
|
+
next()
|
|
599
|
+
}
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
beforeEachGuard(roleGuard)
|
|
603
|
+
|
|
604
|
+
// Try to access admin route as regular user
|
|
605
|
+
window.location.pathname = '/admin'
|
|
606
|
+
mockRoutes[3].meta = { requiredRole: 'admin' }
|
|
607
|
+
|
|
608
|
+
await expect(processRoutes(mockRoutes)).rejects.toThrow('Navigation redirect')
|
|
609
|
+
|
|
610
|
+
// Change to admin role
|
|
611
|
+
auth.user.role = 'admin'
|
|
612
|
+
|
|
613
|
+
const result = await processRoutes(mockRoutes)
|
|
614
|
+
expect(result[0].name).toBe('admin')
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
})
|