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.
Files changed (79) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +13 -15
  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 +29 -11
  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/index.js +0 -328
  39. package/modules/app-mounter.js +0 -104
  40. package/modules/auto-import.js +0 -225
  41. package/modules/cache.js +0 -59
  42. package/modules/composables.js +0 -600
  43. package/modules/error-boundary.js +0 -228
  44. package/modules/fetch.js +0 -51
  45. package/modules/file-router.js +0 -478
  46. package/modules/forms.js +0 -353
  47. package/modules/i18n.js +0 -333
  48. package/modules/layouts.js +0 -431
  49. package/modules/link.js +0 -255
  50. package/modules/meta.js +0 -119
  51. package/modules/odoo-rpc.js +0 -511
  52. package/modules/pwa.js +0 -515
  53. package/modules/router.js +0 -769
  54. package/modules/seo.js +0 -501
  55. package/modules/store.js +0 -409
  56. package/modules/templates-manager.js +0 -89
  57. package/modules/test-utils.js +0 -532
  58. package/test/auto-import.test.js +0 -110
  59. package/test/cache.test.js +0 -55
  60. package/test/composables.test.js +0 -103
  61. package/test/dynamic-routes.test.js +0 -469
  62. package/test/error-boundary.test.js +0 -126
  63. package/test/fetch.test.js +0 -100
  64. package/test/file-router.test.js +0 -55
  65. package/test/forms.test.js +0 -203
  66. package/test/i18n.test.js +0 -188
  67. package/test/layouts.test.js +0 -395
  68. package/test/link.test.js +0 -189
  69. package/test/meta.test.js +0 -146
  70. package/test/odoo-rpc.test.js +0 -547
  71. package/test/pwa.test.js +0 -154
  72. package/test/router-guards.test.js +0 -229
  73. package/test/router.test.js +0 -77
  74. package/test/seo.test.js +0 -353
  75. package/test/store.test.js +0 -476
  76. package/test/templates-manager.test.js +0 -83
  77. package/test/test-utils.test.js +0 -314
  78. package/vite/plugin.js +0 -277
  79. 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
- })