/
githubmirror
/
trpc
Обзор
Документация
Войти
/
githubmirror
/
trpc
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/tanstack-react-query/test/polymorphism.test.tsx
310 строк
8 KB
Alex / KATT
chore: remove `waitFor` from testing (#6221)
19 мар 2025, 20:23
Не верифицирован
19 мар 2025, 20:23
b6c6ddb
Код
Авторство
О чём код?
/* It's common to have a data interface which is used across multiple routes in an API, for instance a shared CSV Export system which can be applied to multiple entities in an application. By default this can present a challenge in tRPC clients, because the @trpc/react-query package produces router interfaces which are not always considered structurally compatible by typescript. The polymorphism types can be used to generate abstract types which routers sharing a common interface are compatible with, and allow you to pass around deep router paths to generic components with ease. */ import { testReactResource } from './__helpers'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import '@testing-library/react'; import userEvent from '@testing-library/user-event'; import type { ReactNode } from 'react'; import React, { useState } from 'react'; import { describe, expect, test } from 'vitest'; import { z } from 'zod'; import type { inferOutput } from '../src'; import { t } from './polymorphism.common'; /** * We define a router factory which can be used many times. * * This also exports types generated by RouterLike and UtilsLike to describe * interfaces which concrete router instances are compatible with */ import * as Factory from './polymorphism.factory'; /** * The tRPC backend is defined here */ function createTRPCApi() { /** * Backend data sources. * * Here we use a simple array for demonstration, but in practice these might be * an ORM Repository, a microservice's API Client, etc. Whatever you write your own router factory around. */ const IssueExportsProvider: Factory.FileExportStatusType[] = []; const DiscussionExportsProvider: Factory.FileExportStatusType[] = []; /** * Create an AppRouter instance, with multiple routes using the data export interface */ const appRouter = t.router({ github: t.router({ issues: t.router({ export: Factory.createExportRoute(t.procedure, IssueExportsProvider), }), discussions: t.router({ export: t.mergeRouters( Factory.createExportRoute(t.procedure, DiscussionExportsProvider), // We want to be sure that routers with abstract types, // which then get merged into a larger router, can be used polymorphically t.router({ someExtraProcedure: t.procedure .input(z.object({ name: z.string().min(0) })) .mutation((opts) => { return 'Hello ' + opts.input.name; }), }), ), }), }), }); return { t, appRouter, IssueExportsProvider, DiscussionExportsProvider, }; } describe('polymorphism', () => { /** * Test setup */ const testContext = () => { const { appRouter } = createTRPCApi(); return testReactResource(appRouter); }; describe('simple factory', () => { test('can use a simple factory router with an abstract interface', async () => { await using ctx = testContext(); const { useTRPC } = ctx; /** * Can now define page components which re-use functionality from components, * and pass the specific backend functionality which is needed need */ function IssuesExportPage() { const trpc = useTRPC(); const client = useQueryClient(); const [currentExport, setCurrentExport] = useState<number | null>(null); const invalidate = useMutation({ mutationFn: () => client.invalidateQueries(trpc.github.pathFilter()), }); return ( <> <StartExportButton route={trpc.github.issues.export} onExportStarted={setCurrentExport} /> <RefreshExportsListButton mutate={invalidate.mutate} isPending={invalidate.isPending} /> <ExportStatus status={trpc.github.issues.export.status} currentExport={currentExport} /> <ExportsList list={trpc.github.issues.export.list} /> </> ); } /** * Test Act & Assertions */ const $ = ctx.renderApp(<IssuesExportPage />); await userEvent.click($.getByTestId('startExportBtn')); await vi.waitFor(() => { expect($.container).toHaveTextContent( 'Last Export: `Search for Polymorphism React` (Working)', ); }); await userEvent.click($.getByTestId('refreshBtn')); await vi.waitFor(() => { expect($.container).toHaveTextContent( 'Last Export: `Search for Polymorphism React` (Ready!)', ); }); }); test('can use the abstract interface with a factory instance which has been merged with some extra procedures', async () => { await using ctx = testContext(); const { useTRPC } = ctx; function DiscussionsExportPage() { const trpc = useTRPC(); const client = useQueryClient(); const [currentExport, setCurrentExport] = useState<number | null>(null); const invalidate = useMutation({ mutationFn: () => client.invalidateQueries(trpc.github.pathFilter()), }); return ( <> <StartExportButton route={trpc.github.discussions.export} onExportStarted={setCurrentExport} /> <RefreshExportsListButton mutate={invalidate.mutate} isPending={invalidate.isPending} /> <ExportStatus status={trpc.github.discussions.export.status} currentExport={currentExport} /> <ExportsList list={trpc.github.discussions.export.list} /> </> ); } /** * Test Act & Assertions */ const $ = ctx.renderApp(<DiscussionsExportPage />); await userEvent.click($.getByTestId('startExportBtn')); await vi.waitFor(() => { expect($.container).toHaveTextContent( 'Last Export: `Search for Polymorphism React` (Working)', ); }); await userEvent.click($.getByTestId('refreshBtn')); await vi.waitFor(() => { expect($.container).toHaveTextContent( 'Last Export: `Search for Polymorphism React` (Ready!)', ); }); }); }); }); /** * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * General use components which can consume any matching route interface * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ function StartExportButton(props: { route: Factory.ExportRouteLike; onExportStarted: (id: number) => void; }) { const client = useQueryClient(); const exportStarter = useMutation( props.route.start.mutationOptions({ async onSuccess(data) { props.onExportStarted(data.id); await client.invalidateQueries(props.route.pathFilter()); }, }), ); return ( <button data-testid="startExportBtn" onClick={() => { exportStarter.mutate({ filter: 'polymorphism react', name: 'Search for Polymorphism React', }); }} > Start Export </button> ); } function RefreshExportsListButton(props: { mutate: () => void; isPending: boolean; }) { return ( <button data-testid="refreshBtn" onClick={props.mutate} disabled={props.isPending} > Refresh </button> ); } function ExportStatus< TStatus extends Factory.ExportRouteLike['status'], >(props: { status: TStatus; renderAdditionalFields?: (data: inferOutput<TStatus>) => ReactNode; currentExport: number | null; }) { const exportStatus = useQuery( props.status.queryOptions( { id: props.currentExport ?? -1 }, { enabled: props.currentExport !== null }, ), ); if (!exportStatus.data) { return null; } return ( <p> Last Export: `{exportStatus.data?.name}` ( {exportStatus.data.downloadUri ? 'Ready!' : 'Working'}) {props.renderAdditionalFields?.(exportStatus.data)} </p> ); } function ExportsList(props: { list: Omit<Factory.ExportRouteLike['list'], 'nothing'>; }) { const exportsList = useQuery(props.list.queryOptions()); return ( <> <h4>Downloads:</h4> <ul> {exportsList.data ?.map((item) => item.downloadUri ? ( <li key={item.id}> <a href={item.downloadUri ?? '#'}>{item.name}</a> </li> ) : null, ) .filter(Boolean)} </ul> </> ); }