React Testing
Test PAI React renderers, chats, hooks, and thread lists with @pai/react-test-utils.
React tests use @pai/react-test-utils. For contract-bound renderer and client
harness tests, create one typed helper from your React binding:
import { createReactTestUtils } from "@pai/react-test-utils";
import { Pai, SupportAI } from "./pai";
const t = createReactTestUtils({ Pai, AI: SupportAI });The examples in this section use React Testing Library and user-event because
they are common React testing tools. PAI does not require either one; the React
test utilities provide typed fixtures and mock client providers that can be used
with your app's existing React test renderer.
There are three normal frontend layers. Pick the smallest layer that includes the boundary you need to prove.
| Layer | What Is Real | What Is Fake | Use For |
|---|---|---|---|
| Renderer tests | One extracted React component | Providers, hooks, client, runtime, agent, model, tools | Tool card rendering, visual states, submit button behavior inside one renderer |
| Client tool tests | One client-defined tool's schemas, executor, renderer, action, and ToolData | Agent, providers, thread, and transport | Browser behavior, live progress UI, manual client-tool output |
| Client harness tests | PAI React providers and hooks | The client transport and client-visible thread state | Chat UI, thread lists, pending-action UI, scripted streaming, hook behavior |
The Boundary
Renderer tests start with typed props. They do not create a thread.
const compareProposals = t.toolProps("compareProposals");
const tool = compareProposals.outputAvailable({ input, output });
render(<CompareProposalsRenderer tool={tool} />);Client harness tests mount the real React integration, but there is no runtime behind it. The test scripts the same public thread state a real client would receive from a server. PAI provides the provider harness; your test suite still uses its normal React render helper.
await using h = t.createClientHarness({ threadId: "thread-1" });
await h.driver.responses.queue((r) =>
r.assistant([r.text("Searching agencies complete.")]),
);
render(<ChatSurface />, { wrapper: h.Provider });Choosing A Layer
| If You Need To Prove | Use |
|---|---|
One renderer handles native output states or derived running and waiting display states | Renderer test |
| One client-defined tool executes browser behavior or coordinates its renderer and ToolData | Client tool test |
| A hook-driven component reacts to messages, runs, pending-action sidecars, or thread list changes | Client harness test |
| A specific stream shape appears over time, but runtime behavior is irrelevant | Client harness test |
useThreads() refreshes when a thread-created event arrives | Client harness test |
| The real server agent chooses good tools or answers well | Agent eval, not React testing |
Use backend agent/runtime tests to prove how runtime state is produced. Use client harness tests to prove React behavior against that public state shape.
What Not To Do
- Do not import the real server agent for a renderer unit test.
- Do not mock React hooks directly when the client harness can mount real providers.
- Do not import the real server agent into frontend tests just to get a typed contract. Prefer generated contracts.
- Do not use
fireEventfor normal user interactions; preferuserEvent.setup()andawait user.click(...).
Next
- Renderer Tests: pure tool renderer and visual state tests.
- Client Tool Tests: execute and render one client-defined tool without app providers.
- Client Harness Tests: React providers, hooks, scripted client state, thread lists, client tools, and timing.
- React Testing API Reference: public
@pai/react-test-utilshelper summary.