metaowl 0.4.1 → 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.
- package/CHANGELOG.md +22 -0
- package/README.md +12 -0
- package/build/runtime/bin/metaowl-build.js +10 -0
- package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
- package/build/runtime/bin/metaowl-dev.js +10 -0
- package/build/runtime/bin/metaowl-generate.js +231 -0
- package/build/runtime/bin/metaowl-lint.js +58 -0
- package/build/runtime/bin/utils.js +68 -0
- package/build/runtime/index.js +141 -0
- package/build/runtime/modules/app-mounter.js +65 -0
- package/build/runtime/modules/auto-import.js +140 -0
- package/build/runtime/modules/cache.js +49 -0
- package/build/runtime/modules/composables.js +353 -0
- package/build/runtime/modules/error-boundary.js +116 -0
- package/build/runtime/modules/fetch.js +31 -0
- package/build/runtime/modules/file-router.js +205 -0
- package/build/runtime/modules/forms.js +193 -0
- package/build/runtime/modules/i18n.js +167 -0
- package/build/runtime/modules/layouts.js +163 -0
- package/build/runtime/modules/link.js +141 -0
- package/build/runtime/modules/meta.js +117 -0
- package/build/runtime/modules/odoo-rpc.js +264 -0
- package/build/runtime/modules/pwa.js +262 -0
- package/build/runtime/modules/router.js +389 -0
- package/build/runtime/modules/seo.js +186 -0
- package/build/runtime/modules/store.js +196 -0
- package/build/runtime/modules/templates-manager.js +52 -0
- package/build/runtime/modules/test-utils.js +238 -0
- package/build/runtime/vite/plugin.js +183 -0
- package/eslint.js +29 -0
- package/package.json +28 -10
- package/CONTRIBUTING.md +0 -49
- package/bin/metaowl-build.js +0 -12
- package/bin/metaowl-dev.js +0 -12
- package/bin/metaowl-generate.js +0 -339
- package/bin/metaowl-lint.js +0 -71
- package/bin/utils.js +0 -82
- package/eslint.config.js +0 -3
- package/index.js +0 -328
- package/modules/app-mounter.js +0 -104
- package/modules/auto-import.js +0 -225
- package/modules/cache.js +0 -59
- package/modules/composables.js +0 -600
- package/modules/error-boundary.js +0 -228
- package/modules/fetch.js +0 -51
- package/modules/file-router.js +0 -478
- package/modules/forms.js +0 -353
- package/modules/i18n.js +0 -333
- package/modules/layouts.js +0 -431
- package/modules/link.js +0 -255
- package/modules/meta.js +0 -119
- package/modules/odoo-rpc.js +0 -511
- package/modules/pwa.js +0 -515
- package/modules/router.js +0 -769
- package/modules/seo.js +0 -501
- package/modules/store.js +0 -409
- package/modules/templates-manager.js +0 -89
- package/modules/test-utils.js +0 -532
- package/test/auto-import.test.js +0 -110
- package/test/cache.test.js +0 -55
- package/test/composables.test.js +0 -103
- package/test/dynamic-routes.test.js +0 -469
- package/test/error-boundary.test.js +0 -126
- package/test/fetch.test.js +0 -100
- package/test/file-router.test.js +0 -55
- package/test/forms.test.js +0 -203
- package/test/i18n.test.js +0 -188
- package/test/layouts.test.js +0 -395
- package/test/link.test.js +0 -189
- package/test/meta.test.js +0 -146
- package/test/odoo-rpc.test.js +0 -547
- package/test/pwa.test.js +0 -154
- package/test/router-guards.test.js +0 -229
- package/test/router.test.js +0 -77
- package/test/seo.test.js +0 -353
- package/test/store.test.js +0 -476
- package/test/templates-manager.test.js +0 -83
- package/test/test-utils.test.js +0 -314
- package/vite/plugin.js +0 -290
- package/vitest.config.js +0 -8
package/test/composables.test.js
DELETED
|
@@ -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
|
-
})
|