import { afterEach, expect, test, vi } from "vitest" import { BrowserFetch, browserFetch } from "#Source/request/index.ts" afterEach(() => { vi.useRealTimers() vi.unstubAllGlobals() vi.restoreAllMocks() }) test("BrowserFetch.getJson builds request URL and JSON body", async () => { let capturedInput: URL | RequestInfo | undefined let capturedInit: RequestInit | undefined vi.stubGlobal("fetch", (input: URL | RequestInfo, init?: RequestInit) => { capturedInput = input capturedInit = init return { json: async (): Promise<{ status: string }> => { return await Promise.resolve({ status: "ok" }) }, } }) const fetcher = new BrowserFetch< { query: { page: number; tags: string[]; filter: { active: boolean } } body: { name: string } }, { type: "json"; data: { status: string } } >({ baseUrl: "https://api.example.com", path: "/users", method: "post", headers: { Authorization: "Bearer token", }, query: { page: 2, tags: ["alpha", "beta"], filter: { active: true }, }, body: { name: "Mobius", }, }) const json = await fetcher.getJson() expect(json).toEqual({ status: "ok" }) expect(capturedInput).toBeDefined() expect(capturedInit).toBeDefined() if (capturedInput === undefined || capturedInit === undefined) { throw new Error("Expected fetch to be called with request data.") } const inputUrl = capturedInput instanceof URL ? capturedInput.toString() : typeof capturedInput === "string" ? capturedInput : capturedInput.url expect(inputUrl).toBe( "https://api.example.com/users?page=2&tags=alpha&tags=beta&filter=%7B%22active%22%3Atrue%7D", ) expect(capturedInit.method).toBe("POST") expect(capturedInit.body).toBe('{"name":"Mobius"}') const headers = new Headers(capturedInit.headers) expect(headers.get("Authorization")).toBe("Bearer token") expect(headers.get("Content-Type")).toBe("application/json") }) test("BrowserFetch reads text, blob, arrayBuffer, and stream responses", async () => { const blob = new Blob(["mobius"]) const arrayBuffer = new TextEncoder().encode("matrix").buffer const stream = new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array([1, 2, 3])) controller.close() }, }) const mockFetch = vi.fn((input: URL | RequestInfo) => { const url = input instanceof URL ? input.pathname : new URL(typeof input === "string" ? input : input.url).pathname if (url.endsWith("/text")) { return { text: async (): Promise => { return await Promise.resolve("hello") }, } } if (url.endsWith("/blob")) { return { blob: async (): Promise => { return await Promise.resolve(blob) }, } } if (url.endsWith("/array-buffer")) { return { arrayBuffer: async (): Promise => { return await Promise.resolve(arrayBuffer) }, } } return { body: stream, } }) vi.stubGlobal("fetch", mockFetch) const textFetcher = new BrowserFetch< { query?: undefined; body?: undefined }, { type: "text"; data: string } >({ baseUrl: "https://api.example.com", path: "/text", method: "get", }) const blobFetcher = new BrowserFetch< { query?: undefined; body?: undefined }, { type: "blob"; data: Blob } >({ baseUrl: "https://api.example.com", path: "/blob", method: "get", }) const arrayBufferFetcher = new BrowserFetch< { query?: undefined; body?: undefined }, { type: "arrayBuffer"; data: ArrayBuffer } >({ baseUrl: "https://api.example.com", path: "/array-buffer", method: "get", }) const streamFetcher = new BrowserFetch< { query?: undefined; body?: undefined }, { type: "stream"; data: ReadableStream } >({ baseUrl: "https://api.example.com", path: "/stream", method: "get", }) await expect(textFetcher.getText()).resolves.toBe("hello") await expect(blobFetcher.getBlob()).resolves.toBe(blob) await expect(arrayBufferFetcher.getArrayBuffer()).resolves.toBe(arrayBuffer) await expect(streamFetcher.getStream()).resolves.toBe(stream) }) test("BrowserFetch rejects with a timeout error when the request exceeds the limit", async () => { vi.useFakeTimers() const mockFetch = vi.fn(async (_input: URL | RequestInfo, init?: RequestInit) => { return await new Promise((_resolve, reject) => { init?.signal?.addEventListener("abort", () => { const reason: unknown = init.signal?.reason reject(reason instanceof Error ? reason : new Error("Request aborted.")) }) }) }) vi.stubGlobal("fetch", mockFetch) const fetcher = new BrowserFetch< { query?: undefined; body?: undefined }, { type: "json"; data: { ok: boolean } } >({ baseUrl: "https://api.example.com", path: "/timeout", method: "get", timeout: 50, }) const resultPromise = fetcher.getJson() const timeoutExpectation = expect(resultPromise).rejects.toThrow("Request timeout after 50ms.") await vi.advanceTimersByTimeAsync(50) await timeoutExpectation }) test("BrowserFetch applies the external abortSignal option", async () => { const abortController = new AbortController() let capturedSignal: AbortSignal | undefined const mockFetch = vi.fn(async (_input: URL | RequestInfo, init?: RequestInit) => { capturedSignal = init?.signal ?? undefined return await new Promise((_resolve, reject) => { init?.signal?.addEventListener("abort", () => { const reason: unknown = init.signal?.reason reject(reason instanceof Error ? reason : new Error("Request aborted.")) }) }) }) vi.stubGlobal("fetch", mockFetch) const fetcher = new BrowserFetch< { query?: undefined; body?: undefined }, { type: "json"; data: { ok: boolean } } >({ baseUrl: "https://api.example.com", path: "/abort", method: "get", abortSignal: abortController.signal, }) const resultPromise = fetcher.getJson() const abortExpectation = expect(resultPromise).rejects.toThrow("Manual abort.") abortController.abort(new Error("Manual abort.")) await abortExpectation expect(capturedSignal).toBeDefined() expect(capturedSignal?.aborted).toBe(true) }) test("browserFetch creates a BrowserFetch instance", () => { const fetcher = browserFetch({ baseUrl: "https://api.example.com", path: "/factory", method: "get", }) expect(fetcher).toBeInstanceOf(BrowserFetch) })