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.
Files changed (80) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +12 -0
  3. package/build/runtime/bin/metaowl-build.js +10 -0
  4. package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
  5. package/build/runtime/bin/metaowl-dev.js +10 -0
  6. package/build/runtime/bin/metaowl-generate.js +231 -0
  7. package/build/runtime/bin/metaowl-lint.js +58 -0
  8. package/build/runtime/bin/utils.js +68 -0
  9. package/build/runtime/index.js +141 -0
  10. package/build/runtime/modules/app-mounter.js +65 -0
  11. package/build/runtime/modules/auto-import.js +140 -0
  12. package/build/runtime/modules/cache.js +49 -0
  13. package/build/runtime/modules/composables.js +353 -0
  14. package/build/runtime/modules/error-boundary.js +116 -0
  15. package/build/runtime/modules/fetch.js +31 -0
  16. package/build/runtime/modules/file-router.js +205 -0
  17. package/build/runtime/modules/forms.js +193 -0
  18. package/build/runtime/modules/i18n.js +167 -0
  19. package/build/runtime/modules/layouts.js +163 -0
  20. package/build/runtime/modules/link.js +141 -0
  21. package/build/runtime/modules/meta.js +117 -0
  22. package/build/runtime/modules/odoo-rpc.js +264 -0
  23. package/build/runtime/modules/pwa.js +262 -0
  24. package/build/runtime/modules/router.js +389 -0
  25. package/build/runtime/modules/seo.js +186 -0
  26. package/build/runtime/modules/store.js +196 -0
  27. package/build/runtime/modules/templates-manager.js +52 -0
  28. package/build/runtime/modules/test-utils.js +238 -0
  29. package/build/runtime/vite/plugin.js +183 -0
  30. package/eslint.js +29 -0
  31. package/package.json +28 -10
  32. package/CONTRIBUTING.md +0 -49
  33. package/bin/metaowl-build.js +0 -12
  34. package/bin/metaowl-dev.js +0 -12
  35. package/bin/metaowl-generate.js +0 -339
  36. package/bin/metaowl-lint.js +0 -71
  37. package/bin/utils.js +0 -82
  38. package/eslint.config.js +0 -3
  39. package/index.js +0 -328
  40. package/modules/app-mounter.js +0 -104
  41. package/modules/auto-import.js +0 -225
  42. package/modules/cache.js +0 -59
  43. package/modules/composables.js +0 -600
  44. package/modules/error-boundary.js +0 -228
  45. package/modules/fetch.js +0 -51
  46. package/modules/file-router.js +0 -478
  47. package/modules/forms.js +0 -353
  48. package/modules/i18n.js +0 -333
  49. package/modules/layouts.js +0 -431
  50. package/modules/link.js +0 -255
  51. package/modules/meta.js +0 -119
  52. package/modules/odoo-rpc.js +0 -511
  53. package/modules/pwa.js +0 -515
  54. package/modules/router.js +0 -769
  55. package/modules/seo.js +0 -501
  56. package/modules/store.js +0 -409
  57. package/modules/templates-manager.js +0 -89
  58. package/modules/test-utils.js +0 -532
  59. package/test/auto-import.test.js +0 -110
  60. package/test/cache.test.js +0 -55
  61. package/test/composables.test.js +0 -103
  62. package/test/dynamic-routes.test.js +0 -469
  63. package/test/error-boundary.test.js +0 -126
  64. package/test/fetch.test.js +0 -100
  65. package/test/file-router.test.js +0 -55
  66. package/test/forms.test.js +0 -203
  67. package/test/i18n.test.js +0 -188
  68. package/test/layouts.test.js +0 -395
  69. package/test/link.test.js +0 -189
  70. package/test/meta.test.js +0 -146
  71. package/test/odoo-rpc.test.js +0 -547
  72. package/test/pwa.test.js +0 -154
  73. package/test/router-guards.test.js +0 -229
  74. package/test/router.test.js +0 -77
  75. package/test/seo.test.js +0 -353
  76. package/test/store.test.js +0 -476
  77. package/test/templates-manager.test.js +0 -83
  78. package/test/test-utils.test.js +0 -314
  79. package/vite/plugin.js +0 -290
  80. package/vitest.config.js +0 -8
