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,395 @@
|
|
|
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
|
+
@layout('admin')
|
|
309
|
+
class AdminPage extends MockComponent {}
|
|
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
|
+
@defineLayout('admin')
|
|
327
|
+
class AdminPage extends MockComponent {}
|
|
328
|
+
|
|
329
|
+
expect(AdminPage.layout).toBe('admin')
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('sets layoutOptions property', () => {
|
|
333
|
+
@defineLayout('admin', { persistent: true })
|
|
334
|
+
class AdminPage extends MockComponent {}
|
|
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
|
+
})
|