From 91f9ea94a7d63045cc2b25389458c57002a49d2d Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Thu, 31 Jul 2025 18:50:29 +0000 Subject: [PATCH] feat: enhance response matchers with utility functions and type definitions Signed-off-by: GitHub --- .../vitest-response-matchers/src/client.ts | 4 +- .../src/matchers/to-have-body.ts | 14 +++---- .../src/matchers/to-have-cookies.ts | 37 ++++++++++++++++--- .../src/matchers/to-have-header.ts | 8 ++-- .../src/matchers/to-have-json-body.ts | 9 ++--- .../src/matchers/to-have-status-text.ts | 12 ++---- .../src/matchers/to-have-status.ts | 10 ++--- .../matchers/to-have-strict-status-text.ts | 10 ++--- .../src/matchers/to-have-text-body.ts | 9 ++--- .../src/matchers/to-match-response.ts | 10 ++--- .../src/matchers/to-throw-response.ts | 5 +-- .../src/matchers/{matcher.ts => types.ts} | 0 .../vitest-response-matchers/src/utils.ts | 11 ++++++ 13 files changed, 74 insertions(+), 65 deletions(-) rename packages/vitest-response-matchers/src/matchers/{matcher.ts => types.ts} (100%) create mode 100644 packages/vitest-response-matchers/src/utils.ts diff --git a/packages/vitest-response-matchers/src/client.ts b/packages/vitest-response-matchers/src/client.ts index 49c6181..c1c22c2 100644 --- a/packages/vitest-response-matchers/src/client.ts +++ b/packages/vitest-response-matchers/src/client.ts @@ -11,7 +11,9 @@ declare namespace matchers { toHaveStrictStatusText(): R; toHaveTextBody(expected: string | null): R; toMatchResponse(expected: Response | ResponseInit): R; - toThrowResponse: (expected: Response | ResponseInit) => R; + toThrowResponse( + expected: Response | { status: number; statusText: string }, + ): R; } } diff --git a/packages/vitest-response-matchers/src/matchers/to-have-body.ts b/packages/vitest-response-matchers/src/matchers/to-have-body.ts index f21ecf7..d9d6ced 100644 --- a/packages/vitest-response-matchers/src/matchers/to-have-body.ts +++ b/packages/vitest-response-matchers/src/matchers/to-have-body.ts @@ -1,16 +1,12 @@ -import type { MatcherResult } from "./matcher"; +import { verifyResponse } from "#src/utils.ts"; +import type { MatcherResult } from "./types"; export function toHaveBody(response: Response): MatcherResult { - if (!(response instanceof Response)) { - return { - message: () => `Expected a Response, but received ${typeof response}`, - pass: false, - }; - } + verifyResponse(response); return { message: () => `Expected response to have body`, - pass: response.body !== null, + pass: typeof response.body !== "undefined", }; } @@ -24,7 +20,7 @@ if (import.meta.vitest) { expect(response).toHaveBody(); }); - it.fails("fails when body is null", () => { + it("allows body to be null", () => { let response = new Response(null); expect(response).toHaveBody(); }); diff --git a/packages/vitest-response-matchers/src/matchers/to-have-cookies.ts b/packages/vitest-response-matchers/src/matchers/to-have-cookies.ts index 89ea7be..2b667b5 100644 --- a/packages/vitest-response-matchers/src/matchers/to-have-cookies.ts +++ b/packages/vitest-response-matchers/src/matchers/to-have-cookies.ts @@ -1,18 +1,36 @@ +import { getHeaders } from "#src/utils.ts"; import { SetCookie } from "@mjackson/headers"; -import type { MatcherResult } from "./matcher"; +import type { MatcherResult } from "./types"; export function toHaveCookies( - response: Response | ResponseInit, + response: Response | { headers?: HeadersInit }, cookies: Array, options?: { strict?: boolean }, ): MatcherResult { - let headers = - response.headers instanceof Headers - ? response.headers - : new Headers(response.headers); + let headers = getHeaders(response); let responseCookies = headers.get("set-cookie"); + if (!responseCookies && cookies.length > 0) { + return { + pass: false, + message: () => { + return `Expected "Set-Cookie" header to be present, but it was not found`; + }, + actual: headers.get("set-cookie"), + expected: cookies, + }; + } + + if (!responseCookies && cookies.length === 0) { + return { + pass: true, + message: () => `Expected no cookies, and none were found.`, + actual: [], + expected: cookies, + }; + } + if (!responseCookies) { return { pass: false, @@ -57,6 +75,13 @@ if (import.meta.vitest) { expect(response).toHaveCookies(["sessionId=abc123; Path=/"]); }); + it.each([new Response("Hello, world!"), {}])( + "passes when no cookies are expected", + (responseOrResponseInit) => { + expect(responseOrResponseInit).toHaveCookies([]); + }, + ); + it("toHaveCookies matcher with inline headers", () => { expect({ headers: { "Set-Cookie": "sessionId=abc123; Path=/" }, diff --git a/packages/vitest-response-matchers/src/matchers/to-have-header.ts b/packages/vitest-response-matchers/src/matchers/to-have-header.ts index 8141bb3..fbc50e0 100644 --- a/packages/vitest-response-matchers/src/matchers/to-have-header.ts +++ b/packages/vitest-response-matchers/src/matchers/to-have-header.ts @@ -1,14 +1,12 @@ -import type { MatcherResult } from "./matcher"; +import { getHeaders } from "#src/utils.ts"; +import type { MatcherResult } from "./types"; export function toHaveHeader( response: Response | ResponseInit, header: string, expected?: string, ): MatcherResult { - let headers = - response.headers instanceof Headers - ? response.headers - : new Headers(response.headers); + let headers = getHeaders(response); if (expected == undefined) { return { diff --git a/packages/vitest-response-matchers/src/matchers/to-have-json-body.ts b/packages/vitest-response-matchers/src/matchers/to-have-json-body.ts index 90eadd2..e5196fb 100644 --- a/packages/vitest-response-matchers/src/matchers/to-have-json-body.ts +++ b/packages/vitest-response-matchers/src/matchers/to-have-json-body.ts @@ -1,13 +1,10 @@ +import { verifyResponse } from "#src/utils.ts"; + export async function toHaveJsonBody( response: Response, expected: object | null, ) { - if (!(response instanceof Response)) { - return { - message: () => `Expected a Response, but received ${typeof response}`, - pass: false, - }; - } + verifyResponse(response); let body = await response.clone().json(); diff --git a/packages/vitest-response-matchers/src/matchers/to-have-status-text.ts b/packages/vitest-response-matchers/src/matchers/to-have-status-text.ts index d3f1ec2..54f7476 100644 --- a/packages/vitest-response-matchers/src/matchers/to-have-status-text.ts +++ b/packages/vitest-response-matchers/src/matchers/to-have-status-text.ts @@ -1,15 +1,11 @@ -import type { MatcherResult } from "./matcher"; +import { verifyResponse } from "#src/utils.ts"; +import type { MatcherResult } from "./types"; export function toHaveStatusText( response: Response, - statusText?: string, + statusText: string, ): MatcherResult { - if (!(response instanceof Response)) { - return { - message: () => `Expected a Response, but received ${typeof response}`, - pass: false, - }; - } + verifyResponse(response); if (typeof statusText === "undefined") { return { diff --git a/packages/vitest-response-matchers/src/matchers/to-have-status.ts b/packages/vitest-response-matchers/src/matchers/to-have-status.ts index fb43857..5bdeb40 100644 --- a/packages/vitest-response-matchers/src/matchers/to-have-status.ts +++ b/packages/vitest-response-matchers/src/matchers/to-have-status.ts @@ -1,15 +1,11 @@ -import type { MatcherResult } from "./matcher"; +import { verifyResponse } from "#src/utils.ts"; +import type { MatcherResult } from "./types"; export function toHaveStatus( response: Response, status?: number, ): MatcherResult { - if (!(response instanceof Response)) { - return { - message: () => `Expected a Response, but received ${typeof response}`, - pass: false, - }; - } + verifyResponse(response); if (typeof status === "undefined") { status = 200; diff --git a/packages/vitest-response-matchers/src/matchers/to-have-strict-status-text.ts b/packages/vitest-response-matchers/src/matchers/to-have-strict-status-text.ts index b975bbd..1069041 100644 --- a/packages/vitest-response-matchers/src/matchers/to-have-strict-status-text.ts +++ b/packages/vitest-response-matchers/src/matchers/to-have-strict-status-text.ts @@ -1,13 +1,9 @@ +import { verifyResponse } from "#src/utils.ts"; import { STATUS_CODES } from "node:http"; -import type { MatcherResult } from "./matcher"; +import type { MatcherResult } from "./types"; export function toHaveStrictStatusText(response: Response): MatcherResult { - if (!(response instanceof Response)) { - return { - message: () => `Expected a Response, but received ${typeof response}`, - pass: false, - }; - } + verifyResponse(response); let found = STATUS_CODES[response.status]; diff --git a/packages/vitest-response-matchers/src/matchers/to-have-text-body.ts b/packages/vitest-response-matchers/src/matchers/to-have-text-body.ts index dd43634..b26ee2b 100644 --- a/packages/vitest-response-matchers/src/matchers/to-have-text-body.ts +++ b/packages/vitest-response-matchers/src/matchers/to-have-text-body.ts @@ -1,13 +1,10 @@ +import { verifyResponse } from "#src/utils.ts"; + export async function toHaveTextBody( response: Response, expected: string | null, ) { - if (!(response instanceof Response)) { - return { - message: () => `Expected a Response, but received ${typeof response}`, - pass: false, - }; - } + verifyResponse(response); let body = await response.clone().text(); diff --git a/packages/vitest-response-matchers/src/matchers/to-match-response.ts b/packages/vitest-response-matchers/src/matchers/to-match-response.ts index e00e0cd..3d017ab 100644 --- a/packages/vitest-response-matchers/src/matchers/to-match-response.ts +++ b/packages/vitest-response-matchers/src/matchers/to-match-response.ts @@ -1,15 +1,11 @@ -import type { MatcherResult } from "./matcher"; +import { verifyResponse } from "#src/utils.ts"; +import type { MatcherResult } from "./types"; export function toMatchResponse( received: Response, expected: { status: number; statusText: string }, ): MatcherResult { - if (!(received instanceof Response)) { - return { - message: () => `Expected a Response, but received ${typeof received}`, - pass: false, - }; - } + verifyResponse(received); return { message() { diff --git a/packages/vitest-response-matchers/src/matchers/to-throw-response.ts b/packages/vitest-response-matchers/src/matchers/to-throw-response.ts index fe6b4cb..e9362ef 100644 --- a/packages/vitest-response-matchers/src/matchers/to-throw-response.ts +++ b/packages/vitest-response-matchers/src/matchers/to-throw-response.ts @@ -1,4 +1,4 @@ -import type { MatcherResult } from "./matcher"; +import type { MatcherResult } from "./types"; export function toThrowResponse( received: () => Response, @@ -8,7 +8,7 @@ export function toThrowResponse( try { received(); - } catch (e: unknown) { + } catch (e) { error = e; } @@ -27,7 +27,6 @@ export function toThrowResponse( return { message: () => `Expected to throw a Response`, pass: - error instanceof Response && error.status === expectedResponse.status && error.statusText === expectedResponse.statusText, actual: { status: error.status, statusText: error.statusText }, diff --git a/packages/vitest-response-matchers/src/matchers/matcher.ts b/packages/vitest-response-matchers/src/matchers/types.ts similarity index 100% rename from packages/vitest-response-matchers/src/matchers/matcher.ts rename to packages/vitest-response-matchers/src/matchers/types.ts diff --git a/packages/vitest-response-matchers/src/utils.ts b/packages/vitest-response-matchers/src/utils.ts new file mode 100644 index 0000000..b4b654a --- /dev/null +++ b/packages/vitest-response-matchers/src/utils.ts @@ -0,0 +1,11 @@ +export function getHeaders(response: Response | { headers?: HeadersInit }) { + if (response.headers instanceof Headers) return response.headers; + return new Headers(response.headers); +} + +export function verifyResponse(response: unknown) { + return { + message: () => `Expected a Response, but received ${typeof response}`, + pass: !(response instanceof Response), + }; +}