Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 106 additions & 1 deletion examples/app-vitest-full/tests/nuxt/server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'

import { listen } from 'listhen'
import { createApp, eventHandler, toNodeListener } from 'h3'
import { createApp, eventHandler, toNodeListener, readBody, getHeaders, getQuery } from 'h3'

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

Expand Down Expand Up @@ -71,4 +71,109 @@ describe('server mocks and data fetching', () => {
})
expect(await $fetch<unknown>('/method')).toMatchObject({ method: 'GET' })
})

it('can mock native fetch requests', async () => {
registerEndpoint('/native/1', {
method: 'POST',
handler: () => ({ path: '/native/1', method: 'POST' }),
})
registerEndpoint('/native/1', {
method: 'GET',
handler: () => ({ path: '/native/1', method: 'GET' }),
})
registerEndpoint('https://jsonplaceholder.typicode.com/native/1', {
method: 'GET',
handler: () => ({ path: 'https://jsonplaceholder.typicode.com/native/1', method: 'GET' }),
})
registerEndpoint('https://jsonplaceholder.typicode.com/native/1', {
method: 'POST',
handler: () => ({ path: 'https://jsonplaceholder.typicode.com/native/1', method: 'POST' }),
})

expect(await fetch('/native/1').then(res => res.json())).toMatchObject({ path: '/native/1', method: 'GET' })
expect(await fetch('/native/1', { method: 'POST' }).then(res => res.json())).toMatchObject({ path: '/native/1', method: 'POST' })
expect(await fetch('https://jsonplaceholder.typicode.com/native/1').then(res => res.json()))
.toMatchObject({ path: 'https://jsonplaceholder.typicode.com/native/1', method: 'GET' })
expect(await fetch('https://jsonplaceholder.typicode.com/native/1', { method: 'POST' }).then(res => res.json()))
.toMatchObject({ path: 'https://jsonplaceholder.typicode.com/native/1', method: 'POST' })
})

it('can mock fetch requests with data', async () => {
registerEndpoint('/with-data', {
method: 'POST',
handler: async (event) => {
return {
body: await readBody(event),
headers: getHeaders(event),
}
},
})

expect(await $fetch<unknown>('/with-data', {
method: 'POST',
body: { data: 'data' },
headers: { 'x-test': 'test' },
})).toMatchObject({
body: { data: 'data' },
headers: { 'x-test': 'test' },
})

expect(await fetch('/with-data', {
method: 'POST',
body: JSON.stringify({ data: 'data' }),
headers: { 'x-test': 'test', 'content-type': 'application/json' },
}).then(res => res.json())).toMatchObject({
body: { data: 'data' },
headers: { 'x-test': 'test' },
})
})

it('can mock fetch requests with query', async () => {
registerEndpoint('/with-data', {
method: 'GET',
handler: async (event) => {
return {
query: getQuery(event),
}
},
})

expect(await $fetch<unknown>('/with-data', { query: { q: 1 } })).toMatchObject({ query: { q: '1' } })
expect(await fetch('/with-data?q=1').then(res => res.json())).toMatchObject({ query: { q: '1' } })
})

it('can mock fetch requests with Request', async () => {
registerEndpoint('http://localhost:3000/with-request', {
method: 'GET',
handler: (event) => {
return { title: 'with-request', data: getQuery(event) }
},
})
registerEndpoint('http://localhost:3000/with-request', {
method: 'POST',
handler: async (event) => {
return { title: 'with-request', data: await readBody(event) }
},
})

const request = new Request('/with-request?q=1', { headers: { 'content-type': 'application/json' } })

expect(await $fetch<unknown>(request)).toMatchObject({ title: 'with-request', data: { q: '1' } })
expect(await fetch(request).then(res => res.json())).toMatchObject({ title: 'with-request', data: { q: '1' } })

expect(await $fetch<unknown>(request, { method: 'POST', body: [1] })).toMatchObject({ title: 'with-request', data: [1] })
expect(await fetch(request, { method: 'POST', body: '[1]' }).then(res => res.json())).toMatchObject({ title: 'with-request', data: [1] })
})

it('can mock fetch requests with URL', async () => {
registerEndpoint('http://localhost:3000/with-url', {
method: 'GET',
handler: (event) => {
return { title: 'with-url', data: getQuery(event) }
},
})

expect(await fetch(new URL('http://localhost:3000/with-url')).then(res => res.json())).toMatchObject({ title: 'with-url', data: {} })
expect(await fetch(new URL('http://localhost:3000/with-url?q=1')).then(res => res.json())).toMatchObject({ title: 'with-url', data: { q: '1' } })
})
})
1 change: 1 addition & 0 deletions src/environments/vitest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export default <Environment>{
const teardownWindow = await setupWindow(win, environmentOptions as any)
const { keys, originals } = populateGlobal(global, win, {
bindFunctions: true,
additionalKeys: ['fetch', 'Request'],
})

return {
Expand Down
49 changes: 38 additions & 11 deletions src/runtime/shared/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,28 +47,55 @@ export async function setupWindow(win: NuxtWindow, environmentOptions: { nuxt: N

const h3App = createApp()

if (!win.fetch) {
if (!win.fetch || !('Request' in win)) {
await import('node-fetch-native/polyfill')
// @ts-expect-error fetch polyfill
win.URLSearchParams = globalThis.URLSearchParams
// @ts-expect-error fetch polyfill
win.Request ??= class Request extends globalThis.Request {
constructor(input: RequestInfo, init?: RequestInit) {
if (typeof input === 'string') {
super(new URL(input, win.location.origin), init)
}
else {
super(input, init)
}
}
}
}

const nodeHandler = toNodeListener(h3App)

const registry = new Set<string>()

win.fetch = async (url, init) => {
if (typeof url === 'string') {
const base = url.split('?')[0]!
if (registry.has(base) || registry.has(url)) {
url = '/_' + url
}
if (url.startsWith('/')) {
const response = await fetchNodeRequestHandler(nodeHandler, url, init)
return normalizeFetchResponse(response)
const _fetch = fetch
win.fetch = async (input, _init) => {
let url: string
let init = _init
if (typeof input === 'string') {
url = input
}
else if (input instanceof URL) {
url = input.toString()
}
else {
url = input.url
init = {
method: init?.method ?? input.method,
body: init?.body ?? input.body,
headers: init?.headers ?? input.headers,
}
}
return fetch(url, init)

const base = url.split('?')[0]!
if (registry.has(base) || registry.has(url)) {
url = '/_' + url
}
if (url.startsWith('/')) {
const response = await fetchNodeRequestHandler(nodeHandler, url, init)
return normalizeFetchResponse(response)
}
return _fetch(input, _init)
}

// @ts-expect-error fetch types differ slightly
Expand Down
Loading