metaowl 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +852 -3
- package/bin/metaowl-create.js +425 -0
- package/index.js +155 -1
- package/modules/app-mounter.js +7 -0
- package/modules/auto-import.js +225 -0
- package/modules/cache.js +2 -0
- package/modules/composables.js +600 -0
- package/modules/error-boundary.js +228 -0
- package/modules/fetch.js +7 -0
- package/modules/file-router.js +425 -19
- package/modules/forms.js +353 -0
- package/modules/i18n.js +333 -0
- package/modules/layouts.js +433 -0
- package/modules/odoo-rpc.js +511 -0
- package/modules/pwa.js +515 -0
- package/modules/router.js +593 -29
- package/modules/seo.js +501 -0
- package/modules/store.js +409 -0
- package/modules/templates-manager.js +5 -0
- package/modules/test-utils.js +532 -0
- package/package.json +1 -1
- package/test/auto-import.test.js +110 -0
- package/test/composables.test.js +103 -0
- package/test/dynamic-routes.test.js +520 -0
- package/test/error-boundary.test.js +126 -0
- package/test/forms.test.js +203 -0
- package/test/i18n.test.js +188 -0
- package/test/layouts.test.js +395 -0
- package/test/odoo-rpc.test.js +547 -0
- package/test/pwa.test.js +154 -0
- package/test/router-guards.test.js +617 -0
- package/test/seo.test.js +353 -0
- package/test/store.test.js +476 -0
- package/test/test-utils.test.js +314 -0
- package/vite/plugin.js +43 -5
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
createMockStore,
|
|
4
|
+
mockRouter,
|
|
5
|
+
mountComponent,
|
|
6
|
+
wait,
|
|
7
|
+
nextTick,
|
|
8
|
+
flushPromises,
|
|
9
|
+
userEvent,
|
|
10
|
+
dom,
|
|
11
|
+
TestUtils
|
|
12
|
+
} from '../modules/test-utils.js'
|
|
13
|
+
|
|
14
|
+
describe('TestUtils', () => {
|
|
15
|
+
describe('Exports', () => {
|
|
16
|
+
it('should export all functions', () => {
|
|
17
|
+
expect(typeof createMockStore).toBe('function')
|
|
18
|
+
expect(typeof mockRouter).toBe('function')
|
|
19
|
+
expect(typeof mountComponent).toBe('function')
|
|
20
|
+
expect(typeof wait).toBe('function')
|
|
21
|
+
expect(typeof nextTick).toBe('function')
|
|
22
|
+
expect(typeof flushPromises).toBe('function')
|
|
23
|
+
expect(typeof userEvent.click).toBe('function')
|
|
24
|
+
expect(typeof dom.query).toBe('function')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should export TestUtils namespace', () => {
|
|
28
|
+
expect(TestUtils.createMockStore).toBe(createMockStore)
|
|
29
|
+
expect(TestUtils.mockRouter).toBe(mockRouter)
|
|
30
|
+
expect(TestUtils.mountComponent).toBe(mountComponent)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('createMockStore', () => {
|
|
35
|
+
it('should create store with initial state', () => {
|
|
36
|
+
const store = createMockStore({
|
|
37
|
+
state: { count: 0, user: null }
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
expect(store.state.count).toBe(0)
|
|
41
|
+
expect(store.state.user).toBeNull()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should handle mutations', () => {
|
|
45
|
+
const store = createMockStore({
|
|
46
|
+
state: { count: 0 },
|
|
47
|
+
mutations: {
|
|
48
|
+
increment: (state) => { state.count++ },
|
|
49
|
+
add: (state, n) => { state.count += n }
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
store.commit('increment')
|
|
54
|
+
expect(store.state.count).toBe(1)
|
|
55
|
+
|
|
56
|
+
store.commit('add', 5)
|
|
57
|
+
expect(store.state.count).toBe(6)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should handle actions', async () => {
|
|
61
|
+
const store = createMockStore({
|
|
62
|
+
state: { user: null },
|
|
63
|
+
mutations: {
|
|
64
|
+
setUser: (state, user) => { state.user = user }
|
|
65
|
+
},
|
|
66
|
+
actions: {
|
|
67
|
+
login: async ({ commit }, credentials) => {
|
|
68
|
+
const user = { name: 'Test User', email: credentials.email }
|
|
69
|
+
commit('setUser', user)
|
|
70
|
+
return user
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const result = await store.dispatch('login', { email: 'test@test.com', password: '123' })
|
|
76
|
+
|
|
77
|
+
expect(store.state.user.name).toBe('Test User')
|
|
78
|
+
expect(result.name).toBe('Test User')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should handle getters', () => {
|
|
82
|
+
const store = createMockStore({
|
|
83
|
+
state: { count: 5 },
|
|
84
|
+
getters: {
|
|
85
|
+
doubled: (state) => state.count * 2,
|
|
86
|
+
isPositive: (state) => state.count > 0
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
expect(store.getters.doubled).toBe(10)
|
|
91
|
+
expect(store.getters.isPositive).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should reset state', () => {
|
|
95
|
+
const store = createMockStore({
|
|
96
|
+
state: { count: 0, name: 'Initial' }
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
store.state.count = 10
|
|
100
|
+
store.state.name = 'Changed'
|
|
101
|
+
|
|
102
|
+
store.reset()
|
|
103
|
+
|
|
104
|
+
expect(store.state.count).toBe(0)
|
|
105
|
+
expect(store.state.name).toBe('Initial')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should set state directly', () => {
|
|
109
|
+
const store = createMockStore({
|
|
110
|
+
state: { count: 0 }
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
store.setState({ count: 42, extra: 'value' })
|
|
114
|
+
|
|
115
|
+
expect(store.state.count).toBe(42)
|
|
116
|
+
expect(store.state.extra).toBe('value')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('mockRouter', () => {
|
|
121
|
+
it('should create router with initial route', () => {
|
|
122
|
+
const router = mockRouter({
|
|
123
|
+
initialRoute: '/dashboard'
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
expect(router.currentRoute.path).toBe('/dashboard')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should navigate with push', async () => {
|
|
130
|
+
const router = mockRouter({
|
|
131
|
+
routes: [{ path: '/', name: 'home' }, { path: '/about', name: 'about' }]
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
await router.push('/about')
|
|
135
|
+
|
|
136
|
+
expect(router.currentRoute.path).toBe('/about')
|
|
137
|
+
expect(router.currentRoute.name).toBe('about')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should parse route params', async () => {
|
|
141
|
+
const router = mockRouter({
|
|
142
|
+
routes: [{ path: '/user/:id', name: 'user' }]
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
await router.push('/user/123')
|
|
146
|
+
|
|
147
|
+
expect(router.currentRoute.params.id).toBe('123')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should parse query params', async () => {
|
|
151
|
+
const router = mockRouter()
|
|
152
|
+
|
|
153
|
+
await router.push('/search?q=test&page=2')
|
|
154
|
+
|
|
155
|
+
expect(router.currentRoute.query.q).toBe('test')
|
|
156
|
+
expect(router.currentRoute.query.page).toBe('2')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should run beforeEach guards', async () => {
|
|
160
|
+
const guard = vi.fn()
|
|
161
|
+
const router = mockRouter()
|
|
162
|
+
|
|
163
|
+
router.beforeEach(guard)
|
|
164
|
+
await router.push('/protected')
|
|
165
|
+
|
|
166
|
+
expect(guard).toHaveBeenCalled()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should run afterEach hooks', async () => {
|
|
170
|
+
const hook = vi.fn()
|
|
171
|
+
const router = mockRouter()
|
|
172
|
+
|
|
173
|
+
router.afterEach(hook)
|
|
174
|
+
await router.push('/page')
|
|
175
|
+
|
|
176
|
+
expect(hook).toHaveBeenCalled()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should unsubscribe from guards', async () => {
|
|
180
|
+
const guard = vi.fn()
|
|
181
|
+
const router = mockRouter()
|
|
182
|
+
|
|
183
|
+
const unsubscribe = router.beforeEach(guard)
|
|
184
|
+
unsubscribe()
|
|
185
|
+
|
|
186
|
+
await router.push('/page')
|
|
187
|
+
expect(guard).not.toHaveBeenCalled()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should resolve named routes', () => {
|
|
191
|
+
const router = mockRouter({
|
|
192
|
+
routes: [
|
|
193
|
+
{ path: '/', name: 'home' },
|
|
194
|
+
{ path: '/user/:id', name: 'user' }
|
|
195
|
+
]
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
expect(router.resolve('home')).toBe('/')
|
|
199
|
+
expect(router.resolve('user', { id: 123 })).toBe('/user/123')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('should handle hash', async () => {
|
|
203
|
+
const router = mockRouter()
|
|
204
|
+
|
|
205
|
+
await router.push('/page#section1')
|
|
206
|
+
|
|
207
|
+
expect(router.currentRoute.hash).toBe('section1')
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
describe('Async utilities', () => {
|
|
212
|
+
it('wait should delay execution', async () => {
|
|
213
|
+
const start = Date.now()
|
|
214
|
+
await wait(50)
|
|
215
|
+
const elapsed = Date.now() - start
|
|
216
|
+
|
|
217
|
+
expect(elapsed).toBeGreaterThanOrEqual(45)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('flushPromises should resolve pending promises', async () => {
|
|
221
|
+
let resolved = false
|
|
222
|
+
Promise.resolve().then(() => { resolved = true })
|
|
223
|
+
|
|
224
|
+
expect(resolved).toBe(false)
|
|
225
|
+
await flushPromises()
|
|
226
|
+
expect(resolved).toBe(true)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe('DOM utilities', () => {
|
|
231
|
+
beforeEach(() => {
|
|
232
|
+
document.body.innerHTML = `
|
|
233
|
+
<div id="app">
|
|
234
|
+
<button class="btn primary">Click me</button>
|
|
235
|
+
<span class="text">Hello World</span>
|
|
236
|
+
</div>
|
|
237
|
+
`
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('should query element', () => {
|
|
241
|
+
const button = dom.query('.btn')
|
|
242
|
+
expect(button).not.toBeNull()
|
|
243
|
+
expect(button.tagName).toBe('BUTTON')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should query all elements', () => {
|
|
247
|
+
const spans = dom.queryAll('.text')
|
|
248
|
+
expect(spans.length).toBe(1)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should check for class', () => {
|
|
252
|
+
const button = dom.query('.btn')
|
|
253
|
+
expect(dom.hasClass(button, 'btn')).toBe(true)
|
|
254
|
+
expect(dom.hasClass(button, 'primary')).toBe(true)
|
|
255
|
+
expect(dom.hasClass(button, 'nonexistent')).toBe(false)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should get text content', () => {
|
|
259
|
+
const span = dom.query('.text')
|
|
260
|
+
expect(dom.text(span)).toBe('Hello World')
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
describe('userEvent utilities', () => {
|
|
265
|
+
beforeEach(() => {
|
|
266
|
+
document.body.innerHTML = `
|
|
267
|
+
<input id="input" type="text" />
|
|
268
|
+
<form id="form">
|
|
269
|
+
<button type="submit">Submit</button>
|
|
270
|
+
</form>
|
|
271
|
+
<select id="select">
|
|
272
|
+
<option value="a">A</option>
|
|
273
|
+
<option value="b">B</option>
|
|
274
|
+
</select>
|
|
275
|
+
`
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('should simulate click', async () => {
|
|
279
|
+
const button = document.querySelector('button')
|
|
280
|
+
const handler = vi.fn()
|
|
281
|
+
button.addEventListener('click', handler)
|
|
282
|
+
|
|
283
|
+
await userEvent.click(button)
|
|
284
|
+
|
|
285
|
+
expect(handler).toHaveBeenCalled()
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('should simulate typing', async () => {
|
|
289
|
+
const input = document.querySelector('#input')
|
|
290
|
+
|
|
291
|
+
await userEvent.type(input, 'hello')
|
|
292
|
+
|
|
293
|
+
expect(input.value).toBe('hello')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should simulate form submit', async () => {
|
|
297
|
+
const form = document.querySelector('#form')
|
|
298
|
+
const handler = vi.fn(e => e.preventDefault())
|
|
299
|
+
form.addEventListener('submit', handler)
|
|
300
|
+
|
|
301
|
+
await userEvent.submit(form)
|
|
302
|
+
|
|
303
|
+
expect(handler).toHaveBeenCalled()
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('should simulate select change', async () => {
|
|
307
|
+
const select = document.querySelector('#select')
|
|
308
|
+
|
|
309
|
+
await userEvent.select(select, 'b')
|
|
310
|
+
|
|
311
|
+
expect(select.value).toBe('b')
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
})
|
package/vite/plugin.js
CHANGED
|
@@ -38,10 +38,13 @@ function collectXml(globPattern) {
|
|
|
38
38
|
* @param {string[]} [options.restartGlobs] - Additional globs that trigger dev-server restart.
|
|
39
39
|
* @param {string} [options.frameworkEntry] - Framework entry for manual chunk.
|
|
40
40
|
* @param {string[]} [options.vendorPackages] - npm packages bundled into the vendor chunk.
|
|
41
|
-
* @param {string} [options.envPrefix] - Only expose env vars with this prefix (plus NODE_ENV) via process.env.
|
|
41
|
+
* @param {string} [options.envPrefix] - Only expose env vars with this prefix (plus NODE_ENV) via process.env.
|
|
42
|
+
* @param {object} [options.autoImport] - Enable component auto-import
|
|
43
|
+
* @param {boolean} [options.autoImport.enabled=false] - Enable auto-import
|
|
44
|
+
* @param {string} [options.autoImport.pattern='*.js'] - Glob pattern for components
|
|
42
45
|
* @returns {import('vite').Plugin[]}
|
|
43
46
|
*/
|
|
44
|
-
export function metaowlPlugin(options = {}) {
|
|
47
|
+
export async function metaowlPlugin(options = {}) {
|
|
45
48
|
const {
|
|
46
49
|
root = 'src',
|
|
47
50
|
outDir = '../dist',
|
|
@@ -67,7 +70,41 @@ export function metaowlPlugin(options = {}) {
|
|
|
67
70
|
|
|
68
71
|
let _outDirResolved = null
|
|
69
72
|
|
|
70
|
-
|
|
73
|
+
// Generate auto-import d.ts for components
|
|
74
|
+
const autoImportDtsPath = path.join(process.cwd(), '.metaowl', 'components.d.ts')
|
|
75
|
+
let autoImportPlugin = null
|
|
76
|
+
|
|
77
|
+
if (autoImport.enabled) {
|
|
78
|
+
const { generateComponentDts, scanComponents } = await import('../modules/auto-import.js')
|
|
79
|
+
const components = await scanComponents(componentsDir, { pattern: autoImport.pattern || '*.js' })
|
|
80
|
+
|
|
81
|
+
// Ensure .metaowl directory exists
|
|
82
|
+
const metaowlDir = dirname(autoImportDtsPath)
|
|
83
|
+
if (!existsSync(metaowlDir)) {
|
|
84
|
+
mkdirSync(metaowlDir, { recursive: true })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await generateComponentDts(components, autoImportDtsPath)
|
|
88
|
+
|
|
89
|
+
autoImportPlugin = {
|
|
90
|
+
name: 'metaowl:auto-import',
|
|
91
|
+
enforce: 'pre',
|
|
92
|
+
configResolved() {
|
|
93
|
+
// Components are scanned at startup
|
|
94
|
+
},
|
|
95
|
+
handleHotUpdate({ file }) {
|
|
96
|
+
// Rescan when component files change
|
|
97
|
+
if (file.startsWith(resolve(componentsDir)) && file.endsWith('.js')) {
|
|
98
|
+
scanComponents(componentsDir, { pattern: autoImport.pattern || '*.js' }).then(comps => {
|
|
99
|
+
generateComponentDts(comps, autoImportDtsPath)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const plugins = [
|
|
107
|
+
...(autoImportPlugin ? [autoImportPlugin] : []),
|
|
71
108
|
tsconfigPaths({ root: process.cwd() }),
|
|
72
109
|
ViteRestart({
|
|
73
110
|
restart: [...defaultRestartGlobs, ...restartGlobs]
|
|
@@ -214,12 +251,13 @@ export function metaowlPlugin(options = {}) {
|
|
|
214
251
|
* @param {*} [options.*] - All other options forwarded to metaowlPlugin().
|
|
215
252
|
* @returns {import('vite').UserConfig}
|
|
216
253
|
*/
|
|
217
|
-
export function metaowlConfig(options = {}) {
|
|
254
|
+
export async function metaowlConfig(options = {}) {
|
|
218
255
|
const { server, preview, build, ...metaowlOptions } = options
|
|
256
|
+
const plugins = await metaowlPlugin(metaowlOptions)
|
|
219
257
|
return {
|
|
220
258
|
server: { port: 3000, strictPort: true, host: true, ...server },
|
|
221
259
|
preview: { port: 4173, strictPort: true, ...preview },
|
|
222
260
|
...(build ? { build } : {}),
|
|
223
|
-
plugins
|
|
261
|
+
plugins
|
|
224
262
|
}
|
|
225
263
|
}
|