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/fetch.test.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
-
import Fetch from '../modules/fetch.js'
|
|
3
|
-
|
|
4
|
-
beforeEach(() => {
|
|
5
|
-
Fetch.configure({ baseUrl: '', onError: null })
|
|
6
|
-
vi.restoreAllMocks()
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
describe('Fetch.configure', () => {
|
|
10
|
-
it('sets baseUrl', () => {
|
|
11
|
-
Fetch.configure({ baseUrl: 'https://api.example.com' })
|
|
12
|
-
expect(Fetch._baseUrl).toBe('https://api.example.com')
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('sets onError callback', () => {
|
|
16
|
-
const handler = vi.fn()
|
|
17
|
-
Fetch.configure({ onError: handler })
|
|
18
|
-
expect(Fetch._onError).toBe(handler)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('defaults baseUrl to empty string', () => {
|
|
22
|
-
Fetch.configure({})
|
|
23
|
-
expect(Fetch._baseUrl).toBe('')
|
|
24
|
-
})
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
describe('Fetch.url', () => {
|
|
28
|
-
it('prepends baseUrl for internal requests', async () => {
|
|
29
|
-
Fetch.configure({ baseUrl: 'https://api.example.com' })
|
|
30
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
31
|
-
json: () => Promise.resolve({ ok: true })
|
|
32
|
-
})
|
|
33
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
34
|
-
|
|
35
|
-
await Fetch.url('/items')
|
|
36
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
37
|
-
'https://api.example.com/items',
|
|
38
|
-
expect.objectContaining({ method: 'GET' })
|
|
39
|
-
)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('does not prepend baseUrl when internal=false', async () => {
|
|
43
|
-
Fetch.configure({ baseUrl: 'https://api.example.com' })
|
|
44
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
45
|
-
json: () => Promise.resolve({})
|
|
46
|
-
})
|
|
47
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
48
|
-
|
|
49
|
-
await Fetch.url('https://other.com/data', 'GET', null, false)
|
|
50
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
51
|
-
'https://other.com/data',
|
|
52
|
-
expect.anything()
|
|
53
|
-
)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('sends JSON body for POST requests', async () => {
|
|
57
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
58
|
-
json: () => Promise.resolve({})
|
|
59
|
-
})
|
|
60
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
61
|
-
|
|
62
|
-
await Fetch.url('/items', 'POST', { name: 'test' })
|
|
63
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
64
|
-
'/items',
|
|
65
|
-
expect.objectContaining({
|
|
66
|
-
method: 'POST',
|
|
67
|
-
body: JSON.stringify({ name: 'test' })
|
|
68
|
-
})
|
|
69
|
-
)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('returns null and calls onError on network failure', async () => {
|
|
73
|
-
const onError = vi.fn()
|
|
74
|
-
Fetch.configure({ onError })
|
|
75
|
-
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')))
|
|
76
|
-
|
|
77
|
-
const result = await Fetch.url('/fail')
|
|
78
|
-
expect(result).toBeNull()
|
|
79
|
-
expect(onError).toHaveBeenCalledOnce()
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('returns null and skips onError when triggerErrorHandler=false', async () => {
|
|
83
|
-
const onError = vi.fn()
|
|
84
|
-
Fetch.configure({ onError })
|
|
85
|
-
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')))
|
|
86
|
-
|
|
87
|
-
const result = await Fetch.url('/fail', 'GET', null, true, false)
|
|
88
|
-
expect(result).toBeNull()
|
|
89
|
-
expect(onError).not.toHaveBeenCalled()
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('returns parsed JSON on success', async () => {
|
|
93
|
-
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
94
|
-
json: () => Promise.resolve({ id: 42 })
|
|
95
|
-
}))
|
|
96
|
-
|
|
97
|
-
const result = await Fetch.url('/item/42')
|
|
98
|
-
expect(result).toEqual({ id: 42 })
|
|
99
|
-
})
|
|
100
|
-
})
|
package/test/file-router.test.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { buildRoutes } from '../modules/file-router.js'
|
|
3
|
-
|
|
4
|
-
const Comp = function IndexPage() {}
|
|
5
|
-
const About = function AboutPage() {}
|
|
6
|
-
const Deep = function DeepPage() {}
|
|
7
|
-
|
|
8
|
-
describe('buildRoutes', () => {
|
|
9
|
-
it('maps index directory to /', () => {
|
|
10
|
-
const routes = buildRoutes({
|
|
11
|
-
'./pages/index/Index.js': { default: Comp }
|
|
12
|
-
})
|
|
13
|
-
expect(routes).toHaveLength(1)
|
|
14
|
-
expect(routes[0].path).toContain('/')
|
|
15
|
-
expect(routes[0].name).toBe('index')
|
|
16
|
-
expect(routes[0].component).toBe(Comp)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('maps a named directory to its URL path', () => {
|
|
20
|
-
const routes = buildRoutes({
|
|
21
|
-
'./pages/about/About.js': { default: About }
|
|
22
|
-
})
|
|
23
|
-
expect(routes[0].path).toContain('/about')
|
|
24
|
-
expect(routes[0].name).toBe('about')
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('maps nested directories to a slug name', () => {
|
|
28
|
-
const routes = buildRoutes({
|
|
29
|
-
'./pages/blog/post/Post.js': { default: Deep }
|
|
30
|
-
})
|
|
31
|
-
expect(routes[0].path).toContain('/blog/post')
|
|
32
|
-
expect(routes[0].name).toBe('blog-post')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('falls back to first function export when no default', () => {
|
|
36
|
-
const routes = buildRoutes({
|
|
37
|
-
'./pages/index/Index.js': { MyComp: Comp }
|
|
38
|
-
})
|
|
39
|
-
expect(routes[0].component).toBe(Comp)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('throws when module has no function export', () => {
|
|
43
|
-
expect(() =>
|
|
44
|
-
buildRoutes({ './pages/index/Index.js': { value: 42 } })
|
|
45
|
-
).toThrow('[metaowl]')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('builds multiple routes from multiple modules', () => {
|
|
49
|
-
const routes = buildRoutes({
|
|
50
|
-
'./pages/index/Index.js': { default: Comp },
|
|
51
|
-
'./pages/about/About.js': { default: About }
|
|
52
|
-
})
|
|
53
|
-
expect(routes).toHaveLength(2)
|
|
54
|
-
})
|
|
55
|
-
})
|
package/test/forms.test.js
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
-
import { useForm, validators, createSchema } from '../modules/forms.js'
|
|
3
|
-
|
|
4
|
-
describe('Forms', () => {
|
|
5
|
-
describe('useForm', () => {
|
|
6
|
-
it('creates form with initial values', () => {
|
|
7
|
-
const form = useForm({
|
|
8
|
-
name: { default: 'John' },
|
|
9
|
-
email: { default: '' }
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
expect(form.fields.name).toBe('John')
|
|
13
|
-
expect(form.fields.email).toBe('')
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('tracks dirty state', () => {
|
|
17
|
-
const form = useForm({
|
|
18
|
-
name: { default: '' }
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
expect(form.isDirty).toBe(false)
|
|
22
|
-
form.setValue('name', 'Jane')
|
|
23
|
-
expect(form.isDirty).toBe(true)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('tracks touched state', () => {
|
|
27
|
-
const form = useForm({
|
|
28
|
-
name: { default: '' }
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
expect(form.isTouched).toBe(false)
|
|
32
|
-
form.setTouched('name')
|
|
33
|
-
expect(form.isTouched).toBe(true)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('validates with sync validators', async () => {
|
|
37
|
-
const form = useForm({
|
|
38
|
-
name: {
|
|
39
|
-
default: '',
|
|
40
|
-
validation: (v) => v.length >= 3 || 'Min 3 chars'
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
// Invalid
|
|
45
|
-
form.setValue('name', 'Jo')
|
|
46
|
-
const isValid1 = await form.validateField('name')
|
|
47
|
-
expect(isValid1).toBe(false)
|
|
48
|
-
expect(form.errors.name).toBe('Min 3 chars')
|
|
49
|
-
|
|
50
|
-
// Valid
|
|
51
|
-
form.setValue('name', 'John')
|
|
52
|
-
const isValid2 = await form.validateField('name')
|
|
53
|
-
expect(isValid2).toBe(true)
|
|
54
|
-
expect(form.errors.name).toBe(null)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('validates all fields', async () => {
|
|
58
|
-
const form = useForm({
|
|
59
|
-
name: {
|
|
60
|
-
default: '',
|
|
61
|
-
validation: (v) => !!v || 'Required'
|
|
62
|
-
},
|
|
63
|
-
email: {
|
|
64
|
-
default: '',
|
|
65
|
-
validation: (v) => !!v || 'Required'
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
const isValid = await form.validate()
|
|
70
|
-
expect(isValid).toBe(false)
|
|
71
|
-
expect(form.errors.name).toBe('Required')
|
|
72
|
-
expect(form.errors.email).toBe('Required')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('handles submit', async () => {
|
|
76
|
-
const onSubmit = vi.fn()
|
|
77
|
-
const form = useForm({
|
|
78
|
-
name: { default: 'John' }
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
const handleSubmit = form.handleSubmit(onSubmit)
|
|
82
|
-
await handleSubmit()
|
|
83
|
-
|
|
84
|
-
expect(onSubmit).toHaveBeenCalledWith({ name: 'John' })
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('validates before submit', async () => {
|
|
88
|
-
const onSubmit = vi.fn()
|
|
89
|
-
const form = useForm({
|
|
90
|
-
name: {
|
|
91
|
-
default: '',
|
|
92
|
-
validation: (v) => !!v || 'Required'
|
|
93
|
-
}
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
const handleSubmit = form.handleSubmit(onSubmit)
|
|
97
|
-
await handleSubmit()
|
|
98
|
-
|
|
99
|
-
expect(onSubmit).not.toHaveBeenCalled()
|
|
100
|
-
expect(form.errors.name).toBe('Required')
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('resets form', () => {
|
|
104
|
-
const form = useForm({
|
|
105
|
-
name: { default: 'John' }
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
form.setValue('name', 'Jane')
|
|
109
|
-
form.setTouched('name')
|
|
110
|
-
form.reset()
|
|
111
|
-
|
|
112
|
-
expect(form.fields.name).toBe('John')
|
|
113
|
-
expect(form.isDirty).toBe(false)
|
|
114
|
-
expect(form.isTouched).toBe(false)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('supports register method', () => {
|
|
118
|
-
const form = useForm({
|
|
119
|
-
name: { default: '' }
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
const props = form.register('name')
|
|
123
|
-
|
|
124
|
-
expect(props.value).toBe('')
|
|
125
|
-
expect(typeof props.onChange).toBe('function')
|
|
126
|
-
expect(typeof props.onBlur).toBe('function')
|
|
127
|
-
})
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
describe('validators', () => {
|
|
131
|
-
describe('required', () => {
|
|
132
|
-
it('validates non-empty', () => {
|
|
133
|
-
const validator = validators.required()
|
|
134
|
-
expect(validator('')).toBe('Required')
|
|
135
|
-
expect(validator('test')).toBe(true)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('allows custom message', () => {
|
|
139
|
-
const validator = validators.required('Field required')
|
|
140
|
-
expect(validator('')).toBe('Field required')
|
|
141
|
-
})
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
describe('minLength', () => {
|
|
145
|
-
it('validates min length', () => {
|
|
146
|
-
const validator = validators.minLength(3)
|
|
147
|
-
expect(validator('ab')).toBe('Min 3 characters')
|
|
148
|
-
expect(validator('abc')).toBe(true)
|
|
149
|
-
})
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
describe('maxLength', () => {
|
|
153
|
-
it('validates max length', () => {
|
|
154
|
-
const validator = validators.maxLength(5)
|
|
155
|
-
expect(validator('abcdef')).toBe('Max 5 characters')
|
|
156
|
-
expect(validator('abc')).toBe(true)
|
|
157
|
-
})
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
describe('email', () => {
|
|
161
|
-
it('validates email format', () => {
|
|
162
|
-
const validator = validators.email()
|
|
163
|
-
expect(validator('invalid')).toBe('Invalid email')
|
|
164
|
-
expect(validator('test@example.com')).toBe(true)
|
|
165
|
-
})
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
describe('match', () => {
|
|
169
|
-
it('validates field match', () => {
|
|
170
|
-
const validator = validators.match('password', 'Passwords must match')
|
|
171
|
-
expect(validator('secret', { password: 'different' })).toBe('Passwords must match')
|
|
172
|
-
expect(validator('secret', { password: 'secret' })).toBe(true)
|
|
173
|
-
})
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
describe('createSchema', () => {
|
|
178
|
-
it('creates form config from schema', () => {
|
|
179
|
-
const schema = createSchema({
|
|
180
|
-
name: [validators.required(), validators.minLength(3)],
|
|
181
|
-
email: validators.email()
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
expect(schema.name).toBeDefined()
|
|
185
|
-
expect(schema.email).toBeDefined()
|
|
186
|
-
|
|
187
|
-
// Test validation
|
|
188
|
-
const nameValidator = schema.name.validation
|
|
189
|
-
expect(nameValidator('ab', {})).toBe('Min 3 characters')
|
|
190
|
-
expect(nameValidator('john', {})).toBe(true)
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('accepts single validator', () => {
|
|
194
|
-
const schema = createSchema({
|
|
195
|
-
email: validators.email()
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
const emailValidator = schema.email.validation
|
|
199
|
-
expect(emailValidator('test', {})).toBe('Invalid email')
|
|
200
|
-
expect(emailValidator('test@example.com', {})).toBe(true)
|
|
201
|
-
})
|
|
202
|
-
})
|
|
203
|
-
})
|
package/test/i18n.test.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
configureI18n,
|
|
4
|
-
t,
|
|
5
|
-
getLocale,
|
|
6
|
-
setLocale,
|
|
7
|
-
i18n,
|
|
8
|
-
loadLocaleMessages,
|
|
9
|
-
formatDate,
|
|
10
|
-
formatNumber,
|
|
11
|
-
formatCurrency,
|
|
12
|
-
formatRelativeTime,
|
|
13
|
-
createNamespacedT
|
|
14
|
-
} from '../modules/i18n.js'
|
|
15
|
-
|
|
16
|
-
describe('i18n', () => {
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
configureI18n({
|
|
19
|
-
locale: 'en',
|
|
20
|
-
fallbackLocale: 'en',
|
|
21
|
-
messages: {
|
|
22
|
-
en: {
|
|
23
|
-
hello: 'Hello',
|
|
24
|
-
greeting: 'Hello {{name}}',
|
|
25
|
-
count: {
|
|
26
|
-
zero: 'No items',
|
|
27
|
-
one: 'One item',
|
|
28
|
-
other: '{{n}} items'
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
de: {
|
|
32
|
-
hello: 'Hallo',
|
|
33
|
-
greeting: 'Hallo {{name}}'
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
})
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
describe('t()', () => {
|
|
40
|
-
it('translates simple key', () => {
|
|
41
|
-
expect(t('hello')).toBe('Hello')
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('interpolates values', () => {
|
|
45
|
-
expect(t('greeting', { name: 'World' })).toBe('Hello World')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('returns key if translation not found', () => {
|
|
49
|
-
expect(t('nonexistent.key')).toBe('nonexistent.key')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('uses fallback locale', () => {
|
|
53
|
-
configureI18n({ locale: 'fr', fallbackLocale: 'en' })
|
|
54
|
-
expect(t('hello')).toBe('Hello') // Falls back to English
|
|
55
|
-
})
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
describe('pluralization', () => {
|
|
59
|
-
it('handles zero', () => {
|
|
60
|
-
const result = t('count', { n: 0 })
|
|
61
|
-
expect(result).toBe('No items')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('handles one', () => {
|
|
65
|
-
const result = t('count', { n: 1 })
|
|
66
|
-
expect(result).toBe('One item')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('handles other', () => {
|
|
70
|
-
const result = t('count', { n: 5 })
|
|
71
|
-
expect(result).toBe('5 items')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('uses fallback when form not defined', () => {
|
|
75
|
-
configureI18n({
|
|
76
|
-
locale: 'de',
|
|
77
|
-
messages: {
|
|
78
|
-
de: {
|
|
79
|
-
items: {
|
|
80
|
-
one: 'Ein Artikel'
|
|
81
|
-
// missing 'other'
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
describe('locale switching', () => {
|
|
90
|
-
it('sets and gets locale', async () => {
|
|
91
|
-
await setLocale('de')
|
|
92
|
-
expect(getLocale()).toBe('de')
|
|
93
|
-
expect(document.documentElement.lang).toBe('de')
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('updates html lang attribute', async () => {
|
|
97
|
-
await setLocale('de')
|
|
98
|
-
expect(document.documentElement.lang).toBe('de')
|
|
99
|
-
})
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
describe('loadLocaleMessages', () => {
|
|
103
|
-
it('loads messages asynchronously', async () => {
|
|
104
|
-
const messages = { goodbye: 'Farewell' }
|
|
105
|
-
await loadLocaleMessages('en', messages)
|
|
106
|
-
|
|
107
|
-
expect(t('goodbye')).toBe('Farewell')
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('merges with existing messages', async () => {
|
|
111
|
-
await loadLocaleMessages('en', { newKey: 'New Value' })
|
|
112
|
-
|
|
113
|
-
expect(t('hello')).toBe('Hello') // Still exists
|
|
114
|
-
expect(t('newKey')).toBe('New Value') // Added
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
describe('formatDate', () => {
|
|
119
|
-
it('formats date according to locale', () => {
|
|
120
|
-
const date = new Date('2024-01-15')
|
|
121
|
-
const result = formatDate(date, { dateStyle: 'short' })
|
|
122
|
-
expect(result).toContain('1')
|
|
123
|
-
expect(result).toContain('15')
|
|
124
|
-
})
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
describe('formatNumber', () => {
|
|
128
|
-
it('formats number according to locale', () => {
|
|
129
|
-
expect(formatNumber(1234.5)).toContain('1')
|
|
130
|
-
expect(formatNumber(1234.5)).toContain('5')
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
describe('formatCurrency', () => {
|
|
135
|
-
it('formats currency', () => {
|
|
136
|
-
const result = formatCurrency(1234.5, 'USD')
|
|
137
|
-
expect(result).toContain('$')
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('formats EUR', () => {
|
|
141
|
-
const result = formatCurrency(1234.5, 'EUR')
|
|
142
|
-
expect(result).toContain('€')
|
|
143
|
-
})
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
describe('formatRelativeTime', () => {
|
|
147
|
-
it('formats relative time', () => {
|
|
148
|
-
const future = new Date(Date.now() + 2 * 60 * 60 * 1000)
|
|
149
|
-
const result = formatRelativeTime(future)
|
|
150
|
-
expect(result).toContain('2')
|
|
151
|
-
})
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
describe('createNamespacedT', () => {
|
|
155
|
-
it('creates namespaced translator', () => {
|
|
156
|
-
configureI18n({
|
|
157
|
-
messages: {
|
|
158
|
-
en: {
|
|
159
|
-
forms: {
|
|
160
|
-
name: { label: 'Name' },
|
|
161
|
-
email: { label: 'Email' }
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
const tf = createNamespacedT('forms')
|
|
168
|
-
expect(tf('name.label')).toBe('Name')
|
|
169
|
-
expect(tf('email.label')).toBe('Email')
|
|
170
|
-
})
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
describe('i18n object', () => {
|
|
174
|
-
it('exposes locale getter', () => {
|
|
175
|
-
expect(i18n.locale).toBe('en')
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
it('exposes t function', () => {
|
|
179
|
-
expect(i18n.t('hello')).toBe('Hello')
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('exposes format functions', () => {
|
|
183
|
-
expect(typeof i18n.formatDate).toBe('function')
|
|
184
|
-
expect(typeof i18n.formatNumber).toBe('function')
|
|
185
|
-
expect(typeof i18n.formatCurrency).toBe('function')
|
|
186
|
-
})
|
|
187
|
-
})
|
|
188
|
-
})
|