@@ -1,476 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest'
2
- import { Store, createPersistencePlugin, createStore } from '../modules/store.js'
3
-
4
- describe('Store', () => {
5
- beforeEach(() => {
6
- Store.clear()
7
- })
8
-
9
- describe('Store.define()', () => {
10
- it('creates a store factory function', () => {
11
- const useCounterStore = Store.define('counter', {
12
- state: () => ({ count: 0 })
13
- })
14
-
15
- expect(typeof useCounterStore).toBe('function')
16
- })
17
-
18
- it('returns singleton instance on multiple calls', () => {
19
- const useCounterStore = Store.define('counter', {
20
- state: () => ({ count: 0 })
21
- })
22
-
23
- const store1 = useCounterStore()
24
- const store2 = useCounterStore()
25
-
26
- expect(store1).toBe(store2)
27
- })
28
-
29
- it('creates reactive state', () => {
30
- const useCounterStore = Store.define('counter', {
31
- state: () => ({ count: 0 })
32
- })
33
-
34
- const store = useCounterStore()
35
-
36
- expect(store.state.count).toBe(0)
37
- })
38
- })
39
-
40
- describe('state management', () => {
41
- let useCounterStore
42
-
43
- beforeEach(() => {
44
- useCounterStore = Store.define('counter', {
45
- state: () => ({ count: 0, name: 'test' }),
46
- mutations: {
47
- increment: (state) => state.count++,
48
- setName: (state, name) => { state.name = name },
49
- add: (state, amount) => { state.count += amount }
50
- }
51
- })
52
- })
53
-
54
- it('commits mutations synchronously', () => {
55
- const store = useCounterStore()
56
-
57
- store.commit('increment')
58
-
59
- expect(store.state.count).toBe(1)
60
- })
61
-
62
- it('commits mutations with payload', () => {
63
- const store = useCounterStore()
64
-
65
- store.commit('setName', 'new name')
66
-
67
- expect(store.state.name).toBe('new name')
68
- })
69
-
70
- it('commits mutations with numeric payload', () => {
71
- const store = useCounterStore()
72
-
73
- store.commit('add', 5)
74
-
75
- expect(store.state.count).toBe(5)
76
- })
77
-
78
- it('throws on unknown mutation', () => {
79
- const store = useCounterStore()
80
-
81
- expect(() => store.commit('unknown')).toThrow('Mutation "unknown" not found')
82
- })
83
- })
84
-
85
- describe('getters', () => {
86
- it('computes getter values', () => {
87
- const useCounterStore = Store.define('counter', {
88
- state: () => ({ count: 5 }),
89
- getters: {
90
- double: (state) => state.count * 2,
91
- triple: (state) => state.count * 3
92
- }
93
- })
94
-
95
- const store = useCounterStore()
96
-
97
- expect(store.getters.double).toBe(10)
98
- expect(store.getters.triple).toBe(15)
99
- })
100
-
101
- it('updates getters when state changes', () => {
102
- const useCounterStore = Store.define('counter', {
103
- state: () => ({ count: 5 }),
104
- getters: {
105
- double: (state) => state.count * 2
106
- },
107
- mutations: {
108
- increment: (state) => state.count++
109
- }
110
- })
111
-
112
- const store = useCounterStore()
113
- expect(store.getters.double).toBe(10)
114
-
115
- store.commit('increment')
116
- expect(store.getters.double).toBe(12)
117
- })
118
- })
119
-
120
- describe('actions', () => {
121
- it('dispatches async actions', async () => {
122
- const useCounterStore = Store.define('counter', {
123
- state: () => ({ count: 0 }),
124
- mutations: {
125
- increment: (state) => state.count++
126
- },
127
- actions: {
128
- async incrementAsync({ commit }) {
129
- await new Promise(resolve => setTimeout(resolve, 10))
130
- commit('increment')
131
- }
132
- }
133
- })
134
-
135
- const store = useCounterStore()
136
-
137
- await store.dispatch('incrementAsync')
138
-
139
- expect(store.state.count).toBe(1)
140
- })
141
-
142
- it('passes payload to actions', async () => {
143
- const useCounterStore = Store.define('counter', {
144
- state: () => ({ count: 0 }),
145
- mutations: {
146
- add: (state, amount) => { state.count += amount }
147
- },
148
- actions: {
149
- async addAsync({ commit }, amount) {
150
- commit('add', amount)
151
- }
152
- }
153
- })
154
-
155
- const store = useCounterStore()
156
-
157
- await store.dispatch('addAsync', 10)
158
-
159
- expect(store.state.count).toBe(10)
160
- })
161
-
162
- it('provides context to actions', async () => {
163
- const actionContext = {}
164
-
165
- const useCounterStore = Store.define('counter2', {
166
- state: () => ({ count: 0 }),
167
- getters: { double: (state) => state.count * 2 },
168
- mutations: { increment: (state) => state.count++ },
169
- actions: {
170
- async testContext(context) {
171
- Object.assign(actionContext, {
172
- hasState: 'state' in context,
173
- hasGetters: 'getters' in context,
174
- hasCommit: typeof context.commit === 'function',
175
- hasDispatch: typeof context.dispatch === 'function'
176
- })
177
- }
178
- }
179
- })
180
-
181
- const store = useCounterStore()
182
- await store.dispatch('testContext')
183
-
184
- expect(actionContext.hasState).toBe(true)
185
- expect(actionContext.hasGetters).toBe(true)
186
- expect(actionContext.hasCommit).toBe(true)
187
- expect(actionContext.hasDispatch).toBe(true)
188
- })
189
-
190
- it('throws on unknown action', async () => {
191
- const useCounterStore = Store.define('counter', {
192
- state: () => ({ count: 0 })
193
- })
194
-
195
- const store = useCounterStore()
196
-
197
- await expect(store.dispatch('unknown')).rejects.toThrow('Action "unknown" not found')
198
- })
199
-
200
- it('returns action result', async () => {
201
- const useCounterStore = Store.define('counter', {
202
- state: () => ({ count: 5 }),
203
- actions: {
204
- async fetchData() {
205
- return { data: 'test' }
206
- }
207
- }
208
- })
209
-
210
- const store = useCounterStore()
211
- const result = await store.dispatch('fetchData')
212
-
213
- expect(result).toEqual({ data: 'test' })
214
- })
215
- })
216
-
217
- describe('subscriptions', () => {
218
- it('notifies subscribers on mutation', () => {
219
- const useCounterStore = Store.define('counter', {
220
- state: () => ({ count: 0 }),
221
- mutations: { increment: (state) => state.count++ }
222
- })
223
-
224
- const store = useCounterStore()
225
- const subscriber = { fn: () => {} }
226
- const spy = { ...subscriber, fn: (mutation, state, prevState) => {} }
227
-
228
- let receivedMutation, receivedState, receivedPrevState
229
- store.subscribe((mutation, state, prevState) => {
230
- receivedMutation = mutation
231
- receivedState = state
232
- receivedPrevState = prevState
233
- })
234
-
235
- store.commit('increment')
236
-
237
- expect(receivedMutation.type).toBe('increment')
238
- expect(receivedState.count).toBe(1)
239
- expect(receivedPrevState.count).toBe(0)
240
- })
241
-
242
- it('allows unsubscribing', () => {
243
- const useCounterStore = Store.define('counter', {
244
- state: () => ({ count: 0 }),
245
- mutations: { increment: (state) => state.count++ }
246
- })
247
-
248
- const store = useCounterStore()
249
- let callCount = 0
250
- const unsubscribe = store.subscribe(() => callCount++)
251
-
252
- store.commit('increment')
253
- expect(callCount).toBe(1)
254
-
255
- unsubscribe()
256
- store.commit('increment')
257
- expect(callCount).toBe(1)
258
- })
259
- })
260
-
261
- describe('action subscriptions', () => {
262
- it('notifies action subscribers', async () => {
263
- const useCounterStore = Store.define('counter', {
264
- state: () => ({ count: 0 }),
265
- actions: {
266
- async testAction() {
267
- return 'result'
268
- }
269
- }
270
- })
271
-
272
- const store = useCounterStore()
273
- const events = []
274
-
275
- store.subscribeAction((action, status, result) => {
276
- events.push({ action, status, result })
277
- })
278
-
279
- await store.dispatch('testAction')
280
-
281
- expect(events.length).toBe(2)
282
- expect(events[0].status).toBe('before')
283
- expect(events[1].status).toBe('after')
284
- expect(events[1].result).toBe('result')
285
- })
286
-
287
- it('notifies on action error', async () => {
288
- const useCounterStore = Store.define('counter', {
289
- actions: {
290
- async failingAction() {
291
- throw new Error('test error')
292
- }
293
- }
294
- })
295
-
296
- const store = useCounterStore()
297
- const events = []
298
-
299
- store.subscribeAction((action, status, result) => {
300
- events.push({ action, status, result })
301
- })
302
-
303
- try {
304
- await store.dispatch('failingAction')
305
- } catch (e) {
306
- // expected
307
- }
308
-
309
- expect(events.length).toBe(2)
310
- expect(events[0].status).toBe('before')
311
- expect(events[1].status).toBe('error')
312
- })
313
- })
314
-
315
- describe('reset', () => {
316
- it('resets state to initial values', () => {
317
- const useCounterStore = Store.define('counter', {
318
- state: () => ({ count: 0, name: 'initial' }),
319
- mutations: {
320
- setCount: (state, val) => { state.count = val },
321
- setName: (state, val) => { state.name = val }
322
- }
323
- })
324
-
325
- const store = useCounterStore()
326
- store.commit('setCount', 100)
327
- store.commit('setName', 'changed')
328
-
329
- store.reset()
330
-
331
- expect(store.state.count).toBe(0)
332
- expect(store.state.name).toBe('initial')
333
- })
334
- })
335
-
336
- describe('static methods', () => {
337
- it('Store.get() retrieves store by id', () => {
338
- const useCounterStore = Store.define('counter', {
339
- state: () => ({ count: 0 })
340
- })
341
-
342
- const store = useCounterStore()
343
- const retrieved = Store.get('counter')
344
-
345
- expect(retrieved).toBe(store)
346
- })
347
-
348
- it('Store.has() checks store existence', () => {
349
- const useCounterStore = Store.define('counter', {
350
- state: () => ({ count: 0 })
351
- })
352
-
353
- useCounterStore() // Initialize the store
354
-
355
- expect(Store.has('counter')).toBe(true)
356
- expect(Store.has('unknown')).toBe(false)
357
- })
358
-
359
- it('Store.remove() removes store', () => {
360
- const useCounterStore = Store.define('counter', {
361
- state: () => ({ count: 0 })
362
- })
363
-
364
- useCounterStore()
365
- expect(Store.has('counter')).toBe(true)
366
-
367
- Store.remove('counter')
368
- expect(Store.has('counter')).toBe(false)
369
- })
370
-
371
- it('Store.storeIds() returns all store IDs', () => {
372
- const useStore1 = Store.define('store1', { state: () => ({}) })
373
- const useStore2 = Store.define('store2', { state: () => ({}) })
374
-
375
- useStore1()
376
- useStore2()
377
-
378
- const ids = Store.storeIds()
379
- expect(ids).toContain('store1')
380
- expect(ids).toContain('store2')
381
- })
382
- })
383
-
384
- describe('persistence plugin', () => {
385
- it('persists state to storage', () => {
386
- const storage = {
387
- data: {},
388
- getItem(key) { return this.data[key] || null },
389
- setItem(key, value) { this.data[key] = value }
390
- }
391
-
392
- Store.use(createPersistencePlugin({ storage }))
393
-
394
- const useCounterStore = Store.define('counter', {
395
- state: () => ({ count: 0 }),
396
- mutations: { increment: (state) => state.count++ }
397
- })
398
-
399
- const store = useCounterStore()
400
- store.commit('increment')
401
-
402
- expect(storage.data['metaowl:store:counter']).toContain('"count":1')
403
- })
404
-
405
- it('restores state from storage', () => {
406
- const storage = {
407
- data: { 'metaowl:store:counter': '{"count":42}' },
408
- getItem(key) { return this.data[key] || null },
409
- setItem(key, value) { this.data[key] = value }
410
- }
411
-
412
- Store.use(createPersistencePlugin({ storage }))
413
-
414
- const useCounterStore = Store.define('counter', {
415
- state: () => ({ count: 0 }),
416
- mutations: { increment: (state) => state.count++ }
417
- })
418
-
419
- const store = useCounterStore()
420
-
421
- expect(store.state.count).toBe(42)
422
- })
423
-
424
- it('persists only specified paths', () => {
425
- const storage = {
426
- data: {},
427
- getItem(key) { return this.data[key] || null },
428
- setItem(key, value) { this.data[key] = value }
429
- }
430
-
431
- Store.use(createPersistencePlugin({ storage, paths: ['persistent'] }))
432
-
433
- const useStore = Store.define('test', {
434
- state: () => ({ persistent: 'value', temporary: 'data' }),
435
- mutations: {
436
- setPersistent: (state, val) => { state.persistent = val },
437
- setTemporary: (state, val) => { state.temporary = val }
438
- }
439
- })
440
-
441
- const store = useStore()
442
- store.commit('setPersistent', 'new value')
443
- store.commit('setTemporary', 'new temp')
444
-
445
- const saved = JSON.parse(storage.data['metaowl:store:test'])
446
- expect(saved.persistent).toBe('new value')
447
- expect(saved.temporary).toBeUndefined()
448
- })
449
- })
450
- })
451
-
452
- describe('createStore', () => {
453
- it('creates reactive state object', () => {
454
- const state = createStore({ count: 0 })
455
-
456
- expect(state.count).toBe(0)
457
- })
458
-
459
- it('has $patch method for batch updates', () => {
460
- const state = createStore({ count: 0, name: 'test' })
461
-
462
- state.$patch({ count: 10, name: 'updated' })
463
-
464
- expect(state.count).toBe(10)
465
- expect(state.name).toBe('updated')
466
- })
467
-
468
- it('has $reset method to restore initial state', () => {
469
- const state = createStore({ count: 0 })
470
-
471
- state.count = 100
472
- state.$reset()
473
-
474
- expect(state.count).toBe(0)
475
- })
476
- })
@@ -1,83 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
- import { mergeTemplates, getInternalTemplates } from '../modules/templates-manager.js'
3
-
4
- vi.mock('@odoo/owl', () => ({
5
- loadFile: vi.fn(),
6
- }))
7
-
8
- import { loadFile } from '@odoo/owl'
9
-
10
- beforeEach(() => {
11
- vi.clearAllMocks()
12
- })
13
-
14
- describe('mergeTemplates', () => {
15
- it('returns wrapped templates string from a single file', async () => {
16
- loadFile.mockResolvedValue('<t t-name="Comp"><div/></t>')
17
- const result = await mergeTemplates(['/components/Comp.xml'])
18
- expect(result).toContain('<templates>')
19
- expect(result).toContain('<t t-name="Comp"><div/></t>')
20
- expect(result).toContain('</templates>')
21
- expect(result).toContain('t-name="Link"') // Internal Link template
22
- })
23
-
24
- it('concatenates multiple template files', async () => {
25
- loadFile
26
- .mockResolvedValueOnce('<t t-name="A"><div/></t>')
27
- .mockResolvedValueOnce('<t t-name="B"><span/></t>')
28
- const result = await mergeTemplates(['/A.xml', '/B.xml'])
29
- expect(result).toContain('<templates>')
30
- expect(result).toContain('<t t-name="A"><div/></t>')
31
- expect(result).toContain('<t t-name="B"><span/></t>')
32
- expect(result).toContain('</templates>')
33
- expect(result).toContain('t-name="Link"') // Internal Link template
34
- })
35
-
36
- it('returns templates with internal components for empty array', async () => {
37
- const result = await mergeTemplates([])
38
- expect(result).toContain('<templates>')
39
- expect(result).toContain('</templates>')
40
- expect(result).toContain('t-name="Link"') // Internal Link template
41
- })
42
-
43
- it('skips failed files and logs error', async () => {
44
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
45
- loadFile.mockRejectedValue(new Error('404'))
46
- const result = await mergeTemplates(['/missing.xml'])
47
- expect(result).toContain('<templates>')
48
- expect(result).toContain('</templates>')
49
- expect(result).toContain('t-name="Link"') // Internal Link template despite error
50
- expect(consoleSpy).toHaveBeenCalledWith(
51
- expect.stringContaining('[metaowl] Failed to load template: /missing.xml'),
52
- expect.any(Error)
53
- )
54
- consoleSpy.mockRestore()
55
- })
56
-
57
- it('collects successful files even when one fails', async () => {
58
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
59
- loadFile
60
- .mockResolvedValueOnce('<t t-name="Good"><div/></t>')
61
- .mockRejectedValueOnce(new Error('404'))
62
- .mockResolvedValueOnce('<t t-name="Also"><span/></t>')
63
- const result = await mergeTemplates(['/good.xml', '/missing.xml', '/also.xml'])
64
- expect(result).toContain('<t t-name="Good"><div/></t>')
65
- expect(result).toContain('<t t-name="Also"><span/></t>')
66
- expect(result).toContain('t-name="Link"') // Internal Link template
67
- consoleSpy.mockRestore()
68
- })
69
-
70
- it('includes internal Link component template', async () => {
71
- const templates = getInternalTemplates()
72
- expect(templates.length).toBeGreaterThan(0)
73
- expect(templates[0]).toContain('t-name="Link"')
74
- expect(templates[0]).toContain('<a')
75
- expect(templates[0]).toContain('t-att-href')
76
- })
77
-
78
- it('calls loadFile once per path', async () => {
79
- loadFile.mockResolvedValue('<t/>')
80
- await mergeTemplates(['/a.xml', '/b.xml', '/c.xml'])
81
- expect(loadFile).toHaveBeenCalledTimes(3)
82
- })
83
- })