metaowl 0.4.0 → 0.5.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +13 -15
  3. package/build/runtime/bin/metaowl-build.js +10 -0
  4. package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
  5. package/build/runtime/bin/metaowl-dev.js +10 -0
  6. package/build/runtime/bin/metaowl-generate.js +231 -0
  7. package/build/runtime/bin/metaowl-lint.js +58 -0
  8. package/build/runtime/bin/utils.js +68 -0
  9. package/build/runtime/index.js +141 -0
  10. package/build/runtime/modules/app-mounter.js +65 -0
  11. package/build/runtime/modules/auto-import.js +140 -0
  12. package/build/runtime/modules/cache.js +49 -0
  13. package/build/runtime/modules/composables.js +353 -0
  14. package/build/runtime/modules/error-boundary.js +116 -0
  15. package/build/runtime/modules/fetch.js +31 -0
  16. package/build/runtime/modules/file-router.js +205 -0
  17. package/build/runtime/modules/forms.js +193 -0
  18. package/build/runtime/modules/i18n.js +167 -0
  19. package/build/runtime/modules/layouts.js +163 -0
  20. package/build/runtime/modules/link.js +141 -0
  21. package/build/runtime/modules/meta.js +117 -0
  22. package/build/runtime/modules/odoo-rpc.js +264 -0
  23. package/build/runtime/modules/pwa.js +262 -0
  24. package/build/runtime/modules/router.js +389 -0
  25. package/build/runtime/modules/seo.js +186 -0
  26. package/build/runtime/modules/store.js +196 -0
  27. package/build/runtime/modules/templates-manager.js +52 -0
  28. package/build/runtime/modules/test-utils.js +238 -0
  29. package/build/runtime/vite/plugin.js +183 -0
  30. package/eslint.js +29 -0
  31. package/package.json +29 -11
  32. package/CONTRIBUTING.md +0 -49
  33. package/bin/metaowl-build.js +0 -12
  34. package/bin/metaowl-dev.js +0 -12
  35. package/bin/metaowl-generate.js +0 -339
  36. package/bin/metaowl-lint.js +0 -71
  37. package/bin/utils.js +0 -82
  38. package/index.js +0 -328
  39. package/modules/app-mounter.js +0 -104
  40. package/modules/auto-import.js +0 -225
  41. package/modules/cache.js +0 -59
  42. package/modules/composables.js +0 -600
  43. package/modules/error-boundary.js +0 -228
  44. package/modules/fetch.js +0 -51
  45. package/modules/file-router.js +0 -478
  46. package/modules/forms.js +0 -353
  47. package/modules/i18n.js +0 -333
  48. package/modules/layouts.js +0 -431
  49. package/modules/link.js +0 -255
  50. package/modules/meta.js +0 -119
  51. package/modules/odoo-rpc.js +0 -511
  52. package/modules/pwa.js +0 -515
  53. package/modules/router.js +0 -769
  54. package/modules/seo.js +0 -501
  55. package/modules/store.js +0 -409
  56. package/modules/templates-manager.js +0 -89
  57. package/modules/test-utils.js +0 -532
  58. package/test/auto-import.test.js +0 -110
  59. package/test/cache.test.js +0 -55
  60. package/test/composables.test.js +0 -103
  61. package/test/dynamic-routes.test.js +0 -469
  62. package/test/error-boundary.test.js +0 -126
  63. package/test/fetch.test.js +0 -100
  64. package/test/file-router.test.js +0 -55
  65. package/test/forms.test.js +0 -203
  66. package/test/i18n.test.js +0 -188
  67. package/test/layouts.test.js +0 -395
  68. package/test/link.test.js +0 -189
  69. package/test/meta.test.js +0 -146
  70. package/test/odoo-rpc.test.js +0 -547
  71. package/test/pwa.test.js +0 -154
  72. package/test/router-guards.test.js +0 -229
  73. package/test/router.test.js +0 -77
  74. package/test/seo.test.js +0 -353
  75. package/test/store.test.js +0 -476
  76. package/test/templates-manager.test.js +0 -83
  77. package/test/test-utils.test.js +0 -314
  78. package/vite/plugin.js +0 -277
  79. package/vitest.config.js +0 -8
