metaowl 0.4.1 → 0.6.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 (83) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +267 -2
  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 +144 -0
  10. package/build/runtime/modules/app-mounter.js +73 -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/constants.js +38 -0
  15. package/build/runtime/modules/error-boundary.js +116 -0
  16. package/build/runtime/modules/fetch.js +31 -0
  17. package/build/runtime/modules/file-router.js +207 -0
  18. package/build/runtime/modules/fonts.js +172 -0
  19. package/build/runtime/modules/forms.js +193 -0
  20. package/build/runtime/modules/i18n.js +180 -0
  21. package/build/runtime/modules/image.js +175 -0
  22. package/build/runtime/modules/layouts.js +214 -0
  23. package/build/runtime/modules/link.js +141 -0
  24. package/build/runtime/modules/meta.js +117 -0
  25. package/build/runtime/modules/odoo-rpc.js +265 -0
  26. package/build/runtime/modules/pwa.js +272 -0
  27. package/build/runtime/modules/router.js +384 -0
  28. package/build/runtime/modules/seo.js +186 -0
  29. package/build/runtime/modules/store.js +198 -0
  30. package/build/runtime/modules/templates-manager.js +52 -0
  31. package/build/runtime/modules/test-utils.js +238 -0
  32. package/build/runtime/vite/plugin.js +197 -0
  33. package/eslint.js +29 -0
  34. package/package.json +45 -27
  35. package/CONTRIBUTING.md +0 -49
  36. package/bin/metaowl-build.js +0 -12
  37. package/bin/metaowl-dev.js +0 -12
  38. package/bin/metaowl-generate.js +0 -339
  39. package/bin/metaowl-lint.js +0 -71
  40. package/bin/utils.js +0 -82
  41. package/eslint.config.js +0 -3
  42. package/index.js +0 -328
  43. package/modules/app-mounter.js +0 -104
  44. package/modules/auto-import.js +0 -225
  45. package/modules/cache.js +0 -59
  46. package/modules/composables.js +0 -600
  47. package/modules/error-boundary.js +0 -228
  48. package/modules/fetch.js +0 -51
  49. package/modules/file-router.js +0 -478
  50. package/modules/forms.js +0 -353
  51. package/modules/i18n.js +0 -333
  52. package/modules/layouts.js +0 -431
  53. package/modules/link.js +0 -255
  54. package/modules/meta.js +0 -119
  55. package/modules/odoo-rpc.js +0 -511
  56. package/modules/pwa.js +0 -515
  57. package/modules/router.js +0 -769
  58. package/modules/seo.js +0 -501
  59. package/modules/store.js +0 -409
  60. package/modules/templates-manager.js +0 -89
  61. package/modules/test-utils.js +0 -532
  62. package/test/auto-import.test.js +0 -110
  63. package/test/cache.test.js +0 -55
  64. package/test/composables.test.js +0 -103
  65. package/test/dynamic-routes.test.js +0 -469
  66. package/test/error-boundary.test.js +0 -126
  67. package/test/fetch.test.js +0 -100
  68. package/test/file-router.test.js +0 -55
  69. package/test/forms.test.js +0 -203
  70. package/test/i18n.test.js +0 -188
  71. package/test/layouts.test.js +0 -395
  72. package/test/link.test.js +0 -189
  73. package/test/meta.test.js +0 -146
  74. package/test/odoo-rpc.test.js +0 -547
  75. package/test/pwa.test.js +0 -154
  76. package/test/router-guards.test.js +0 -229
  77. package/test/router.test.js +0 -77
  78. package/test/seo.test.js +0 -353
  79. package/test/store.test.js +0 -476
  80. package/test/templates-manager.test.js +0 -83
  81. package/test/test-utils.test.js +0 -314
  82. package/vite/plugin.js +0 -290
  83. 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
- })