Last active
April 5, 2024 19:41
-
-
Save martin-fv/9bad75363245a16c918e112eb6a19f0d to your computer and use it in GitHub Desktop.
Storybook mocking and Supertest with tRPC v10
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { t } from "./trpcRouter"; | |
import { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; | |
export const appRouter = t.router({ | |
account: accountRoutes, | |
post: postRoutes, | |
}); | |
// export type definition of API | |
export type AppRouter = typeof appRouter; | |
export type RouterInput = inferRouterInputs<AppRouter>; | |
export type RouterOutput = inferRouterOutputs<AppRouter>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { RouterInput, RouterOutput } from "../appRouter"; | |
import { jsonRpcSuccessResponse } from "../trpc"; | |
import { rest } from "msw"; | |
import path from "path"; | |
/** | |
* Mocks a TRPC endpoint and returns a msw handler for Storybook. | |
* Only supports routes with two levels. | |
* The path and response is fully typed and infers the type from your routes file. | |
* @todo make it accept multiple endpoints | |
* @param endpoint.path - path to the endpoint ex. ["post", "create"] | |
* @param endpoint.response - response to return ex. {id: 1} | |
* @param endpoint.type - specific type of the endpoint ex. "query" or "mutation" (defaults to "query") | |
* @returns - msw endpoint | |
* @example | |
* Page.parameters = { | |
msw: { | |
handlers: [ | |
getTRPCMock({ | |
path: ["post", "getMany"], | |
type: "query", | |
response: [ | |
{ id: 0, title: "test" }, | |
{ id: 1, title: "test" }, | |
], | |
}), | |
], | |
}, | |
}; | |
*/ | |
export const getTRPCMock = < | |
K1 extends keyof RouterInput, | |
K2 extends keyof RouterInput[K1], // object itself | |
O extends RouterOutput[K1][K2] // all its keys | |
>(endpoint: { | |
path: [K1, K2]; | |
response: O; | |
type?: "query" | "mutation"; | |
}) => { | |
const fn = endpoint.type === "mutation" ? rest.post : rest.get; | |
const route = path.join( | |
process.env.BASE_URL, | |
"/trpc/", | |
endpoint.path[0] + "." + (endpoint.path[1] as string) | |
); | |
return fn(route, (req, res, ctx) => { | |
return res(ctx.json(jsonRpcSuccessResponse(endpoint.response))); | |
}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Meta } from "@storybook/react/types-6-0"; | |
import { PostList } from "../PostList"; | |
import { getTRPCMock } from "../getTrpcMock"; | |
export default { | |
title: "Components/PostList", | |
component: PostList, | |
} as Meta; | |
export const PostListPage = () => { | |
return <PostList />; | |
}; | |
PostList.parameters = { | |
msw: { | |
handlers: [ | |
getTRPCMock({ | |
path: ["post", "listPosts"], | |
response: [ | |
{ | |
id: "1", | |
title: "Hello", | |
description: "World", | |
}, | |
], | |
}), | |
], | |
}, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//https://github.com/mswjs/msw-storybook-addon | |
//Install ths package first | |
import { initialize, mswDecorator } from 'msw-storybook-addon'; | |
// Initialize MSW | |
initialize(); | |
// Provide the MSW addon decorator globally | |
export const decorators = [mswDecorator]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { RouterInput, RouterOutput } from "../../src/trpc/appRouter"; | |
import { TRPCSuccessResponse } from "@trpc/server/rpc"; | |
import server from "../../src/server"; | |
import supertest from "supertest"; | |
export type SuperRequestResponse<T = any> = { | |
body: T; | |
}; | |
/** | |
* Endpoint testing with TRPC using supertest | |
* Only supports routes with two levels | |
* @todo make it accept multiple endpoints | |
* @param enpoint.token - auth token | |
* @param enpoint.input - Input to the endpoint | |
* @param enpoint.method - HTTP method, defaults to GET | |
* @param enpoint.expectStatus - expected status code, defaults to 200 | |
* @returns - The typed HTTP response | |
* @example | |
* await superTrpc("follow", "setFollow", { | |
token: token, | |
input: { | |
person_uuid: creator.uuid, | |
follow: false, | |
}, | |
method: "POST", | |
expectStatus: 200, | |
}); | |
*/ | |
export const superTrpc = async < | |
K1 extends keyof RouterInput, | |
K2 extends keyof RouterInput[K1], // object itself | |
I extends RouterInput[K1][K2], // all its keys | |
O extends RouterOutput[K1][K2] // all its keys | |
>( | |
parentRoute: K1, | |
childRoute: K2, | |
{ | |
input, | |
expectStatus = 200, | |
token, | |
method, | |
}: { | |
input?: I; | |
expectStatus?: number; | |
token?: string; | |
method?: "GET" | "POST"; | |
} | |
): Promise<SuperRequestResponse<TRPCSuccessResponse<O>>> => { | |
let headers: any = {}; | |
if (token) headers["X-Auth-Token"] = token; | |
const request = await supertest(server); | |
const route = `${TRPC_ENDPOINT_PREFIX}${parentRoute}.${childRoute.toString()}`; | |
const res = | |
method === "POST" | |
? await request.post(route).send(input).set(headers) | |
: await request.get(route).send(input).set(headers); | |
if (res.statusCode !== expectStatus) console.error("Error Body", res.body); | |
expect(res.statusCode).toBe(expectStatus); | |
return res; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type RpcResponse<Data> = RpcSuccessResponse<Data> | RpcErrorResponse; | |
export type RpcSuccessResponse<Data> = { | |
id: null; | |
result: { type: "data"; data: Data }; | |
}; | |
export type RpcErrorResponse = { | |
id: null; | |
error: { | |
message: string; | |
code: number; | |
data: { | |
code: string; | |
httpStatus: number; | |
stack: string; | |
path: string; //TQuery | |
}; | |
}; | |
}; | |
// According to JSON-RPC 2.0 and tRPC documentation. | |
// https://trpc.io/docs/rpc | |
export const jsonRpcSuccessResponse = (data: unknown) => ({ | |
id: null, | |
result: { type: "data", data }, | |
}); |
I went with https://github.com/maloguertin/msw-trpc instead :
npx msw init ./public/
preview.ts
import { initialize, mswDecorator } from "msw-storybook-addon";
...
initialize();
...
export const decorators = [mswDecorator, ...OtherDecorators];
src/services/Api/mock.ts
import { createTRPCMsw } from "msw-trpc";
import { getBaseUrl } from "./getBaseUrl";
import type { AppRouter } from "./router";
export const trpcMsw = createTRPCMsw<AppRouter>({
basePath: "/api/trpc",
baseUrl: getBaseUrl(),
});
src/components/TheComponent/TheComponent.stories.tsx
import React from "react";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { trpcMsw } from "../../../services/Api/mock";
import { TheComponent } from "./TheComponent";
export default {
title: "Your/Storybook/Story",
component: TheComponent,
decorators: [
(Story) => (
<TrpcProvider> // :one:
<Story />
</TrpcProvider>
),
],
parameters: {
msw: {
handlers: [
trpcMsw.the.query.path.for.your.schema.query((req, res, ctx) => { // :two:
return res(ctx.status(200), ctx.data({ id: 'trololololololol' }))
})
],
},
},
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
} as ComponentMeta<typeof TheComponent>;
export const Basic:ComponentStory<typeof TheComponent> = (args) => (
<TheComponent {...args} />
);
- 1️⃣ is for mocking the client, mine is a nextjs project so by default it ends up using the trpc/next hoc, here in storybook we just use a plain react provider.
- 2️⃣
the.query.path.for.your.schema
is my specific schema... yours will be different.
This is super helpful @airtonix !
My setup is also using TRPC and Storybook with Next - though I'm still confused with something here: what exactly is <TrpcProvider>
as you've mentioned it will probably use withTRPC
hoc? I.e. my _app.tsx
has trpc.withTRPC(MyApp)
- but not a <ContextWrapper>
?
I'm getting: Cannot destructure property 'abortOnUnmount' of 'useContext(...)' as it is null.
in my Stories, so I'm obviously missing some of the trpc context it needs.
Edit: just spotted this ...
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If we're in storybook, what's the point of the
superTrpc.ts
?Seems like distracting noise towards the example of "mock response of trpc in storybook".
Remember that this also needs to work on static builds of storybook hosted on a static website.