@@ -1,103 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import {
3
- useAuth,
4
- useLocalStorage,
5
- useFetch,
6
- useDebounce,
7
- useThrottle,
8
- useWindowSize,
9
- useOnlineStatus,
10
- useAsyncState,
11
- useCache,
12
- Composables
13
- } from '../modules/composables.js'
14
-
15
- describe('Composables', () => {
16
- describe('Exports', () => {
17
- it('should export useAuth function', () => {
18
- expect(typeof useAuth).toBe('function')
19
- })
20
-
21
- it('should export useLocalStorage function', () => {
22
- expect(typeof useLocalStorage).toBe('function')
23
- })
24
-
25
- it('should export useFetch function', () => {
26
- expect(typeof useFetch).toBe('function')
27
- })
28
-
29
- it('should export useDebounce function', () => {
30
- expect(typeof useDebounce).toBe('function')
31
- })
32
-
33
- it('should export useThrottle function', () => {
34
- expect(typeof useThrottle).toBe('function')
35
- })
36
-
37
- it('should export useWindowSize function', () => {
38
- expect(typeof useWindowSize).toBe('function')
39
- })
40
-
41
- it('should export useOnlineStatus function', () => {
42
- expect(typeof useOnlineStatus).toBe('function')
43
- })
44
-
45
- it('should export useAsyncState function', () => {
46
- expect(typeof useAsyncState).toBe('function')
47
- })
48
-
49
- it('should export useCache function', () => {
50
- expect(typeof useCache).toBe('function')
51
- })
52
- })
53
-
54
- describe('Composables namespace', () => {
55
- it('should export all composables via namespace', () => {
56
- expect(Composables.useAuth).toBe(useAuth)
57
- expect(Composables.useLocalStorage).toBe(useLocalStorage)
58
- expect(Composables.useFetch).toBe(useFetch)
59
- expect(Composables.useDebounce).toBe(useDebounce)
60
- expect(Composables.useThrottle).toBe(useThrottle)
61
- expect(Composables.useWindowSize).toBe(useWindowSize)
62
- expect(Composables.useOnlineStatus).toBe(useOnlineStatus)
63
- expect(Composables.useAsyncState).toBe(useAsyncState)
64
- expect(Composables.useCache).toBe(useCache)
65
- })
66
-
67
- it('should have correct function signatures', () => {
68
- // Check that all functions accept parameters
69
- // Note: function.length only counts required parameters before first default
70
- expect(useAuth.length).toBeGreaterThanOrEqual(0)
71
- expect(useLocalStorage.length).toBeGreaterThanOrEqual(0)
72
- expect(useFetch.length).toBeGreaterThanOrEqual(0)
73
- expect(useDebounce.length).toBeGreaterThanOrEqual(0)
74
- expect(useThrottle.length).toBeGreaterThanOrEqual(0)
75
- expect(useWindowSize.length).toBeGreaterThanOrEqual(0)
76
- expect(useOnlineStatus.length).toBeGreaterThanOrEqual(0)
77
- expect(useAsyncState.length).toBeGreaterThanOrEqual(0)
78
- expect(useCache.length).toBeGreaterThanOrEqual(0)
79
- })
80
- })
81
-
82
- describe('Documentation', () => {
83
- it('should have JSDoc comments', () => {
84
- // Check that functions are documented by checking they're exported
85
- const exportedFunctions = [
86
- useAuth,
87
- useLocalStorage,
88
- useFetch,
89
- useDebounce,
90
- useThrottle,
91
- useWindowSize,
92
- useOnlineStatus,
93
- useAsyncState,
94
- useCache
95
- ]
96
-
97
- for (const fn of exportedFunctions) {
98
- expect(typeof fn).toBe('function')
99
- expect(fn.name).toBeTruthy()
100
- }
101
- })
102
- })
103
- })
@@ -1,469 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest'
2
- import {
3
- pathFromKey,
4
- matchRoute,
5
- isDynamicRoute,
6
- findRoute,
7
- generateUrl,
8
- validateRouteParams,
9
- createCatchAllRoute,
10
- createRedirectRoute,
11
- buildRoutes,
12
- defineRoute,
13
- route
14
- } from '../modules/file-router.js'
15
-
16
- // Mock Component class
17
- class MockComponent {
18
- static template = '<div>Mock</div>'
19
- }
20
-
21
- class TestPage extends MockComponent {}
22
- class UserPage extends MockComponent {}
23
- class ProductPage extends MockComponent {}
24
- class CatchAllPage extends MockComponent {}
25
-
26
- describe('Dynamic Routes', () => {
27
- describe('pathFromKey', () => {
28
- it('converts static routes', () => {
29
- expect(pathFromKey('./pages/about/About.js')).toBe('/about')
30
- expect(pathFromKey('./pages/blog/post/Post.js')).toBe('/blog/post')
31
- })
32
-
33
- it('converts index route', () => {
34
- expect(pathFromKey('./pages/index/Index.js')).toBe('/')
35
- })
36
-
37
- it('converts [param] routes', () => {
38
- expect(pathFromKey('./pages/user/[id]/User.js')).toBe('/user/:id')
39
- })
40
-
41
- it('converts nested [params]', () => {
42
- expect(pathFromKey('./pages/product/[category]/[slug]/Product.js'))
43
- .toBe('/product/:category/:slug')
44
- })
45
-
46
- it('converts optional params [param?]', () => {
47
- expect(pathFromKey('./pages/blog/[id]/[slug?]/Blog.js'))
48
- .toBe('/blog/:id/:slug?')
49
- })
50
-
51
- it('converts catch-all [...path]', () => {
52
- expect(pathFromKey('./pages/docs/[...path]/Docs.js'))
53
- .toBe('/docs/:path(.*)')
54
- })
55
-
56
- it('handles root level params', () => {
57
- expect(pathFromKey('./pages/[id]/Page.js')).toBe('/:id')
58
- })
59
- })
60
-
61
- describe('matchRoute', () => {
62
- it('matches static routes', () => {
63
- const match = matchRoute('/about', '/about')
64
- expect(match).toEqual({ params: {}, pattern: '/about' })
65
- })
66
-
67
- it('extracts single param', () => {
68
- const match = matchRoute('/user/:id', '/user/123')
69
- expect(match.params).toEqual({ id: '123' })
70
- })
71
-
72
- it('extracts multiple params', () => {
73
- const match = matchRoute('/product/:category/:slug', '/product/tech/hello-world')
74
- expect(match.params).toEqual({ category: 'tech', slug: 'hello-world' })
75
- })
76
-
77
- it('returns null for non-matching route', () => {
78
- const match = matchRoute('/user/:id', '/about')
79
- expect(match).toBeNull()
80
- })
81
-
82
- it('matches optional params when provided', () => {
83
- const match = matchRoute('/blog/:id/:slug?', '/blog/123/my-post')
84
- expect(match.params).toEqual({ id: '123', slug: 'my-post' })
85
- })
86
-
87
- it('matches optional params when omitted', () => {
88
- const match = matchRoute('/blog/:id/:slug?', '/blog/123')
89
- expect(match.params).toEqual({ id: '123' })
90
- })
91
-
92
- it('matches catch-all routes', () => {
93
- const match = matchRoute('/docs/:path(.*)', '/docs/getting-started/install')
94
- expect(match.params).toEqual({ path: 'getting-started/install' })
95
- })
96
-
97
- it('handles root catch-all', () => {
98
- const match = matchRoute('/:path(.*)', '/anything/here')
99
- expect(match.params).toEqual({ path: 'anything/here' })
100
- })
101
-
102
- it('handles routes with special characters in params', () => {
103
- const match = matchRoute('/user/:id', '/user/user%40example.com')
104
- expect(match.params.id).toBe('user%40example.com')
105
- })
106
-
107
- it('does not match when required param is missing', () => {
108
- const match = matchRoute('/user/:id/profile', '/user/')
109
- expect(match).toBeNull()
110
- })
111
- })
112
-
113
- describe('isDynamicRoute', () => {
114
- it('returns true for routes with params', () => {
115
- expect(isDynamicRoute('/user/:id')).toBe(true)
116
- expect(isDynamicRoute('/product/:category/:id')).toBe(true)
117
- })
118
-
119
- it('returns false for static routes', () => {
120
- expect(isDynamicRoute('/about')).toBe(false)
121
- expect(isDynamicRoute('/blog/post')).toBe(false)
122
- })
123
-
124
- it('returns true for optional params', () => {
125
- expect(isDynamicRoute('/blog/:id?')).toBe(true)
126
- })
127
-
128
- it('returns true for catch-all', () => {
129
- expect(isDynamicRoute('/:path(.*)')).toBe(true)
130
- })
131
- })
132
-
133
- describe('findRoute', () => {
134
- const routes = [
135
- { name: 'index', path: ['/'], component: MockComponent },
136
- { name: 'about', path: ['/about'], component: MockComponent },
137
- { name: 'user', path: ['/user/:id'], component: UserPage, params: ['id'] },
138
- { name: 'product', path: ['/product/:category/:slug'], component: ProductPage, params: ['category', 'slug'] },
139
- { name: 'catch-all', path: ['/:path(.*)'], component: CatchAllPage, params: ['path'] }
140
- ]
141
-
142
- it('finds static route', () => {
143
- const route = findRoute(routes, '/about')
144
- expect(route.name).toBe('about')
145
- })
146
-
147
- it('finds dynamic route with params', () => {
148
- const route = findRoute(routes, '/user/123')
149
- expect(route.name).toBe('user')
150
- expect(route.params).toEqual({ id: '123' })
151
- })
152
-
153
- it('finds route with multiple params', () => {
154
- const route = findRoute(routes, '/product/tech/hello')
155
- expect(route.name).toBe('product')
156
- expect(route.params).toEqual({ category: 'tech', slug: 'hello' })
157
- })
158
-
159
- it('finds catch-all route', () => {
160
- const route = findRoute(routes, '/not/a/known/path')
161
- expect(route.name).toBe('catch-all')
162
- expect(route.params.path).toBe('not/a/known/path')
163
- })
164
-
165
- it('returns null for unknown route', () => {
166
- const route = findRoute(routes, '/unknown')
167
- // Should find catch-all instead
168
- expect(route?.name).toBe('catch-all')
169
- })
170
-
171
- it('prefers exact match over dynamic', () => {
172
- const specificRoutes = [
173
- { name: 'specific', path: ['/user/me'], component: MockComponent },
174
- { name: 'dynamic', path: ['/user/:id'], component: MockComponent, params: ['id'] }
175
- ]
176
-
177
- const route = findRoute(specificRoutes, '/user/me')
178
- expect(route.name).toBe('specific')
179
- })
180
- })
181
-
182
- describe('generateUrl', () => {
183
- const routes = [
184
- { name: 'index', path: ['/'] },
185
- { name: 'about', path: ['/about'] },
186
- { name: 'user', path: ['/user/:id'] },
187
- { name: 'product', path: ['/product/:category/:slug'] },
188
- { name: 'optional', path: ['/blog/:id/:slug?'] }
189
- ]
190
-
191
- it('generates static URL', () => {
192
- expect(generateUrl(routes, 'about')).toBe('/about')
193
- })
194
-
195
- it('generates URL with single param', () => {
196
- expect(generateUrl(routes, 'user', { id: '123' })).toBe('/user/123')
197
- })
198
-
199
- it('generates URL with multiple params', () => {
200
- expect(generateUrl(routes, 'product', { category: 'tech', slug: 'hello' }))
201
- .toBe('/product/tech/hello')
202
- })
203
-
204
- it('throws for unknown route', () => {
205
- expect(() => generateUrl(routes, 'unknown')).toThrow('Route "unknown" not found')
206
- })
207
-
208
- it('handles optional param when provided', () => {
209
- expect(generateUrl(routes, 'optional', { id: '123', slug: 'my-post' }))
210
- .toBe('/blog/123/my-post')
211
- })
212
-
213
- it('handles optional param when omitted', () => {
214
- expect(generateUrl(routes, 'optional', { id: '123' }))
215
- .toBe('/blog/123')
216
- })
217
- })
218
-
219
- describe('validateRouteParams', () => {
220
- const route = {
221
- name: 'user',
222
- params: ['id', 'name']
223
- }
224
-
225
- it('returns valid when all params provided', () => {
226
- const result = validateRouteParams(route, { id: '123', name: 'John' })
227
- expect(result.valid).toBe(true)
228
- expect(result.missing).toEqual([])
229
- expect(result.extra).toEqual([])
230
- })
231
-
232
- it('returns missing params', () => {
233
- const result = validateRouteParams(route, { id: '123' })
234
- expect(result.valid).toBe(false)
235
- expect(result.missing).toEqual(['name'])
236
- expect(result.extra).toEqual([])
237
- })
238
-
239
- it('returns extra params', () => {
240
- const result = validateRouteParams(route, { id: '123', name: 'John', extra: 'value' })
241
- expect(result.valid).toBe(true)
242
- expect(result.missing).toEqual([])
243
- expect(result.extra).toEqual(['extra'])
244
- })
245
-
246
- it('handles route without params', () => {
247
- const staticRoute = { name: 'about' }
248
- const result = validateRouteParams(staticRoute, {})
249
- expect(result.valid).toBe(true)
250
- })
251
- })
252
-
253
- describe('createCatchAllRoute', () => {
254
- it('creates catch-all route', () => {
255
- const route = createCatchAllRoute(CatchAllPage)
256
-
257
- expect(route.name).toBe('404')
258
- expect(route.path).toEqual(['/:path(.*)'])
259
- expect(route.component).toBe(CatchAllPage)
260
- expect(route.params).toEqual(['path'])
261
- expect(route.meta.catchAll).toBe(true)
262
- })
263
-
264
- it('accepts custom name', () => {
265
- const route = createCatchAllRoute(CatchAllPage, { name: 'not-found' })
266
- expect(route.name).toBe('not-found')
267
- })
268
-
269
- it('accepts custom meta', () => {
270
- const route = createCatchAllRoute(CatchAllPage, { meta: { requiresAuth: true } })
271
- expect(route.meta.requiresAuth).toBe(true)
272
- expect(route.meta.catchAll).toBe(true)
273
- })
274
- })
275
-
276
- describe('createRedirectRoute', () => {
277
- it('creates redirect route', () => {
278
- const route = createRedirectRoute('/old-path', '/new-path')
279
-
280
- expect(route.name).toBe('redirect-old-path')
281
- expect(route.path).toEqual(['/old-path'])
282
- expect(route.redirect).toBe('/new-path')
283
- expect(route.component).toBeNull()
284
- })
285
-
286
- it('sanitizes name from path', () => {
287
- const route = createRedirectRoute('/path/with/many/slashes', '/new')
288
- expect(route.name).toBe('redirect-path-with-many-slashes')
289
- })
290
- })
291
-
292
- describe('buildRoutes', () => {
293
- it('extracts component from module', () => {
294
- const modules = {
295
- './pages/Test.js': { default: TestPage }
296
- }
297
-
298
- const routes = buildRoutes(modules)
299
-
300
- expect(routes[0].component).toBe(TestPage)
301
- })
302
-
303
- it('falls back to first named export', () => {
304
- const modules = {
305
- './pages/About.js': { AboutPage: MockComponent }
306
- }
307
-
308
- const routes = buildRoutes(modules)
309
-
310
- expect(routes[0].component).toBe(MockComponent)
311
- })
312
-
313
- it('copies route config from component', () => {
314
- class ConfiguredPage extends MockComponent {
315
- static route = {
316
- meta: { requiresAuth: true },
317
- beforeEnter: () => {}
318
- }
319
- }
320
-
321
- const modules = {
322
- './pages/Configured.js': { default: ConfiguredPage }
323
- }
324
-
325
- const routes = buildRoutes(modules)
326
-
327
- expect(routes[0].meta).toEqual({ requiresAuth: true })
328
- expect(routes[0].beforeEnter).toBeDefined()
329
- })
330
-
331
- it('throws on missing component export', () => {
332
- const modules = {
333
- './pages/Empty.js': {}
334
- }
335
-
336
- expect(() => buildRoutes(modules)).toThrow('No component export found')
337
- })
338
- })
339
-
340
- describe('defineRoute', () => {
341
- it('returns route config object', () => {
342
- const config = defineRoute({
343
- path: '/custom',
344
- meta: { requiresAuth: true }
345
- })
346
-
347
- expect(config.path).toBe('/custom')
348
- expect(config.meta.requiresAuth).toBe(true)
349
- })
350
-
351
- it('can be assigned to component', () => {
352
- class MyPage extends MockComponent {}
353
- MyPage.route = defineRoute({
354
- path: '/my-page',
355
- meta: { title: 'My Page' }
356
- })
357
-
358
- expect(MyPage.route.path).toBe('/my-page')
359
- })
360
- })
361
-
362
- describe('route decorator', () => {
363
- it('sets route config on component', () => {
364
- const decorator = route({ meta: { requiresAuth: true } })
365
- class TestComponent extends MockComponent {}
366
-
367
- decorator(TestComponent)
368
-
369
- expect(TestComponent.route.meta.requiresAuth).toBe(true)
370
- })
371
-
372
- it('returns the component class', () => {
373
- const decorator = route({})
374
- class TestComponent extends MockComponent {}
375
-
376
- const result = decorator(TestComponent)
377
-
378
- expect(result).toBe(TestComponent)
379
- })
380
- })
381
-
382
- describe('complex routing scenarios', () => {
383
- it('handles blog with categories and posts', () => {
384
- const modules = {
385
- './pages/blog/Blog.js': { default: MockComponent },
386
- './pages/blog/[category]/Category.js': { default: MockComponent },
387
- './pages/blog/[category]/[slug]/Post.js': { default: MockComponent }
388
- }
389
-
390
- const routes = buildRoutes(modules)
391
-
392
- expect(routes).toHaveLength(3)
393
-
394
- // Check that routes are sorted correctly
395
- const blogRoute = routes.find(r => r.name === 'blog')
396
- const categoryRoute = routes.find(r => r.name.includes('category') && !r.name.includes('slug'))
397
- const postRoute = routes.find(r => r.name.includes('slug'))
398
-
399
- expect(blogRoute.path[0]).toBe('/blog')
400
- expect(categoryRoute.path[0]).toBe('/blog/:category')
401
- expect(postRoute.path[0]).toBe('/blog/:category/:slug')
402
- })
403
-
404
- it('handles admin area with nested routes', () => {
405
- const modules = {
406
- './pages/admin/dashboard/Dashboard.js': { default: MockComponent },
407
- './pages/admin/users/[id]/User.js': { default: MockComponent },
408
- './pages/admin/settings/Settings.js': { default: MockComponent }
409
- }
410
-
411
- const routes = buildRoutes(modules)
412
-
413
- expect(routes).toHaveLength(3)
414
- expect(routes.every(r => r.name.startsWith('admin'))).toBe(true)
415
- })
416
-
417
- it('handles catch-all 404 page', () => {
418
- const modules = {
419
- './pages/index/Index.js': { default: MockComponent },
420
- './pages/[...path]/NotFound.js': { default: MockComponent }
421
- }
422
-
423
- const routes = buildRoutes(modules)
424
-
425
- const notFoundRoute = routes.find(r => r.name === 'path')
426
- expect(notFoundRoute.path[0]).toBe('/:path(.*)')
427
- })
428
- })
429
-
430
- describe('edge cases', () => {
431
- it('handles empty modules', () => {
432
- const routes = buildRoutes({})
433
- expect(routes).toEqual([])
434
- })
435
-
436
- it('handles deep nesting', () => {
437
- const modules = {
438
- './pages/a/b/c/d/e/Deep.js': { default: MockComponent }
439
- }
440
-
441
- const routes = buildRoutes(modules)
442
-
443
- expect(routes[0].path[0]).toBe('/a/b/c/d/e')
444
- expect(routes[0].name).toBe('a-b-c-d-e')
445
- })
446
-
447
- it('handles special characters in paths', () => {
448
- // These would be unusual but should work
449
- const modules = {
450
- './pages/[id]/[action]/Dynamic.js': { default: MockComponent }
451
- }
452
-
453
- const routes = buildRoutes(modules)
454
-
455
- expect(routes[0].path[0]).toBe('/:id/:action')
456
- })
457
-
458
- it('handles single letter params', () => {
459
- const modules = {
460
- './pages/[a]/[b]/[c]/Page.js': { default: MockComponent }
461
- }
462
-
463
- const routes = buildRoutes(modules)
464
-
465
- expect(routes[0].path[0]).toBe('/:a/:b/:c')
466
- expect(routes[0].params).toEqual(['a', 'b', 'c'])
467
- })
468
- })
469
- })
@@ -1,126 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest'
2
- import {
3
- onError,
4
- setErrorContext,
5
- getErrorContext,
6
- clearErrorContext,
7
- captureError,
8
- initGlobalErrorHandling
9
- } from '../modules/error-boundary.js'
10
-
11
- describe('Error Boundary', () => {
12
- beforeEach(() => {
13
- clearErrorContext()
14
- // Clear all handlers
15
- vi.clearAllMocks()
16
- })
17
-
18
- describe('onError', () => {
19
- it('registers an error handler', () => {
20
- const handler = vi.fn()
21
- const unsubscribe = onError(handler)
22
-
23
- // Simulate error
24
- const error = new Error('Test error')
25
- captureError(error)
26
-
27
- expect(handler).toHaveBeenCalled()
28
- unsubscribe()
29
- })
30
-
31
- it('returns unsubscribe function', () => {
32
- const handler = vi.fn()
33
- const unsubscribe = onError(handler)
34
-
35
- unsubscribe()
36
-
37
- // Simulate error after unsubscribe
38
- const error = new Error('Test error')
39
- captureError(error)
40
-
41
- expect(handler).not.toHaveBeenCalled()
42
- })
43
-
44
- it('calls multiple handlers', () => {
45
- const handler1 = vi.fn()
46
- const handler2 = vi.fn()
47
-
48
- onError(handler1)
49
- onError(handler2)
50
-
51
- const error = new Error('Test error')
52
- captureError(error)
53
-
54
- expect(handler1).toHaveBeenCalled()
55
- expect(handler2).toHaveBeenCalled()
56
- })
57
-
58
- it('passes error and context to handler', () => {
59
- const handler = vi.fn()
60
- setErrorContext({ route: '/test' })
61
-
62
- onError(handler)
63
-
64
- const error = new Error('Test error')
65
- captureError(error, { component: 'TestComponent' })
66
-
67
- expect(handler).toHaveBeenCalledWith(error, {
68
- route: '/test',
69
- component: 'TestComponent'
70
- })
71
- })
72
- })
73
-
74
- describe('setErrorContext / getErrorContext', () => {
75
- it('sets and gets error context', () => {
76
- setErrorContext({ route: '/home' })
77
-
78
- expect(getErrorContext()).toEqual({ route: '/home' })
79
- })
80
-
81
- it('merges context objects', () => {
82
- setErrorContext({ route: '/home' })
83
- setErrorContext({ user: 'john' })
84
-
85
- expect(getErrorContext()).toEqual({
86
- route: '/home',
87
- user: 'john'
88
- })
89
- })
90
- })
91
-
92
- describe('clearErrorContext', () => {
93
- it('clears all context', () => {
94
- setErrorContext({ route: '/home', user: 'john' })
95
- clearErrorContext()
96
-
97
- expect(getErrorContext()).toEqual({})
98
- })
99
- })
100
-
101
- describe('captureError', () => {
102
- it('captures error and notifies handlers', () => {
103
- const handler = vi.fn()
104
- onError(handler)
105
-
106
- const error = new Error('Captured error')
107
- captureError(error)
108
-
109
- expect(handler).toHaveBeenCalledWith(error, {})
110
- })
111
-
112
- it('includes context in captured error', () => {
113
- const handler = vi.fn()
114
- setErrorContext({ app: 'test' })
115
- onError(handler)
116
-
117
- const error = new Error('Test error')
118
- captureError(error, { component: 'Home' })
119
-
120
- expect(handler).toHaveBeenCalledWith(error, {
121
- app: 'test',
122
- component: 'Home'
123
- })
124
- })
125
- })
126
- })