UNPKG

@cardbrother/up-fetch

Version:

Advanced fetch client builder for typescript.

1,340 lines (1,170 loc) 42.2 kB
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/require-await */ import { afterAll, afterEach, beforeAll, describe, expect, expectTypeOf, test, vi, } from 'vitest' import { up } from './up' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { isResponseError, ResponseError } from './response-error' import { bodyMock } from './_mocks' import { fallbackOptions } from './fallback-options' import { object, pipe, string, transform } from 'valibot' import { scheduler } from 'timers/promises' import { z } from 'zod' import { isValidationError, ValidationError } from './validation-error' import type { FetcherOptions } from './types' describe('up', () => { let server = setupServer() beforeAll(() => server.listen()) afterEach(() => server.resetHandlers()) afterAll(() => server.close()) test('Should receive the fetcher arguments', async () => { up(fetch, (input, options) => { expectTypeOf(input).toEqualTypeOf<string | Request | URL>() expectTypeOf(options).toEqualTypeOf< FetcherOptions<typeof fetch, any, any, any> >() return {} }) }) // TODO remove in 2.0 describe('throwResponseError', () => { test('should throw ResponseError by default when response.ok is false', async () => { server.use( http.get('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let catchCount = 0 let upfetch = up(fetch, (input) => ({ baseUrl: 'https://example.com', })) await upfetch('').catch((error) => { expect(isResponseError(error)).toEqual(true) catchCount++ }) expect(catchCount).toEqual(1) }) test('should not throw when throwResponseError returns false', async () => { server.use( http.get('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let catchCount = 0 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', throwResponseError: () => false, })) await upfetch('').catch(() => { catchCount++ }) expect(catchCount).toEqual(0) }) test('should execute throwResponseError before up.parseResponseError', async () => { server.use( http.get('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let catchCount = 0 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', throwResponseError: () => { expect(catchCount).toBe(0) catchCount++ return true }, parseResponseError: async (e) => { expect(catchCount).toBe(1) catchCount++ return e }, })) await upfetch('').catch(() => { expect(catchCount).toBe(2) catchCount++ }) expect(catchCount).toEqual(3) }) test('should execute throwResponseError before upfetch.parseResponseError', async () => { server.use( http.get('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let catchCount = 0 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', throwResponseError: () => { expect(catchCount).toBe(0) catchCount++ return true }, })) await upfetch('', { parseResponseError: async (e) => { expect(catchCount).toBe(1) catchCount++ return e }, }).catch(() => { expect(catchCount).toBe(2) catchCount++ }) expect(catchCount).toEqual(3) }) test('should support asynchronous throwResponseError functions', async () => { server.use( http.get('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let catchCount = 0 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', throwResponseError: async () => { return new Promise((resolve) => { catchCount++ setTimeout(() => resolve(true), 100) }) }, })) await upfetch('', { parseResponseError: async (e) => { expect(catchCount).toBe(1) catchCount++ return e }, }).catch(() => { expect(catchCount).toBe(2) catchCount++ }) expect(catchCount).toEqual(3) }) }) describe('reject', () => { test('should throw ResponseError by default when response.ok is false', async () => { server.use( http.get('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let catchCount = 0 let upfetch = up(fetch, (input) => ({ baseUrl: 'https://example.com', })) await upfetch('').catch((error) => { expect(isResponseError(error)).toEqual(true) catchCount++ }) expect(catchCount).toEqual(1) }) test('should not throw when reject returns false', async () => { server.use( http.get('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let catchCount = 0 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', reject: () => false, })) await upfetch('').catch(() => { catchCount++ }) expect(catchCount).toEqual(0) }) test('should execute reject before up.parseRejected', async () => { server.use( http.get('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let catchCount = 0 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', reject: () => { expect(catchCount).toBe(0) catchCount++ return true }, parseRejected: async (e) => { expect(catchCount).toBe(1) catchCount++ return e }, })) await upfetch('').catch(() => { expect(catchCount).toBe(2) catchCount++ }) expect(catchCount).toEqual(3) }) test('should execute reject before upfetch.parseRejected', async () => { server.use( http.get('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let catchCount = 0 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', reject: () => { expect(catchCount).toBe(0) catchCount++ return true }, })) await upfetch('', { parseRejected: async (e) => { expect(catchCount).toBe(1) catchCount++ return e }, }).catch(() => { expect(catchCount).toBe(2) catchCount++ }) expect(catchCount).toEqual(3) }) test('should support asynchronous reject functions', async () => { server.use( http.get('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let catchCount = 0 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', reject: async () => { return new Promise((resolve) => { catchCount++ setTimeout(() => resolve(true), 100) }) }, })) await upfetch('', { parseRejected: async (e) => { expect(catchCount).toBe(1) catchCount++ return e }, }).catch(() => { expect(catchCount).toBe(2) catchCount++ }) expect(catchCount).toEqual(3) }) }) describe('body', () => { test('should ignore up.body configuration', async () => { server.use( http.post('https://example.com', async ({ request }) => { let body = await request.text() if (count === 1) { expect(body).toBe('') } if (count === 2) { expect(body).toBe('my body') } return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let count = 1 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', method: 'POST', body: 'my body', })) await upfetch('') count++ await upfetch('', { body: 'my body' }) }) }) describe('headers', () => { test.each` body | expected ${{}} | ${true} ${{ a: 1 }} | ${true} ${[1, 2]} | ${true} ${bodyMock.classJsonifiable} | ${true} ${bodyMock.classNonJsonifiable} | ${false} ${bodyMock.buffer} | ${false} ${bodyMock.dataview} | ${false} ${bodyMock.blob} | ${false} ${bodyMock.typedArray} | ${false} ${bodyMock.formData} | ${false} ${bodyMock.urlSearchParams} | ${false} ${bodyMock.getStream()} | ${false} ${''} | ${false} ${0} | ${false} ${undefined} | ${false} ${null} | ${false} `( 'should automatically set content-type to application/json for jsonifiable bodies that serialize to strings', async ({ body, expected }) => { server.use( http.post('https://example.com', async ({ request }) => { let hasApplicationJsonHeader = request.headers.get('content-type') === 'application/json' expect(hasApplicationJsonHeader).toEqual(expected) return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', method: 'POST', serializeBody: (body) => JSON.stringify(body), })) await upfetch('', { body, // @ts-ignore the global fetch type does not include "duplex" duplex: body?.getReader ? 'half' : undefined, }) }, ) test.each` body | expected ${{}} | ${false} ${{ a: 1 }} | ${false} ${[1, 2]} | ${false} ${bodyMock.classJsonifiable} | ${false} `( 'should not set content-type to application/json when body is jsonifiable but serializes to non-string', async ({ body, expected }) => { server.use( http.post('https://example.com', async ({ request }) => { let hasApplicationJsonHeader = request.headers.get('content-type') === 'application/json' expect(hasApplicationJsonHeader).toEqual(expected) return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', method: 'POST', serializeBody: () => new FormData(), })) await upfetch('', { body, // @ts-ignore the global fetch type does not include "duplex" duplex: body?.getReader ? 'half' : undefined, }) }, ) test.each` body ${{}} ${{ a: 1 }} ${[1, 2]} ${bodyMock.classJsonifiable} `( 'should respect existing content-type header when already set', async ({ body }) => { server.use( http.post('https://example.com', async ({ request }) => { expect(request.headers.get('content-type')).toEqual( 'html/text', ) return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', headers: { 'content-type': 'html/text' }, method: 'POST', })) await upfetch('', { body }) }, ) test('should allow upfetch.headers to override up.headers', async () => { server.use( http.post('https://example.com', async ({ request }) => { expect(request.headers.get('content-type')).toEqual( 'from upfetch', ) return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', headers: { 'content-type': 'from up' }, method: 'POST', })) await upfetch('', { headers: { 'content-type': 'from upfetch' } }) }) test('should support removing up headers by setting `undefined` in upfetch headers', async () => { server.use( http.post('https://example.com', async ({ request }) => { expect(request.headers.get('content-type')).toEqual(null) return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', headers: { 'content-type': 'text/html' }, method: 'POST', })) await upfetch('', { headers: { 'content-type': undefined } }) }) }) describe('params', () => { test('should prioritize input URL params over up.params', async () => { server.use( http.get('https://example.com', ({ request }) => { expect(new URL(request.url).search).toEqual('?hello=people') return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', params: { hello: 'world' }, })) await upfetch('/?hello=people') }) test('should merge up.params and upfetch.params with input URL params (input URL params are not deduped)', async () => { server.use( http.get('https://example.com', ({ request }) => { expect(new URL(request.url).search).toEqual( '?input=param&hello=people&input=test', ) return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com' })) await upfetch('/?input=param', { params: { hello: 'people', input: 'test' }, }) }) test('should support removing up params by setting `undefined` in upfetch params', async () => { server.use( http.get('https://example.com', ({ request }) => { expect(new URL(request.url).search).toEqual('?hello=world') return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', params: { hello: 'world', input: 'test' }, })) await upfetch('/', { params: { input: undefined }, }) }) }) describe('serializeParams', () => { test('should receive the params object for serialization', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', serializeParams(params) { expect(params).toEqual({ a: 1 }) return '' }, })) await upfetch('', { params: { a: 1 } }) }) test('URL-defined params should not be passed to the serialization process', async () => { server.use( http.get('https://example.com/path', ({ request }) => { expect(new URL(request.url).search).toEqual('?b=2&a=1') return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', serializeParams(params) { expect(params).toEqual({ a: 1 }) return fallbackOptions.serializeParams(params) }, })) await upfetch('path?b=2', { params: { a: 1 } }) }) test('should be called even if no params are defined', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let count = 1 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', serializeParams() { count++ return '' }, })) await upfetch('') expect(count).toEqual(2) }) test('should allow upfetch.serializeParams to override up.serializeParams', async () => { server.use( http.get('https://example.com', ({ request }) => { expect(new URL(request.url).search).toEqual('?from=upfetch') return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', serializeParams: () => 'from=up', })) await upfetch('', { serializeParams: () => 'from=upfetch' }) }) }) describe('serializeBody', () => { test.each` body | shouldCallSerializeBody ${{}} | ${true} ${{ a: 1 }} | ${true} ${[1, 2]} | ${true} ${bodyMock.classJsonifiable} | ${true} ${bodyMock.classNonJsonifiable} | ${true} ${bodyMock.buffer} | ${true} ${bodyMock.dataview} | ${true} ${bodyMock.blob} | ${true} ${bodyMock.typedArray} | ${true} ${bodyMock.formData} | ${true} ${bodyMock.urlSearchParams} | ${true} ${bodyMock.stream} | ${true} ${''} | ${true} ${0} | ${true} ${undefined} | ${false} ${null} | ${false} `( 'Should be called on any non-nullish an non-string body', async ({ body, shouldCallSerializeBody }) => { server.use( http.post('https://example.com', async ({ request }) => { let actualBody = await request.text() expect(actualBody === 'serialized').toBe( shouldCallSerializeBody, ) return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', method: 'POST', })) await upfetch('', { body, serializeBody: () => 'serialized', // @ts-ignore the global fetch type does not include "duplex" duplex: body === bodyMock.stream ? 'half' : undefined, }) }, ) test('serializeBody in upfetch should override serializeBody in up', async () => { server.use( http.post('https://example.com', async ({ request }) => { expect(await request.text()).toEqual('from=upfetch') return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', serializeBody: () => 'from=up', })) await upfetch('', { body: { a: 1 }, method: 'POST', serializeBody: () => 'from=upfetch', }) }) }) describe('parseResponse', () => { test('should parse JSON responses by default', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', })) let data = await upfetch('') expect(data).toEqual({ hello: 'world' }) }) test('should parse TEXT responses by default', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.text('some text', { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', })) let data = await upfetch('') expect(data).toEqual('some text') }) test('should provide response and options to parseResponse function', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.text('some text', { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', parseResponse(res, options) { expect(res instanceof Response).toEqual(true) expect(options.input).toEqual('https://example.com') return res.text() }, })) await upfetch('') }) test('should execute parseResponse before onSuccess callback', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.text('some text', { status: 200 }) }), ) let count = 1 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', parseResponse(res) { expect(count).toEqual(1) count++ return res.text() }, onSuccess() { expect(count).toEqual(2) count++ }, })) await upfetch('') expect(count).toEqual(3) }) test('should also receive responses with empty body', async () => { server.use( http.get('https://example.com', () => { return new Response(null, { status: 200 }) }), ) let count = 1 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', parseResponse(res) { expect(res.body).toEqual(null) expect(count).toEqual(1) count++ return res.text() }, })) await upfetch('') expect(count).toEqual(2) }) test('should allow upfetch.parseResponse to override up.parseResponse', async () => { server.use( http.post('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', parseResponse: () => Promise.resolve('from=up'), })) let data = await upfetch('', { body: { a: 1 }, method: 'POST', parseResponse: () => Promise.resolve('from=upfetch'), }) expect(data).toEqual('from=upfetch') }) }) // TODO: remove in 2.0 describe('parseResponseError', () => { test('should parse JSON error responses by default', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', })) await upfetch('').catch((error) => { expect(error.data).toEqual({ hello: 'world' }) }) }) test('should parse TEXT error responses by default', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.text('some text', { status: 400 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', })) await upfetch('').catch((error) => { expect(error.data).toEqual('some text') }) }) test('should provide response and options to parseResponseError', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.text('some text', { status: 400 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', parseResponseError(res, options) { expect(res instanceof Response).toEqual(true) expect(options.input).toEqual('https://example.com') return res.text() }, })) await upfetch('').catch((error) => { expect(error).toEqual('some text') }) }) test('should execute parseResponseError before onError callback', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.text('some text', { status: 400 }) }), ) let count = 1 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', parseResponseError(res) { expect(count).toEqual(1) count++ return res.text() }, onError() { expect(count).toEqual(2) count++ }, })) await upfetch('').catch(() => { expect(count).toEqual(3) }) }) test('should receive error responses with empty body', async () => { server.use( http.get('https://example.com', () => { return new Response(null, { status: 300 }) }), ) let count = 1 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', parseResponseError(res) { expect(res.body).toEqual(null) expect(count).toEqual(1) count++ return Promise.resolve('some error') }, })) await upfetch('').catch((error) => expect(error).toEqual('some error')) expect(count).toEqual(2) }) test('should allow upfetch.parseResponseError to override up.parseResponseError', async () => { server.use( http.post('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', parseResponseError: () => Promise.resolve('from=up'), })) await upfetch('', { body: { a: 1 }, method: 'POST', parseResponseError: () => Promise.resolve('from=upfetch'), }).catch((error) => { expect(error).toEqual('from=upfetch') }) }) }) describe('parseRejected', () => { test('should parse JSON error responses by default', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', })) await upfetch('').catch((error) => { expect(error.data).toEqual({ hello: 'world' }) }) }) test('should parse TEXT error responses by default', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.text('some text', { status: 400 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', })) await upfetch('').catch((error) => { expect(error.data).toEqual('some text') }) }) test('should provide response and options to parseRejected', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.text('some text', { status: 400 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', parseRejected(res, options) { expect(res instanceof Response).toEqual(true) expect(options.input).toEqual('https://example.com') return res.text() }, })) await upfetch('').catch((error) => { expect(error).toEqual('some text') }) }) test('should execute parseRejected before onError callback', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.text('some text', { status: 400 }) }), ) let count = 1 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', parseRejected(res) { expect(count).toEqual(1) count++ return res.text() }, onError() { expect(count).toEqual(2) count++ }, })) await upfetch('').catch(() => { expect(count).toEqual(3) }) }) test('should receive error responses with empty body', async () => { server.use( http.get('https://example.com', () => { return new Response(null, { status: 300 }) }), ) let count = 1 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', parseRejected(res) { expect(res.body).toEqual(null) expect(count).toEqual(1) count++ return Promise.resolve('some error') }, })) await upfetch('').catch((error) => expect(error).toEqual('some error')) expect(count).toEqual(2) }) test('should allow upfetch.parseRejected to override up.parseRejected', async () => { server.use( http.post('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', parseRejected: () => Promise.resolve('from=up'), })) await upfetch('', { body: { a: 1 }, method: 'POST', parseRejected: () => Promise.resolve('from=upfetch'), }).catch((error) => { expect(error).toEqual('from=upfetch') }) }) }) describe('onError', () => { test('should receive validation errors', async () => { server.use( http.get('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let exec = 0 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', onError(error, options) { if (isValidationError(error)) { exec++ expectTypeOf(error).toEqualTypeOf<ValidationError>() } }, })) await upfetch('', { schema: z.object({ hello: z.number() }), }).catch(() => {}) expect(exec).toBe(1) }) test('should receive the response errors', async () => { server.use( http.get('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 400 }) }), ) let exec = 0 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', onError(error, options) { if (isResponseError(error)) { exec++ expectTypeOf(error).toEqualTypeOf<ResponseError>() } }, })) await upfetch('', {}).catch(() => {}) expect(exec).toBe(1) }) test('should receive any error', async () => { server.use( http.get('https://example.com', async () => { return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let exec = 0 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', onError(error, options) { exec++ expectTypeOf(error).toEqualTypeOf<any>() }, })) await upfetch('', { parseResponse: () => { throw new Error('any error') }, }).catch(() => {}) expect(exec).toBe(1) }) }) describe('onSuccess', () => { test('should execute onSuccess when no error occurs', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let count = 0 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', onSuccess() { expect(count).toBe(0) count++ }, })) await upfetch('') expect(count).toBe(1) }) test('should provide validated data and options to onSuccess callback', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', onSuccess(data, options) { expect(data).toEqual({ hello: 'world!' }) expect(options.input).toEqual('https://example.com') }, })) await upfetch('', { schema: object({ hello: pipe( string(), transform((v) => (v += '!')), ), }), }) }) test('should skip onSuccess when parseResponse throws an error', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let count = 1 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', onSuccess() { count++ throw new Error('onSuccess should not be called') }, parseResponse: () => { throw new Error('Some error') }, })) await upfetch('').catch((error) => { expect(error.message).toEqual('Some error') }) expect(count).toBe(1) }) }) describe('onRequest', () => { test('should execute onRequest', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let count = 0 let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', onRequest() { expect(count).toBe(0) count++ }, })) await upfetch('') expect(count).toBe(1) }) test('should provide complete options object to onRequest callback', async () => { server.use( http.get('https://example.com', () => { return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', onRequest(options) { expect(options.input).toBe('https://example.com') }, })) await upfetch('') }) }) describe('timeout', () => { test('should apply up.timeout when upfetch.timeout is not defined', async () => { server.use( http.get('https://example.com', async () => { await scheduler.wait(2) return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', timeout: 1, })) let exec = 0 await upfetch('').catch((error) => { exec++ expect(error.name).toBe('TimeoutError') }) expect(exec).toBe(1) }) test('should allow upfetch.timeout to override up.timeout', async () => { server.use( http.get('https://example.com', async () => { await scheduler.wait(2) return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', timeout: undefined, })) let exec = 0 await upfetch('').catch((error) => { exec++ expect(error.name).toBe('TimeoutError') }) expect(exec).toBe(0) }) test('should maintain timeout functionality when an AbortSignal is passed to upfetch.signal', async () => { server.use( http.get('https://example.com', async () => { await scheduler.wait(2) return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', timeout: 1, })) let exec = 0 await upfetch('', { signal: new AbortController().signal, }).catch((error) => { exec++ expect(error.name).toBe('TimeoutError') }) expect(exec).toBe(1) }) test('should maintain upfetch.signal functionality when timeout is defined', async () => { server.use( http.get('https://example.com', async () => { await scheduler.wait(10000) return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', timeout: 9000, })) let exec = 0 let controller = new AbortController() let signal = controller.signal let promise = upfetch('', { signal, }).catch((error) => { exec++ expect(error.name).toBe('AbortError') }) controller.abort() await promise expect(exec).toBe(1) }) describe('environment compatibility', () => { beforeAll(() => { vi.stubGlobal('AbortSignal', {}) }) afterAll(() => { vi.unstubAllGlobals() }) test('should gracefully handle environments without AbortSignal.any and AbortSignal.timeout', async () => { server.use( http.get('https://example.com', async () => { await scheduler.wait(2) return HttpResponse.json({ hello: 'world' }, { status: 200 }) }), ) let upfetch = up(fetch, () => ({ baseUrl: 'https://example.com', })) let controller = new AbortController() let signal = controller.signal await upfetch('', { // having both timeout and signal will use AbortSignal.any timeout: 1, signal, }) }) }) }) })