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
package/test/meta.test.js DELETED
@@ -1,146 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest'
2
- import { JSDOM } from 'jsdom'
3
- import * as Meta from '../modules/meta.js'
4
-
5
- let dom
6
-
7
- beforeEach(() => {
8
- dom = new JSDOM('<!doctype html><html><head></head><body></body></html>')
9
- globalThis.document = dom.window.document
10
- })
11
-
12
- describe('Meta.title', () => {
13
- it('sets document.title', () => {
14
- Meta.title('My Page')
15
- expect(document.title).toBe('My Page')
16
- })
17
-
18
- it('updates title on second call', () => {
19
- Meta.title('First')
20
- Meta.title('Second')
21
- expect(document.title).toBe('Second')
22
- })
23
-
24
- it('does nothing when called with empty string', () => {
25
- document.title = 'Original'
26
- Meta.title('')
27
- expect(document.title).toBe('Original')
28
- })
29
- })
30
-
31
- describe('Meta name-based tags (description, keywords, author)', () => {
32
- it('creates a meta[name="description"] tag', () => {
33
- Meta.description('A test page')
34
- const el = document.querySelector('meta[name="description"]')
35
- expect(el).not.toBeNull()
36
- expect(el.content).toBe('A test page')
37
- })
38
-
39
- it('updates description content on second call', () => {
40
- Meta.description('First')
41
- Meta.description('Second')
42
- const els = document.querySelectorAll('meta[name="description"]')
43
- expect(els).toHaveLength(1)
44
- expect(els[0].content).toBe('Second')
45
- })
46
-
47
- it('creates meta[name="keywords"]', () => {
48
- Meta.keywords('owl, odoo')
49
- expect(document.querySelector('meta[name="keywords"]').content).toBe('owl, odoo')
50
- })
51
-
52
- it('creates meta[name="author"]', () => {
53
- Meta.author('Dennis')
54
- expect(document.querySelector('meta[name="author"]').content).toBe('Dennis')
55
- })
56
-
57
- it('does nothing for falsy value', () => {
58
- Meta.description(null)
59
- expect(document.querySelector('meta[name="description"]')).toBeNull()
60
- })
61
- })
62
-
63
- describe('Meta.canonical', () => {
64
- it('creates a link[rel="canonical"]', () => {
65
- Meta.canonical('https://example.com/page')
66
- const el = document.querySelector('link[rel="canonical"]')
67
- expect(el).not.toBeNull()
68
- expect(el.href).toBe('https://example.com/page')
69
- })
70
-
71
- it('updates href on second call without duplicating', () => {
72
- Meta.canonical('https://example.com/a')
73
- Meta.canonical('https://example.com/b')
74
- const els = document.querySelectorAll('link[rel="canonical"]')
75
- expect(els).toHaveLength(1)
76
- expect(els[0].href).toBe('https://example.com/b')
77
- })
78
- })
79
-
80
- describe('Meta Open Graph tags', () => {
81
- it('creates og:title', () => {
82
- Meta.ogTitle('OG Title')
83
- expect(document.querySelector('meta[property="og:title"]').content).toBe('OG Title')
84
- })
85
-
86
- it('updates og:title on second call without duplicating', () => {
87
- Meta.ogTitle('First')
88
- Meta.ogTitle('Second')
89
- expect(document.querySelectorAll('meta[property="og:title"]')).toHaveLength(1)
90
- expect(document.querySelector('meta[property="og:title"]').content).toBe('Second')
91
- })
92
-
93
- it('creates og:description', () => {
94
- Meta.ogDescription('Desc')
95
- expect(document.querySelector('meta[property="og:description"]').content).toBe('Desc')
96
- })
97
-
98
- it('creates og:image', () => {
99
- Meta.ogImage('https://example.com/img.png')
100
- expect(document.querySelector('meta[property="og:image"]').content).toBe('https://example.com/img.png')
101
- })
102
-
103
- it('creates og:url', () => {
104
- Meta.ogUrl('https://example.com')
105
- expect(document.querySelector('meta[property="og:url"]').content).toBe('https://example.com')
106
- })
107
-
108
- it('creates og:type', () => {
109
- Meta.ogType('website')
110
- expect(document.querySelector('meta[property="og:type"]').content).toBe('website')
111
- })
112
-
113
- it('creates og:site_name', () => {
114
- Meta.ogSiteName('My Site')
115
- expect(document.querySelector('meta[property="og:site_name"]').content).toBe('My Site')
116
- })
117
- })
118
-
119
- describe('Meta Twitter Card tags', () => {
120
- it('creates twitter:card', () => {
121
- Meta.twitterCard('summary_large_image')
122
- expect(document.querySelector('meta[name="twitter:card"]').content).toBe('summary_large_image')
123
- })
124
-
125
- it('creates twitter:title', () => {
126
- Meta.twitterTitle('TW Title')
127
- expect(document.querySelector('meta[name="twitter:title"]').content).toBe('TW Title')
128
- })
129
-
130
- it('creates twitter:description', () => {
131
- Meta.twitterDescription('TW Desc')
132
- expect(document.querySelector('meta[name="twitter:description"]').content).toBe('TW Desc')
133
- })
134
-
135
- it('creates twitter:image', () => {
136
- Meta.twitterImage('https://example.com/tw.png')
137
- expect(document.querySelector('meta[name="twitter:image"]').content).toBe('https://example.com/tw.png')
138
- })
139
-
140
- it('updates twitter:card on second call without duplicating', () => {
141
- Meta.twitterCard('summary')
142
- Meta.twitterCard('summary_large_image')
143
- expect(document.querySelectorAll('meta[name="twitter:card"]')).toHaveLength(1)
144
- expect(document.querySelector('meta[name="twitter:card"]').content).toBe('summary_large_image')
145
- })
146
- })
@@ -1,547 +0,0 @@
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
- })