From e89573e81443c79771b5f5706295648fd25945cc Mon Sep 17 00:00:00 2001 From: Ville Penttinen Date: Fri, 29 Mar 2024 15:19:13 +0200 Subject: [PATCH] replace vitest-fetch-mock with msw (#1592) * replace vitest-fetch-mock with msw in index.test This replaces vitest-fetch-mock with msw in the index.test.ts file. * Rename getCookies to getRequestCookies * Rename testPath to toAbsoluteURL * Rename TestRequestHandler to MockRequestHandler * Move msw utils to separate fixture * Update v7-beta.test.ts to utilize msw * Update index.bench.js to utilize msw Additionally ensures all clients are setup correctly for GET tests. * remove vitest-fetch-mock dependency * Update testing.md to include short example of msw * Fix failing test * Add MultipleResponse example to api.yaml and run generate-types * Include 'use the selected content' in v7-beta.test.ts --- docs/openapi-fetch/testing.md | 48 + packages/openapi-fetch/package.json | 4 +- packages/openapi-fetch/test/fixtures/api.d.ts | 2 + packages/openapi-fetch/test/fixtures/api.yaml | 33 + .../test/fixtures/mock-server.ts | 124 +++ .../openapi-fetch/test/fixtures/v7-beta.d.ts | 44 + packages/openapi-fetch/test/index.bench.js | 73 +- packages/openapi-fetch/test/index.test.ts | 811 +++++++++++----- packages/openapi-fetch/test/v7-beta.test.ts | 901 +++++++++++++----- pnpm-lock.yaml | 350 ++++++- 10 files changed, 1866 insertions(+), 524 deletions(-) create mode 100644 packages/openapi-fetch/test/fixtures/mock-server.ts diff --git a/docs/openapi-fetch/testing.md b/docs/openapi-fetch/testing.md index 4b1c8b1cc..3e5e31167 100644 --- a/docs/openapi-fetch/testing.md +++ b/docs/openapi-fetch/testing.md @@ -65,3 +65,51 @@ test("my API call", async () => { expect(error).toBeUndefined(); }); ``` + +## Mock Service Worker + +[Mock Service Worker](https://mswjs.io/) can also be used for testing and mocking actual responses: + +```ts +import { http, HttpResponse } from "msw"; +import { setupServer } from "msw/node"; +import createClient from "openapi-fetch"; +import { afterEach, beforeAll, expect, test } from "vitest"; +import type { paths } from "./api/v1"; + +const server = setupServer(); + +beforeAll(() => { + // NOTE: server.listen must be called before `createClient` is used to ensure + // the msw can inject its version of `fetch` to intercept the requests. + server.listen({ + onUnhandledRequest: (request) => { + throw new Error( + `No request handler found for ${request.method} ${request.url}`, + ); + }, + }); +}); + +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +test("my API call", async () => { + const rawData = { test: { data: "foo" } }; + + const BASE_URL = "https://my-site.com"; + + server.use( + http.get(`${BASE_URL}/api/v1/foo`, () => HttpResponse.json(rawData, { status: 200 })) + ); + + const client = createClient({ + baseUrl: BASE_URL, + }); + + const { data, error } = await client.GET("/api/v1/foo"); + + expect(data).toEqual(rawData); + expect(error).toBeUndefined(); +}); +``` diff --git a/packages/openapi-fetch/package.json b/packages/openapi-fetch/package.json index caedd15f6..9500cc23f 100644 --- a/packages/openapi-fetch/package.json +++ b/packages/openapi-fetch/package.json @@ -69,12 +69,12 @@ "axios": "^1.6.7", "del-cli": "^5.1.0", "esbuild": "^0.20.0", + "msw": "^2.2.3", "openapi-typescript": "^6.7.4", "openapi-typescript-codegen": "^0.25.0", "openapi-typescript-fetch": "^1.1.3", "superagent": "^8.1.2", "typescript": "^5.3.3", - "vitest": "^1.3.1", - "vitest-fetch-mock": "^0.2.2" + "vitest": "^1.3.1" } } diff --git a/packages/openapi-fetch/test/fixtures/api.d.ts b/packages/openapi-fetch/test/fixtures/api.d.ts index 7131c0e31..1ab3518ac 100644 --- a/packages/openapi-fetch/test/fixtures/api.d.ts +++ b/packages/openapi-fetch/test/fixtures/api.d.ts @@ -3,6 +3,7 @@ * Do not make direct changes to the file. */ + export interface paths { "/comment": { put: { @@ -577,6 +578,7 @@ export type $defs = Record; export type external = Record; export interface operations { + getHeaderParams: { parameters: { header: { diff --git a/packages/openapi-fetch/test/fixtures/api.yaml b/packages/openapi-fetch/test/fixtures/api.yaml index f104581b2..18afb82da 100644 --- a/packages/openapi-fetch/test/fixtures/api.yaml +++ b/packages/openapi-fetch/test/fixtures/api.yaml @@ -447,6 +447,11 @@ paths: responses: 200: $ref: '#/components/responses/Contact' + /multiple-response-content: + get: + responses: + 200: + $ref: '#/components/responses/MultipleResponse' components: schemas: Post: @@ -672,3 +677,31 @@ components: application/json: schema: $ref: '#/components/schemas/User' + MultipleResponse: + content: + application/json: + schema: + type: object + properties: + id: + type: string + email: + type: string + name: + type: string + required: + - id + - email + application/ld+json: + schema: + type: object + properties: + '@id': + type: string + email: + type: string + name: + type: string + required: + - '@id' + - email diff --git a/packages/openapi-fetch/test/fixtures/mock-server.ts b/packages/openapi-fetch/test/fixtures/mock-server.ts new file mode 100644 index 000000000..35fbd228d --- /dev/null +++ b/packages/openapi-fetch/test/fixtures/mock-server.ts @@ -0,0 +1,124 @@ +import { + http, + HttpResponse, + type JsonBodyType, + type StrictRequest, + type DefaultBodyType, + type HttpResponseResolver, + type PathParams, + type AsyncResponseResolverReturnType, +} from "msw"; +import { setupServer } from "msw/node"; + +/** + * Mock server instance + */ +export const server = setupServer(); + +/** + * Default baseUrl for tests + */ +export const baseUrl = "https://api.example.com" as const; + +/** + * Test path helper, returns a an absolute URL based on + * the given path and base + */ +export function toAbsoluteURL(path: string, base: string = baseUrl) { + // If we have absolute path + if (URL.canParse(path)) { + return new URL(path).toString(); + } + + // Otherwise we want to support relative paths + // where base may also contain some part of the path + // e.g. + // base = https://api.foo.bar/v1/ + // path = /self + // should result in https://api.foo.bar/v1/self + + // Construct base URL + const baseUrlInstance = new URL(base); + + // prepend base url url pathname to path and ensure only one slash between the URL parts + const newPath = `${baseUrlInstance.pathname}/${path}`.replace(/\/+/g, "/"); + + return new URL(newPath, baseUrlInstance).toString(); +} + +export type MswHttpMethod = keyof typeof http; + +export interface MockRequestHandlerOptions< + // Recreate the generic signature of the HTTP resolver + // so the arguments passed to http handlers propagate here. + Params extends PathParams = PathParams, + RequestBodyType extends DefaultBodyType = DefaultBodyType, + ResponseBodyType extends DefaultBodyType = undefined, +> { + baseUrl?: string; + method: MswHttpMethod; + /** + * Relative or absolute path to match. + * When relative, baseUrl will be used as base. + */ + path: string; + body?: JsonBodyType; + headers?: Record; + status?: number; + + /** + * Optional handler which will be called instead of using the body, headers and status + */ + handler?: HttpResponseResolver; +} + +/** + * Configures a msw request handler using the provided options. + */ +export function useMockRequestHandler< + // Recreate the generic signature of the HTTP resolver + // so the arguments passed to http handlers propagate here. + Params extends PathParams = PathParams, + RequestBodyType extends DefaultBodyType = DefaultBodyType, + ResponseBodyType extends DefaultBodyType = undefined, +>({ + baseUrl: requestBaseUrl, + method, + path, + body, + headers, + status, + handler, +}: MockRequestHandlerOptions) { + let requestUrl = ""; + let receivedRequest: null | StrictRequest = null; + let receivedCookies: null | Record = null; + + const resolvedPath = toAbsoluteURL(path, requestBaseUrl); + + server.use( + http[method]( + resolvedPath, + (args) => { + requestUrl = args.request.url; + receivedRequest = args.request.clone(); + receivedCookies = { ...args.cookies }; + + if (handler) { + return handler(args); + } + + return HttpResponse.json(body, { + status: status ?? 200, + headers, + }) as AsyncResponseResolverReturnType; + }, + ), + ); + + return { + getRequestCookies: () => receivedCookies!, + getRequest: () => receivedRequest!, + getRequestUrl: () => new URL(requestUrl), + }; +} diff --git a/packages/openapi-fetch/test/fixtures/v7-beta.d.ts b/packages/openapi-fetch/test/fixtures/v7-beta.d.ts index 9eaf40817..0c6872c15 100644 --- a/packages/openapi-fetch/test/fixtures/v7-beta.d.ts +++ b/packages/openapi-fetch/test/fixtures/v7-beta.d.ts @@ -729,6 +729,33 @@ export interface paths { patch?: never; trace?: never; }; + "/multiple-response-content": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["MultipleResponse"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -859,6 +886,23 @@ export interface components { "application/json": components["schemas"]["User"]; }; }; + MultipleResponse: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + email: string; + name?: string; + }; + "application/ld+json": { + "@id": string; + email: string; + name?: string; + }; + }; + }; }; parameters: never; requestBodies: { diff --git a/packages/openapi-fetch/test/index.bench.js b/packages/openapi-fetch/test/index.bench.js index 27950319d..e5bc8b2c0 100644 --- a/packages/openapi-fetch/test/index.bench.js +++ b/packages/openapi-fetch/test/index.bench.js @@ -1,34 +1,58 @@ import axios from "axios"; +import { http, HttpResponse } from "msw"; +import { setupServer } from "msw/node"; import { Fetcher } from "openapi-typescript-fetch"; import superagent from "superagent"; -import { afterAll, bench, describe, vi } from "vitest"; -import createFetchMock from "vitest-fetch-mock"; +import { afterAll, bench, describe } from "vitest"; import createClient from "../dist/index.js"; import * as openapiTSCodegen from "./fixtures/openapi-typescript-codegen.min.js"; const BASE_URL = "https://api.test.local"; -const fetchMocker = createFetchMock(vi); +const server = setupServer( + http.get(`${BASE_URL}/url`, () => + HttpResponse.json({ + message: "success", + }), + ), + + // Used by openapi-typescript-codegen + http.get("https://api.github.com/repos/test1/test2/pulls/test3", () => + HttpResponse.json({ + message: "success", + }), + ), +); + +// Ensure we are listening early enough so all the requests are intercepted +server.listen({ + onUnhandledRequest: (request) => { + throw new Error( + `No request handler found for ${request.method} ${request.url}`, + ); + }, +}); -fetchMocker.enableMocks(); -fetchMocker.mockResponse("{}"); afterAll(() => { - fetchMocker.resetMocks(); + server.close(); }); describe("setup", () => { bench("openapi-fetch", async () => { - createClient(); + createClient({ baseUrl: BASE_URL }); }); bench("openapi-typescript-fetch", async () => { const fetcher = Fetcher.for(); + fetcher.configure({ + baseUrl: BASE_URL, + }); fetcher.path("/pet/findByStatus").method("get").create(); }); bench("axios", async () => { axios.create({ - baseURL: "https://api.test.local", + baseURL: BASE_URL, }); }); @@ -39,10 +63,14 @@ describe("get (only URL)", () => { let openapiFetch = createClient({ baseUrl: BASE_URL }); let openapiTSFetch = Fetcher.for(); openapiTSFetch.configure({ - init: { baseUrl: BASE_URL }, + baseUrl: BASE_URL, }); let openapiTSFetchGET = openapiTSFetch.path("/url").method("get").create(); + let axiosInstance = axios.create({ + baseURL: BASE_URL, + }); + bench("openapi-fetch", async () => { await openapiFetch.GET("/url"); }); @@ -52,19 +80,15 @@ describe("get (only URL)", () => { }); bench("openapi-typescript-codegen", async () => { - await openapiTSCodegen.PullsService.pullsGet(); + await openapiTSCodegen.PullsService.pullsGet("test1", "test2", "test3"); }); bench("axios", async () => { - await axios.get("/url", { - async adapter() { - return { data: {} }; - }, - }); + await axiosInstance.get("/url"); }); bench("superagent", async () => { - await superagent.get("/url").end(); + await superagent.get(`${BASE_URL}/url`); }); }); @@ -75,10 +99,15 @@ describe("get (headers)", () => { }); let openapiTSFetch = Fetcher.for(); openapiTSFetch.configure({ - init: { baseUrl: BASE_URL, headers: { "x-base-header": 123 } }, + baseUrl: BASE_URL, + init: { headers: { "x-base-header": 123 } }, }); let openapiTSFetchGET = openapiTSFetch.path("/url").method("get").create(); + let axiosInstance = axios.create({ + baseURL: BASE_URL, + }); + bench("openapi-fetch", async () => { await openapiFetch.GET("/url", { headers: { "x-header-1": 123, "x-header-2": 456 }, @@ -92,15 +121,12 @@ describe("get (headers)", () => { }); bench("openapi-typescript-codegen", async () => { - await openapiTSCodegen.PullsService.pullsGet(); + await openapiTSCodegen.PullsService.pullsGet("test1", "test2", "test3"); }); bench("axios", async () => { - await axios.get(`${BASE_URL}/url`, { + await axiosInstance.get("/url", { headers: { "x-header-1": 123, "x-header-2": 456 }, - async adapter() { - return { data: {} }; - }, }); }); @@ -108,7 +134,6 @@ describe("get (headers)", () => { await superagent .get(`${BASE_URL}/url`) .set("x-header-1", 123) - .set("x-header-2", 456) - .end(); + .set("x-header-2", 456); }); }); diff --git a/packages/openapi-fetch/test/index.test.ts b/packages/openapi-fetch/test/index.test.ts index 66ab694a0..827644688 100644 --- a/packages/openapi-fetch/test/index.test.ts +++ b/packages/openapi-fetch/test/index.test.ts @@ -1,34 +1,30 @@ -// @ts-expect-error -import createFetchMock from "vitest-fetch-mock"; +import { HttpResponse, type StrictResponse } from "msw"; import createClient, { type Middleware, type MiddlewareRequest, type QuerySerializerOptions, } from "../src/index.js"; import type { paths } from "./fixtures/api.js"; - -const fetchMocker = createFetchMock(vi); +import { + server, + baseUrl, + useMockRequestHandler, + toAbsoluteURL, +} from "./fixtures/mock-server.js"; beforeAll(() => { - fetchMocker.enableMocks(); -}); -afterEach(() => { - fetchMocker.resetMocks(); + server.listen({ + onUnhandledRequest: (request) => { + throw new Error( + `No request handler found for ${request.method} ${request.url}`, + ); + }, + }); }); -interface MockResponse { - headers?: Record; - status: number; - body: any; -} - -function mockFetch(res: MockResponse) { - fetchMocker.mockResponse(() => res); -} +afterEach(() => server.resetHandlers()); -function mockFetchOnce(res: MockResponse) { - fetchMocker.mockResponseOnce(() => res); -} +afterAll(() => server.close()); describe("client", () => { it("generates all proper functions", () => { @@ -46,13 +42,19 @@ describe("client", () => { describe("TypeScript checks", () => { it("marks data or error as undefined, but never both", async () => { - const client = createClient(); + const client = createClient({ + baseUrl, + }); // data - mockFetchOnce({ + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/string-array", status: 200, - body: JSON.stringify(["one", "two", "three"]), + body: ["one", "two", "three"], }); + const dataRes = await client.GET("/string-array"); // … is initially possibly undefined @@ -71,9 +73,12 @@ describe("client", () => { } // error - mockFetchOnce({ + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/string-array", status: 500, - body: JSON.stringify({ code: 500, message: "Something went wrong" }), + body: { code: 500, message: "Something went wrong" }, }); const errorRes = await client.GET("/string-array"); @@ -97,9 +102,16 @@ describe("client", () => { describe("path", () => { it("typechecks", async () => { const client = createClient({ - baseUrl: "https://myapi.com/v1", + baseUrl, + }); + + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + status: 200, + body: { message: "OK" }, }); - mockFetch({ status: 200, body: JSON.stringify({ message: "OK" }) }); // expect error on missing 'params' // @ts-expect-error @@ -119,23 +131,43 @@ describe("client", () => { params: { path: { post_id: 1234 } }, }); + // expect error on unknown property in 'params' + await client.GET("/blogposts/{post_id}", { + // @ts-expect-error + TODO: "this should be an error", + }); + // (no error) + let calledPostId = ""; + useMockRequestHandler<{ post_id: string }>({ + baseUrl, + method: "get", + path: `/blogposts/:post_id`, + handler: ({ params }) => { + calledPostId = params.post_id; + return HttpResponse.json({ message: "OK" }, { status: 200 }); + }, + }); + await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "1234" } }, }); // expect param passed correctly - const lastCall = - fetchMocker.mock.calls[fetchMocker.mock.calls.length - 1]; - expect(lastCall[0].url).toBe("https://myapi.com/v1/blogposts/1234"); + expect(calledPostId).toBe("1234"); }); it("serializes", async () => { - const client = createClient(); - mockFetch({ - status: 200, - body: JSON.stringify({ status: "success" }), + const client = createClient({ + baseUrl, }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: `/path-params/*`, + }); + await client.GET( "/path-params/{simple_primitive}/{simple_obj_flat}/{simple_arr_flat}/{simple_obj_explode*}/{simple_arr_explode*}/{.label_primitive}/{.label_obj_flat}/{.label_arr_flat}/{.label_obj_explode*}/{.label_arr_explode*}/{;matrix_primitive}/{;matrix_obj_flat}/{;matrix_arr_flat}/{;matrix_obj_explode*}/{;matrix_arr_explode*}", { @@ -161,8 +193,7 @@ describe("client", () => { }, ); - const reqURL = fetchMocker.mock.calls[0][0].url; - expect(reqURL).toBe( + expect(getRequestUrl().pathname).toBe( `/path-params/${[ // simple "simple", @@ -187,24 +218,49 @@ describe("client", () => { }); it("allows UTF-8 characters", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/*", + }); + await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "post?id = 🥴" } }, }); // expect post_id to be encoded properly - expect(fetchMocker.mock.calls[0][0].url).toBe( - `/blogposts/post?id%20=%20🥴`, + const url = getRequestUrl(); + expect(url.searchParams.get("id ")).toBe(" 🥴"); + expect(url.pathname + url.search).toBe( + `/blogposts/post?id%20=%20%F0%9F%A5%B4`, ); }); }); it("header", async () => { - const client = createClient({ baseUrl: "https://myapi.com/v1" }); - mockFetch({ status: 200, body: JSON.stringify({ status: "success" }) }); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/header-params", + handler: ({ request }) => { + const header = request.headers.get("x-required-header"); + if (header !== "correct") { + return HttpResponse.json( + { code: 500, message: "missing correct header" }, + { status: 500 }, + ) as StrictResponse; + } + return HttpResponse.json( + { status: header }, + { status: 200, headers: request.headers }, + ); + }, + }); - // expet error on missing header + // expect error on missing header // @ts-expect-error await client.GET("/header-params"); @@ -221,54 +277,76 @@ describe("client", () => { }); // (no error) - await client.GET("/header-params", { + const response = await client.GET("/header-params", { params: { header: { "x-required-header": "correct" } }, }); // expect param passed correctly - const lastCall = - fetchMocker.mock.calls[fetchMocker.mock.calls.length - 1][0]; - expect(lastCall.headers.get("x-required-header")).toBe("correct"); + expect(response.response.headers.get("x-required-header")).toBe( + "correct", + ); }); describe("query", () => { describe("querySerializer", () => { it("primitives", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/query-params*", + }); + await client.GET("/query-params", { params: { query: { string: "string", number: 0, boolean: false }, }, }); - expect(fetchMocker.mock.calls[0][0].url).toBe( - "/query-params?string=string&number=0&boolean=false", + expect(getRequestUrl().search).toBe( + "?string=string&number=0&boolean=false", ); }); it("array params (empty)", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/query-params*", + }); + await client.GET("/query-params", { params: { query: { array: [] }, }, }); - expect(fetchMocker.mock.calls[0][0].url).toBe("/query-params"); + const url = getRequestUrl(); + expect(url.pathname).toBe("/query-params"); + expect(url.search).toBe(""); }); it("empty/null params", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/query-params*", + }); + await client.GET("/query-params", { params: { query: { string: undefined, number: null as any }, }, }); - expect(fetchMocker.mock.calls[0][0].url).toBe("/query-params"); + const url = getRequestUrl(); + expect(url.pathname).toBe("/query-params"); + expect(url.search).toBe(""); }); describe("array", () => { @@ -323,16 +401,25 @@ describe("client", () => { }, ][])("%s", async (_, { given, want }) => { const client = createClient({ + baseUrl, querySerializer: { array: given }, }); - mockFetch({ status: 200, body: "{}" }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/query-params*", + }); + await client.GET("/query-params", { params: { query: { array: ["1", "2", "3"], boolean: true }, }, }); - expect(fetchMocker.mock.calls[0][0].url.split("?")[1]).toBe(want); + const url = getRequestUrl(); + // skip leading '?' + expect(url.search.substring(1)).toBe(want); }); }); @@ -374,24 +461,37 @@ describe("client", () => { }, ][])("%s", async (_, { given, want }) => { const client = createClient({ + baseUrl, querySerializer: { object: given }, }); - mockFetch({ status: 200, body: "{}" }); + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/query-params*", + }); + await client.GET("/query-params", { params: { query: { object: { foo: "bar", bar: "baz" }, boolean: true }, }, }); - expect(fetchMocker.mock.calls[0][0].url.split("?")[1]).toBe(want); + const url = getRequestUrl(); + // skip leading '?' + expect(url.search.substring(1)).toBe(want); }); }); it("allowReserved", async () => { const client = createClient({ + baseUrl, querySerializer: { allowReserved: true }, }); - mockFetch({ status: 200, body: "{}" }); + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/query-params*", + }); await client.GET("/query-params", { params: { query: { @@ -399,8 +499,12 @@ describe("client", () => { }, }, }); - expect(fetchMocker.mock.calls[0][0].url.split("?")[1]).toBe( - "string=bad/character🐶", + + expect(getRequestUrl().search).toBe( + "?string=bad/character%F0%9F%90%B6", + ); + expect(getRequestUrl().searchParams.get("string")).toBe( + "bad/character🐶", ); await client.GET("/query-params", { @@ -413,17 +517,28 @@ describe("client", () => { allowReserved: false, }, }); - expect(fetchMocker.mock.calls[1][0].url.split("?")[1]).toBe( - "string=bad%2Fcharacter%F0%9F%90%B6", + + expect(getRequestUrl().search).toBe( + "?string=bad%2Fcharacter%F0%9F%90%B6", + ); + expect(getRequestUrl().searchParams.get("string")).toBe( + "bad/character🐶", ); }); describe("function", () => { it("global default", async () => { const client = createClient({ + baseUrl, querySerializer: (q) => `alpha=${q.version}&beta=${q.format}`, }); - mockFetchOnce({ status: 200, body: "{}" }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + }); + await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "my-post" }, @@ -431,16 +546,24 @@ describe("client", () => { }, }); - expect(fetchMocker.mock.calls[0][0].url).toBe( + const url = getRequestUrl(); + expect(url.pathname + url.search).toBe( "/blogposts/my-post?alpha=2&beta=json", ); }); it("per-request", async () => { const client = createClient({ + baseUrl, querySerializer: () => "query", }); - mockFetchOnce({ status: 200, body: "{}" }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + }); + await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "my-post" }, @@ -449,7 +572,8 @@ describe("client", () => { querySerializer: (q) => `alpha=${q.version}&beta=${q.format}`, }); - expect(fetchMocker.mock.calls[0][0].url).toBe( + const url = getRequestUrl(); + expect(url.pathname + url.search).toBe( "/blogposts/my-post?alpha=2&beta=json", ); }); @@ -457,18 +581,22 @@ describe("client", () => { it("ignores leading ? characters", async () => { const client = createClient({ + baseUrl, querySerializer: () => "?query", }); - mockFetchOnce({ status: 200, body: "{}" }); + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + }); await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "my-post" }, query: { version: 2, format: "json" }, }, }); - expect(fetchMocker.mock.calls[0][0].url).toBe( - "/blogposts/my-post?query", - ); + const url = getRequestUrl(); + expect(url.pathname + url.search).toBe("/blogposts/my-post?query"); }); }); }); @@ -478,8 +606,13 @@ describe("client", () => { // these are pure type tests; no runtime assertions needed /* eslint-disable vitest/expect-expect */ it("requires necessary requestBodies", async () => { - const client = createClient({ baseUrl: "https://myapi.com/v1" }); - mockFetch({ status: 200, body: JSON.stringify({ message: "OK" }) }); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "put", + path: "/blogposts", + }); // expect error on missing `body` // @ts-expect-error @@ -501,8 +634,14 @@ describe("client", () => { }); it("requestBody (inline)", async () => { - mockFetch({ status: 201, body: "{}" }); - const client = createClient(); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "put", + path: "/blogposts-optional-inline", + status: 201, + }); // expect error on wrong body type await client.PUT("/blogposts-optional-inline", { @@ -521,8 +660,14 @@ describe("client", () => { }); it("requestBody with required: false", async () => { - mockFetch({ status: 201, body: "{}" }); - const client = createClient(); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "put", + path: "/blogposts-optional", + status: 201, + }); // assert missing `body` doesn’t raise a TS error await client.PUT("/blogposts-optional"); @@ -546,36 +691,45 @@ describe("client", () => { describe("options", () => { it("baseUrl", async () => { - let client = createClient({ baseUrl: "https://myapi.com/v1" }); - mockFetch({ status: 200, body: JSON.stringify({ message: "OK" }) }); + let client = createClient({ baseUrl }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", + status: 200, + body: { message: "OK" }, + }); + await client.GET("/self"); // assert baseUrl and path mesh as expected - expect(fetchMocker.mock.calls[0][0].url).toBe( - "https://myapi.com/v1/self", - ); + expect(getRequestUrl().href).toBe(toAbsoluteURL("/self")); - client = createClient({ baseUrl: "https://myapi.com/v1/" }); + client = createClient({ baseUrl }); await client.GET("/self"); // assert trailing '/' was removed - expect(fetchMocker.mock.calls[1][0].url).toBe( - "https://myapi.com/v1/self", - ); + expect(getRequestUrl().href).toBe(toAbsoluteURL("/self")); }); describe("headers", () => { it("persist", async () => { const headers: HeadersInit = { Authorization: "Bearer secrettoken" }; - const client = createClient({ headers }); - mockFetchOnce({ + const client = createClient({ headers, baseUrl }); + + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", status: 200, - body: JSON.stringify({ email: "user@user.com" }), + body: { email: "user@user.com" }, }); + await client.GET("/self"); // assert default headers were passed - expect(fetchMocker.mock.calls[0][0].headers).toEqual( + expect(getRequest().headers).toEqual( new Headers({ ...headers, // assert new header got passed "Content-Type": "application/json", // probably doesn’t need to get tested, but this was simpler than writing lots of code to ignore these @@ -585,19 +739,25 @@ describe("client", () => { it("can be overridden", async () => { const client = createClient({ + baseUrl, headers: { "Cache-Control": "max-age=10000000" }, }); - mockFetchOnce({ + + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", status: 200, - body: JSON.stringify({ email: "user@user.com" }), + body: { email: "user@user.com" }, }); + await client.GET("/self", { params: {}, headers: { "Cache-Control": "no-cache" }, }); // assert default headers were passed - expect(fetchMocker.mock.calls[0][0].headers).toEqual( + expect(getRequest().headers).toEqual( new Headers({ "Cache-Control": "no-cache", "Content-Type": "application/json", @@ -607,29 +767,40 @@ describe("client", () => { it("can be unset", async () => { const client = createClient({ + baseUrl, headers: { "Content-Type": null }, }); - mockFetchOnce({ + + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", status: 200, - body: JSON.stringify({ email: "user@user.com" }), + body: { email: "user@user.com" }, }); + await client.GET("/self", { params: {} }); // assert default headers were passed - expect(fetchMocker.mock.calls[0][0].headers).toEqual(new Headers()); + expect(getRequest().headers).toEqual(new Headers()); }); it("supports arrays", async () => { - const client = createClient(); + const client = createClient({ baseUrl }); const list = ["one", "two", "three"]; - mockFetchOnce({ status: 200, body: "{}" }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", + status: 200, + body: {}, + }); + await client.GET("/self", { headers: { list } }); - expect(fetchMocker.mock.calls[0][0].headers.get("list")).toEqual( - list.join(", "), - ); + expect(getRequest().headers.get("list")).toEqual(list.join(", ")); }); }); @@ -647,16 +818,15 @@ describe("client", () => { } const customFetch = createCustomFetch({ works: true }); - mockFetchOnce({ status: 200, body: "{}" }); - const client = createClient({ fetch: customFetch }); + const client = createClient({ fetch: customFetch, baseUrl }); const { data } = await client.GET("/self"); // assert data was returned from custom fetcher expect(data).toEqual({ works: true }); - // assert global fetch was never called - expect(fetchMocker).not.toHaveBeenCalled(); + // TODO: do we need to assert nothing was called? + // msw should throw an error if there was an unused handler }); it("per-request", async () => { @@ -674,9 +844,7 @@ describe("client", () => { const fallbackFetch = createCustomFetch({ fetcher: "fallback" }); const overrideFetch = createCustomFetch({ fetcher: "override" }); - mockFetchOnce({ status: 200, body: "{}" }); - - const client = createClient({ fetch: fallbackFetch }); + const client = createClient({ fetch: fallbackFetch, baseUrl }); // assert override function was called const fetch1 = await client.GET("/self", { fetch: overrideFetch }); @@ -686,16 +854,14 @@ describe("client", () => { const fetch2 = await client.GET("/self"); expect(fetch2.data).toEqual({ fetcher: "fallback" }); - // assert global fetch was never called - expect(fetchMocker).not.toHaveBeenCalled(); + // TODO: do we need to assert nothing was called? + // msw should throw an error if there was an unused handler }); }); describe("middleware", () => { it("can modify request", async () => { - mockFetchOnce({ status: 200, body: "{}" }); - - const client = createClient(); + const client = createClient({ baseUrl }); client.use({ async onRequest(req) { return new Request("https://foo.bar/api/v1", { @@ -705,9 +871,18 @@ describe("client", () => { }); }, }); + + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "options", + path: `https://foo.bar/api/v1`, + status: 200, + body: {}, + }); + await client.GET("/self"); - const req = fetchMocker.mock.calls[0][0]; + const req = getRequest(); expect(req.url).toBe("https://foo.bar/api/v1"); expect(req.method).toBe("OPTIONS"); expect(req.headers.get("foo")).toBe("bar"); @@ -721,13 +896,17 @@ describe("client", () => { created_at: "2024-01-01T00:00:00Z", updated_at: "2024-01-20T00:00:00Z", }; - mockFetchOnce({ + + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", status: 200, - body: JSON.stringify(rawBody), + body: rawBody, headers: { foo: "bar" }, }); - const client = createClient(); + const client = createClient({ baseUrl }); client.use({ // convert date string to unix time async onResponse(res) { @@ -738,7 +917,7 @@ describe("client", () => { headers.set("middleware", "value"); return new Response(JSON.stringify(body), { ...res, - status: 205, + status: 201, headers, }); }, @@ -752,7 +931,7 @@ describe("client", () => { // assert rest of body was preserved expect(data?.email).toBe(rawBody.email); // assert status changed - expect(response.status).toBe(205); + expect(response.status).toBe(201); // assert server headers were preserved expect(response.headers.get("foo")).toBe("bar"); // assert middleware heaers were added @@ -760,9 +939,15 @@ describe("client", () => { }); it("executes in expected order", async () => { - mockFetchOnce({ status: 200, body: "{}" }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", + status: 200, + body: {}, + }); - const client = createClient(); + const client = createClient({ baseUrl }); // this middleware passes along the “step” header // for both requests and responses, but first checks if // it received the end result of the previous middleware step @@ -810,33 +995,44 @@ describe("client", () => { const { response } = await client.GET("/self"); // assert requests ended up on step C (array order) - expect(fetchMocker.mock.calls[0][0].headers.get("step")).toBe("C"); + expect(getRequest().headers.get("step")).toBe("C"); // assert responses ended up on step A (reverse order) expect(response.headers.get("step")).toBe("A"); }); it("receives correct options", async () => { - mockFetchOnce({ status: 200, body: "{}" }); + useMockRequestHandler({ + baseUrl: "https://api.foo.bar/v1/", + method: "get", + path: "/self", + status: 200, + body: {}, + }); - let baseUrl = ""; + let requestBaseUrl = ""; const client = createClient({ baseUrl: "https://api.foo.bar/v1/", }); client.use({ onRequest(_, options) { - baseUrl = options.baseUrl; + requestBaseUrl = options.baseUrl; return undefined; }, }); await client.GET("/self"); - expect(baseUrl).toBe("https://api.foo.bar/v1"); + expect(requestBaseUrl).toBe("https://api.foo.bar/v1"); }); it("receives OpenAPI options passed in from parent", async () => { - mockFetchOnce({ status: 200, body: "{}" }); + useMockRequestHandler({ + method: "put", + path: `https://api.foo.bar/v1/tag*`, + status: 200, + body: {}, + }); const pathname = "/tag/{name}"; const tagData = { @@ -873,7 +1069,13 @@ describe("client", () => { }); it("can be skipped without interrupting request", async () => { - mockFetchOnce({ status: 200, body: JSON.stringify({ success: true }) }); + useMockRequestHandler({ + baseUrl: "https://api.foo.bar/v1/", + method: "get", + path: "/blogposts", + status: 200, + body: { success: true }, + }); const client = createClient({ baseUrl: "https://api.foo.bar/v1/", @@ -889,7 +1091,13 @@ describe("client", () => { }); it("can be ejected", async () => { - mockFetchOnce({ status: 200, body: "{}" }); + useMockRequestHandler({ + baseUrl: "https://api.foo.bar/v1", + method: "get", + path: "/blogposts", + status: 200, + body: { success: true }, + }); let called = false; const errorMiddleware = { @@ -913,8 +1121,14 @@ describe("client", () => { describe("requests", () => { it("multipart/form-data", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: JSON.stringify({ success: true }) }); + const client = createClient({ baseUrl }); + + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "put", + path: "/contact", + }); + const reqBody = { name: "John Doe", email: "test@email.email", @@ -932,31 +1146,45 @@ describe("client", () => { }, }); - // expect post_id to be encoded properly - const req = fetchMocker.mock.calls[0][0]; - // note: this is FormData, but Node.js doesn’t handle new Request() properly with formData bodies. So this is only in tests. - expect(req.body).toBeInstanceOf(Buffer); - expect((req.headers as Headers).get("Content-Type")).toBe( - "text/plain;charset=UTF-8", - ); + // expect request to contain correct headers and body + const req = getRequest(); + expect(req.body).toBeInstanceOf(ReadableStream); + const body = await req.formData(); + expect(body.get("name")).toBe("John Doe"); + expect(req.headers.get("Content-Type")).toMatch(/multipart\/form-data;/); }); - // Node Requests eat credentials (no cookies), but this works in frontend - // TODO: find a way to reliably test this without too much mocking - it.skip("respects cookie", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); - await client.GET("/blogposts", { credentials: "include" }); + it("respects cookie", async () => { + const client = createClient({ baseUrl }); + + const { getRequestCookies } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts", + }); - const req = fetchMocker.mock.calls[0][0]; - expect(req.credentials).toBe("include"); + await client.GET("/blogposts", { + credentials: "include", + headers: { + Cookie: "session=1234", + }, + }); + + const cookies = getRequestCookies(); + expect(cookies).toEqual({ session: "1234" }); }); }); describe("responses", () => { it("returns empty object on 204", async () => { - const client = createClient(); - mockFetchOnce({ status: 204, body: "" }); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "delete", + path: "/tag/*", + handler: () => new HttpResponse(null, { status: 204 }), + }); + const { data, error, response } = await client.DELETE("/tag/{name}", { params: { path: { name: "New Tag" } }, }); @@ -971,15 +1199,18 @@ describe("client", () => { it("treats `default` as an error", async () => { const client = createClient({ + baseUrl, headers: { "Cache-Control": "max-age=10000000" }, }); - mockFetchOnce({ + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/default-as-error", status: 500, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ + body: { code: 500, message: "An unexpected error occurred", - }), + }, }); const { error } = await client.GET("/default-as-error"); @@ -996,20 +1227,30 @@ describe("client", () => { describe("parseAs", () => { it("text", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/anyMethod", + body: {}, + }); const { data, error } = (await client.GET("/anyMethod", { parseAs: "text", })) satisfies { data?: string }; if (error) { throw new Error(`parseAs text: error`); } - expect(data.toLowerCase()).toBe("{}"); + expect(data).toBe("{}"); }); it("arrayBuffer", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/anyMethod", + body: {}, + }); const { data, error } = (await client.GET("/anyMethod", { parseAs: "arrayBuffer", })) satisfies { data?: ArrayBuffer }; @@ -1020,8 +1261,13 @@ describe("client", () => { }); it("blob", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/anyMethod", + body: {}, + }); const { data, error } = (await client.GET("/anyMethod", { parseAs: "blob", })) satisfies { data?: Blob }; @@ -1033,8 +1279,13 @@ describe("client", () => { }); it("stream", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/anyMethod", + body: {}, + }); const { data } = (await client.GET("/anyMethod", { parseAs: "stream", })) satisfies { data?: ReadableStream | null }; @@ -1042,25 +1293,28 @@ describe("client", () => { throw new Error(`parseAs stream: error`); } - expect(data instanceof Buffer).toBe(true); - if (!(data instanceof Buffer)) { - throw Error("Data should be an instance of Buffer in Node context"); - } - - expect(data.byteLength).toBe(2); + expect(data).toBeInstanceOf(ReadableStream); + const reader = data.getReader(); + const result = await reader.read(); + expect(result.value!.length).toBe(2); }); it("use the selected content", async () => { - const client = createClient(); - mockFetchOnce({ + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/multiple-response-content", status: 200, headers: { "Content-Type": "application/ld+json" }, - body: JSON.stringify({ + body: { "@id": "some-resource-identifier", email: "foo@bar.fr", name: null, - }), + }, }); + const { data } = await client.GET("/multiple-response-content", { headers: { Accept: "application/ld+json", @@ -1090,10 +1344,14 @@ describe("client", () => { describe("GET()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/anyMethod", + }); await client.GET("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("GET"); + expect(getRequest().method).toBe("GET"); }); it("sends correct options, returns success", async () => { @@ -1102,8 +1360,15 @@ describe("client", () => { body: "

This is a very good post

", publish_date: new Date("2023-03-01T12:00:00Z").getTime(), }; - const client = createClient(); - mockFetchOnce({ status: 200, body: JSON.stringify(mockData) }); + const client = createClient({ baseUrl }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + status: 200, + body: mockData, + }); const { data, error, response } = await client.GET( "/blogposts/{post_id}", { @@ -1112,7 +1377,7 @@ describe("client", () => { ); // assert correct URL was called - expect(fetchMocker.mock.calls[0][0].url).toBe("/blogposts/my-post"); + expect(getRequestUrl().pathname).toBe("/blogposts/my-post"); // assert correct data was returned expect(data).toEqual(mockData); @@ -1124,8 +1389,16 @@ describe("client", () => { it("sends correct options, returns error", async () => { const mockError = { code: 404, message: "Post not found" }; - const client = createClient(); - mockFetchOnce({ status: 404, body: JSON.stringify(mockError) }); + const client = createClient({ baseUrl }); + + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + status: 404, + body: mockError, + }); + const { data, error, response } = await client.GET( "/blogposts/{post_id}", { @@ -1134,10 +1407,10 @@ describe("client", () => { ); // assert correct URL was called - expect(fetchMocker.mock.calls[0][0].url).toBe("/blogposts/my-post"); + expect(getRequest().url).toBe(baseUrl + "/blogposts/my-post"); // assert correct method was called - expect(fetchMocker.mock.calls[0][0].method).toBe("GET"); + expect(getRequest().method).toBe("GET"); // assert correct error was returned expect(error).toEqual(mockError); @@ -1149,8 +1422,16 @@ describe("client", () => { // note: this was a previous bug in the type inference it("handles array-type responses", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "[]" }); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts", + status: 200, + body: [], + }); + const { data } = await client.GET("/blogposts", { params: {} }); if (!data) { throw new Error("data empty"); @@ -1161,8 +1442,16 @@ describe("client", () => { }); it("handles literal 2XX and 4XX codes", async () => { - const client = createClient(); - mockFetch({ status: 201, body: '{"status": "success"}' }); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "put", + path: "/media", + status: 201, + body: { status: "success" }, + }); + const { data, error } = await client.PUT("/media", { body: { media: "base64", name: "myImage" }, }); @@ -1178,8 +1467,16 @@ describe("client", () => { }); it("gracefully handles invalid JSON for errors", async () => { - const client = createClient(); - mockFetchOnce({ status: 401, body: "Unauthorized" }); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts", + status: 401, + body: "Unauthorized", + }); + const { data, error } = await client.GET("/blogposts"); expect(data).toBeUndefined(); @@ -1189,16 +1486,28 @@ describe("client", () => { describe("POST()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "post", + path: "/anyMethod", + }); await client.POST("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("POST"); + expect(getRequest().method).toBe("POST"); }); it("sends correct options, returns success", async () => { const mockData = { status: "success" }; - const client = createClient(); - mockFetchOnce({ status: 201, body: JSON.stringify(mockData) }); + + const client = createClient({ baseUrl }); + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "put", + path: "/blogposts", + status: 201, + body: mockData, + }); + const { data, error, response } = await client.PUT("/blogposts", { body: { title: "New Post", @@ -1208,7 +1517,7 @@ describe("client", () => { }); // assert correct URL was called - expect(fetchMocker.mock.calls[0][0].url).toBe("/blogposts"); + expect(getRequestUrl().pathname).toBe("/blogposts"); // assert correct data was returned expect(data).toEqual(mockData); @@ -1220,8 +1529,15 @@ describe("client", () => { it("supports sepecifying utf-8 encoding", async () => { const mockData = { message: "My reply" }; - const client = createClient(); - mockFetchOnce({ status: 201, body: JSON.stringify(mockData) }); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "put", + path: "/comment", + status: 201, + body: mockData, + }); + const { data, error, response } = await client.PUT("/comment", { params: {}, body: { @@ -1241,15 +1557,24 @@ describe("client", () => { describe("DELETE()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "delete", + path: "/anyMethod", + }); await client.DELETE("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("DELETE"); + expect(getRequest().method).toBe("DELETE"); }); it("returns empty object on 204", async () => { - const client = createClient(); - mockFetchOnce({ status: 204, body: "" }); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "delete", + path: "/blogposts/:post_id", + handler: () => new HttpResponse(null, { status: 204 }), + }); const { data, error } = await client.DELETE("/blogposts/{post_id}", { params: { path: { post_id: "123" }, @@ -1264,12 +1589,20 @@ describe("client", () => { }); it("returns empty object on Content-Length: 0", async () => { - const client = createClient(); - mockFetchOnce({ - headers: { "Content-Length": "0" }, - status: 200, - body: "", + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "delete", + path: `/blogposts/:post_id`, + handler: () => + new HttpResponse(null, { + status: 200, + headers: { + "Content-Length": "0", + }, + }), }); + const { data, error } = await client.DELETE("/blogposts/{post_id}", { params: { path: { post_id: "123" }, @@ -1286,37 +1619,57 @@ describe("client", () => { describe("OPTIONS()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "options", + path: `/anyMethod`, + }); await client.OPTIONS("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("OPTIONS"); + expect(getRequest().method).toBe("OPTIONS"); }); }); describe("HEAD()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "head", + path: "/anyMethod", + }); await client.HEAD("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("HEAD"); + expect(getRequest().method).toBe("HEAD"); }); }); describe("PATCH()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "patch", + path: "/anyMethod", + }); await client.PATCH("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("PATCH"); + expect(getRequest().method).toBe("PATCH"); }); }); + // NOTE: msw does not support TRACE method + // so instead we verify that calling TRACE() with msw throws an error describe("TRACE()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); - await client.TRACE("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("TRACE"); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "all", // note: msw doesn’t support TRACE method + path: "/anyMethod", + }); + + await expect( + async () => await client.TRACE("/anyMethod"), + ).rejects.toThrowError("'TRACE' HTTP method is unsupported"); }); }); }); @@ -1334,25 +1687,27 @@ describe("examples", () => { }, }; - const client = createClient(); + const client = createClient({ baseUrl }); client.use(authMiddleware); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + }); + // assert initial call is unauthenticated - mockFetchOnce({ status: 200, body: "{}" }); await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "1234" } }, }); - expect( - fetchMocker.mock.calls[0][0].headers.get("authorization"), - ).toBeNull(); + expect(getRequest().headers.get("authorization")).toBeNull(); // assert after setting token, client is authenticated accessToken = "real_token"; - mockFetchOnce({ status: 200, body: "{}" }); await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "1234" } }, }); - expect(fetchMocker.mock.calls[1][0].headers.get("authorization")).toBe( + expect(getRequest().headers.get("authorization")).toBe( `Bearer ${accessToken}`, ); }); diff --git a/packages/openapi-fetch/test/v7-beta.test.ts b/packages/openapi-fetch/test/v7-beta.test.ts index a84074053..20b8aa76d 100644 --- a/packages/openapi-fetch/test/v7-beta.test.ts +++ b/packages/openapi-fetch/test/v7-beta.test.ts @@ -1,10 +1,15 @@ -// @ts-expect-error -import createFetchMock from "vitest-fetch-mock"; +import { HttpResponse, type StrictResponse } from "msw"; import createClient, { type Middleware, type MiddlewareRequest, type QuerySerializerOptions, } from "../src/index.js"; +import { + server, + baseUrl, + useMockRequestHandler, + toAbsoluteURL, +} from "./fixtures/mock-server.js"; import type { paths } from "./fixtures/v7-beta.js"; // Note @@ -12,28 +17,19 @@ import type { paths } from "./fixtures/v7-beta.js"; // This tests upcoming compatibility until openapi-typescript@7 is stable and the two tests // merged together. -const fetchMocker = createFetchMock(vi); - beforeAll(() => { - fetchMocker.enableMocks(); -}); -afterEach(() => { - fetchMocker.resetMocks(); + server.listen({ + onUnhandledRequest: (request) => { + throw new Error( + `No request handler found for ${request.method} ${request.url}`, + ); + }, + }); }); -interface MockResponse { - headers?: Record; - status: number; - body: any; -} - -function mockFetch(res: MockResponse) { - fetchMocker.mockResponse(() => res); -} +afterEach(() => server.resetHandlers()); -function mockFetchOnce(res: MockResponse) { - fetchMocker.mockResponseOnce(() => res); -} +afterAll(() => server.close()); describe("client", () => { it("generates all proper functions", () => { @@ -51,13 +47,19 @@ describe("client", () => { describe("TypeScript checks", () => { it("marks data or error as undefined, but never both", async () => { - const client = createClient(); + const client = createClient({ + baseUrl, + }); // data - mockFetchOnce({ + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/string-array", status: 200, - body: JSON.stringify(["one", "two", "three"]), + body: ["one", "two", "three"], }); + const dataRes = await client.GET("/string-array"); // … is initially possibly undefined @@ -76,9 +78,12 @@ describe("client", () => { } // error - mockFetchOnce({ + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/string-array", status: 500, - body: JSON.stringify({ code: 500, message: "Something went wrong" }), + body: { code: 500, message: "Something went wrong" }, }); const errorRes = await client.GET("/string-array"); @@ -102,21 +107,24 @@ describe("client", () => { describe("path", () => { it("typechecks", async () => { const client = createClient({ - baseUrl: "https://myapi.com/v1", + baseUrl, }); - mockFetch({ status: 200, body: JSON.stringify({ message: "OK" }) }); - // expect error on missing 'params' - await client.GET("/blogposts/{post_id}", { - // @ts-expect-error - TODO: "this should be an error", + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + status: 200, + body: { message: "OK" }, }); + // expect error on missing 'params' + // @ts-expect-error + await client.GET("/blogposts/{post_id}"); + // expect error on empty params - await client.GET("/blogposts/{post_id}", { - // @ts-expect-error - params: { TODO: "this should be an error" }, - }); + // @ts-expect-error + await client.GET("/blogposts/{post_id}", { params: {} }); // expect error on empty params.path // @ts-expect-error @@ -128,23 +136,43 @@ describe("client", () => { params: { path: { post_id: 1234 } }, }); + // expect error on unknown property in 'params' + await client.GET("/blogposts/{post_id}", { + // @ts-expect-error + TODO: "this should be an error", + }); + // (no error) + let calledPostId = ""; + useMockRequestHandler<{ post_id: string }>({ + baseUrl, + method: "get", + path: `/blogposts/:post_id`, + handler: ({ params }) => { + calledPostId = params.post_id; + return HttpResponse.json({ message: "OK" }, { status: 200 }); + }, + }); + await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "1234" } }, }); // expect param passed correctly - const lastCall = - fetchMocker.mock.calls[fetchMocker.mock.calls.length - 1]; - expect(lastCall[0].url).toBe("https://myapi.com/v1/blogposts/1234"); + expect(calledPostId).toBe("1234"); }); it("serializes", async () => { - const client = createClient(); - mockFetch({ - status: 200, - body: JSON.stringify({ status: "success" }), + const client = createClient({ + baseUrl, }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: `/path-params/*`, + }); + await client.GET( "/path-params/{simple_primitive}/{simple_obj_flat}/{simple_arr_flat}/{simple_obj_explode*}/{simple_arr_explode*}/{.label_primitive}/{.label_obj_flat}/{.label_arr_flat}/{.label_obj_explode*}/{.label_arr_explode*}/{;matrix_primitive}/{;matrix_obj_flat}/{;matrix_arr_flat}/{;matrix_obj_explode*}/{;matrix_arr_explode*}", { @@ -170,7 +198,7 @@ describe("client", () => { }, ); - expect(fetchMocker.mock.calls[0][0].url).toBe( + expect(getRequestUrl().pathname).toBe( `/path-params/${[ // simple "simple", @@ -195,27 +223,51 @@ describe("client", () => { }); it("allows UTF-8 characters", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/*", + }); await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "post?id = 🥴" } }, }); // expect post_id to be encoded properly - expect(fetchMocker.mock.calls[0][0].url).toBe( - `/blogposts/post?id%20=%20🥴`, + const url = getRequestUrl(); + expect(url.searchParams.get("id ")).toBe(" 🥴"); + expect(url.pathname + url.search).toBe( + `/blogposts/post?id%20=%20%F0%9F%A5%B4`, ); }); }); it("header", async () => { - const client = createClient({ baseUrl: "https://myapi.com/v1" }); - mockFetch({ status: 200, body: JSON.stringify({ status: "success" }) }); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/header-params", + handler: ({ request }) => { + const header = request.headers.get("x-required-header"); + if (header !== "correct") { + return HttpResponse.json( + { code: 500, message: "missing correct header" }, + { status: 500 }, + ) as StrictResponse; + } + return HttpResponse.json( + { status: header }, + { status: 200, headers: request.headers }, + ); + }, + }); - // expet error on missing header + // expect error on missing header // @ts-expect-error - await client.GET("/header-params", { TODO: "this should be an error" }); + await client.GET("/header-params"); // expect error on incorrect header await client.GET("/header-params", { @@ -230,54 +282,76 @@ describe("client", () => { }); // (no error) - await client.GET("/header-params", { + const response = await client.GET("/header-params", { params: { header: { "x-required-header": "correct" } }, }); // expect param passed correctly - const lastCall = - fetchMocker.mock.calls[fetchMocker.mock.calls.length - 1][0]; - expect(lastCall.headers.get("x-required-header")).toBe("correct"); + expect(response.response.headers.get("x-required-header")).toBe( + "correct", + ); }); describe("query", () => { describe("querySerializer", () => { it("primitives", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/query-params*", + }); + await client.GET("/query-params", { params: { query: { string: "string", number: 0, boolean: false }, }, }); - expect(fetchMocker.mock.calls[0][0].url).toBe( - "/query-params?string=string&number=0&boolean=false", + expect(getRequestUrl().search).toBe( + "?string=string&number=0&boolean=false", ); }); it("array params (empty)", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/query-params*", + }); + await client.GET("/query-params", { params: { query: { array: [] }, }, }); - expect(fetchMocker.mock.calls[0][0].url).toBe("/query-params"); + const url = getRequestUrl(); + expect(url.pathname).toBe("/query-params"); + expect(url.search).toBe(""); }); it("empty/null params", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/query-params*", + }); + await client.GET("/query-params", { params: { query: { string: undefined, number: null as any }, }, }); - expect(fetchMocker.mock.calls[0][0].url).toBe("/query-params"); + const url = getRequestUrl(); + expect(url.pathname).toBe("/query-params"); + expect(url.search).toBe(""); }); describe("array", () => { @@ -332,17 +406,25 @@ describe("client", () => { }, ][])("%s", async (_, { given, want }) => { const client = createClient({ + baseUrl, querySerializer: { array: given }, }); - mockFetch({ status: 200, body: "{}" }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/query-params*", + }); + await client.GET("/query-params", { params: { query: { array: ["1", "2", "3"], boolean: true }, }, }); - const req = fetchMocker.mock.calls[0][0].url; - expect(req.split("?")[1]).toBe(want); + const url = getRequestUrl(); + // skip leading '?' + expect(url.search.substring(1)).toBe(want); }); }); @@ -384,24 +466,37 @@ describe("client", () => { }, ][])("%s", async (_, { given, want }) => { const client = createClient({ + baseUrl, querySerializer: { object: given }, }); - mockFetch({ status: 200, body: "{}" }); + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/query-params*", + }); + await client.GET("/query-params", { params: { query: { object: { foo: "bar", bar: "baz" }, boolean: true }, }, }); - expect(fetchMocker.mock.calls[0][0].url.split("?")[1]).toBe(want); + const url = getRequestUrl(); + // skip leading '?' + expect(url.search.substring(1)).toBe(want); }); }); it("allowReserved", async () => { const client = createClient({ + baseUrl, querySerializer: { allowReserved: true }, }); - mockFetch({ status: 200, body: "{}" }); + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/query-params*", + }); await client.GET("/query-params", { params: { query: { @@ -409,8 +504,12 @@ describe("client", () => { }, }, }); - expect(fetchMocker.mock.calls[0][0].url.split("?")[1]).toBe( - "string=bad/character🐶", + + expect(getRequestUrl().search).toBe( + "?string=bad/character%F0%9F%90%B6", + ); + expect(getRequestUrl().searchParams.get("string")).toBe( + "bad/character🐶", ); await client.GET("/query-params", { @@ -423,17 +522,28 @@ describe("client", () => { allowReserved: false, }, }); - expect(fetchMocker.mock.calls[1][0].url.split("?")[1]).toBe( - "string=bad%2Fcharacter%F0%9F%90%B6", + + expect(getRequestUrl().search).toBe( + "?string=bad%2Fcharacter%F0%9F%90%B6", + ); + expect(getRequestUrl().searchParams.get("string")).toBe( + "bad/character🐶", ); }); describe("function", () => { it("global default", async () => { const client = createClient({ + baseUrl, querySerializer: (q) => `alpha=${q.version}&beta=${q.format}`, }); - mockFetchOnce({ status: 200, body: "{}" }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + }); + await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "my-post" }, @@ -441,16 +551,24 @@ describe("client", () => { }, }); - expect(fetchMocker.mock.calls[0][0].url).toBe( + const url = getRequestUrl(); + expect(url.pathname + url.search).toBe( "/blogposts/my-post?alpha=2&beta=json", ); }); it("per-request", async () => { const client = createClient({ + baseUrl, querySerializer: () => "query", }); - mockFetchOnce({ status: 200, body: "{}" }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + }); + await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "my-post" }, @@ -459,7 +577,8 @@ describe("client", () => { querySerializer: (q) => `alpha=${q.version}&beta=${q.format}`, }); - expect(fetchMocker.mock.calls[0][0].url).toBe( + const url = getRequestUrl(); + expect(url.pathname + url.search).toBe( "/blogposts/my-post?alpha=2&beta=json", ); }); @@ -467,18 +586,22 @@ describe("client", () => { it("ignores leading ? characters", async () => { const client = createClient({ + baseUrl, querySerializer: () => "?query", }); - mockFetchOnce({ status: 200, body: "{}" }); + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + }); await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "my-post" }, query: { version: 2, format: "json" }, }, }); - expect(fetchMocker.mock.calls[0][0].url).toBe( - "/blogposts/my-post?query", - ); + const url = getRequestUrl(); + expect(url.pathname + url.search).toBe("/blogposts/my-post?query"); }); }); }); @@ -488,8 +611,13 @@ describe("client", () => { // these are pure type tests; no runtime assertions needed /* eslint-disable vitest/expect-expect */ it("requires necessary requestBodies", async () => { - const client = createClient({ baseUrl: "https://myapi.com/v1" }); - mockFetch({ status: 200, body: JSON.stringify({ message: "OK" }) }); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "put", + path: "/blogposts", + }); // expect error on missing `body` // @ts-expect-error @@ -511,8 +639,14 @@ describe("client", () => { }); it("requestBody (inline)", async () => { - mockFetch({ status: 201, body: "{}" }); - const client = createClient(); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "put", + path: "/blogposts-optional-inline", + status: 201, + }); // expect error on wrong body type await client.PUT("/blogposts-optional-inline", { @@ -531,8 +665,14 @@ describe("client", () => { }); it("requestBody with required: false", async () => { - mockFetch({ status: 201, body: "{}" }); - const client = createClient(); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "put", + path: "/blogposts-optional", + status: 201, + }); // assert missing `body` doesn’t raise a TS error await client.PUT("/blogposts-optional"); @@ -556,36 +696,45 @@ describe("client", () => { describe("options", () => { it("baseUrl", async () => { - let client = createClient({ baseUrl: "https://myapi.com/v1" }); - mockFetch({ status: 200, body: JSON.stringify({ message: "OK" }) }); + let client = createClient({ baseUrl }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", + status: 200, + body: { message: "OK" }, + }); + await client.GET("/self"); // assert baseUrl and path mesh as expected - expect(fetchMocker.mock.calls[0][0].url).toBe( - "https://myapi.com/v1/self", - ); + expect(getRequestUrl().href).toBe(toAbsoluteURL("/self")); - client = createClient({ baseUrl: "https://myapi.com/v1/" }); + client = createClient({ baseUrl }); await client.GET("/self"); // assert trailing '/' was removed - expect(fetchMocker.mock.calls[1][0].url).toBe( - "https://myapi.com/v1/self", - ); + expect(getRequestUrl().href).toBe(toAbsoluteURL("/self")); }); describe("headers", () => { it("persist", async () => { const headers: HeadersInit = { Authorization: "Bearer secrettoken" }; - const client = createClient({ headers }); - mockFetchOnce({ + const client = createClient({ headers, baseUrl }); + + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", status: 200, - body: JSON.stringify({ email: "user@user.com" }), + body: { email: "user@user.com" }, }); + await client.GET("/self"); // assert default headers were passed - expect(fetchMocker.mock.calls[0][0].headers).toEqual( + expect(getRequest().headers).toEqual( new Headers({ ...headers, // assert new header got passed "Content-Type": "application/json", // probably doesn’t need to get tested, but this was simpler than writing lots of code to ignore these @@ -595,19 +744,25 @@ describe("client", () => { it("can be overridden", async () => { const client = createClient({ + baseUrl, headers: { "Cache-Control": "max-age=10000000" }, }); - mockFetchOnce({ + + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", status: 200, - body: JSON.stringify({ email: "user@user.com" }), + body: { email: "user@user.com" }, }); + await client.GET("/self", { params: {}, headers: { "Cache-Control": "no-cache" }, }); // assert default headers were passed - expect(fetchMocker.mock.calls[0][0].headers).toEqual( + expect(getRequest().headers).toEqual( new Headers({ "Cache-Control": "no-cache", "Content-Type": "application/json", @@ -617,29 +772,40 @@ describe("client", () => { it("can be unset", async () => { const client = createClient({ + baseUrl, headers: { "Content-Type": null }, }); - mockFetchOnce({ + + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", status: 200, - body: JSON.stringify({ email: "user@user.com" }), + body: { email: "user@user.com" }, }); + await client.GET("/self", { params: {} }); // assert default headers were passed - expect(fetchMocker.mock.calls[0][0].headers).toEqual(new Headers()); + expect(getRequest().headers).toEqual(new Headers()); }); it("supports arrays", async () => { - const client = createClient(); + const client = createClient({ baseUrl }); const list = ["one", "two", "three"]; - mockFetchOnce({ status: 200, body: "{}" }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", + status: 200, + body: {}, + }); + await client.GET("/self", { headers: { list } }); - expect(fetchMocker.mock.calls[0][0].headers.get("list")).toEqual( - list.join(", "), - ); + expect(getRequest().headers.get("list")).toEqual(list.join(", ")); }); }); @@ -657,16 +823,15 @@ describe("client", () => { } const customFetch = createCustomFetch({ works: true }); - mockFetchOnce({ status: 200, body: "{}" }); - const client = createClient({ fetch: customFetch }); + const client = createClient({ fetch: customFetch, baseUrl }); const { data } = await client.GET("/self"); // assert data was returned from custom fetcher expect(data).toEqual({ works: true }); - // assert global fetch was never called - expect(fetchMocker).not.toHaveBeenCalled(); + // TODO: do we need to assert nothing was called? + // msw should throw an error if there was an unused handler }); it("per-request", async () => { @@ -684,9 +849,7 @@ describe("client", () => { const fallbackFetch = createCustomFetch({ fetcher: "fallback" }); const overrideFetch = createCustomFetch({ fetcher: "override" }); - mockFetchOnce({ status: 200, body: "{}" }); - - const client = createClient({ fetch: fallbackFetch }); + const client = createClient({ fetch: fallbackFetch, baseUrl }); // assert override function was called const fetch1 = await client.GET("/self", { fetch: overrideFetch }); @@ -696,16 +859,14 @@ describe("client", () => { const fetch2 = await client.GET("/self"); expect(fetch2.data).toEqual({ fetcher: "fallback" }); - // assert global fetch was never called - expect(fetchMocker).not.toHaveBeenCalled(); + // TODO: do we need to assert nothing was called? + // msw should throw an error if there was an unused handler }); }); describe("middleware", () => { it("can modify request", async () => { - mockFetchOnce({ status: 200, body: "{}" }); - - const client = createClient(); + const client = createClient({ baseUrl }); client.use({ async onRequest(req) { return new Request("https://foo.bar/api/v1", { @@ -715,9 +876,18 @@ describe("client", () => { }); }, }); + + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "options", + path: `https://foo.bar/api/v1`, + status: 200, + body: {}, + }); + await client.GET("/self"); - const req = fetchMocker.mock.calls[0][0]; + const req = getRequest(); expect(req.url).toBe("https://foo.bar/api/v1"); expect(req.method).toBe("OPTIONS"); expect(req.headers.get("foo")).toBe("bar"); @@ -731,13 +901,17 @@ describe("client", () => { created_at: "2024-01-01T00:00:00Z", updated_at: "2024-01-20T00:00:00Z", }; - mockFetchOnce({ + + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", status: 200, - body: JSON.stringify(rawBody), + body: rawBody, headers: { foo: "bar" }, }); - const client = createClient(); + const client = createClient({ baseUrl }); client.use({ // convert date string to unix time async onResponse(res) { @@ -748,7 +922,7 @@ describe("client", () => { headers.set("middleware", "value"); return new Response(JSON.stringify(body), { ...res, - status: 205, + status: 201, headers, }); }, @@ -762,7 +936,7 @@ describe("client", () => { // assert rest of body was preserved expect(data?.email).toBe(rawBody.email); // assert status changed - expect(response.status).toBe(205); + expect(response.status).toBe(201); // assert server headers were preserved expect(response.headers.get("foo")).toBe("bar"); // assert middleware heaers were added @@ -770,9 +944,15 @@ describe("client", () => { }); it("executes in expected order", async () => { - mockFetchOnce({ status: 200, body: "{}" }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/self", + status: 200, + body: {}, + }); - const client = createClient(); + const client = createClient({ baseUrl }); // this middleware passes along the “step” header // for both requests and responses, but first checks if // it received the end result of the previous middleware step @@ -820,33 +1000,44 @@ describe("client", () => { const { response } = await client.GET("/self"); // assert requests ended up on step C (array order) - expect(fetchMocker.mock.calls[0][0].headers.get("step")).toBe("C"); + expect(getRequest().headers.get("step")).toBe("C"); // assert responses ended up on step A (reverse order) expect(response.headers.get("step")).toBe("A"); }); it("receives correct options", async () => { - mockFetchOnce({ status: 200, body: "{}" }); + useMockRequestHandler({ + baseUrl: "https://api.foo.bar/v1/", + method: "get", + path: "/self", + status: 200, + body: {}, + }); - let baseUrl = ""; + let requestBaseUrl = ""; const client = createClient({ baseUrl: "https://api.foo.bar/v1/", }); client.use({ onRequest(_, options) { - baseUrl = options.baseUrl; + requestBaseUrl = options.baseUrl; return undefined; }, }); await client.GET("/self"); - expect(baseUrl).toBe("https://api.foo.bar/v1"); + expect(requestBaseUrl).toBe("https://api.foo.bar/v1"); }); it("receives OpenAPI options passed in from parent", async () => { - mockFetchOnce({ status: 200, body: "{}" }); + useMockRequestHandler({ + method: "put", + path: `https://api.foo.bar/v1/tag*`, + status: 200, + body: {}, + }); const pathname = "/tag/{name}"; const tagData = { @@ -883,7 +1074,13 @@ describe("client", () => { }); it("can be skipped without interrupting request", async () => { - mockFetchOnce({ status: 200, body: JSON.stringify({ success: true }) }); + useMockRequestHandler({ + baseUrl: "https://api.foo.bar/v1/", + method: "get", + path: "/blogposts", + status: 200, + body: { success: true }, + }); const client = createClient({ baseUrl: "https://api.foo.bar/v1/", @@ -899,7 +1096,13 @@ describe("client", () => { }); it("can be ejected", async () => { - mockFetchOnce({ status: 200, body: "{}" }); + useMockRequestHandler({ + baseUrl: "https://api.foo.bar/v1", + method: "get", + path: "/blogposts", + status: 200, + body: { success: true }, + }); let called = false; const errorMiddleware = { @@ -923,15 +1126,22 @@ describe("client", () => { describe("requests", () => { it("multipart/form-data", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "put", + path: "/contact", + }); + + const reqBody = { + name: "John Doe", + email: "test@email.email", + subject: "Test Message", + message: "This is a test message", + }; await client.PUT("/contact", { - body: { - name: "John Doe", - email: "test@email.email", - subject: "Test Message", - message: "This is a test message", - }, + body: reqBody, bodySerializer(body) { const fd = new FormData(); for (const name in body) { @@ -941,31 +1151,45 @@ describe("client", () => { }, }); - // expect post_id to be encoded properly - const req = fetchMocker.mock.calls[0][0]; - // note: this is FormData, but Node.js doesn’t handle new Request() properly with formData bodies. So this is only in tests. - expect(req.body).toBeInstanceOf(Buffer); - expect((req.headers as Headers).get("Content-Type")).toBe( - "text/plain;charset=UTF-8", - ); + // expect request to contain correct headers and body + const req = getRequest(); + expect(req.body).toBeInstanceOf(ReadableStream); + const body = await req.formData(); + expect(body.get("name")).toBe("John Doe"); + expect(req.headers.get("Content-Type")).toMatch(/multipart\/form-data;/); }); - // Node Requests eat credentials (no cookies), but this works in frontend - // TODO: find a way to reliably test this without too much mocking - it.skip("respects cookie", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); - await client.GET("/blogposts", { credentials: "include" }); + it("respects cookie", async () => { + const client = createClient({ baseUrl }); + + const { getRequestCookies } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts", + }); + + await client.GET("/blogposts", { + credentials: "include", + headers: { + Cookie: "session=1234", + }, + }); - const req = fetchMocker.mock.calls[0][0]; - expect(req.credentials).toBe("include"); + const cookies = getRequestCookies(); + expect(cookies).toEqual({ session: "1234" }); }); }); describe("responses", () => { it("returns empty object on 204", async () => { - const client = createClient(); - mockFetchOnce({ status: 204, body: "" }); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "delete", + path: "/tag/*", + handler: () => new HttpResponse(null, { status: 204 }), + }); + const { data, error, response } = await client.DELETE("/tag/{name}", { params: { path: { name: "New Tag" } }, }); @@ -980,15 +1204,18 @@ describe("client", () => { it("treats `default` as an error", async () => { const client = createClient({ + baseUrl, headers: { "Cache-Control": "max-age=10000000" }, }); - mockFetchOnce({ + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/default-as-error", status: 500, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ + body: { code: 500, message: "An unexpected error occurred", - }), + }, }); const { error } = await client.GET("/default-as-error"); @@ -1005,54 +1232,131 @@ describe("client", () => { describe("parseAs", () => { it("text", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); - const { data }: { data?: string } = await client.GET("/anyMethod", { - parseAs: "text", + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/anyMethod", + body: {}, }); + const { data, error } = (await client.GET("/anyMethod", { + parseAs: "text", + })) satisfies { data?: string }; + if (error) { + throw new Error(`parseAs text: error`); + } expect(data).toBe("{}"); }); it("arrayBuffer", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); - const { data }: { data?: ArrayBuffer } = await client.GET( - "/anyMethod", - { - parseAs: "arrayBuffer", - }, - ); - expect(data instanceof ArrayBuffer).toBe(true); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/anyMethod", + body: {}, + }); + const { data, error } = (await client.GET("/anyMethod", { + parseAs: "arrayBuffer", + })) satisfies { data?: ArrayBuffer }; + if (error) { + throw new Error(`parseAs arrayBuffer: error`); + } + expect(data.byteLength).toBe(2); }); it("blob", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); - const { data }: { data?: Blob } = await client.GET("/anyMethod", { - parseAs: "blob", + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/anyMethod", + body: {}, }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((data as any).constructor.name).toBe("Blob"); + const { data, error } = (await client.GET("/anyMethod", { + parseAs: "blob", + })) satisfies { data?: Blob }; + if (error) { + throw new Error(`parseAs blob: error`); + } + + expect(data.constructor.name).toBe("Blob"); }); it("stream", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); - const { data }: { data?: ReadableStream | null } = await client.GET( - "/anyMethod", - { parseAs: "stream" }, - ); - expect(data instanceof Buffer).toBe(true); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/anyMethod", + body: {}, + }); + const { data } = (await client.GET("/anyMethod", { + parseAs: "stream", + })) satisfies { data?: ReadableStream | null }; + if (!data) { + throw new Error(`parseAs stream: error`); + } + + expect(data).toBeInstanceOf(ReadableStream); + const reader = data.getReader(); + const result = await reader.read(); + expect(result.value!.length).toBe(2); + }); + + it("use the selected content", async () => { + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/multiple-response-content", + status: 200, + headers: { "Content-Type": "application/ld+json" }, + body: { + "@id": "some-resource-identifier", + email: "foo@bar.fr", + name: null, + }, + }); + + const { data } = await client.GET("/multiple-response-content", { + headers: { + Accept: "application/ld+json", + }, + }); + + data satisfies + | { + "@id": string; + email: string; + name?: string; + } + | undefined; + + if (!data) { + throw new Error(`Missing response`); + } + + expect(data).toEqual({ + "@id": "some-resource-identifier", + email: "foo@bar.fr", + name: null, + }); }); }); }); describe("GET()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/anyMethod", + }); await client.GET("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("GET"); + expect(getRequest().method).toBe("GET"); }); it("sends correct options, returns success", async () => { @@ -1061,8 +1365,15 @@ describe("client", () => { body: "

