Skip to content

Commit 7690cfa

Browse files
committed
fix: refactor useCall with TDD
- setup vitest for unit testing - setup msw for mocking fetch calls
1 parent 8b35ff4 commit 7690cfa

File tree

13 files changed

+1128
-241
lines changed

13 files changed

+1128
-241
lines changed

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "A set of components and utilities for rapid UI development",
55
"main": "./src/index.js",
66
"scripts": {
7-
"test": "npx prettier --check ./src",
7+
"test": "vitest",
88
"prettier": "npx prettier -w ./src",
99
"bump-and-release": "git pull --rebase origin main && yarn version --patch && git push && git push --tags",
1010
"dev": "vite",
@@ -66,13 +66,14 @@
6666
"@histoire/plugin-vue": "^0.17.14",
6767
"@vitejs/plugin-vue": "^4.0.0",
6868
"autoprefixer": "^10.4.13",
69-
"cross-fetch": "^3.1.5",
7069
"histoire": "^0.17.14",
7170
"lint-staged": ">=10",
71+
"msw": "^2.7.0",
7272
"postcss": "^8.4.21",
7373
"prettier-plugin-tailwindcss": "^0.1.13",
7474
"tailwindcss": "^3.2.7",
75-
"vite": "^4.1.0",
75+
"vite": "^6.0.3",
76+
"vitest": "^2.1.8",
7677
"vue": "^3.3.0",
7778
"vue-router": "^4.1.6"
7879
},

src/data-fetching/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { useCall } from './useCall'
2+
export { useList } from './useList'
3+
export { useDoc } from './useDoc'
4+
export { useFrappeFetch } from './useFrappeFetch'

src/data-fetching/useCall.test.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
5+
import { nextTick, ref } from 'vue'
6+
import { useCall } from './index'
7+
import { url } from '../mocks/utils'
8+
9+
describe('msw works', () => {
10+
it('ping responds with pong', async () => {
11+
const response = await fetch(url('/api/v2/method/ping'))
12+
13+
expect(response.status).toBe(200)
14+
expect(response.statusText).toBe('OK')
15+
expect(await response.json()).toEqual({
16+
data: 'pong',
17+
})
18+
})
19+
})
20+
21+
describe('useCall', () => {
22+
it('it returns expected reactive object', async () => {
23+
type PingResponse = string
24+
let ping = useCall<PingResponse>({
25+
url: url('/api/v2/method/ping'),
26+
immediate: false,
27+
})
28+
29+
// Verify initial state
30+
expect(ping.loading).toBe(false)
31+
expect(ping.data).toBe(null)
32+
expect(ping.error).toBe(null)
33+
expect(typeof ping.execute).toBe('function')
34+
expect(typeof ping.submit).toBe('function')
35+
expect(typeof ping.reset).toBe('function')
36+
37+
// Execute the call
38+
ping.execute()
39+
expect(ping.loading).toBe(true)
40+
41+
// Wait for response
42+
await ping.promise
43+
44+
// TODO: 3 nextTicks are required to ensure loading is updated
45+
// find a better way to handle this
46+
await nextTick()
47+
await nextTick()
48+
await nextTick()
49+
50+
// Verify final state
51+
expect(ping.data).toBe('pong')
52+
expect(ping.error).toBe(null)
53+
expect(ping.isFinished).toBe(true)
54+
expect(ping.loading).toBe(false)
55+
})
56+
57+
it('handles error responses', async () => {
58+
const onError = vi.fn()
59+
const errorCall = useCall({
60+
url: url('/api/v2/method/error'),
61+
onError,
62+
immediate: false,
63+
})
64+
65+
errorCall.fetch()
66+
await errorCall.promise.catch(() => {})
67+
68+
await nextTick()
69+
await nextTick()
70+
await nextTick()
71+
72+
expect(errorCall.loading).toBe(false)
73+
expect(errorCall.error).toBeInstanceOf(Error)
74+
expect(errorCall.error.message).toEqual(
75+
'ServerError: Internal Server Error occurred',
76+
)
77+
expect(errorCall.data).toBe(null)
78+
expect(onError).toHaveBeenCalledWith(errorCall.error)
79+
})
80+
81+
it('handles POST requests with params', async () => {
82+
type Response = { success: boolean; received: any }
83+
const postCall = useCall<Response>({
84+
url: url('/api/v2/method/post'),
85+
method: 'POST',
86+
params: { name: 'test' },
87+
immediate: false,
88+
})
89+
90+
postCall.fetch()
91+
await postCall.promise
92+
93+
expect(postCall.data).toEqual({
94+
success: true,
95+
received: { name: 'test' },
96+
})
97+
})
98+
99+
it('supports dynamic params with reactive values', async () => {
100+
const dynamicValue = ref('test')
101+
const call = useCall<{ value: string }>({
102+
url: url('/api/v2/method/get'),
103+
params: () => ({ value: dynamicValue.value }),
104+
immediate: false,
105+
})
106+
107+
call.fetch()
108+
await call.promise
109+
110+
expect(call.url).toContain('value=test')
111+
expect(call.data).toEqual({ value: 'test' })
112+
})
113+
114+
it('transforms response data correctly', async () => {
115+
type Response = { numbers: number[] }
116+
const call = useCall<Response>({
117+
url: url('/api/v2/method/numbers'),
118+
transform: (data) => ({ numbers: data.numbers.map((n) => n * 2) }),
119+
refetch: true,
120+
})
121+
await call.promise
122+
expect(call.data).toEqual({ numbers: [2, 4, 6, 8] })
123+
})
124+
125+
it('supports submit with different params', async () => {
126+
type Response = { success: boolean; received: any }
127+
const call = useCall<Response>({
128+
url: url('/api/v2/method/post'),
129+
method: 'POST',
130+
refetch: true,
131+
immediate: false,
132+
})
133+
134+
call.submit({ value: 'first submit' })
135+
await call.promise
136+
expect(call.data).toEqual({
137+
success: true,
138+
received: { value: 'first submit' },
139+
})
140+
141+
// submit with another set of params
142+
call.submit({ value: 'second submit' })
143+
await call.promise
144+
expect(call.data).toEqual({
145+
success: true,
146+
received: { value: 'second submit' },
147+
})
148+
})
149+
150+
it('handles abort correctly', async () => {
151+
const call = useCall({
152+
url: url('/api/v2/method/slow'),
153+
immediate: false,
154+
})
155+
156+
call.fetch()
157+
expect(call.loading).toBe(true)
158+
159+
call.abort()
160+
expect(call.aborted).toBe(true)
161+
})
162+
})

0 commit comments

Comments
 (0)