Skip to content

Commit a4f0c91

Browse files
committed
fix(runtime): allow registerEndpoint to work with native fetch
1 parent f37ebef commit a4f0c91

File tree

3 files changed

+134
-12
lines changed

3 files changed

+134
-12
lines changed

examples/app-vitest-full/tests/nuxt/server.spec.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
33
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
44

55
import { listen } from 'listhen'
6-
import { createApp, eventHandler, toNodeListener } from 'h3'
6+
import { createApp, eventHandler, toNodeListener, readBody, getHeaders } from 'h3'
77

88
import FetchComponent from '~/components/FetchComponent.vue'
99

@@ -71,4 +71,98 @@ describe('server mocks and data fetching', () => {
7171
})
7272
expect(await $fetch<unknown>('/method')).toMatchObject({ method: 'GET' })
7373
})
74+
75+
it('can mock native fetch requests', async () => {
76+
registerEndpoint('/native/1', {
77+
method: 'POST',
78+
handler: () => ({ path: '/native/1', method: 'POST' }),
79+
})
80+
registerEndpoint('/native/1', {
81+
method: 'GET',
82+
handler: () => ({ path: '/native/1', method: 'GET' }),
83+
})
84+
registerEndpoint('https://jsonplaceholder.typicode.com/native/1', {
85+
method: 'GET',
86+
handler: () => ({ path: 'https://jsonplaceholder.typicode.com/native/1', method: 'GET' }),
87+
})
88+
registerEndpoint('https://jsonplaceholder.typicode.com/native/1', {
89+
method: 'POST',
90+
handler: () => ({ path: 'https://jsonplaceholder.typicode.com/native/1', method: 'POST' }),
91+
})
92+
93+
expect(await fetch('/native/1').then(res => res.json())).toMatchObject({ path: '/native/1', method: 'GET' })
94+
expect(await fetch('/native/1', { method: 'POST' }).then(res => res.json())).toMatchObject({ path: '/native/1', method: 'POST' })
95+
expect(await fetch('https://jsonplaceholder.typicode.com/native/1').then(res => res.json()))
96+
.toMatchObject({ path: 'https://jsonplaceholder.typicode.com/native/1', method: 'GET' })
97+
98+
expect(await fetch(new Request('/native/1', { method: 'GET' })).then(res => res.json())).toMatchObject({ path: '/native/1', method: 'GET' })
99+
expect(await fetch(new Request('/native/1', { method: 'POST' })).then(res => res.json())).toMatchObject({ path: '/native/1', method: 'POST' })
100+
expect(await fetch(new Request('https://jsonplaceholder.typicode.com/native/1', { method: 'POST' })).then(res => res.json()))
101+
.toMatchObject({ path: 'https://jsonplaceholder.typicode.com/native/1', method: 'POST' })
102+
103+
expect(await fetch(new Request('/native/1'), { method: 'GET' }).then(res => res.json())).toMatchObject({ path: '/native/1', method: 'GET' })
104+
expect(await fetch(new Request('/native/1'), { method: 'POST' }).then(res => res.json())).toMatchObject({ path: '/native/1', method: 'POST' })
105+
expect(await fetch(new Request('https://jsonplaceholder.typicode.com/native/1'), { method: 'POST' }).then(res => res.json()))
106+
.toMatchObject({ path: 'https://jsonplaceholder.typicode.com/native/1', method: 'POST' })
107+
})
108+
109+
it('can mock fetch requests with data', async () => {
110+
registerEndpoint('/with-data', {
111+
method: 'POST',
112+
handler: async (event) => {
113+
return {
114+
body: await readBody(event),
115+
headers: getHeaders(event),
116+
}
117+
},
118+
})
119+
120+
expect(await $fetch<unknown>('/with-data', {
121+
method: 'POST',
122+
body: { data: 'data' },
123+
headers: { 'x-test': 'test' },
124+
})).toMatchObject({
125+
body: { data: 'data' },
126+
headers: { 'x-test': 'test' },
127+
})
128+
})
129+
130+
it('can mock native fetch requests with data', async () => {
131+
registerEndpoint('/with-data-native', {
132+
method: 'POST',
133+
handler: async (event) => {
134+
return {
135+
body: await readBody(event),
136+
headers: getHeaders(event),
137+
}
138+
},
139+
})
140+
141+
expect(await fetch('/with-data-native', {
142+
method: 'POST',
143+
body: JSON.stringify({ data: 'data' }),
144+
headers: { 'x-test': 'test', 'content-type': 'application/json' },
145+
}).then(res => res.json())).toMatchObject({
146+
body: { data: 'data' },
147+
headers: { 'x-test': 'test' },
148+
})
149+
150+
const request = new Request('/with-data-native', {
151+
method: 'POST',
152+
body: JSON.stringify({ data: 'data' }),
153+
headers: { 'x-test': 'test', 'content-type': 'application/json' },
154+
})
155+
expect(await fetch(request).then(res => res.json())).toMatchObject({
156+
body: { data: 'data' },
157+
headers: { 'x-test': 'test', 'content-type': 'application/json' },
158+
})
159+
160+
expect(await fetch(request, {
161+
body: JSON.stringify({ data: 'data2' }),
162+
headers: { 'x-test2': 'test2', 'content-type': 'application/json' },
163+
}).then(res => res.json())).toMatchObject({
164+
body: { data: 'data2' },
165+
headers: { 'x-test2': 'test2' },
166+
})
167+
})
74168
})

