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.
- package/CHANGELOG.md +52 -0
- package/README.md +13 -15
- 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 +29 -11
- 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/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 -277
- package/vitest.config.js +0 -8
package/modules/test-utils.js
DELETED
|
@@ -1,532 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module TestUtils
|
|
3
|
-
*
|
|
4
|
-
* Testing utilities for MetaOwl OWL applications.
|
|
5
|
-
*
|
|
6
|
-
* Provides:
|
|
7
|
-
* - Mock Store for state management testing
|
|
8
|
-
* - Router mocking for navigation testing
|
|
9
|
-
* - Component mounting helpers
|
|
10
|
-
* - Async test utilities
|
|
11
|
-
*
|
|
12
|
-
* Usage:
|
|
13
|
-
* import { createMockStore, mockRouter, mountComponent } from 'metaowl/test'
|
|
14
|
-
*
|
|
15
|
-
* // Mock store
|
|
16
|
-
* const store = createMockStore({
|
|
17
|
-
* state: { count: 0 },
|
|
18
|
-
* mutations: { increment: (s) => s.count++ }
|
|
19
|
-
* })
|
|
20
|
-
*
|
|
21
|
-
* // Mock router
|
|
22
|
-
* const router = mockRouter({
|
|
23
|
-
* initialRoute: '/dashboard',
|
|
24
|
-
* routes: [
|
|
25
|
-
* { path: '/', component: HomePage },
|
|
26
|
-
* { path: '/dashboard', component: DashboardPage }
|
|
27
|
-
* ]
|
|
28
|
-
* })
|
|
29
|
-
*
|
|
30
|
-
* // Mount component with mocks
|
|
31
|
-
* const component = await mountComponent(MyComponent, {
|
|
32
|
-
* store,
|
|
33
|
-
* router,
|
|
34
|
-
* props: { title: 'Test' }
|
|
35
|
-
* })
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
import { mount, reactive } from '@odoo/owl'
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Create a mock store for testing.
|
|
42
|
-
*
|
|
43
|
-
* @param {Object} config - Store configuration
|
|
44
|
-
* @param {Object} [config.state={}] - Initial state
|
|
45
|
-
* @param {Object} [config.getters={}] - Getters object
|
|
46
|
-
* @param {Object} [config.mutations={}] - Mutations object
|
|
47
|
-
* @param {Object} [config.actions={}] - Actions object
|
|
48
|
-
* @returns {Object} Mock store instance
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
|
-
* const store = createMockStore({
|
|
52
|
-
* state: { user: null, count: 0 },
|
|
53
|
-
* getters: {
|
|
54
|
-
* isLoggedIn: (state) => !!state.user
|
|
55
|
-
* },
|
|
56
|
-
* mutations: {
|
|
57
|
-
* setUser: (state, user) => { state.user = user },
|
|
58
|
-
* increment: (state) => { state.count++ }
|
|
59
|
-
* },
|
|
60
|
-
* actions: {
|
|
61
|
-
* login: async ({ commit }, credentials) => {
|
|
62
|
-
* const user = await fakeApi.login(credentials)
|
|
63
|
-
* commit('setUser', user)
|
|
64
|
-
* }
|
|
65
|
-
* }
|
|
66
|
-
* })
|
|
67
|
-
*
|
|
68
|
-
* // Use in tests
|
|
69
|
-
* store.commit('setUser', { name: 'John' })
|
|
70
|
-
* console.log(store.state.user.name) // 'John'
|
|
71
|
-
* console.log(store.getters.isLoggedIn.value) // true
|
|
72
|
-
*
|
|
73
|
-
* await store.dispatch('login', { email, password })
|
|
74
|
-
*/
|
|
75
|
-
export function createMockStore(config = {}) {
|
|
76
|
-
const {
|
|
77
|
-
state: initialState = {},
|
|
78
|
-
getters: getterDefs = {},
|
|
79
|
-
mutations: mutationDefs = {},
|
|
80
|
-
actions: actionDefs = {}
|
|
81
|
-
} = config
|
|
82
|
-
|
|
83
|
-
// Create reactive state
|
|
84
|
-
const state = reactive({ ...initialState })
|
|
85
|
-
|
|
86
|
-
// Create computed getters
|
|
87
|
-
const getters = {}
|
|
88
|
-
for (const [name, fn] of Object.entries(getterDefs)) {
|
|
89
|
-
Object.defineProperty(getters, name, {
|
|
90
|
-
get: () => fn(state),
|
|
91
|
-
enumerable: true
|
|
92
|
-
})
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Create mutations
|
|
96
|
-
const mutations = {}
|
|
97
|
-
for (const [name, fn] of Object.entries(mutationDefs)) {
|
|
98
|
-
mutations[name] = (payload) => {
|
|
99
|
-
fn(state, payload)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Create actions
|
|
104
|
-
const actions = {}
|
|
105
|
-
for (const [name, fn] of Object.entries(actionDefs)) {
|
|
106
|
-
actions[name] = async (payload) => {
|
|
107
|
-
const context = {
|
|
108
|
-
state,
|
|
109
|
-
getters,
|
|
110
|
-
commit: (mutation, payload) => mutations[mutation]?.(payload),
|
|
111
|
-
dispatch: (action, payload) => actions[action]?.(payload)
|
|
112
|
-
}
|
|
113
|
-
return await fn(context, payload)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Store instance
|
|
118
|
-
return {
|
|
119
|
-
state,
|
|
120
|
-
getters,
|
|
121
|
-
mutations,
|
|
122
|
-
actions,
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Commit a mutation.
|
|
126
|
-
* @param {string} name - Mutation name
|
|
127
|
-
* @param {any} payload - Mutation payload
|
|
128
|
-
*/
|
|
129
|
-
commit(name, payload) {
|
|
130
|
-
if (mutations[name]) {
|
|
131
|
-
mutations[name](payload)
|
|
132
|
-
} else {
|
|
133
|
-
console.warn(`[TestUtils] Mutation '${name}' not found`)
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Dispatch an action.
|
|
139
|
-
* @param {string} name - Action name
|
|
140
|
-
* @param {any} payload - Action payload
|
|
141
|
-
* @returns {Promise<any>}
|
|
142
|
-
*/
|
|
143
|
-
async dispatch(name, payload) {
|
|
144
|
-
if (actions[name]) {
|
|
145
|
-
return await actions[name](payload)
|
|
146
|
-
} else {
|
|
147
|
-
console.warn(`[TestUtils] Action '${name}' not found`)
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Reset store to initial state.
|
|
153
|
-
*/
|
|
154
|
-
reset() {
|
|
155
|
-
Object.keys(state).forEach(key => delete state[key])
|
|
156
|
-
Object.assign(state, initialState)
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Set state directly (for testing).
|
|
161
|
-
* @param {Object} newState
|
|
162
|
-
*/
|
|
163
|
-
setState(newState) {
|
|
164
|
-
Object.assign(state, newState)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Create a mock router for testing.
|
|
171
|
-
*
|
|
172
|
-
* @param {Object} config - Router configuration
|
|
173
|
-
* @param {string} [config.initialRoute='/'] - Initial route path
|
|
174
|
-
* @param {Array} [config.routes=[]] - Route definitions
|
|
175
|
-
* @returns {Object} Mock router instance
|
|
176
|
-
*
|
|
177
|
-
* @example
|
|
178
|
-
* const router = mockRouter({
|
|
179
|
-
* initialRoute: '/',
|
|
180
|
-
* routes: [
|
|
181
|
-
* { path: '/', name: 'home' },
|
|
182
|
-
* { path: '/user/:id', name: 'user' }
|
|
183
|
-
* ]
|
|
184
|
-
* })
|
|
185
|
-
*
|
|
186
|
-
* await router.push('/user/123')
|
|
187
|
-
* console.log(router.currentRoute.value.path) // '/user/123'
|
|
188
|
-
* console.log(router.currentRoute.value.params.id) // '123'
|
|
189
|
-
*/
|
|
190
|
-
export function mockRouter(config = {}) {
|
|
191
|
-
const {
|
|
192
|
-
initialRoute = '/',
|
|
193
|
-
routes = []
|
|
194
|
-
} = config
|
|
195
|
-
|
|
196
|
-
// Reactive current route
|
|
197
|
-
const currentRoute = reactive({
|
|
198
|
-
path: initialRoute,
|
|
199
|
-
name: null,
|
|
200
|
-
params: {},
|
|
201
|
-
query: {},
|
|
202
|
-
hash: ''
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
// Navigation guards
|
|
206
|
-
const beforeEachGuards = []
|
|
207
|
-
const afterEachHooks = []
|
|
208
|
-
|
|
209
|
-
// Parse URL into route object
|
|
210
|
-
function parseUrl(url) {
|
|
211
|
-
const [pathAndQuery, hash = ''] = url.split('#')
|
|
212
|
-
const [path, queryString = ''] = pathAndQuery.split('?')
|
|
213
|
-
|
|
214
|
-
const query = {}
|
|
215
|
-
if (queryString) {
|
|
216
|
-
queryString.split('&').forEach(param => {
|
|
217
|
-
const [key, value] = param.split('=')
|
|
218
|
-
query[decodeURIComponent(key)] = decodeURIComponent(value || '')
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Find matching route
|
|
223
|
-
let matchedRoute = null
|
|
224
|
-
let params = {}
|
|
225
|
-
|
|
226
|
-
for (const route of routes) {
|
|
227
|
-
const match = matchPath(path, route.path)
|
|
228
|
-
if (match) {
|
|
229
|
-
matchedRoute = route
|
|
230
|
-
params = match.params
|
|
231
|
-
break
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
path,
|
|
237
|
-
name: matchedRoute?.name || null,
|
|
238
|
-
params,
|
|
239
|
-
query,
|
|
240
|
-
hash
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Match path against route pattern
|
|
245
|
-
function matchPath(path, pattern) {
|
|
246
|
-
// Simple pattern matching (supports :param and * wildcards)
|
|
247
|
-
const paramNames = []
|
|
248
|
-
const regexPattern = pattern
|
|
249
|
-
.replace(/\*/g, '.*')
|
|
250
|
-
.replace(/:([^/]+)/g, (match, name) => {
|
|
251
|
-
paramNames.push(name)
|
|
252
|
-
return '([^/]+)'
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
const regex = new RegExp(`^${regexPattern}$`)
|
|
256
|
-
const match = path.match(regex)
|
|
257
|
-
|
|
258
|
-
if (!match) return null
|
|
259
|
-
|
|
260
|
-
const params = {}
|
|
261
|
-
paramNames.forEach((name, i) => {
|
|
262
|
-
params[name] = match[i + 1]
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
return { params }
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Initialize
|
|
269
|
-
Object.assign(currentRoute, parseUrl(initialRoute))
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
currentRoute,
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Navigate to a new route.
|
|
276
|
-
* @param {string} path - Target path
|
|
277
|
-
*/
|
|
278
|
-
async push(path) {
|
|
279
|
-
const to = parseUrl(path)
|
|
280
|
-
const from = { ...currentRoute }
|
|
281
|
-
|
|
282
|
-
// Run beforeEach guards
|
|
283
|
-
for (const guard of beforeEachGuards) {
|
|
284
|
-
const result = await guard(to, from, () => {})
|
|
285
|
-
if (result === false) return
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
Object.assign(currentRoute, to)
|
|
289
|
-
|
|
290
|
-
// Run afterEach hooks
|
|
291
|
-
for (const hook of afterEachHooks) {
|
|
292
|
-
await hook(to, from)
|
|
293
|
-
}
|
|
294
|
-
},
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Replace current route.
|
|
298
|
-
* @param {string} path - Target path
|
|
299
|
-
*/
|
|
300
|
-
async replace(path) {
|
|
301
|
-
await this.push(path)
|
|
302
|
-
},
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Go back in history.
|
|
306
|
-
*/
|
|
307
|
-
back() {
|
|
308
|
-
// Mock - does nothing in test environment
|
|
309
|
-
},
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Register beforeEach guard.
|
|
313
|
-
* @param {Function} guard
|
|
314
|
-
* @returns {Function} Unsubscribe function
|
|
315
|
-
*/
|
|
316
|
-
beforeEach(guard) {
|
|
317
|
-
beforeEachGuards.push(guard)
|
|
318
|
-
return () => {
|
|
319
|
-
const index = beforeEachGuards.indexOf(guard)
|
|
320
|
-
if (index > -1) beforeEachGuards.splice(index, 1)
|
|
321
|
-
}
|
|
322
|
-
},
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Register afterEach hook.
|
|
326
|
-
* @param {Function} hook
|
|
327
|
-
* @returns {Function} Unsubscribe function
|
|
328
|
-
*/
|
|
329
|
-
afterEach(hook) {
|
|
330
|
-
afterEachHooks.push(hook)
|
|
331
|
-
return () => {
|
|
332
|
-
const index = afterEachHooks.indexOf(hook)
|
|
333
|
-
if (index > -1) afterEachHooks.splice(index, 1)
|
|
334
|
-
}
|
|
335
|
-
},
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Generate URL for named route.
|
|
339
|
-
* @param {string} name - Route name
|
|
340
|
-
* @param {Object} params - Route params
|
|
341
|
-
* @returns {string}
|
|
342
|
-
*/
|
|
343
|
-
resolve(name, params = {}) {
|
|
344
|
-
const route = routes.find(r => r.name === name)
|
|
345
|
-
if (!route) return '/'
|
|
346
|
-
|
|
347
|
-
let path = route.path
|
|
348
|
-
for (const [key, value] of Object.entries(params)) {
|
|
349
|
-
path = path.replace(`:${key}`, value)
|
|
350
|
-
}
|
|
351
|
-
return path
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Mount a component with test utilities.
|
|
358
|
-
*
|
|
359
|
-
* @param {Component} Component - OWL component class
|
|
360
|
-
* @param {Object} [options={}] - Mount options
|
|
361
|
-
* @param {Object} [options.props={}] - Component props
|
|
362
|
-
* @param {Object} [options.store] - Mock store
|
|
363
|
-
* @param {Object} [options.router] - Mock router
|
|
364
|
-
* @param {Element} [options.target] - Mount target (default: detached div)
|
|
365
|
-
* @returns {Promise<Object>} Mounted component instance
|
|
366
|
-
*
|
|
367
|
-
* @example
|
|
368
|
-
* const component = await mountComponent(MyComponent, {
|
|
369
|
-
* props: { title: 'Test' },
|
|
370
|
-
* store: createMockStore({ state: { user: null } }),
|
|
371
|
-
* router: mockRouter({ initialRoute: '/' })
|
|
372
|
-
* })
|
|
373
|
-
*
|
|
374
|
-
* // Access component
|
|
375
|
-
* expect(component.props.title).toBe('Test')
|
|
376
|
-
* expect(component.env.store.state.user).toBeNull()
|
|
377
|
-
*/
|
|
378
|
-
export async function mountComponent(Component, options = {}) {
|
|
379
|
-
const {
|
|
380
|
-
props = {},
|
|
381
|
-
store = null,
|
|
382
|
-
router = null,
|
|
383
|
-
target = document.createElement('div')
|
|
384
|
-
} = options
|
|
385
|
-
|
|
386
|
-
// Create environment with mocks
|
|
387
|
-
const env = {}
|
|
388
|
-
if (store) env.store = store
|
|
389
|
-
if (router) env.router = router
|
|
390
|
-
|
|
391
|
-
// Mount component
|
|
392
|
-
const component = await mount(Component, {
|
|
393
|
-
props,
|
|
394
|
-
env,
|
|
395
|
-
target
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
return component
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Wait for a specific time (for async tests).
|
|
403
|
-
*
|
|
404
|
-
* @param {number} ms - Milliseconds to wait
|
|
405
|
-
* @returns {Promise<void>}
|
|
406
|
-
*/
|
|
407
|
-
export function wait(ms) {
|
|
408
|
-
return new Promise(resolve => setTimeout(resolve, ms))
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Wait for next tick (DOM update).
|
|
413
|
-
*
|
|
414
|
-
* @returns {Promise<void>}
|
|
415
|
-
*/
|
|
416
|
-
export function nextTick() {
|
|
417
|
-
return new Promise(resolve => {
|
|
418
|
-
requestAnimationFrame(resolve)
|
|
419
|
-
})
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Flush all promises (useful after state changes).
|
|
424
|
-
*
|
|
425
|
-
* @returns {Promise<void>}
|
|
426
|
-
*/
|
|
427
|
-
export async function flushPromises() {
|
|
428
|
-
await new Promise(resolve => setTimeout(resolve, 0))
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Simulate user events.
|
|
433
|
-
*/
|
|
434
|
-
export const userEvent = {
|
|
435
|
-
/**
|
|
436
|
-
* Click an element.
|
|
437
|
-
* @param {Element} element
|
|
438
|
-
*/
|
|
439
|
-
async click(element) {
|
|
440
|
-
element.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
441
|
-
await flushPromises()
|
|
442
|
-
},
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Type text into an input.
|
|
446
|
-
* @param {HTMLInputElement} input
|
|
447
|
-
* @param {string} text
|
|
448
|
-
*/
|
|
449
|
-
async type(input, text) {
|
|
450
|
-
input.value = text
|
|
451
|
-
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
452
|
-
await flushPromises()
|
|
453
|
-
},
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Submit a form.
|
|
457
|
-
* @param {HTMLFormElement} form
|
|
458
|
-
*/
|
|
459
|
-
async submit(form) {
|
|
460
|
-
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
|
461
|
-
await flushPromises()
|
|
462
|
-
},
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Change a select value.
|
|
466
|
-
* @param {HTMLSelectElement} select
|
|
467
|
-
* @param {string} value
|
|
468
|
-
*/
|
|
469
|
-
async select(select, value) {
|
|
470
|
-
select.value = value
|
|
471
|
-
select.dispatchEvent(new Event('change', { bubbles: true }))
|
|
472
|
-
await flushPromises()
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Create DOM element helpers.
|
|
478
|
-
*/
|
|
479
|
-
export const dom = {
|
|
480
|
-
/**
|
|
481
|
-
* Query element by selector.
|
|
482
|
-
* @param {string} selector
|
|
483
|
-
* @param {Element} [container=document]
|
|
484
|
-
* @returns {Element|null}
|
|
485
|
-
*/
|
|
486
|
-
query(selector, container = document) {
|
|
487
|
-
return container.querySelector(selector)
|
|
488
|
-
},
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Query all elements by selector.
|
|
492
|
-
* @param {string} selector
|
|
493
|
-
* @param {Element} [container=document]
|
|
494
|
-
* @returns {NodeList}
|
|
495
|
-
*/
|
|
496
|
-
queryAll(selector, container = document) {
|
|
497
|
-
return container.querySelectorAll(selector)
|
|
498
|
-
},
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Check if element has class.
|
|
502
|
-
* @param {Element} element
|
|
503
|
-
* @param {string} className
|
|
504
|
-
* @returns {boolean}
|
|
505
|
-
*/
|
|
506
|
-
hasClass(element, className) {
|
|
507
|
-
return element?.classList?.contains(className) || false
|
|
508
|
-
},
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Get text content.
|
|
512
|
-
* @param {Element} element
|
|
513
|
-
* @returns {string}
|
|
514
|
-
*/
|
|
515
|
-
text(element) {
|
|
516
|
-
return element?.textContent?.trim() || ''
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Export namespace
|
|
521
|
-
export const TestUtils = {
|
|
522
|
-
createMockStore,
|
|
523
|
-
mockRouter,
|
|
524
|
-
mountComponent,
|
|
525
|
-
wait,
|
|
526
|
-
nextTick,
|
|
527
|
-
flushPromises,
|
|
528
|
-
userEvent,
|
|
529
|
-
dom
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
export default TestUtils
|
package/test/auto-import.test.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { scanComponents, generateComponentDts } from '../modules/auto-import.js'
|
|
3
|
-
import { mkdirSync, writeFileSync, rmSync } from 'fs'
|
|
4
|
-
import { join } from 'path'
|
|
5
|
-
import { tmpdir } from 'os'
|
|
6
|
-
|
|
7
|
-
describe('auto-import', () => {
|
|
8
|
-
let tempDir
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
tempDir = join(tmpdir(), `metaowl-test-${Date.now()}`)
|
|
12
|
-
mkdirSync(tempDir, { recursive: true })
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
try {
|
|
17
|
-
rmSync(tempDir, { recursive: true, force: true })
|
|
18
|
-
} catch {
|
|
19
|
-
// Ignore cleanup errors
|
|
20
|
-
}
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
describe('scanComponents', () => {
|
|
24
|
-
it('should find all component files', async () => {
|
|
25
|
-
// Create test components
|
|
26
|
-
mkdirSync(join(tempDir, 'Button'), { recursive: true })
|
|
27
|
-
mkdirSync(join(tempDir, 'Card'), { recursive: true })
|
|
28
|
-
writeFileSync(join(tempDir, 'Button', 'Button.js'), 'export default {}')
|
|
29
|
-
writeFileSync(join(tempDir, 'Card', 'Card.js'), 'export default {}')
|
|
30
|
-
|
|
31
|
-
const components = await scanComponents(tempDir)
|
|
32
|
-
|
|
33
|
-
expect(components).toHaveLength(2)
|
|
34
|
-
expect(components).toContain('Button')
|
|
35
|
-
expect(components).toContain('Card')
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('should filter helpers', async () => {
|
|
39
|
-
mkdirSync(join(tempDir, 'Button'), { recursive: true })
|
|
40
|
-
mkdirSync(join(tempDir, 'utils'), { recursive: true })
|
|
41
|
-
writeFileSync(join(tempDir, 'Button', 'Button.js'), 'export default {}')
|
|
42
|
-
writeFileSync(join(tempDir, 'utils', 'helpers.js'), 'export const utils = {}')
|
|
43
|
-
|
|
44
|
-
const components = await scanComponents(tempDir)
|
|
45
|
-
|
|
46
|
-
// Finds all JS files recursively
|
|
47
|
-
expect(components).toContain('Button')
|
|
48
|
-
expect(components).toContain('helpers')
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('should return empty array for non-existent directory', async () => {
|
|
52
|
-
const components = await scanComponents('/non-existent/path')
|
|
53
|
-
expect(components).toEqual([])
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
describe('generateComponentDts', () => {
|
|
58
|
-
it('should generate TypeScript declarations', async () => {
|
|
59
|
-
const components = ['Button', 'Card', 'Modal']
|
|
60
|
-
const outputPath = join(tempDir, 'components.d.ts')
|
|
61
|
-
|
|
62
|
-
await generateComponentDts(components, outputPath)
|
|
63
|
-
|
|
64
|
-
// Check file was created
|
|
65
|
-
const fs = await import('fs')
|
|
66
|
-
const content = fs.readFileSync(outputPath, 'utf-8')
|
|
67
|
-
|
|
68
|
-
expect(content).toContain('Button')
|
|
69
|
-
expect(content).toContain('Card')
|
|
70
|
-
expect(content).toContain('Modal')
|
|
71
|
-
expect(content).toContain('declare module')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('should handle empty components array', async () => {
|
|
75
|
-
await generateComponentDts([], join(tempDir, 'empty.d.ts'))
|
|
76
|
-
|
|
77
|
-
const fs = await import('fs')
|
|
78
|
-
const content = fs.readFileSync(join(tempDir, 'empty.d.ts'), 'utf-8')
|
|
79
|
-
|
|
80
|
-
expect(content).toContain('declare module')
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
describe('integration', () => {
|
|
85
|
-
it('should work with nested components', async () => {
|
|
86
|
-
// Create nested structure
|
|
87
|
-
const nestedPath = join(tempDir, 'ui', 'forms')
|
|
88
|
-
mkdirSync(nestedPath, { recursive: true })
|
|
89
|
-
writeFileSync(join(nestedPath, 'TextField.js'), 'export default {}')
|
|
90
|
-
writeFileSync(join(nestedPath, 'SelectField.js'), 'export default {}')
|
|
91
|
-
|
|
92
|
-
const components = await scanComponents(tempDir)
|
|
93
|
-
|
|
94
|
-
expect(components).toContain('TextField')
|
|
95
|
-
expect(components).toContain('SelectField')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('should ignore non-js files', async () => {
|
|
99
|
-
mkdirSync(join(tempDir, 'Button'), { recursive: true })
|
|
100
|
-
writeFileSync(join(tempDir, 'Button', 'Button.js'), 'export default {}')
|
|
101
|
-
writeFileSync(join(tempDir, 'Button', 'Button.test.js'), 'test')
|
|
102
|
-
writeFileSync(join(tempDir, 'Button', 'styles.css'), '/* styles */')
|
|
103
|
-
|
|
104
|
-
const components = await scanComponents(tempDir)
|
|
105
|
-
|
|
106
|
-
expect(components).toHaveLength(1)
|
|
107
|
-
expect(components).toContain('Button')
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
})
|
package/test/cache.test.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
-
import Cache from '../modules/cache.js'
|
|
3
|
-
|
|
4
|
-
// Minimal localStorage stub
|
|
5
|
-
const store = {}
|
|
6
|
-
const localStorageMock = {
|
|
7
|
-
getItem: (k) => store[k] ?? null,
|
|
8
|
-
setItem: (k, v) => { store[k] = v },
|
|
9
|
-
removeItem: (k) => { delete store[k] },
|
|
10
|
-
clear: () => { Object.keys(store).forEach(k => delete store[k]) },
|
|
11
|
-
get length() { return Object.keys(store).length },
|
|
12
|
-
key: (i) => Object.keys(store)[i] ?? null
|
|
13
|
-
}
|
|
14
|
-
Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, writable: true })
|
|
15
|
-
|
|
16
|
-
beforeEach(() => localStorageMock.clear())
|
|
17
|
-
|
|
18
|
-
describe('Cache', () => {
|
|
19
|
-
it('set and get a string value', async () => {
|
|
20
|
-
await Cache.set('name', 'metaowl')
|
|
21
|
-
expect(await Cache.get('name')).toBe('metaowl')
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('set and get an object', async () => {
|
|
25
|
-
await Cache.set('obj', { a: 1, b: [2, 3] })
|
|
26
|
-
expect(await Cache.get('obj')).toEqual({ a: 1, b: [2, 3] })
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('get returns null for missing key', async () => {
|
|
30
|
-
expect(await Cache.get('missing')).toBeNull()
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('remove deletes the key', async () => {
|
|
34
|
-
await Cache.set('tmp', 'x')
|
|
35
|
-
await Cache.remove('tmp')
|
|
36
|
-
expect(await Cache.get('tmp')).toBeNull()
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('clear removes all keys', async () => {
|
|
40
|
-
await Cache.set('a', 1)
|
|
41
|
-
await Cache.set('b', 2)
|
|
42
|
-
await Cache.clear()
|
|
43
|
-
expect(await Cache.get('a')).toBeNull()
|
|
44
|
-
expect(await Cache.get('b')).toBeNull()
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('keys returns all stored keys', async () => {
|
|
48
|
-
await Cache.set('x', 1)
|
|
49
|
-
await Cache.set('y', 2)
|
|
50
|
-
const keys = await Cache.keys()
|
|
51
|
-
expect(keys).toContain('x')
|
|
52
|
-
expect(keys).toContain('y')
|
|
53
|
-
expect(keys).toHaveLength(2)
|
|
54
|
-
})
|
|
55
|
-
})
|