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/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
|
-
})
|
package/test/odoo-rpc.test.js
DELETED
|
@@ -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
|
-
})
|