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,395 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest'
2
- import {
3
- registerLayout,
4
- unregisterLayout,
5
- getLayout,
6
- hasLayout,
7
- getLayoutNames,
8
- setDefaultLayout,
9
- getDefaultLayout,
10
- resolveLayout,
11
- setRouteLayout,
12
- getRouteLayout,
13
- createLayoutWrapper,
14
- subscribeToLayouts,
15
- clearLayouts,
16
- layout,
17
- defineLayout,
18
- buildLayouts
19
- } from '../modules/layouts.js'
20
-
21
- // Mock Component class
22
- class MockComponent {
23
- static template = '<div>Mock</div>'
24
- }
25
-
26
- class DefaultLayout extends MockComponent {
27
- static template = '<div class="default"><t t-slot="default"/></div>'
28
- }
29
-
30
- class AdminLayout extends MockComponent {
31
- static template = '<div class="admin"><t t-slot="default"/></div>'
32
- }
33
-
34
- describe('Layouts', () => {
35
- beforeEach(() => {
36
- clearLayouts()
37
- })
38
-
39
- describe('registerLayout', () => {
40
- it('registers a layout component', () => {
41
- registerLayout('default', DefaultLayout)
42
-
43
- expect(hasLayout('default')).toBe(true)
44
- expect(getLayout('default')).toBe(DefaultLayout)
45
- })
46
-
47
- it('can register multiple layouts', () => {
48
- registerLayout('default', DefaultLayout)
49
- registerLayout('admin', AdminLayout)
50
-
51
- expect(getLayoutNames()).toContain('default')
52
- expect(getLayoutNames()).toContain('admin')
53
- })
54
-
55
- it('sets default layout when option is true', () => {
56
- registerLayout('custom', DefaultLayout, { default: true })
57
-
58
- expect(getDefaultLayout()).toBe('custom')
59
- })
60
-
61
- it('notifies listeners on register', () => {
62
- const listener = vi.fn()
63
- subscribeToLayouts(listener)
64
-
65
- registerLayout('test', MockComponent)
66
-
67
- expect(listener).toHaveBeenCalledWith({
68
- type: 'register',
69
- name: 'test',
70
- layout: MockComponent
71
- })
72
- })
73
- })
74
-
75
- describe('unregisterLayout', () => {
76
- it('removes a registered layout', () => {
77
- registerLayout('default', DefaultLayout)
78
- expect(hasLayout('default')).toBe(true)
79
-
80
- unregisterLayout('default')
81
- expect(hasLayout('default')).toBe(false)
82
- })
83
-
84
- it('returns false for unregistered layout', () => {
85
- expect(unregisterLayout('unknown')).toBe(false)
86
- })
87
-
88
- it('notifies listeners on unregister', () => {
89
- const listener = vi.fn()
90
- subscribeToLayouts(listener)
91
-
92
- registerLayout('test', MockComponent)
93
- unregisterLayout('test')
94
-
95
- expect(listener).toHaveBeenLastCalledWith({
96
- type: 'unregister',
97
- name: 'test'
98
- })
99
- })
100
- })
101
-
102
- describe('getLayout', () => {
103
- it('returns undefined for unregistered layout', () => {
104
- expect(getLayout('unknown')).toBeUndefined()
105
- })
106
-
107
- it('returns the correct layout component', () => {
108
- registerLayout('admin', AdminLayout)
109
-
110
- expect(getLayout('admin')).toBe(AdminLayout)
111
- })
112
- })
113
-
114
- describe('getLayoutNames', () => {
115
- it('returns empty array when no layouts', () => {
116
- expect(getLayoutNames()).toEqual([])
117
- })
118
-
119
- it('returns all registered layout names', () => {
120
- registerLayout('default', DefaultLayout)
121
- registerLayout('admin', AdminLayout)
122
-
123
- const names = getLayoutNames()
124
- expect(names).toHaveLength(2)
125
- expect(names).toContain('default')
126
- expect(names).toContain('admin')
127
- })
128
- })
129
-
130
- describe('setDefaultLayout / getDefaultLayout', () => {
131
- it('default is "default" initially', () => {
132
- expect(getDefaultLayout()).toBe('default')
133
- })
134
-
135
- it('sets and gets default layout', () => {
136
- registerLayout('admin', AdminLayout)
137
-
138
- setDefaultLayout('admin')
139
-
140
- expect(getDefaultLayout()).toBe('admin')
141
- })
142
-
143
- it('warns when setting unregistered layout', () => {
144
- const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
145
-
146
- setDefaultLayout('unknown')
147
-
148
- expect(consoleSpy).toHaveBeenCalledWith('[metaowl] Layout "unknown" is not registered yet')
149
- consoleSpy.mockRestore()
150
- })
151
- })
152
-
153
- describe('resolveLayout', () => {
154
- beforeEach(() => {
155
- registerLayout('default', DefaultLayout)
156
- registerLayout('admin', AdminLayout)
157
- })
158
-
159
- it('returns default layout when component has no layout property', () => {
160
- class MyPage extends MockComponent {}
161
-
162
- expect(resolveLayout(MyPage)).toBe('default')
163
- })
164
-
165
- it('returns component layout property', () => {
166
- class AdminPage extends MockComponent {
167
- static layout = 'admin'
168
- }
169
-
170
- expect(resolveLayout(AdminPage)).toBe('admin')
171
- })
172
-
173
- it('returns route-specific layout over component layout', () => {
174
- class MyPage extends MockComponent {
175
- static layout = 'default'
176
- }
177
-
178
- setRouteLayout('/admin/dashboard', 'admin')
179
-
180
- expect(resolveLayout(MyPage, '/admin/dashboard')).toBe('admin')
181
- })
182
-
183
- it('returns component layout when no route-specific layout', () => {
184
- class AdminPage extends MockComponent {
185
- static layout = 'admin'
186
- }
187
-
188
- expect(resolveLayout(AdminPage, '/other/path')).toBe('admin')
189
- })
190
-
191
- it('returns _layout property if set', () => {
192
- class MyPage extends MockComponent {}
193
- MyPage._layout = 'admin'
194
-
195
- expect(resolveLayout(MyPage)).toBe('admin')
196
- })
197
- })
198
-
199
- describe('setRouteLayout / getRouteLayout', () => {
200
- it('assigns layout to route', () => {
201
- registerLayout('admin', AdminLayout)
202
-
203
- setRouteLayout('/admin/users', 'admin')
204
-
205
- expect(getRouteLayout('/admin/users')).toBe('admin')
206
- })
207
-
208
- it('returns undefined for unassigned route', () => {
209
- expect(getRouteLayout('/unknown')).toBeUndefined()
210
- })
211
-
212
- it('can override previous assignment', () => {
213
- registerLayout('default', DefaultLayout)
214
- registerLayout('admin', AdminLayout)
215
-
216
- setRouteLayout('/page', 'default')
217
- expect(getRouteLayout('/page')).toBe('default')
218
-
219
- setRouteLayout('/page', 'admin')
220
- expect(getRouteLayout('/page')).toBe('admin')
221
- })
222
- })
223
-
224
- describe('createLayoutWrapper', () => {
225
- it('creates a wrapper component', () => {
226
- const Wrapper = createLayoutWrapper(DefaultLayout, MockComponent)
227
-
228
- expect(Wrapper).toBeDefined()
229
- expect(typeof Wrapper).toBe('function')
230
- })
231
-
232
- it('wrapper extends Component', () => {
233
- const Wrapper = createLayoutWrapper(DefaultLayout, MockComponent)
234
-
235
- expect(Wrapper.prototype).toBeDefined()
236
- })
237
-
238
- it('wrapper has template property', () => {
239
- const Wrapper = createLayoutWrapper(DefaultLayout, MockComponent)
240
-
241
- expect(Wrapper.template).toBeDefined()
242
- })
243
- })
244
-
245
- describe('subscribeToLayouts', () => {
246
- it('subscribes to layout events', () => {
247
- const listener = vi.fn()
248
- subscribeToLayouts(listener)
249
-
250
- registerLayout('test', MockComponent)
251
-
252
- expect(listener).toHaveBeenCalled()
253
- })
254
-
255
- it('returns unsubscribe function', () => {
256
- const listener = vi.fn()
257
- const unsubscribe = subscribeToLayouts(listener)
258
-
259
- unsubscribe()
260
- registerLayout('test', MockComponent)
261
-
262
- // Should only be called once (before unsubscribe)
263
- expect(listener).toHaveBeenCalledTimes(0)
264
- })
265
- })
266
-
267
- describe('clearLayouts', () => {
268
- it('removes all layouts', () => {
269
- registerLayout('default', DefaultLayout)
270
- registerLayout('admin', AdminLayout)
271
-
272
- clearLayouts()
273
-
274
- expect(getLayoutNames()).toHaveLength(0)
275
- })
276
-
277
- it('resets default layout', () => {
278
- registerLayout('admin', AdminLayout, { default: true })
279
- expect(getDefaultLayout()).toBe('admin')
280
-
281
- clearLayouts()
282
-
283
- expect(getDefaultLayout()).toBe('default')
284
- })
285
-
286
- it('clears route layouts', () => {
287
- setRouteLayout('/page', 'admin')
288
-
289
- clearLayouts()
290
-
291
- expect(getRouteLayout('/page')).toBeUndefined()
292
- })
293
-
294
- it('clears listeners', () => {
295
- const listener = vi.fn()
296
- subscribeToLayouts(listener)
297
-
298
- clearLayouts()
299
- registerLayout('test', MockComponent)
300
-
301
- // Listener should not be called after clear
302
- expect(listener).not.toHaveBeenCalled()
303
- })
304
- })
305
-
306
- describe('layout decorator', () => {
307
- it('sets layout property on component', () => {
308
- class AdminPage extends MockComponent {}
309
- layout('admin')(AdminPage)
310
-
311
- expect(AdminPage.layout).toBe('admin')
312
- })
313
-
314
- it('returns the component class', () => {
315
- const Decorator = layout('admin')
316
- class TestPage extends MockComponent {}
317
-
318
- const result = Decorator(TestPage)
319
-
320
- expect(result).toBe(TestPage)
321
- })
322
- })
323
-
324
- describe('defineLayout decorator', () => {
325
- it('sets layout property', () => {
326
- class AdminPage extends MockComponent {}
327
- defineLayout('admin')(AdminPage)
328
-
329
- expect(AdminPage.layout).toBe('admin')
330
- })
331
-
332
- it('sets layoutOptions property', () => {
333
- class AdminPage extends MockComponent {}
334
- defineLayout('admin', { persistent: true })(AdminPage)
335
-
336
- expect(AdminPage.layoutOptions).toEqual({ persistent: true })
337
- })
338
- })
339
-
340
- describe('buildLayouts', () => {
341
- it('builds layouts from glob modules', () => {
342
- const modules = {
343
- './layouts/default/DefaultLayout.js': { default: DefaultLayout },
344
- './layouts/admin/AdminLayout.js': { default: AdminLayout }
345
- }
346
-
347
- const layouts = buildLayouts(modules)
348
-
349
- expect(layouts.default).toBe(DefaultLayout)
350
- expect(layouts.admin).toBe(AdminLayout)
351
- })
352
-
353
- it('extracts layout name from path', () => {
354
- const modules = {
355
- './layouts/custom/CustomLayout.js': { default: MockComponent }
356
- }
357
-
358
- const layouts = buildLayouts(modules)
359
-
360
- expect(layouts.custom).toBe(MockComponent)
361
- })
362
-
363
- it('handles non-default exports', () => {
364
- const modules = {
365
- './layouts/default/DefaultLayout.js': { DefaultLayout }
366
- }
367
-
368
- const layouts = buildLayouts(modules)
369
-
370
- expect(layouts.default).toBe(DefaultLayout)
371
- })
372
-
373
- it('ignores invalid paths', () => {
374
- const modules = {
375
- './components/Button.js': { default: MockComponent },
376
- './layouts/valid/ValidLayout.js': { default: DefaultLayout }
377
- }
378
-
379
- const layouts = buildLayouts(modules)
380
-
381
- expect(layouts.valid).toBe(DefaultLayout)
382
- expect(layouts.Button).toBeUndefined()
383
- })
384
-
385
- it('registers layouts automatically', () => {
386
- const modules = {
387
- './layouts/test/TestLayout.js': { default: MockComponent }
388
- }
389
-
390
- buildLayouts(modules)
391
-
392
- expect(hasLayout('test')).toBe(true)
393
- })
394
- })
395
- })
package/test/link.test.js DELETED
@@ -1,189 +0,0 @@
1
- /**
2
- * @module Link Tests
3
- *
4
- * Tests for the Link component and SPA navigation.
5
- */
6
- import { describe, it, expect, beforeEach, vi } from 'vitest'
7
- import { Link, registerLinkTemplate } from '../modules/link.js'
8
- import {
9
- navigateTo,
10
- setSpaMode,
11
- isSpaMode,
12
- _setSpaNavigationCallback,
13
- resetRouter
14
- } from '../modules/router.js'
15
-
16
- // Mock für window
17
- const mockPushState = vi.fn()
18
- const mockReplaceState = vi.fn()
19
- const mockHistoryBack = vi.fn()
20
- const mockHistoryForward = vi.fn()
21
- const mockHistoryGo = vi.fn()
22
- const mockAddEventListener = vi.fn()
23
- const mockRemoveEventListener = vi.fn()
24
-
25
- // Setup global mocks
26
- beforeEach(() => {
27
- vi.resetAllMocks()
28
- resetRouter()
29
-
30
- // Mock window.location
31
- Object.defineProperty(globalThis, 'window', {
32
- value: {
33
- location: {
34
- pathname: '/',
35
- href: 'http://localhost/',
36
- replace: vi.fn()
37
- },
38
- history: {
39
- pushState: mockPushState,
40
- replaceState: mockReplaceState,
41
- back: mockHistoryBack,
42
- forward: mockHistoryForward,
43
- go: mockHistoryGo
44
- },
45
- addEventListener: mockAddEventListener,
46
- removeEventListener: mockRemoveEventListener
47
- },
48
- writable: true,
49
- configurable: true
50
- })
51
-
52
- // Mock document.location
53
- Object.defineProperty(globalThis, 'document', {
54
- value: {
55
- location: {
56
- pathname: '/'
57
- }
58
- },
59
- writable: true,
60
- configurable: true
61
- })
62
- })
63
-
64
- describe('Link Component', () => {
65
- describe('isExternalUrl', () => {
66
- it('should return false for internal paths', () => {
67
- const internalPaths = ['/', '/about', '/user/123', '/blog/post-slug']
68
-
69
- for (const path of internalPaths) {
70
- // Test durch Instanziierung der Komponente und Prüfung des Verhaltens
71
- const link = new Link()
72
- expect(link).toBeDefined()
73
- }
74
- })
75
-
76
- it('should return true for external URLs', () => {
77
- const externalUrls = [
78
- 'http://example.com',
79
- 'https://example.com',
80
- '//example.com',
81
- 'mailto:test@example.com',
82
- 'tel:+1234567890',
83
- 'ftp://ftp.example.com',
84
- 'javascript:void(0)'
85
- ]
86
-
87
- for (const url of externalUrls) {
88
- const link = new Link()
89
- expect(link).toBeDefined()
90
- }
91
- })
92
- })
93
-
94
- describe('Link props', () => {
95
- it('should accept required "to" prop', () => {
96
- const link = new Link()
97
- expect(Link.props.to.optional).toBe(false)
98
- expect(Link.props.to.type).toBe(String)
99
- })
100
-
101
- it('should accept optional props', () => {
102
- expect(Link.props.class.optional).toBe(true)
103
- expect(Link.props.activeClass.optional).toBe(true)
104
- expect(Link.props.target.optional).toBe(true)
105
- expect(Link.props.rel.optional).toBe(true)
106
- expect(Link.props.title.optional).toBe(true)
107
- expect(Link.props.download.optional).toBe(true)
108
- })
109
-
110
- it('should have correct static template', () => {
111
- expect(Link.template).toBe('Link')
112
- })
113
- })
114
-
115
- describe('registerLinkTemplate', () => {
116
- it('should add Link template to string templates', () => {
117
- const templates = '<templates><t t-name="Test"></t></templates>'
118
- const result = registerLinkTemplate(templates)
119
-
120
- expect(result).toContain('t-name="Link"')
121
- expect(result).toContain('<a')
122
- expect(result).toContain('</templates>')
123
- })
124
-
125
- it('should add Link template to object templates', () => {
126
- const templates = { Test: '<t t-name="Test"></t>' }
127
- registerLinkTemplate(templates)
128
-
129
- expect(templates.Link).toBeDefined()
130
- expect(templates.Link).toContain('t-name="Link"')
131
- })
132
- })
133
- })
134
-
135
- describe('SPA Navigation', () => {
136
- describe('navigateTo', () => {
137
- it('should use window.location when SPA mode is disabled', async () => {
138
- setSpaMode(false)
139
- const locationHrefSpy = vi.spyOn(window.location, 'href', 'set')
140
-
141
- await navigateTo('/about')
142
-
143
- expect(locationHrefSpy).toHaveBeenCalledWith('/about')
144
- })
145
-
146
- it('should use history.pushState when SPA mode is enabled', async () => {
147
- setSpaMode(true)
148
- const mockCallback = vi.fn().mockResolvedValue(undefined)
149
- _setSpaNavigationCallback(mockCallback)
150
-
151
- await navigateTo('/about')
152
-
153
- expect(mockPushState).toHaveBeenCalledWith({ path: '/about' }, '', '/about')
154
- expect(mockCallback).toHaveBeenCalledWith('/about')
155
- })
156
-
157
- it('should use history.replaceState when replace option is true', async () => {
158
- setSpaMode(true)
159
- const mockCallback = vi.fn().mockResolvedValue(undefined)
160
- _setSpaNavigationCallback(mockCallback)
161
-
162
- await navigateTo('/about', { replace: true })
163
-
164
- expect(mockReplaceState).toHaveBeenCalledWith({ path: '/about' }, '', '/about')
165
- })
166
-
167
- it('should fallback to window.location on navigation error', async () => {
168
- setSpaMode(true)
169
- const mockCallback = vi.fn().mockRejectedValue(new Error('Navigation failed'))
170
- _setSpaNavigationCallback(mockCallback)
171
-
172
- const locationHrefSpy = vi.spyOn(window.location, 'href', 'set')
173
-
174
- await navigateTo('/about')
175
-
176
- expect(locationHrefSpy).toHaveBeenCalledWith('/about')
177
- })
178
- })
179
-
180
- describe('setSpaMode / isSpaMode', () => {
181
- it('should enable and disable SPA mode', () => {
182
- setSpaMode(true)
183
- expect(isSpaMode()).toBe(true)
184
-
185
- setSpaMode(false)
186
- expect(isSpaMode()).toBe(false)
187
- })
188
- })
189
- })