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.
- package/CHANGELOG.md +52 -0
- package/README.md +13 -15
- package/build/runtime/bin/metaowl-build.js +10 -0
- package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
- package/build/runtime/bin/metaowl-dev.js +10 -0
- package/build/runtime/bin/metaowl-generate.js +231 -0
- package/build/runtime/bin/metaowl-lint.js +58 -0
- package/build/runtime/bin/utils.js +68 -0
- package/build/runtime/index.js +141 -0
- package/build/runtime/modules/app-mounter.js +65 -0
- package/build/runtime/modules/auto-import.js +140 -0
- package/build/runtime/modules/cache.js +49 -0
- package/build/runtime/modules/composables.js +353 -0
- package/build/runtime/modules/error-boundary.js +116 -0
- package/build/runtime/modules/fetch.js +31 -0
- package/build/runtime/modules/file-router.js +205 -0
- package/build/runtime/modules/forms.js +193 -0
- package/build/runtime/modules/i18n.js +167 -0
- package/build/runtime/modules/layouts.js +163 -0
- package/build/runtime/modules/link.js +141 -0
- package/build/runtime/modules/meta.js +117 -0
- package/build/runtime/modules/odoo-rpc.js +264 -0
- package/build/runtime/modules/pwa.js +262 -0
- package/build/runtime/modules/router.js +389 -0
- package/build/runtime/modules/seo.js +186 -0
- package/build/runtime/modules/store.js +196 -0
- package/build/runtime/modules/templates-manager.js +52 -0
- package/build/runtime/modules/test-utils.js +238 -0
- package/build/runtime/vite/plugin.js +183 -0
- package/eslint.js +29 -0
- package/package.json +29 -11
- package/CONTRIBUTING.md +0 -49
- package/bin/metaowl-build.js +0 -12
- package/bin/metaowl-dev.js +0 -12
- package/bin/metaowl-generate.js +0 -339
- package/bin/metaowl-lint.js +0 -71
- package/bin/utils.js +0 -82
- package/index.js +0 -328
- package/modules/app-mounter.js +0 -104
- package/modules/auto-import.js +0 -225
- package/modules/cache.js +0 -59
- package/modules/composables.js +0 -600
- package/modules/error-boundary.js +0 -228
- package/modules/fetch.js +0 -51
- package/modules/file-router.js +0 -478
- package/modules/forms.js +0 -353
- package/modules/i18n.js +0 -333
- package/modules/layouts.js +0 -431
- package/modules/link.js +0 -255
- package/modules/meta.js +0 -119
- package/modules/odoo-rpc.js +0 -511
- package/modules/pwa.js +0 -515
- package/modules/router.js +0 -769
- package/modules/seo.js +0 -501
- package/modules/store.js +0 -409
- package/modules/templates-manager.js +0 -89
- package/modules/test-utils.js +0 -532
- package/test/auto-import.test.js +0 -110
- package/test/cache.test.js +0 -55
- package/test/composables.test.js +0 -103
- package/test/dynamic-routes.test.js +0 -469
- package/test/error-boundary.test.js +0 -126
- package/test/fetch.test.js +0 -100
- package/test/file-router.test.js +0 -55
- package/test/forms.test.js +0 -203
- package/test/i18n.test.js +0 -188
- package/test/layouts.test.js +0 -395
- package/test/link.test.js +0 -189
- package/test/meta.test.js +0 -146
- package/test/odoo-rpc.test.js +0 -547
- package/test/pwa.test.js +0 -154
- package/test/router-guards.test.js +0 -229
- package/test/router.test.js +0 -77
- package/test/seo.test.js +0 -353
- package/test/store.test.js +0 -476
- package/test/templates-manager.test.js +0 -83
- package/test/test-utils.test.js +0 -314
- package/vite/plugin.js +0 -277
- package/vitest.config.js +0 -8
package/test/layouts.test.js
DELETED
|
@@ -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
|
-
})
|