metaowl 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +267 -2
  3. package/build/runtime/bin/metaowl-build.js +10 -0
  4. package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
  5. package/build/runtime/bin/metaowl-dev.js +10 -0
  6. package/build/runtime/bin/metaowl-generate.js +231 -0
  7. package/build/runtime/bin/metaowl-lint.js +58 -0
  8. package/build/runtime/bin/utils.js +68 -0
  9. package/build/runtime/index.js +144 -0
  10. package/build/runtime/modules/app-mounter.js +73 -0
  11. package/build/runtime/modules/auto-import.js +140 -0
  12. package/build/runtime/modules/cache.js +49 -0
  13. package/build/runtime/modules/composables.js +353 -0
  14. package/build/runtime/modules/constants.js +38 -0
  15. package/build/runtime/modules/error-boundary.js +116 -0
  16. package/build/runtime/modules/fetch.js +31 -0
  17. package/build/runtime/modules/file-router.js +207 -0
  18. package/build/runtime/modules/fonts.js +172 -0
  19. package/build/runtime/modules/forms.js +193 -0
  20. package/build/runtime/modules/i18n.js +180 -0
  21. package/build/runtime/modules/image.js +175 -0
  22. package/build/runtime/modules/layouts.js +214 -0
  23. package/build/runtime/modules/link.js +141 -0
  24. package/build/runtime/modules/meta.js +117 -0
  25. package/build/runtime/modules/odoo-rpc.js +265 -0
  26. package/build/runtime/modules/pwa.js +272 -0
  27. package/build/runtime/modules/router.js +384 -0
  28. package/build/runtime/modules/seo.js +186 -0
  29. package/build/runtime/modules/store.js +198 -0
  30. package/build/runtime/modules/templates-manager.js +52 -0
  31. package/build/runtime/modules/test-utils.js +238 -0
  32. package/build/runtime/vite/plugin.js +197 -0
  33. package/eslint.js +29 -0
  34. package/package.json +45 -27
  35. package/CONTRIBUTING.md +0 -49
  36. package/bin/metaowl-build.js +0 -12
  37. package/bin/metaowl-dev.js +0 -12
  38. package/bin/metaowl-generate.js +0 -339
  39. package/bin/metaowl-lint.js +0 -71
  40. package/bin/utils.js +0 -82
  41. package/eslint.config.js +0 -3
  42. package/index.js +0 -328
  43. package/modules/app-mounter.js +0 -104
  44. package/modules/auto-import.js +0 -225
  45. package/modules/cache.js +0 -59
  46. package/modules/composables.js +0 -600
  47. package/modules/error-boundary.js +0 -228
  48. package/modules/fetch.js +0 -51
  49. package/modules/file-router.js +0 -478
  50. package/modules/forms.js +0 -353
  51. package/modules/i18n.js +0 -333
  52. package/modules/layouts.js +0 -431
  53. package/modules/link.js +0 -255
  54. package/modules/meta.js +0 -119
  55. package/modules/odoo-rpc.js +0 -511
  56. package/modules/pwa.js +0 -515
  57. package/modules/router.js +0 -769
  58. package/modules/seo.js +0 -501
  59. package/modules/store.js +0 -409
  60. package/modules/templates-manager.js +0 -89
  61. package/modules/test-utils.js +0 -532
  62. package/test/auto-import.test.js +0 -110
  63. package/test/cache.test.js +0 -55
  64. package/test/composables.test.js +0 -103
  65. package/test/dynamic-routes.test.js +0 -469
  66. package/test/error-boundary.test.js +0 -126
  67. package/test/fetch.test.js +0 -100
  68. package/test/file-router.test.js +0 -55
  69. package/test/forms.test.js +0 -203
  70. package/test/i18n.test.js +0 -188
  71. package/test/layouts.test.js +0 -395
  72. package/test/link.test.js +0 -189
  73. package/test/meta.test.js +0 -146
  74. package/test/odoo-rpc.test.js +0 -547
  75. package/test/pwa.test.js +0 -154
  76. package/test/router-guards.test.js +0 -229
  77. package/test/router.test.js +0 -77
  78. package/test/seo.test.js +0 -353
  79. package/test/store.test.js +0 -476
  80. package/test/templates-manager.test.js +0 -83
  81. package/test/test-utils.test.js +0 -314
  82. package/vite/plugin.js +0 -290
  83. package/vitest.config.js +0 -8
@@ -1,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
- })