This is a very good post

", publish_date: new Date("2023-03-01T12:00:00Z").getTime(), }; - const client = createClient(); - mockFetchOnce({ status: 200, body: JSON.stringify(mockData) }); + const client = createClient({ baseUrl }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + status: 200, + body: mockData, + }); const { data, error, response } = await client.GET( "/blogposts/{post_id}", { @@ -1071,7 +1382,7 @@ describe("client", () => { ); // assert correct URL was called - expect(fetchMocker.mock.calls[0][0].url).toBe("/blogposts/my-post"); + expect(getRequestUrl().pathname).toBe("/blogposts/my-post"); // assert correct data was returned expect(data).toEqual(mockData); @@ -1083,8 +1394,16 @@ describe("client", () => { it("sends correct options, returns error", async () => { const mockError = { code: 404, message: "Post not found" }; - const client = createClient(); - mockFetchOnce({ status: 404, body: JSON.stringify(mockError) }); + const client = createClient({ baseUrl }); + + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + status: 404, + body: mockError, + }); + const { data, error, response } = await client.GET( "/blogposts/{post_id}", { @@ -1093,10 +1412,10 @@ describe("client", () => { ); // assert correct URL was called - expect(fetchMocker.mock.calls[0][0].url).toBe("/blogposts/my-post"); + expect(getRequest().url).toBe(baseUrl + "/blogposts/my-post"); // assert correct method was called - expect(fetchMocker.mock.calls[0][0].method).toBe("GET"); + expect(getRequest().method).toBe("GET"); // assert correct error was returned expect(error).toEqual(mockError); @@ -1108,8 +1427,16 @@ describe("client", () => { // note: this was a previous bug in the type inference it("handles array-type responses", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "[]" }); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts", + status: 200, + body: [], + }); + const { data } = await client.GET("/blogposts", { params: {} }); if (!data) { throw new Error("data empty"); @@ -1120,8 +1447,16 @@ describe("client", () => { }); it("handles literal 2XX and 4XX codes", async () => { - const client = createClient(); - mockFetch({ status: 201, body: '{"status": "success"}' }); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "put", + path: "/media", + status: 201, + body: { status: "success" }, + }); + const { data, error } = await client.PUT("/media", { body: { media: "base64", name: "myImage" }, }); @@ -1137,8 +1472,16 @@ describe("client", () => { }); it("gracefully handles invalid JSON for errors", async () => { - const client = createClient(); - mockFetchOnce({ status: 401, body: "Unauthorized" }); + const client = createClient({ baseUrl }); + + useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts", + status: 401, + body: "Unauthorized", + }); + const { data, error } = await client.GET("/blogposts"); expect(data).toBeUndefined(); @@ -1148,16 +1491,28 @@ describe("client", () => { describe("POST()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "post", + path: "/anyMethod", + }); await client.POST("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("POST"); + expect(getRequest().method).toBe("POST"); }); it("sends correct options, returns success", async () => { const mockData = { status: "success" }; - const client = createClient(); - mockFetchOnce({ status: 201, body: JSON.stringify(mockData) }); + + const client = createClient({ baseUrl }); + const { getRequestUrl } = useMockRequestHandler({ + baseUrl, + method: "put", + path: "/blogposts", + status: 201, + body: mockData, + }); + const { data, error, response } = await client.PUT("/blogposts", { body: { title: "New Post", @@ -1167,7 +1522,7 @@ describe("client", () => { }); // assert correct URL was called - expect(fetchMocker.mock.calls[0][0].url).toBe("/blogposts"); + expect(getRequestUrl().pathname).toBe("/blogposts"); // assert correct data was returned expect(data).toEqual(mockData); @@ -1179,8 +1534,15 @@ describe("client", () => { it("supports sepecifying utf-8 encoding", async () => { const mockData = { message: "My reply" }; - const client = createClient(); - mockFetchOnce({ status: 201, body: JSON.stringify(mockData) }); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "put", + path: "/comment", + status: 201, + body: mockData, + }); + const { data, error, response } = await client.PUT("/comment", { params: {}, body: { @@ -1200,15 +1562,24 @@ describe("client", () => { describe("DELETE()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "delete", + path: "/anyMethod", + }); await client.DELETE("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("DELETE"); + expect(getRequest().method).toBe("DELETE"); }); it("returns empty object on 204", async () => { - const client = createClient(); - mockFetchOnce({ status: 204, body: "" }); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "delete", + path: "/blogposts/:post_id", + handler: () => new HttpResponse(null, { status: 204 }), + }); const { data, error } = await client.DELETE("/blogposts/{post_id}", { params: { path: { post_id: "123" }, @@ -1223,12 +1594,20 @@ describe("client", () => { }); it("returns empty object on Content-Length: 0", async () => { - const client = createClient(); - mockFetchOnce({ - headers: { "Content-Length": "0" }, - status: 200, - body: "", + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "delete", + path: `/blogposts/:post_id`, + handler: () => + new HttpResponse(null, { + status: 200, + headers: { + "Content-Length": "0", + }, + }), }); + const { data, error } = await client.DELETE("/blogposts/{post_id}", { params: { path: { post_id: "123" }, @@ -1245,37 +1624,57 @@ describe("client", () => { describe("OPTIONS()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "options", + path: `/anyMethod`, + }); await client.OPTIONS("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("OPTIONS"); + expect(getRequest().method).toBe("OPTIONS"); }); }); describe("HEAD()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "head", + path: "/anyMethod", + }); await client.HEAD("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("HEAD"); + expect(getRequest().method).toBe("HEAD"); }); }); describe("PATCH()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); + const client = createClient({ baseUrl }); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "patch", + path: "/anyMethod", + }); await client.PATCH("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("PATCH"); + expect(getRequest().method).toBe("PATCH"); }); }); + // NOTE: msw does not support TRACE method + // so instead we verify that calling TRACE() with msw throws an error describe("TRACE()", () => { it("sends the correct method", async () => { - const client = createClient(); - mockFetchOnce({ status: 200, body: "{}" }); - await client.TRACE("/anyMethod"); - expect(fetchMocker.mock.calls[0][0].method).toBe("TRACE"); + const client = createClient({ baseUrl }); + useMockRequestHandler({ + baseUrl, + method: "all", // note: msw doesn’t support TRACE method + path: "/anyMethod", + }); + + await expect( + async () => await client.TRACE("/anyMethod"), + ).rejects.toThrowError("'TRACE' HTTP method is unsupported"); }); }); }); @@ -1293,25 +1692,27 @@ describe("examples", () => { }, }; - const client = createClient(); + const client = createClient({ baseUrl }); client.use(authMiddleware); + const { getRequest } = useMockRequestHandler({ + baseUrl, + method: "get", + path: "/blogposts/:post_id", + }); + // assert initial call is unauthenticated - mockFetchOnce({ status: 200, body: "{}" }); await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "1234" } }, }); - expect( - fetchMocker.mock.calls[0][0].headers.get("authorization"), - ).toBeNull(); + expect(getRequest().headers.get("authorization")).toBeNull(); // assert after setting token, client is authenticated accessToken = "real_token"; - mockFetchOnce({ status: 200, body: "{}" }); await client.GET("/blogposts/{post_id}", { params: { path: { post_id: "1234" } }, }); - expect(fetchMocker.mock.calls[1][0].headers.get("authorization")).toBe( + expect(getRequest().headers.get("authorization")).toBe( `Bearer ${accessToken}`, ); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3dae14e39..4161dfc53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ importers: esbuild: specifier: ^0.20.0 version: 0.20.0 + msw: + specifier: ^2.2.3 + version: 2.2.3(typescript@5.3.3) openapi-typescript: specifier: ^6.7.4 version: 6.7.4 @@ -86,10 +89,7 @@ importers: version: 5.3.3 vitest: specifier: ^1.3.1 - version: 1.3.1(@types/node@20.11.24)(supports-color@9.4.0) - vitest-fetch-mock: - specifier: ^0.2.2 - version: 0.2.2(vitest@1.3.1) + version: 1.3.1 packages/openapi-fetch/examples/nextjs: dependencies: @@ -456,6 +456,18 @@ packages: to-fast-properties: 2.0.0 dev: true + /@bundled-es-modules/cookie@2.0.0: + resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} + dependencies: + cookie: 0.5.0 + dev: true + + /@bundled-es-modules/statuses@1.0.1: + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + dependencies: + statuses: 2.0.1 + dev: true + /@changesets/apply-release-plan@7.0.0: resolution: {integrity: sha512-vfi69JR416qC9hWmFGSxj7N6wA5J222XNBmezSVATPWDVPIF7gkd4d8CpbEbXmRWbVrkoli3oerGS6dcL/BGsQ==} dependencies: @@ -1384,6 +1396,39 @@ packages: resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} dev: true + /@inquirer/confirm@3.0.2: + resolution: {integrity: sha512-eEhoJXten380e2t2yahRRMz1LqB4gKknl5//38k1KvKXhBcV+lFfkIPmr6nFivpIwtOwkaRjGcHz67wBmi3h6Q==} + engines: {node: '>=18'} + dependencies: + '@inquirer/core': 7.0.2 + '@inquirer/type': 1.2.0 + dev: true + + /@inquirer/core@7.0.2: + resolution: {integrity: sha512-yya2GLO8lIi+yGytrOQ6unbrRGi8JiC+lWtlIsCUsDgMcCdO75vOuqGIUKXvfBkeZLOzs4WcSioXvpBzo0B0+Q==} + engines: {node: '>=18'} + dependencies: + '@inquirer/type': 1.2.0 + '@types/mute-stream': 0.0.4 + '@types/node': 20.11.25 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-spinners: 2.9.2 + cli-width: 4.1.0 + figures: 3.2.0 + mute-stream: 1.0.0 + run-async: 3.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: true + + /@inquirer/type@1.2.0: + resolution: {integrity: sha512-/vvkUkYhrjbm+RolU7V1aUFDydZVKNKqKHR5TsE+j5DXgXFwrsOPcoGUJ02K0O7q7O53CU2DOTMYCHeGZ25WHA==} + engines: {node: '>=18'} + dev: true + /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1445,6 +1490,23 @@ packages: read-yaml-file: 1.1.0 dev: true + /@mswjs/cookies@1.1.0: + resolution: {integrity: sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==} + engines: {node: '>=18'} + dev: true + + /@mswjs/interceptors@0.25.16: + resolution: {integrity: sha512-8QC8JyKztvoGAdPgyZy49c9vSHHAZjHagwl4RY9E8carULk8ym3iTaiawrT1YoLF/qb449h48f71XDPgkUSOUg==} + engines: {node: '>=18'} + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.2 + strict-event-emitter: 0.5.1 + dev: true + /@next/env@14.1.0: resolution: {integrity: sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==} dev: false @@ -1551,6 +1613,21 @@ packages: fastq: 1.15.0 dev: true + /@open-draft/deferred-promise@2.2.0: + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + dev: true + + /@open-draft/logger@0.3.0: + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.2 + dev: true + + /@open-draft/until@2.1.0: + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + dev: true + /@pkgr/core@0.1.1: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1961,6 +2038,12 @@ packages: resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} dev: true + /@types/mute-stream@0.0.4: + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + dependencies: + '@types/node': 20.11.24 + dev: true + /@types/node@12.20.55: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: true @@ -1971,6 +2054,12 @@ packages: undici-types: 5.26.5 dev: true + /@types/node@20.11.25: + resolution: {integrity: sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==} + dependencies: + undici-types: 5.26.5 + dev: true + /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true @@ -2009,10 +2098,18 @@ packages: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} dev: true + /@types/statuses@2.0.5: + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + dev: true + /@types/web-bluetooth@0.0.20: resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} dev: true + /@types/wrap-ansi@3.0.0: + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + dev: true + /@typescript-eslint/eslint-plugin@7.1.1(@typescript-eslint/parser@7.1.1)(eslint@8.57.0)(typescript@5.3.3): resolution: {integrity: sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2506,6 +2603,13 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2849,6 +2953,16 @@ packages: escape-string-regexp: 5.0.0 dev: true + /cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + dev: true + + /cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + dev: true + /client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} dev: false @@ -2930,6 +3044,11 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: true + /cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -2939,14 +3058,6 @@ packages: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} dev: true - /cross-fetch@3.1.8: - resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - dev: true - /cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} dependencies: @@ -3013,6 +3124,18 @@ packages: ms: 2.1.3 dev: true + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + /debug@4.3.4(supports-color@9.4.0): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -3728,6 +3851,13 @@ packages: reusify: 1.0.4 dev: true + /figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + dependencies: + escape-string-regexp: 1.0.5 + dev: true + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -4005,6 +4135,11 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true + /graphql@16.8.1: + resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + dev: true + /handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -4079,6 +4214,10 @@ packages: function-bind: 1.1.2 dev: true + /headers-polyfill@4.0.2: + resolution: {integrity: sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw==} + dev: true + /hexoid@1.0.0: resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} engines: {node: '>=8'} @@ -4256,6 +4395,10 @@ packages: engines: {node: '>= 0.4'} dev: true + /is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + dev: true + /is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -4740,6 +4883,42 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true + /msw@2.2.3(typescript@5.3.3): + resolution: {integrity: sha512-84CoNCkcJ/EvY8Tv0tD/6HKVd4S5HyGowHjM5W12K8Wgryp4fikqS7IaTOceyQgP5dNedxo2icTLDXo7dkpxCg==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + peerDependencies: + typescript: '>= 4.7.x' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@bundled-es-modules/cookie': 2.0.0 + '@bundled-es-modules/statuses': 1.0.1 + '@inquirer/confirm': 3.0.2 + '@mswjs/cookies': 1.1.0 + '@mswjs/interceptors': 0.25.16 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + chalk: 4.1.2 + graphql: 16.8.1 + headers-polyfill: 4.0.2 + is-node-process: 1.2.0 + outvariant: 1.4.2 + path-to-regexp: 6.2.1 + strict-event-emitter: 0.5.1 + type-fest: 4.12.0 + typescript: 5.3.3 + yargs: 17.7.2 + dev: true + + /mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -4943,6 +5122,10 @@ packages: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} dev: true + /outvariant@1.4.2: + resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==} + dev: true + /p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -5043,6 +5226,10 @@ packages: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true + /path-to-regexp@6.2.1: + resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} + dev: true + /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -5389,6 +5576,11 @@ packages: fsevents: 2.3.3 dev: true + /run-async@3.0.0: + resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} + engines: {node: '>=0.12.0'} + dev: true + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -5656,6 +5848,11 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: true + /std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} dev: true @@ -5671,6 +5868,10 @@ packages: engines: {node: '>=10.0.0'} dev: false + /strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + dev: true + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -5770,7 +5971,7 @@ packages: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 fast-safe-stringify: 2.1.1 form-data: 4.0.0 formidable: 2.1.2 @@ -6044,6 +6245,11 @@ packages: engines: {node: '>=10'} dev: true + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + /type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -6059,6 +6265,11 @@ packages: engines: {node: '>=10'} dev: true + /type-fest@4.12.0: + resolution: {integrity: sha512-5Y2/pp2wtJk8o08G0CMkuFPCO354FGwk/vbidxrdhRGZfd0tFnb4Qb8anp9XxXriwBgVPjdWbKpGl4J9lJY2jQ==} + engines: {node: '>=16'} + dev: true + /typed-array-buffer@1.0.2: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} engines: {node: '>= 0.4'} @@ -6163,6 +6374,27 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /vite-node@1.3.1: + resolution: {integrity: sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.0 + vite: 5.1.5 + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite-node@1.3.1(@types/node@20.11.24)(supports-color@9.4.0): resolution: {integrity: sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==} engines: {node: ^18.0.0 || >=20.0.0} @@ -6219,6 +6451,41 @@ packages: fsevents: 2.3.3 dev: true + /vite@5.1.5: + resolution: {integrity: sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.19.12 + postcss: 8.4.35 + rollup: 4.12.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /vite@5.1.5(@types/node@20.11.24): resolution: {integrity: sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==} engines: {node: ^18.0.0 || >=20.0.0} @@ -6321,16 +6588,59 @@ packages: - universal-cookie dev: true - /vitest-fetch-mock@0.2.2(vitest@1.3.1): - resolution: {integrity: sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==} - engines: {node: '>=14.14.0'} + /vitest@1.3.1: + resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true peerDependencies: - vitest: '>=0.16.0' + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.3.1 + '@vitest/ui': 1.3.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true dependencies: - cross-fetch: 3.1.8 - vitest: 1.3.1(@types/node@20.11.24)(supports-color@9.4.0) + '@vitest/expect': 1.3.1 + '@vitest/runner': 1.3.1 + '@vitest/snapshot': 1.3.1 + '@vitest/spy': 1.3.1 + '@vitest/utils': 1.3.1 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.8 + pathe: 1.1.2 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 2.0.0 + tinybench: 2.6.0 + tinypool: 0.8.2 + vite: 5.1.5 + vite-node: 1.3.1 + why-is-node-running: 2.2.2 transitivePeerDependencies: - - encoding + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser dev: true /vitest@1.3.1(@types/node@20.11.24)(supports-color@9.4.0):