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,547 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
OdooService,
|
|
4
|
+
configure,
|
|
5
|
+
isConfigured,
|
|
6
|
+
isAuthenticated,
|
|
7
|
+
getSession,
|
|
8
|
+
getConfig,
|
|
9
|
+
onAuthChange,
|
|
10
|
+
authenticate,
|
|
11
|
+
logout,
|
|
12
|
+
searchRead,
|
|
13
|
+
call,
|
|
14
|
+
read,
|
|
15
|
+
create,
|
|
16
|
+
write,
|
|
17
|
+
unlink,
|
|
18
|
+
searchCount,
|
|
19
|
+
listDatabases,
|
|
20
|
+
versionInfo
|
|
21
|
+
} from '../modules/odoo-rpc.js'
|
|
22
|
+
|
|
23
|
+
describe('Odoo RPC Service', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
// Reset state
|
|
26
|
+
logout()
|
|
27
|
+
localStorage.clear()
|
|
28
|
+
vi.restoreAllMocks()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('Configuration', () => {
|
|
32
|
+
it('should configure with valid config', () => {
|
|
33
|
+
configure({
|
|
34
|
+
baseUrl: 'https://test.odoo.com',
|
|
35
|
+
database: 'test_db',
|
|
36
|
+
username: 'admin',
|
|
37
|
+
password: 'admin'
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
expect(isConfigured()).toBe(true)
|
|
41
|
+
expect(getConfig()).toMatchObject({
|
|
42
|
+
baseUrl: 'https://test.odoo.com',
|
|
43
|
+
database: 'test_db',
|
|
44
|
+
username: 'admin',
|
|
45
|
+
password: 'admin',
|
|
46
|
+
persistSession: true
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should not be configured without baseUrl', () => {
|
|
51
|
+
configure({
|
|
52
|
+
database: 'test_db'
|
|
53
|
+
})
|
|
54
|
+
expect(isConfigured()).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should not be configured without database', () => {
|
|
58
|
+
configure({
|
|
59
|
+
baseUrl: 'https://test.odoo.com'
|
|
60
|
+
})
|
|
61
|
+
expect(isConfigured()).toBe(false)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should allow apiKey instead of password', () => {
|
|
65
|
+
configure({
|
|
66
|
+
baseUrl: 'https://test.odoo.com',
|
|
67
|
+
database: 'test_db',
|
|
68
|
+
username: 'admin',
|
|
69
|
+
apiKey: 'test-api-key-123'
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(getConfig().apiKey).toBe('test-api-key-123')
|
|
73
|
+
expect(getConfig().password).toBeUndefined()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should support disabling session persistence', () => {
|
|
77
|
+
configure({
|
|
78
|
+
baseUrl: 'https://test.odoo.com',
|
|
79
|
+
database: 'test_db',
|
|
80
|
+
persistSession: false
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(getConfig().persistSession).toBe(false)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('Authentication', () => {
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
configure({
|
|
90
|
+
baseUrl: 'https://test.odoo.com',
|
|
91
|
+
database: 'test_db',
|
|
92
|
+
username: 'admin',
|
|
93
|
+
password: 'admin'
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should authenticate successfully', async () => {
|
|
98
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
99
|
+
ok: true,
|
|
100
|
+
headers: new Headers(),
|
|
101
|
+
json: async () => ({
|
|
102
|
+
jsonrpc: '2.0',
|
|
103
|
+
id: 123,
|
|
104
|
+
result: 1 // User ID
|
|
105
|
+
})
|
|
106
|
+
}).mockResolvedValueOnce({ // For user info search_read
|
|
107
|
+
ok: true,
|
|
108
|
+
headers: new Headers(),
|
|
109
|
+
json: async () => ({
|
|
110
|
+
jsonrpc: '2.0',
|
|
111
|
+
id: 123,
|
|
112
|
+
result: [{
|
|
113
|
+
id: 1,
|
|
114
|
+
name: 'Administrator',
|
|
115
|
+
partner_id: [3, 'Administrator'],
|
|
116
|
+
lang: 'en_US',
|
|
117
|
+
tz: 'Europe/Brussels'
|
|
118
|
+
}]
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const session = await authenticate()
|
|
123
|
+
|
|
124
|
+
expect(session.uid).toBe(1)
|
|
125
|
+
expect(session.username).toBe('admin')
|
|
126
|
+
expect(session.name).toBe('Administrator')
|
|
127
|
+
expect(isAuthenticated()).toBe(true)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should throw error without credentials', async () => {
|
|
131
|
+
configure({
|
|
132
|
+
baseUrl: 'https://test.odoo.com',
|
|
133
|
+
database: 'test_db'
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
await expect(authenticate()).rejects.toThrow('requires username and password')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should allow custom credentials in authenticate()', async () => {
|
|
140
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
141
|
+
ok: true,
|
|
142
|
+
headers: new Headers(),
|
|
143
|
+
json: async () => ({
|
|
144
|
+
jsonrpc: '2.0',
|
|
145
|
+
id: 123,
|
|
146
|
+
result: 2
|
|
147
|
+
})
|
|
148
|
+
}).mockResolvedValueOnce({
|
|
149
|
+
ok: true,
|
|
150
|
+
headers: new Headers(),
|
|
151
|
+
json: async () => ({
|
|
152
|
+
jsonrpc: '2.0',
|
|
153
|
+
id: 123,
|
|
154
|
+
result: []
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const session = await authenticate('custom_user', 'custom_pass')
|
|
159
|
+
expect(session.username).toBe('custom_user')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should handle authentication failure', async () => {
|
|
163
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
164
|
+
ok: true,
|
|
165
|
+
headers: new Headers(),
|
|
166
|
+
json: async () => ({
|
|
167
|
+
jsonrpc: '2.0',
|
|
168
|
+
id: 123,
|
|
169
|
+
result: false // Authentication failed
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
await expect(authenticate()).rejects.toThrow('invalid credentials')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should logout and clear session', async () => {
|
|
177
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
178
|
+
ok: true,
|
|
179
|
+
headers: new Headers(),
|
|
180
|
+
json: async () => ({
|
|
181
|
+
jsonrpc: '2.0',
|
|
182
|
+
id: 123,
|
|
183
|
+
result: 1
|
|
184
|
+
})
|
|
185
|
+
}).mockResolvedValueOnce({
|
|
186
|
+
ok: true,
|
|
187
|
+
headers: new Headers(),
|
|
188
|
+
json: async () => ({
|
|
189
|
+
jsonrpc: '2.0',
|
|
190
|
+
id: 123,
|
|
191
|
+
result: []
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
await authenticate()
|
|
196
|
+
expect(isAuthenticated()).toBe(true)
|
|
197
|
+
|
|
198
|
+
logout()
|
|
199
|
+
expect(isAuthenticated()).toBe(false)
|
|
200
|
+
expect(getSession()).toBeNull()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should notify auth listeners on change', async () => {
|
|
204
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
205
|
+
ok: true,
|
|
206
|
+
headers: new Headers(),
|
|
207
|
+
json: async () => ({
|
|
208
|
+
jsonrpc: '2.0',
|
|
209
|
+
id: 123,
|
|
210
|
+
result: 1
|
|
211
|
+
})
|
|
212
|
+
}).mockResolvedValueOnce({
|
|
213
|
+
ok: true,
|
|
214
|
+
headers: new Headers(),
|
|
215
|
+
json: async () => ({
|
|
216
|
+
jsonrpc: '2.0',
|
|
217
|
+
id: 123,
|
|
218
|
+
result: []
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const listener = vi.fn()
|
|
223
|
+
const unsubscribe = onAuthChange(listener)
|
|
224
|
+
|
|
225
|
+
await authenticate()
|
|
226
|
+
expect(listener).toHaveBeenCalledTimes(1)
|
|
227
|
+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ uid: 1 }))
|
|
228
|
+
|
|
229
|
+
unsubscribe()
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('RPC Operations', () => {
|
|
234
|
+
beforeEach(() => {
|
|
235
|
+
configure({
|
|
236
|
+
baseUrl: 'https://test.odoo.com',
|
|
237
|
+
database: 'test_db',
|
|
238
|
+
username: 'admin',
|
|
239
|
+
password: 'admin'
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
// Mock successful authentication
|
|
243
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
244
|
+
ok: true,
|
|
245
|
+
headers: new Headers(),
|
|
246
|
+
json: async () => ({
|
|
247
|
+
jsonrpc: '2.0',
|
|
248
|
+
id: 123,
|
|
249
|
+
result: 1
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('should throw if not authenticated', async () => {
|
|
255
|
+
await expect(searchRead('res.partner')).rejects.toThrow('Not authenticated')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should perform search_read', async () => {
|
|
259
|
+
const mockResult = [
|
|
260
|
+
{ id: 1, name: 'Partner 1' },
|
|
261
|
+
{ id: 2, name: 'Partner 2' }
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
265
|
+
ok: true,
|
|
266
|
+
headers: new Headers(),
|
|
267
|
+
json: async () => ({
|
|
268
|
+
jsonrpc: '2.0',
|
|
269
|
+
id: 123,
|
|
270
|
+
result: 1
|
|
271
|
+
})
|
|
272
|
+
}).mockResolvedValueOnce({
|
|
273
|
+
ok: true,
|
|
274
|
+
headers: new Headers(),
|
|
275
|
+
json: async () => ({
|
|
276
|
+
jsonrpc: '2.0',
|
|
277
|
+
id: 123,
|
|
278
|
+
result: []
|
|
279
|
+
})
|
|
280
|
+
}).mockResolvedValueOnce({
|
|
281
|
+
ok: true,
|
|
282
|
+
headers: new Headers(),
|
|
283
|
+
json: async () => ({
|
|
284
|
+
jsonrpc: '2.0',
|
|
285
|
+
id: 123,
|
|
286
|
+
result: mockResult
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
await authenticate()
|
|
291
|
+
const partners = await searchRead('res.partner', {
|
|
292
|
+
domain: [['is_company', '=', true]],
|
|
293
|
+
fields: ['name', 'email'],
|
|
294
|
+
limit: 10
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
expect(partners).toEqual(mockResult)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('should call model methods', async () => {
|
|
301
|
+
global.fetch = vi.fn()
|
|
302
|
+
.mockResolvedValueOnce({
|
|
303
|
+
ok: true,
|
|
304
|
+
headers: new Headers(),
|
|
305
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: 1 })
|
|
306
|
+
})
|
|
307
|
+
.mockResolvedValueOnce({
|
|
308
|
+
ok: true,
|
|
309
|
+
headers: new Headers(),
|
|
310
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: [] })
|
|
311
|
+
})
|
|
312
|
+
.mockResolvedValueOnce({
|
|
313
|
+
ok: true,
|
|
314
|
+
headers: new Headers(),
|
|
315
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: 42 })
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
await authenticate()
|
|
319
|
+
const result = await call('res.partner', 'custom_method', [['arg1']])
|
|
320
|
+
|
|
321
|
+
expect(result).toBe(42)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('should read specific records', async () => {
|
|
325
|
+
global.fetch = vi.fn()
|
|
326
|
+
.mockResolvedValueOnce({
|
|
327
|
+
ok: true,
|
|
328
|
+
headers: new Headers(),
|
|
329
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: 1 })
|
|
330
|
+
})
|
|
331
|
+
.mockResolvedValueOnce({
|
|
332
|
+
ok: true,
|
|
333
|
+
headers: new Headers(),
|
|
334
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: [] })
|
|
335
|
+
})
|
|
336
|
+
.mockResolvedValueOnce({
|
|
337
|
+
ok: true,
|
|
338
|
+
headers: new Headers(),
|
|
339
|
+
json: async () => ({
|
|
340
|
+
jsonrpc: '2.0',
|
|
341
|
+
id: 123,
|
|
342
|
+
result: [{ id: 1, name: 'Partner 1' }]
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
await authenticate()
|
|
347
|
+
const records = await read('res.partner', [1], ['name', 'email'])
|
|
348
|
+
|
|
349
|
+
expect(records).toEqual([{ id: 1, name: 'Partner 1' }])
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('should create records', async () => {
|
|
353
|
+
global.fetch = vi.fn()
|
|
354
|
+
.mockResolvedValueOnce({
|
|
355
|
+
ok: true,
|
|
356
|
+
headers: new Headers(),
|
|
357
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: 1 })
|
|
358
|
+
})
|
|
359
|
+
.mockResolvedValueOnce({
|
|
360
|
+
ok: true,
|
|
361
|
+
headers: new Headers(),
|
|
362
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: [] })
|
|
363
|
+
})
|
|
364
|
+
.mockResolvedValueOnce({
|
|
365
|
+
ok: true,
|
|
366
|
+
headers: new Headers(),
|
|
367
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: 123 })
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
await authenticate()
|
|
371
|
+
const newId = await create('res.partner', { name: 'New Partner' })
|
|
372
|
+
|
|
373
|
+
expect(newId).toBe(123)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('should update records', async () => {
|
|
377
|
+
global.fetch = vi.fn()
|
|
378
|
+
.mockResolvedValueOnce({
|
|
379
|
+
ok: true,
|
|
380
|
+
headers: new Headers(),
|
|
381
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: 1 })
|
|
382
|
+
})
|
|
383
|
+
.mockResolvedValueOnce({
|
|
384
|
+
ok: true,
|
|
385
|
+
headers: new Headers(),
|
|
386
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: [] })
|
|
387
|
+
})
|
|
388
|
+
.mockResolvedValueOnce({
|
|
389
|
+
ok: true,
|
|
390
|
+
headers: new Headers(),
|
|
391
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: true })
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
await authenticate()
|
|
395
|
+
const result = await write('res.partner', [1], { name: 'Updated Partner' })
|
|
396
|
+
|
|
397
|
+
expect(result).toBe(true)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('should delete records', async () => {
|
|
401
|
+
global.fetch = vi.fn()
|
|
402
|
+
.mockResolvedValueOnce({
|
|
403
|
+
ok: true,
|
|
404
|
+
headers: new Headers(),
|
|
405
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: 1 })
|
|
406
|
+
})
|
|
407
|
+
.mockResolvedValueOnce({
|
|
408
|
+
ok: true,
|
|
409
|
+
headers: new Headers(),
|
|
410
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: [] })
|
|
411
|
+
})
|
|
412
|
+
.mockResolvedValueOnce({
|
|
413
|
+
ok: true,
|
|
414
|
+
headers: new Headers(),
|
|
415
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: true })
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
await authenticate()
|
|
419
|
+
const result = await unlink('res.partner', [1])
|
|
420
|
+
|
|
421
|
+
expect(result).toBe(true)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('should get search count', async () => {
|
|
425
|
+
global.fetch = vi.fn()
|
|
426
|
+
.mockResolvedValueOnce({
|
|
427
|
+
ok: true,
|
|
428
|
+
headers: new Headers(),
|
|
429
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: 1 })
|
|
430
|
+
})
|
|
431
|
+
.mockResolvedValueOnce({
|
|
432
|
+
ok: true,
|
|
433
|
+
headers: new Headers(),
|
|
434
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: [] })
|
|
435
|
+
})
|
|
436
|
+
.mockResolvedValueOnce({
|
|
437
|
+
ok: true,
|
|
438
|
+
headers: new Headers(),
|
|
439
|
+
json: async () => ({ jsonrpc: '2.0', id: 123, result: 42 })
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
await authenticate()
|
|
443
|
+
const count = await searchCount('res.partner', [['is_company', '=', true]])
|
|
444
|
+
|
|
445
|
+
expect(count).toBe(42)
|
|
446
|
+
})
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
describe('Database Operations', () => {
|
|
450
|
+
beforeEach(() => {
|
|
451
|
+
configure({
|
|
452
|
+
baseUrl: 'https://test.odoo.com',
|
|
453
|
+
database: 'test_db'
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('should list databases', async () => {
|
|
458
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
459
|
+
ok: true,
|
|
460
|
+
headers: new Headers(),
|
|
461
|
+
json: async () => ({
|
|
462
|
+
jsonrpc: '2.0',
|
|
463
|
+
id: 123,
|
|
464
|
+
result: ['db1', 'db2', 'db3']
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
const dbs = await listDatabases()
|
|
469
|
+
expect(dbs).toEqual(['db1', 'db2', 'db3'])
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it('should get version info', async () => {
|
|
473
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
474
|
+
ok: true,
|
|
475
|
+
headers: new Headers(),
|
|
476
|
+
json: async () => ({
|
|
477
|
+
result: {
|
|
478
|
+
server_version: '16.0',
|
|
479
|
+
server_version_info: [16, 0, 0, 'final', 0],
|
|
480
|
+
protocol_version: 1
|
|
481
|
+
}
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
const version = await versionInfo()
|
|
486
|
+
expect(version.server_version).toBe('16.0')
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
describe('Error Handling', () => {
|
|
491
|
+
beforeEach(() => {
|
|
492
|
+
configure({
|
|
493
|
+
baseUrl: 'https://test.odoo.com',
|
|
494
|
+
database: 'test_db',
|
|
495
|
+
username: 'admin',
|
|
496
|
+
password: 'admin'
|
|
497
|
+
})
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it('should handle RPC errors', async () => {
|
|
501
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
502
|
+
ok: true,
|
|
503
|
+
headers: new Headers(),
|
|
504
|
+
json: async () => ({
|
|
505
|
+
jsonrpc: '2.0',
|
|
506
|
+
id: 123,
|
|
507
|
+
error: {
|
|
508
|
+
message: 'Access Denied',
|
|
509
|
+
data: { message: 'You do not have permission' }
|
|
510
|
+
}
|
|
511
|
+
})
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
await expect(authenticate()).rejects.toThrow('Access Denied')
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
it('should handle HTTP errors', async () => {
|
|
518
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
519
|
+
ok: false,
|
|
520
|
+
status: 500,
|
|
521
|
+
statusText: 'Internal Server Error',
|
|
522
|
+
headers: new Headers(),
|
|
523
|
+
json: async () => ({})
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
await expect(authenticate()).rejects.toThrow('HTTP 500')
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
it('should throw if not configured', async () => {
|
|
530
|
+
// Reset configuration by configuring with empty values
|
|
531
|
+
configure({ baseUrl: '', database: '' })
|
|
532
|
+
|
|
533
|
+
await expect(listDatabases()).rejects.toThrow('not configured')
|
|
534
|
+
})
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
describe('OdooService Namespace', () => {
|
|
538
|
+
it('should expose all methods via namespace', () => {
|
|
539
|
+
expect(OdooService.configure).toBe(configure)
|
|
540
|
+
expect(OdooService.authenticate).toBe(authenticate)
|
|
541
|
+
expect(OdooService.logout).toBe(logout)
|
|
542
|
+
expect(OdooService.searchRead).toBe(searchRead)
|
|
543
|
+
expect(OdooService.call).toBe(call)
|
|
544
|
+
expect(OdooService.isAuthenticated).toBe(isAuthenticated)
|
|
545
|
+
})
|
|
546
|
+
})
|
|
547
|
+
})
|
package/test/pwa.test.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
generateManifest,
|
|
4
|
+
isOnline,
|
|
5
|
+
subscribeToConnectivity,
|
|
6
|
+
sync,
|
|
7
|
+
showNotification,
|
|
8
|
+
cache,
|
|
9
|
+
checkCapabilities,
|
|
10
|
+
PWA
|
|
11
|
+
} from '../modules/pwa.js'
|
|
12
|
+
|
|
13
|
+
describe('PWA', () => {
|
|
14
|
+
describe('Exports', () => {
|
|
15
|
+
it('should export all functions', () => {
|
|
16
|
+
expect(typeof generateManifest).toBe('function')
|
|
17
|
+
expect(typeof isOnline).toBe('function')
|
|
18
|
+
expect(typeof subscribeToConnectivity).toBe('function')
|
|
19
|
+
expect(typeof sync).toBe('function')
|
|
20
|
+
expect(typeof showNotification).toBe('function')
|
|
21
|
+
expect(typeof cache).toBe('object')
|
|
22
|
+
expect(typeof checkCapabilities).toBe('function')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should export PWA namespace', () => {
|
|
26
|
+
expect(PWA.generateManifest).toBe(generateManifest)
|
|
27
|
+
expect(PWA.isOnline).toBe(isOnline)
|
|
28
|
+
expect(PWA.cache).toBe(cache)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('generateManifest', () => {
|
|
33
|
+
it('should generate basic manifest', () => {
|
|
34
|
+
const manifest = generateManifest({
|
|
35
|
+
name: 'My App',
|
|
36
|
+
shortName: 'MyApp'
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
expect(manifest.name).toBe('My App')
|
|
40
|
+
expect(manifest.short_name).toBe('MyApp')
|
|
41
|
+
expect(manifest.display).toBe('standalone')
|
|
42
|
+
expect(manifest.theme_color).toBe('#000000')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should include optional fields', () => {
|
|
46
|
+
const manifest = generateManifest({
|
|
47
|
+
name: 'Full App',
|
|
48
|
+
shortName: 'Full',
|
|
49
|
+
description: 'A complete app',
|
|
50
|
+
startUrl: '/home',
|
|
51
|
+
display: 'fullscreen',
|
|
52
|
+
themeColor: '#007bff',
|
|
53
|
+
backgroundColor: '#ffffff',
|
|
54
|
+
scope: '/app',
|
|
55
|
+
icons: [
|
|
56
|
+
{ src: '/icon.png', sizes: '192x192', type: 'image/png' }
|
|
57
|
+
]
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
expect(manifest.description).toBe('A complete app')
|
|
61
|
+
expect(manifest.start_url).toBe('/home')
|
|
62
|
+
expect(manifest.display).toBe('fullscreen')
|
|
63
|
+
expect(manifest.theme_color).toBe('#007bff')
|
|
64
|
+
expect(manifest.background_color).toBe('#ffffff')
|
|
65
|
+
expect(manifest.scope).toBe('/app')
|
|
66
|
+
expect(manifest.icons).toHaveLength(1)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should use default values', () => {
|
|
70
|
+
const manifest = generateManifest({
|
|
71
|
+
name: 'Minimal'
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
expect(manifest.display).toBe('standalone')
|
|
75
|
+
expect(manifest.theme_color).toBe('#000000')
|
|
76
|
+
expect(manifest.start_url).toBe('./')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should include orientation', () => {
|
|
80
|
+
const manifest = generateManifest({ name: 'Test' })
|
|
81
|
+
expect(manifest.orientation).toBe('any')
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('isOnline', () => {
|
|
86
|
+
it('should return navigator.onLine', () => {
|
|
87
|
+
const result = isOnline()
|
|
88
|
+
expect(typeof result).toBe('boolean')
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('subscribeToConnectivity', () => {
|
|
93
|
+
it('should return unsubscribe function', () => {
|
|
94
|
+
const unsubscribe = subscribeToConnectivity({})
|
|
95
|
+
expect(typeof unsubscribe).toBe('function')
|
|
96
|
+
|
|
97
|
+
// Cleanup
|
|
98
|
+
unsubscribe()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should not throw without callbacks', () => {
|
|
102
|
+
const unsubscribe = subscribeToConnectivity({})
|
|
103
|
+
expect(typeof unsubscribe).toBe('function')
|
|
104
|
+
unsubscribe()
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('sync', () => {
|
|
109
|
+
it('should return false without service worker', async () => {
|
|
110
|
+
const result = await sync('test-tag')
|
|
111
|
+
expect(result).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('showNotification', () => {
|
|
116
|
+
it('should not throw', async () => {
|
|
117
|
+
await showNotification('Test', { body: 'Body' })
|
|
118
|
+
// Should complete without error
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('cache', () => {
|
|
123
|
+
it('should have cache methods', () => {
|
|
124
|
+
expect(typeof cache.add).toBe('function')
|
|
125
|
+
expect(typeof cache.remove).toBe('function')
|
|
126
|
+
expect(typeof cache.clear).toBe('function')
|
|
127
|
+
expect(typeof cache.info).toBe('function')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should return empty array for info without caches', async () => {
|
|
131
|
+
const info = await cache.info()
|
|
132
|
+
expect(Array.isArray(info)).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('checkCapabilities', () => {
|
|
137
|
+
it('should return capability object', () => {
|
|
138
|
+
const caps = checkCapabilities()
|
|
139
|
+
|
|
140
|
+
expect(typeof caps.serviceWorker).toBe('boolean')
|
|
141
|
+
expect(typeof caps.push).toBe('boolean')
|
|
142
|
+
expect(typeof caps.notifications).toBe('boolean')
|
|
143
|
+
expect(typeof caps.backgroundSync).toBe('boolean')
|
|
144
|
+
expect(typeof caps.persistentStorage).toBe('boolean')
|
|
145
|
+
expect(typeof caps.addToHomeScreen).toBe('boolean')
|
|
146
|
+
expect(typeof caps.offline).toBe('boolean')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should detect service worker support', () => {
|
|
150
|
+
const caps = checkCapabilities()
|
|
151
|
+
expect(caps.serviceWorker).toBeDefined()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|