Skip to content

Commit 4f04851

Browse files
authored
feat(runtime-utils): support once option in registerEndpoint (#1475)
1 parent 9fdaf38 commit 4f04851

File tree

3 files changed

+140
-7
lines changed

3 files changed

+140
-7
lines changed

examples/app-vitest/components/TestFetchComponent.vue

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,46 @@ async function customFetcher() {
1010
})
1111
await fetcher('/test2/')
1212
}
13+
14+
async function onceFetcher() {
15+
try {
16+
const fetcher = $fetch.create({})
17+
await fetcher('/test-once/')
18+
}
19+
catch {
20+
// Expected to fail on second call
21+
}
22+
}
23+
24+
async function onceReregisterFetcher() {
25+
try {
26+
const fetcher = $fetch.create({})
27+
await fetcher('/test-once-reregister/')
28+
}
29+
catch {
30+
// Expected to fail on second call
31+
}
32+
}
33+
34+
async function onceMethodGetFetcher() {
35+
try {
36+
const fetcher = $fetch.create({})
37+
await fetcher('/test-method-once/', { method: 'GET' })
38+
}
39+
catch {
40+
// Expected to fail on second call
41+
}
42+
}
43+
44+
async function onceMethodPostFetcher() {
45+
try {
46+
const fetcher = $fetch.create({})
47+
await fetcher('/test-method-once/', { method: 'POST' })
48+
}
49+
catch {
50+
// Expected to fail on second call
51+
}
52+
}
1353
</script>
1454

1555
<template>
@@ -26,5 +66,29 @@ async function customFetcher() {
2666
>
2767
custom fetch
2868
</button>
69+
<button
70+
id="once-fetcher"
71+
@click="onceFetcher"
72+
>
73+
once fetch
74+
</button>
75+
<button
76+
id="once-reregister-fetcher"
77+
@click="onceReregisterFetcher"
78+
>
79+
once reregister fetch
80+
</button>
81+
<button
82+
id="once-method-get-fetcher"
83+
@click="onceMethodGetFetcher"
84+
>
85+
once method GET fetch
86+
</button>
87+
<button
88+
id="once-method-post-fetcher"
89+
@click="onceMethodPostFetcher"
90+
>
91+
once method POST fetch
92+
</button>
2993
</div>
3094
</template>

examples/app-vitest/test/registerEndpoint.nuxt.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,49 @@ describe('registerEndpoint tests', () => {
2828
expect(endpoint).toHaveBeenCalled()
2929
component.unmount()
3030
})
31+
32+
describe('once option', () => {
33+
it('should handle the endpoint only once', async () => {
34+
const endpoint = vi.fn(() => ({ name: 'Alice' }))
35+
registerEndpoint('/test-once/', {
36+
handler: () => endpoint(),
37+
once: true,
38+
})
39+
40+
const component = await mountSuspended(TestFetchComponent)
41+
42+
await component.find<HTMLButtonElement>('#once-fetcher').trigger('click')
43+
expect(endpoint).toHaveBeenCalledTimes(1)
44+
45+
await component.find<HTMLButtonElement>('#once-fetcher').trigger('click')
46+
expect(endpoint).toHaveBeenCalledTimes(1)
47+
48+
component.unmount()
49+
})
50+
51+
it('should allow re-registering endpoint after once is consumed', async () => {
52+
const endpoint1 = vi.fn(() => ({ name: 'Bob' }))
53+
registerEndpoint('/test-once-reregister/', {
54+
handler: () => endpoint1(),
55+
once: true,
56+
})
57+
58+
const component = await mountSuspended(TestFetchComponent)
59+
60+
await component.find<HTMLButtonElement>('#once-reregister-fetcher').trigger('click')
61+
expect(endpoint1).toHaveBeenCalledTimes(1)
62+
63+
// Re-register with different handler
64+
const endpoint2 = vi.fn(() => ({ name: 'Charlie' }))
65+
registerEndpoint('/test-once-reregister/', {
66+
handler: () => endpoint2(),
67+
once: true,
68+
})
69+
70+
await component.find<HTMLButtonElement>('#once-reregister-fetcher').trigger('click')
71+
expect(endpoint2).toHaveBeenCalledTimes(1)
72+
73+
component.unmount()
74+
})
75+
})
3176
})