src/environments/vitest/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export default <Environment>{
4848
const teardownWindow = await setupWindow(win, environmentOptions as any)
4949
const { keys, originals } = populateGlobal(global, win, {
5050
bindFunctions: true,
51+
additionalKeys: ['fetch', 'Request'],
5152
})
5253

5354
return {

src/runtime/shared/environment.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,28 +47,55 @@ export async function setupWindow(win: NuxtWindow, environmentOptions: { nuxt: N
4747

4848
const h3App = createApp()
4949

50-
if (!win.fetch) {
50+
if (!win.fetch || !('Request' in win)) {
5151
await import('node-fetch-native/polyfill')
5252
// @ts-expect-error fetch polyfill
5353
win.URLSearchParams = globalThis.URLSearchParams
54+
// @ts-expect-error fetch polyfill
55+
win.Request ??= class Request extends globalThis.Request {
56+
constructor(input: RequestInfo, init?: RequestInit) {
57+
if (typeof input === 'string') {
58+
super(new URL(input, win.location.origin), init)
59+
}
60+
else {
61+
super(input, init)
62+
}
63+
}
64+
}
5465
}
5566

5667
const nodeHandler = toNodeListener(h3App)
5768

5869
const registry = new Set<string>()
5970

60-
win.fetch = async (url, init) => {
61-
if (typeof url === 'string') {
62-
const base = url.split('?')[0]!
63-
if (registry.has(base) || registry.has(url)) {
64-
url = '/_' + url
65-
}
66-
if (url.startsWith('/')) {
67-
const response = await fetchNodeRequestHandler(nodeHandler, url, init)
68-
return normalizeFetchResponse(response)
71+
const _fetch = fetch
72+
win.fetch = async (input, _init) => {
73+
let url: string
74+
let init = _init
75+
if (typeof input === 'string') {
76+
url = input
77+
}
78+
else if (input instanceof URL) {
79+
url = input.origin === win.location.origin ? input.pathname : input.toString()
80+
}
81+
else {
82+
url = input.url.startsWith(win.location.origin) ? new URL(input.url).pathname : input.url
83+
init = {
84+
method: init?.method ?? input.method,
85+
body: init?.body ?? input.body,
86+
headers: init?.headers ?? input.headers,
6987
}
7088
}
71-
return fetch(url, init)
89+
90+
const base = url.split('?')[0]!
91+
if (registry.has(base) || registry.has(url)) {
92+
url = '/_' + url
93+
}
94+
if (url.startsWith('/')) {
95+
const response = await fetchNodeRequestHandler(nodeHandler, url, init)
96+
return normalizeFetchResponse(response)
97+
}
98+
return _fetch(input, _init)
7299
}
73100

74101
// @ts-expect-error fetch types differ slightly

0 commit comments

Comments
 (0)