From 3b5c2e16770148a2abd8ba37ed379ccd1547e073 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sat, 30 Mar 2024 11:09:34 +0100 Subject: [PATCH 1/3] feat(HttpResponse): support explicitly empty response body via null generic type --- src/core/HttpResponse.ts | 30 +++++++++--------- src/core/handlers/RequestHandler.ts | 13 +++++--- src/core/passthrough.ts | 6 ++-- test/typings/http.test-d.ts | 48 +++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 22 deletions(-) diff --git a/src/core/HttpResponse.ts b/src/core/HttpResponse.ts index de3f6ea9d..15f9fd915 100644 --- a/src/core/HttpResponse.ts +++ b/src/core/HttpResponse.ts @@ -11,16 +11,14 @@ export interface HttpResponseInit extends ResponseInit { declare const bodyType: unique symbol -export interface StrictRequest - extends Request { +export interface StrictRequest extends Request { json(): Promise } /** * Opaque `Response` type that supports strict body type. */ -export interface StrictResponse - extends Response { +interface StrictResponse extends Response { readonly [bodyType]: BodyType } @@ -35,10 +33,15 @@ export interface StrictResponse * * @see {@link https://mswjs.io/docs/api/http-response `HttpResponse` API reference} */ -export class HttpResponse extends Response { - constructor(body?: BodyInit | null, init?: HttpResponseInit) { +export class HttpResponse + extends Response + implements StrictResponse +{ + [bodyType]: BodyType = null as any + + constructor(body?: NoInfer | null, init?: HttpResponseInit) { const responseInit = normalizeResponseInit(init) - super(body, responseInit) + super(body as BodyInit, responseInit) decorateResponse(this, responseInit) } @@ -51,7 +54,7 @@ export class HttpResponse extends Response { static text( body?: NoInfer | null, init?: HttpResponseInit, - ): StrictResponse { + ): HttpResponse { const responseInit = normalizeResponseInit(init) if (!responseInit.headers.has('Content-Type')) { @@ -68,7 +71,7 @@ export class HttpResponse extends Response { ) } - return new HttpResponse(body, responseInit) as StrictResponse + return new HttpResponse(body, responseInit) } /** @@ -78,9 +81,9 @@ export class HttpResponse extends Response { * HttpResponse.json({ error: 'Not Authorized' }, { status: 401 }) */ static json( - body?: NoInfer | null, + body?: NoInfer | null | undefined, init?: HttpResponseInit, - ): StrictResponse { + ): HttpResponse { const responseInit = normalizeResponseInit(init) if (!responseInit.headers.has('Content-Type')) { @@ -100,10 +103,7 @@ export class HttpResponse extends Response { ) } - return new HttpResponse( - responseText, - responseInit, - ) as StrictResponse + return new HttpResponse(responseText as BodyType, responseInit) } /** diff --git a/src/core/handlers/RequestHandler.ts b/src/core/handlers/RequestHandler.ts index 5c0c3ffa7..38da00109 100644 --- a/src/core/handlers/RequestHandler.ts +++ b/src/core/handlers/RequestHandler.ts @@ -3,7 +3,7 @@ import { getCallFrame } from '../utils/internal/getCallFrame' import { isIterable } from '../utils/internal/isIterable' import type { ResponseResolutionContext } from '../utils/executeHandlers' import type { MaybePromise } from '../typeUtils' -import { StrictRequest, StrictResponse } from '..//HttpResponse' +import type { StrictRequest, HttpResponse } from '..//HttpResponse' export type DefaultRequestMultipartBody = Record< string, @@ -13,6 +13,11 @@ export type DefaultRequestMultipartBody = Record< export type DefaultBodyType = | Record | DefaultRequestMultipartBody + | URLSearchParams + | ReadableStream + | FormData + | BufferSource + | Blob | string | number | boolean @@ -40,7 +45,7 @@ export type ResponseResolverReturnType< > = | ([ResponseBodyType] extends [undefined] ? Response - : StrictResponse) + : HttpResponse) | undefined | void @@ -122,7 +127,7 @@ export abstract class RequestHandler< MaybeAsyncResponseResolverReturnType, MaybeAsyncResponseResolverReturnType > - private resolverGeneratorResult?: Response | StrictResponse + private resolverGeneratorResult?: Response | HttpResponse private options?: HandlerOptions constructor(args: RequestHandlerArgs) { @@ -326,7 +331,7 @@ export abstract class RequestHandler< // Clone the previously stored response from the generator // so that it could be read again. - return this.resolverGeneratorResult.clone() as StrictResponse + return this.resolverGeneratorResult.clone() as HttpResponse } if (!this.resolverGenerator) { diff --git a/src/core/passthrough.ts b/src/core/passthrough.ts index 2dbe84f67..7d28f83f6 100644 --- a/src/core/passthrough.ts +++ b/src/core/passthrough.ts @@ -1,4 +1,4 @@ -import type { StrictResponse } from './HttpResponse' +import type { HttpResponse } from './HttpResponse' /** * Performs the intercepted request as-is. @@ -14,12 +14,12 @@ import type { StrictResponse } from './HttpResponse' * * @see {@link https://mswjs.io/docs/api/passthrough `passthrough()` API reference} */ -export function passthrough(): StrictResponse { +export function passthrough(): HttpResponse { return new Response(null, { status: 302, statusText: 'Passthrough', headers: { 'x-msw-intention': 'passthrough', }, - }) as StrictResponse + }) as HttpResponse } diff --git a/test/typings/http.test-d.ts b/test/typings/http.test-d.ts index f782db09f..c07f01496 100644 --- a/test/typings/http.test-d.ts +++ b/test/typings/http.test-d.ts @@ -55,6 +55,54 @@ it('returns plain Response withouth explicit response body generic', () => { }) }) +it('returns HttpResponse with URLSearchParams as response body', () => { + http.get('/', () => { + return new HttpResponse(new URLSearchParams()) + }) +}) + +it('returns HttpResponse with FormData as response body', () => { + http.get('/', () => { + return new HttpResponse(new FormData()) + }) +}) + +it('returns HttpResponse with ReadableStream as response body', () => { + http.get('/', () => { + return new HttpResponse(new ReadableStream()) + }) +}) + +it('returns HttpResponse with Blob as response body', () => { + http.get('/', () => { + return new HttpResponse(new Blob(['hello'])) + }) +}) + +it('returns HttpResponse with ArrayBuffer as response body', () => { + http.get('/', () => { + return new HttpResponse(new ArrayBuffer(5)) + }) +}) + +it('supports null as a response body generic argument', () => { + http.get('/', () => { + return new HttpResponse() + }) + http.get('/', () => { + return new HttpResponse( + // @ts-expect-error Expected null, got a string. + 'hello', + ) + }) + http.get('/', () => { + return HttpResponse.json( + // @ts-expect-error Expected null, got an object. + { id: 1 }, + ) + }) +}) + it('supports string as a response body generic argument', () => { http.get('/', ({ request }) => { if (request.headers.has('x-foo')) { From 0428383e7111917709a8fca23c9cea905dfb8d0e Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sat, 30 Mar 2024 11:14:48 +0100 Subject: [PATCH 2/3] fix(RequestHandler): remove XMLHttpRequestInit types --- src/core/handlers/RequestHandler.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/core/handlers/RequestHandler.ts b/src/core/handlers/RequestHandler.ts index 38da00109..d9d920580 100644 --- a/src/core/handlers/RequestHandler.ts +++ b/src/core/handlers/RequestHandler.ts @@ -13,11 +13,6 @@ export type DefaultRequestMultipartBody = Record< export type DefaultBodyType = | Record | DefaultRequestMultipartBody - | URLSearchParams - | ReadableStream - | FormData - | BufferSource - | Blob | string | number | boolean From f409490c86d8cb745042ec78c9a7fda67c9842a4 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sat, 30 Mar 2024 11:23:33 +0100 Subject: [PATCH 3/3] fix(HttpResponse): define "bodyType" symbol --- src/core/HttpResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/HttpResponse.ts b/src/core/HttpResponse.ts index 15f9fd915..fe429fbb1 100644 --- a/src/core/HttpResponse.ts +++ b/src/core/HttpResponse.ts @@ -9,7 +9,7 @@ export interface HttpResponseInit extends ResponseInit { type?: ResponseType } -declare const bodyType: unique symbol +const bodyType: unique symbol = Symbol('bodyType') export interface StrictRequest extends Request { json(): Promise