src/runtime-utils/mock.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,39 @@ import type {
1818
type Awaitable<T> = T | Promise<T>
1919
type OptionalFunction<T> = T | (() => Awaitable<T>)
2020

21+
const endpointRegistry: Record<string, Array<{ handler: EventHandler, method?: HTTPMethod, once?: boolean }>> = {}
2122
/**
2223
* `registerEndpoint` allows you create Nitro endpoint that returns mocked data. It can come in handy if you want to test a component that makes requests to API to display some data.
2324
* @param url - endpoint name (e.g. `/test/`).
24-
* @param options - factory function that returns the mocked data or an object containing both the `handler` and the `method` properties.
25+
* @param options - factory function that returns the mocked data or an object containing the `handler`, `method`, and `once` properties.
26+
* - `handler`: the event handler function
27+
* - `method`: (optional) HTTP method to match (e.g., 'GET', 'POST')
28+
* - `once`: (optional) if true, the handler will only be used for the first matching request and then automatically removed
2529
* @example
2630
* ```ts
2731
* import { registerEndpoint } from '@nuxt/test-utils/runtime'
2832
*
29-
* registerEndpoint("/test/", () => {
33+
* registerEndpoint("/test/", () => ({
3034
* test: "test-field"
35+
* }))
36+
*
37+
* // With once option
38+
* registerEndpoint("/api/user", {
39+
* handler: () => ({ name: "Alice" }),
40+
* once: true
3141
* })
3242
* ```
3343
* @see https://nuxt.com/docs/getting-started/testing#registerendpoint
3444
*/
35-
const endpointRegistry: Record<string, Array<{ handler: EventHandler, method?: HTTPMethod }>> = {}
36-
export function registerEndpoint(url: string, options: EventHandler | { handler: EventHandler, method: HTTPMethod }) {
45+
export function registerEndpoint(url: string, options: EventHandler | { handler: EventHandler, method?: HTTPMethod, once?: boolean }) {
3746
// @ts-expect-error private property
3847
const app: App = window.__app
3948

4049
if (!app) {
4150
throw new Error('registerEndpoint() can only be used in a `@nuxt/test-utils` runtime environment')
4251
}
4352

44-
const config = typeof options === 'function' ? { handler: options, method: undefined } : options
53+
const config = typeof options === 'function' ? { handler: options, method: undefined, once: false } : options
4554
config.handler = defineEventHandler(config.handler)
4655

4756
// @ts-expect-error private property
@@ -54,9 +63,24 @@ export function registerEndpoint(url: string, options: EventHandler | { handler:
5463
// @ts-expect-error private property
5564
window.__registry.add(url)
5665

57-
app.use('/_' + url, defineEventHandler((event) => {
66+
app.use('/_' + url, defineEventHandler(async (event) => {
5867
const latestHandler = [...endpointRegistry[url] || []].reverse().find(config => config.method ? event.method === config.method : true)
59-
return latestHandler?.handler(event)
68+
if (!latestHandler) return
69+
70+
const result = await latestHandler.handler(event)
71+
72+
if (!latestHandler.once) return result
73+
74+
const index = endpointRegistry[url]?.indexOf(latestHandler)
75+
if (index === undefined || index === -1) return result
76+
77+
endpointRegistry[url]?.splice(index, 1)
78+
if (endpointRegistry[url]?.length === 0) {
79+
// @ts-expect-error private property
80+
window.__registry.delete(url)
81+
}
82+
83+
return result
6084
}), {
6185
match(_, event) {
6286
return endpointRegistry[url]?.some(config => config.method ? event?.method === config.method : true) ?? false

0 commit comments

Comments
 (0)