From 0234e18425c16aac9b591cc3432859808e90ad73 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Fri, 10 Jul 2026 16:32:03 +0200 Subject: [PATCH] fix(core-testing): handle batched paths + transformed errors in mock tRPC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit httpBatchLink joins same-tick calls into one request whose path is a comma-separated list of procedure paths; the stub matched the joined string against a single mock key, so any batched pair failed with 'No mock for a.one,a.two'. It now answers one element per procedure, in order. Error bodies are also superjson-serialized — raw error JSON made the client throw 'Unable to transform response' instead of surfacing the intended error. Regression tests added. Co-Authored-By: Claude Fable 5 --- .../core-testing/src/react/mock-trpc.test.ts | 44 +++++++++++++++++++ packages/core-testing/src/react/mock-trpc.ts | 27 +++++++++--- 2 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 packages/core-testing/src/react/mock-trpc.test.ts diff --git a/packages/core-testing/src/react/mock-trpc.test.ts b/packages/core-testing/src/react/mock-trpc.test.ts new file mode 100644 index 0000000..fea835d --- /dev/null +++ b/packages/core-testing/src/react/mock-trpc.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { initTRPC } from "@trpc/server"; +import { z } from "zod"; +import { createMockTrpcClient } from "@/react/mock-trpc"; + +const t = initTRPC.create(); +const _router = t.router({ + blog: t.router({ + articles: t.procedure.output(z.string()).query(() => "articles"), + articleBySlug: t.procedure.output(z.string()).query(() => "article"), + }), +}); +type Router = typeof _router; + +describe("createMockTrpcClient", () => { + it("resolves a single call from its mock", async () => { + const client = createMockTrpcClient({ + "blog.articles": "mock-articles", + }); + await expect(client.blog.articles.query()).resolves.toBe("mock-articles"); + }); + + it("resolves two same-tick (batched) calls, one result per procedure", async () => { + const client = createMockTrpcClient({ + "blog.articles": "mock-articles", + "blog.articleBySlug": "mock-article", + }); + // httpBatchLink joins these into ONE request with a comma-separated + // procedure path — the stub must answer per procedure, in order. + const [articles, article] = await Promise.all([ + client.blog.articles.query(), + client.blog.articleBySlug.query(), + ]); + expect(articles).toBe("mock-articles"); + expect(article).toBe("mock-article"); + }); + + it("rejects a call whose procedure has no mock", async () => { + const client = createMockTrpcClient({}); + await expect(client.blog.articles.query()).rejects.toThrow( + /No mock for blog\.articles/, + ); + }); +}); diff --git a/packages/core-testing/src/react/mock-trpc.ts b/packages/core-testing/src/react/mock-trpc.ts index 62801bf..ff2f876 100644 --- a/packages/core-testing/src/react/mock-trpc.ts +++ b/packages/core-testing/src/react/mock-trpc.ts @@ -10,12 +10,27 @@ export function createMockTrpcClient( ) { const fetchStub: typeof fetch = async (input) => { const url = typeof input === "string" ? input : (input as Request).url; - const path = new URL(url, "http://mock").pathname.replace(/^\/api\/trpc\//, ""); - const result = mocks[path]; - if (result === undefined) { - return new Response(JSON.stringify([{ error: { code: -32603, message: `No mock for ${path}` } }]), { status: 200 }); - } - return new Response(JSON.stringify([{ result: { data: superjson.serialize(result) } }]), { status: 200 }); + const path = new URL(url, "http://mock").pathname.replace( + /^\/api\/trpc\//, + "", + ); + // httpBatchLink joins same-tick calls into one request whose path is the + // comma-separated list of procedure paths; respond with one element per + // call, in order. + const paths = path.split(","); + const body = paths.map((p) => + Object.hasOwn(mocks, p) + ? { result: { data: superjson.serialize(mocks[p]) } } + : { + // Error payloads pass through the transformer too — serialize + // them or the client throws "Unable to transform response". + error: superjson.serialize({ + code: -32603, + message: `No mock for ${p}`, + }), + }, + ); + return new Response(JSON.stringify(body), { status: 200 }); }; // httpBatchLink requires a conditional TransformerOptions that