<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Msw on RockB</title><link>https://baeseokjae.github.io/tags/msw/</link><description>Recent content in Msw on RockB</description><image><title>RockB</title><url>https://baeseokjae.github.io/images/og-default.png</url><link>https://baeseokjae.github.io/images/og-default.png</link></image><generator>Hugo</generator><language>en-us</language><lastBuildDate>Mon, 18 May 2026 12:03:55 +0000</lastBuildDate><atom:link href="https://baeseokjae.github.io/tags/msw/index.xml" rel="self" type="application/rss+xml"/><item><title>React Testing Library AI Component Integration Developer Guide 2026</title><link>https://baeseokjae.github.io/posts/react-testing-library-ai-component-integration-guide-2026/</link><pubDate>Mon, 18 May 2026 12:03:55 +0000</pubDate><guid>https://baeseokjae.github.io/posts/react-testing-library-ai-component-integration-guide-2026/</guid><description>Complete guide to testing AI-powered React components in 2026 using RTL, Vitest, MSW, and Vercel AI SDK mock helpers.</description><content:encoded><![CDATA[<p>React Testing Library (RTL) remains the default choice for component tests in 2026, but testing components that call AI APIs — streaming chat, autocomplete, content generation — requires async patterns, mock strategies, and setup choices that standard RTL tutorials skip entirely. This guide covers the complete modern stack: Vitest + RTL + MSW + Vercel AI SDK test helpers, with concrete code you can paste into a real project.</p>
<h2 id="why-testing-ai-powered-react-components-is-different-in-2026">Why Testing AI-Powered React Components Is Different in 2026</h2>
<p>AI-powered React components introduce three testing challenges that have no equivalent in a plain CRUD app: non-deterministic outputs, streaming responses that arrive in chunks over time, and expensive external API calls that you can never make in a test suite. React is used by 44.7% of all developers (Stack Overflow Survey 2025) and holds a 69.74% market share among JavaScript frameworks — which means millions of developers are now wiring AI APIs into React UIs for the first time and discovering that <code>waitFor(() =&gt; expect(...))</code> alone is not enough. A chat component built on <code>useChat</code> from the Vercel AI SDK will fire a POST request, receive a Server-Sent Events (SSE) stream, and progressively update the DOM as tokens arrive. Standard synchronous render tests break immediately. The strategies that work are: deterministic mocks at the network layer via MSW, first-party mock providers from the AI SDK itself (<code>MockLanguageModelV3</code>, <code>simulateReadableStream</code>), and RTL&rsquo;s async query helpers (<code>findBy*</code>, <code>waitFor</code>) used correctly. Without all three in place, tests either hit live APIs (slow, flaky, costly) or silently pass while the real network behavior goes untested.</p>
<h3 id="what-makes-ai-components-hard-to-test">What Makes AI Components Hard to Test</h3>
<p>AI component tests fail for one of three reasons: the test doesn&rsquo;t wait for the streamed DOM update to settle, the mock returns a complete response when the real API streams tokens, or the test environment has no way to intercept the outgoing fetch/XHR at all. Each failure mode has a different fix, and conflating them wastes hours.</p>
<h3 id="the-2026-baseline-what-you-need-before-you-start">The 2026 Baseline: What You Need Before You Start</h3>
<p>Before writing a single test, you need four things: a test runner (Vitest for Vite-based projects, Jest for CRA/Webpack legacy), <code>@testing-library/react</code>, <code>@testing-library/user-event</code> v14+, and <code>msw</code> v2. If you are on the Vercel AI SDK, add <code>ai/test</code> to your dev dependencies. React 19 reached 48.4% daily usage among State of React 2025 respondents within months of release — if you are on React 19, ensure your RTL version is <code>@testing-library/react</code> v16 or later, which ships React 19 support.</p>
<h2 id="setting-up-the-modern-testing-stack-vitest--react-testing-library--msw">Setting Up the Modern Testing Stack: Vitest + React Testing Library + MSW</h2>
<p>The modern stack for testing AI-powered React components in 2026 is Vitest + React Testing Library + MSW — not Jest. Vitest delivers up to 3x faster test startup times compared to Jest for large codebases due to direct Vite engine integration, and it ships with ESM support out of the box, which matters because the Vercel AI SDK and most modern AI client libraries are ESM-only packages. MSW v2 replaced the old <code>setupServer</code> API and now works identically in Node (for Vitest/Jest unit tests) and the browser (for Playwright/Storybook tests), giving you one mock layer for everything. The setup is three files: <code>vitest.config.ts</code>, <code>src/setupTests.ts</code>, and <code>src/mocks/server.ts</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#75715e">// vitest.config.ts
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">defineConfig</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;vitest/config&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">react</span> <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@vitejs/plugin-react&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#a6e22e">defineConfig</span>({
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">plugins</span><span style="color:#f92672">:</span> [<span style="color:#a6e22e">react</span>()],
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">test</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">environment</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;jsdom&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">setupFiles</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;./src/setupTests.ts&#39;</span>],
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">globals</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#75715e">// src/setupTests.ts
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#e6db74">&#39;@testing-library/jest-dom&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">beforeAll</span>, <span style="color:#a6e22e">afterEach</span>, <span style="color:#a6e22e">afterAll</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;vitest&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">server</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;./mocks/server&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">beforeAll</span>(() <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">listen</span>({ <span style="color:#a6e22e">onUnhandledRequest</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;error&#39;</span> }))
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">afterEach</span>(() <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">resetHandlers</span>())
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">afterAll</span>(() <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">close</span>())
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#75715e">// src/mocks/server.ts
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">setupServer</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;msw/node&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">handlers</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;./handlers&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">server</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">setupServer</span>(...<span style="color:#a6e22e">handlers</span>)
</span></span></code></pre></div><h3 id="installing-dependencies">Installing Dependencies</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install -D vitest @vitejs/plugin-react jsdom
</span></span><span style="display:flex;"><span>npm install -D @testing-library/react @testing-library/user-event @testing-library/jest-dom
</span></span><span style="display:flex;"><span>npm install -D msw
</span></span><span style="display:flex;"><span><span style="color:#75715e"># For Vercel AI SDK projects:</span>
</span></span><span style="display:flex;"><span>npm install ai @ai-sdk/openai
</span></span></code></pre></div><p>Set <code>&quot;type&quot;: &quot;module&quot;</code> in <code>package.json</code> if you are not already using it, and add <code>&quot;test&quot;: &quot;vitest&quot;</code> to your scripts. Run <code>npx msw init public/</code> once to generate <code>mockServiceWorker.js</code> for browser-mode tests.</p>
<h2 id="mocking-ai-api-responses-with-mock-service-worker-msw">Mocking AI API Responses with Mock Service Worker (MSW)</h2>
<p>Mocking AI API endpoints with MSW is the gold standard for testing React components that call OpenAI, Anthropic, or Gemini in 2026 — better than mocking <code>fetch</code> directly because MSW intercepts at the network layer, meaning the exact same handler works whether your component uses <code>fetch</code>, <code>axios</code>, or the AI SDK&rsquo;s internal HTTP client. MSW v2 uses a <code>http</code> namespace with typed request/response helpers, replacing the old <code>rest</code> API. For a component that calls <code>POST /api/chat</code>, you write one handler that returns a mocked response and every test that renders that component uses it automatically — no per-test <code>jest.mock()</code> required. The critical pattern for AI APIs is returning the correct <code>Content-Type</code> header: <code>text/event-stream</code> for streaming endpoints, <code>application/json</code> for non-streaming completions. MSW lets you control both, and <code>server.use(overrideHandler)</code> lets individual tests override the default to simulate error states, empty responses, or rate-limit errors without touching shared handler state.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#75715e">// src/mocks/handlers.ts
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">http</span>, <span style="color:#a6e22e">HttpResponse</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;msw&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Non-streaming completion mock
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">handlers</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">post</span>(<span style="color:#e6db74">&#39;/api/complete&#39;</span>, () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">HttpResponse</span>.<span style="color:#a6e22e">json</span>({
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">id</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;chatcmpl-test&#39;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">choices</span><span style="color:#f92672">:</span> [{ <span style="color:#a6e22e">message</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Mocked AI response&#39;</span> }, <span style="color:#a6e22e">finish_reason</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;stop&#39;</span> }],
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  }),
</span></span><span style="display:flex;"><span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Streaming SSE mock helper
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">streamingChatHandler</span>(<span style="color:#a6e22e">chunks</span>: <span style="color:#66d9ef">string</span>[]) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">post</span>(<span style="color:#e6db74">&#39;/api/chat&#39;</span>, () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">encoder</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">TextEncoder</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">stream</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">ReadableStream</span>({
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">async</span> <span style="color:#a6e22e">start</span>(<span style="color:#a6e22e">controller</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">chunk</span> <span style="color:#66d9ef">of</span> <span style="color:#a6e22e">chunks</span>) {
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">line</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`data: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">stringify</span>({ <span style="color:#a6e22e">choices</span><span style="color:#f92672">:</span> [{ <span style="color:#a6e22e">delta</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">content</span>: <span style="color:#66d9ef">chunk</span> <span style="color:#e6db74">}</span><span style="color:#e6db74"> }] })}</span><span style="color:#960050;background-color:#1e0010">\</span><span style="color:#e6db74">n</span><span style="color:#960050;background-color:#1e0010">\</span><span style="color:#e6db74">n`</span>
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">controller</span>.<span style="color:#a6e22e">enqueue</span>(<span style="color:#a6e22e">encoder</span>.<span style="color:#a6e22e">encode</span>(<span style="color:#a6e22e">line</span>))
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">await</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Promise</span>(<span style="color:#a6e22e">r</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">setTimeout</span>(<span style="color:#a6e22e">r</span>, <span style="color:#ae81ff">10</span>))
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">controller</span>.<span style="color:#a6e22e">enqueue</span>(<span style="color:#a6e22e">encoder</span>.<span style="color:#a6e22e">encode</span>(<span style="color:#e6db74">&#39;data: [DONE]\n\n&#39;</span>))
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">controller</span>.<span style="color:#a6e22e">close</span>()
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">HttpResponse</span>(<span style="color:#a6e22e">stream</span>, {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> { <span style="color:#e6db74">&#39;Content-Type&#39;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;text/event-stream&#39;</span> },
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="testing-error-states-with-msw">Testing Error States with MSW</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">server</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;../mocks/server&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">http</span>, <span style="color:#a6e22e">HttpResponse</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;msw&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">test</span>(<span style="color:#e6db74">&#39;shows error when AI API returns 429&#39;</span>, <span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">use</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">post</span>(<span style="color:#e6db74">&#39;/api/chat&#39;</span>, () <span style="color:#f92672">=&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">HttpResponse</span>.<span style="color:#a6e22e">json</span>({ <span style="color:#a6e22e">error</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Rate limit exceeded&#39;</span> }, { <span style="color:#a6e22e">status</span>: <span style="color:#66d9ef">429</span> })
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">render</span>(&lt;<span style="color:#f92672">ChatComponent</span> /&gt;)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#66d9ef">type</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;textbox&#39;</span>), <span style="color:#e6db74">&#39;Hello&#39;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#a6e22e">click</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;button&#39;</span>, { <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">/send/i</span> }))
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">expect</span>(<span style="color:#66d9ef">await</span> <span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">findByText</span>(<span style="color:#e6db74">/rate limit/i</span>)).<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><h2 id="testing-vercel-ai-sdk-hooks-usechat-and-usecompletion">Testing Vercel AI SDK Hooks: useChat and useCompletion</h2>
<p>Testing <code>useChat</code> and <code>useCompletion</code> hooks from the Vercel AI SDK requires two things that MSW alone cannot provide: wrapping the component in the correct React provider and using the AI SDK&rsquo;s own <code>MockLanguageModelV1</code> test helper for unit-level hook tests. The AI SDK Core ships built-in mock providers — <code>MockLanguageModelV1</code> and <code>MockEmbeddingModelV1</code> — that replace real OpenAI/Anthropic models with fully deterministic, synchronous or streaming fakes without any network call. This means your hook tests run in under 100ms, produce identical output every run, and never fail because an external API was slow or rate-limited. TanStack Query, used in many AI data-fetching workflows, has 68% adoption among React developers with only 1% negative sentiment according to the State of React 2025 survey — if your AI hooks use it, wrap your render call in <code>QueryClientProvider</code> with a fresh <code>QueryClient</code> per test to prevent shared state between tests.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">render</span>, <span style="color:#a6e22e">screen</span>, <span style="color:#a6e22e">waitFor</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@testing-library/react&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">userEvent</span> <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@testing-library/user-event&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useChat</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;ai/react&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">MockLanguageModelV1</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;ai/test&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Component under test
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">ChatBox() {</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">messages</span>, <span style="color:#a6e22e">input</span>, <span style="color:#a6e22e">handleInputChange</span>, <span style="color:#a6e22e">handleSubmit</span> } <span style="color:#f92672">=</span> <span style="color:#a6e22e">useChat</span>()
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">form</span> <span style="color:#a6e22e">onSubmit</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">handleSubmit</span>}&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">input</span> <span style="color:#a6e22e">value</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">input</span>} <span style="color:#a6e22e">onChange</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">handleInputChange</span>} <span style="color:#a6e22e">aria-label</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Message&#34;</span> /&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;submit&#34;</span>&gt;<span style="color:#a6e22e">Send</span>&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">ul</span>&gt;
</span></span><span style="display:flex;"><span>        {<span style="color:#a6e22e">messages</span>.<span style="color:#a6e22e">map</span>(<span style="color:#a6e22e">m</span> <span style="color:#f92672">=&gt;</span> &lt;<span style="color:#f92672">li</span> <span style="color:#a6e22e">key</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">id</span>} <span style="color:#a6e22e">role</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;listitem&#34;</span>&gt;{<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">content</span>}&lt;/<span style="color:#f92672">li</span>&gt;)}
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">ul</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">form</span>&gt;
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">test</span>(<span style="color:#e6db74">&#39;sends message and displays AI reply&#39;</span>, <span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">use</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">post</span>(<span style="color:#e6db74">&#39;/api/chat&#39;</span>, <span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// Return Vercel AI SDK data stream format
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">encoder</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">TextEncoder</span>()
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">body</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">ReadableStream</span>({
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">start</span>(<span style="color:#a6e22e">c</span>) {
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">enqueue</span>(<span style="color:#a6e22e">encoder</span>.<span style="color:#a6e22e">encode</span>(<span style="color:#e6db74">&#39;0:&#34;Hello from mock AI&#34;\n&#39;</span>))
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">enqueue</span>(<span style="color:#a6e22e">encoder</span>.<span style="color:#a6e22e">encode</span>(<span style="color:#e6db74">&#39;d:{&#34;finishReason&#34;:&#34;stop&#34;}\n&#39;</span>))
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">close</span>()
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>      })
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">HttpResponse</span>(<span style="color:#a6e22e">body</span>, {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#e6db74">&#39;Content-Type&#39;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;text/plain; charset=utf-8&#39;</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#e6db74">&#39;X-Vercel-AI-Data-Stream&#39;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;v1&#39;</span>,
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>      })
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">render</span>(&lt;<span style="color:#f92672">ChatBox</span> /&gt;)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#66d9ef">type</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByLabelText</span>(<span style="color:#e6db74">&#39;Message&#39;</span>), <span style="color:#e6db74">&#39;Hi there&#39;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#a6e22e">click</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;button&#39;</span>, { <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">/send/i</span> }))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">expect</span>(<span style="color:#66d9ef">await</span> <span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">findByText</span>(<span style="color:#e6db74">&#39;Hello from mock AI&#39;</span>)).<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><h3 id="testing-usecompletion">Testing useCompletion</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#a6e22e">test</span>(<span style="color:#e6db74">&#39;useCompletion updates text on streaming input&#39;</span>, <span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">use</span>(<span style="color:#a6e22e">streamingCompletionHandler</span>([<span style="color:#e6db74">&#39;Once &#39;</span>, <span style="color:#e6db74">&#39;upon &#39;</span>, <span style="color:#e6db74">&#39;a time&#39;</span>]))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">render</span>(&lt;<span style="color:#f92672">CompletionBox</span> /&gt;)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#66d9ef">type</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByLabelText</span>(<span style="color:#e6db74">&#39;Prompt&#39;</span>), <span style="color:#e6db74">&#39;Write a story&#39;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#a6e22e">click</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;button&#39;</span>, { <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">/generate/i</span> }))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">waitFor</span>(() <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByText</span>(<span style="color:#e6db74">&#39;Once upon a time&#39;</span>)).<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><h2 id="testing-streaming-llm-responses-with-simulatereadablestream">Testing Streaming LLM Responses with simulateReadableStream</h2>
<p>Testing streaming LLM responses in React is the most technically demanding part of the AI testing stack, and <code>simulateReadableStream</code> from <code>ai/test</code> is the cleanest solution available in 2026. It creates a <code>ReadableStream</code> that emits chunks on a controlled schedule — you pass an array of chunks and an optional delay per chunk, and the helper produces a real <code>ReadableStream</code> instance your component code cannot distinguish from a real API stream. The advantage over writing your own <code>ReadableStream</code> mock is that <code>simulateReadableStream</code> understands the Vercel AI SDK&rsquo;s internal data stream protocol (the <code>0:</code>, <code>d:</code>, <code>8:</code> prefix format), so you can produce correctly-formatted AI SDK streams without reverse-engineering the wire format. Combined with RTL&rsquo;s <code>findBy*</code> async queries — which poll the DOM until an element appears or a timeout fires — you can write tests that verify each intermediate streaming state: &ldquo;loading spinner visible&rdquo;, &ldquo;first token appears&rdquo;, &ldquo;response complete, input re-enabled&rdquo;.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">simulateReadableStream</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;ai/test&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">render</span>, <span style="color:#a6e22e">screen</span>, <span style="color:#a6e22e">waitFor</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@testing-library/react&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">userEvent</span> <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@testing-library/user-event&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">test</span>(<span style="color:#e6db74">&#39;renders tokens progressively as stream arrives&#39;</span>, <span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">chunks</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    { <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;text-delta&#39;</span>, <span style="color:#a6e22e">textDelta</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;React &#39;</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;text-delta&#39;</span>, <span style="color:#a6e22e">textDelta</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;is &#39;</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;text-delta&#39;</span>, <span style="color:#a6e22e">textDelta</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;great.&#39;</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;finish&#39;</span>, <span style="color:#a6e22e">finishReason</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;stop&#39;</span>, <span style="color:#a6e22e">usage</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">promptTokens</span>: <span style="color:#66d9ef">5</span>, <span style="color:#a6e22e">completionTokens</span>: <span style="color:#66d9ef">3</span> } },
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">use</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">post</span>(<span style="color:#e6db74">&#39;/api/stream&#39;</span>, () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">stream</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">simulateReadableStream</span>({
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">chunks</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">chunkDelayInMs</span>: <span style="color:#66d9ef">50</span>,
</span></span><span style="display:flex;"><span>      })
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">HttpResponse</span>(<span style="color:#a6e22e">stream</span>, {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> { <span style="color:#e6db74">&#39;Content-Type&#39;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;text/plain; charset=utf-8&#39;</span>, <span style="color:#e6db74">&#39;X-Vercel-AI-Data-Stream&#39;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;v1&#39;</span> },
</span></span><span style="display:flex;"><span>      })
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">render</span>(&lt;<span style="color:#f92672">StreamingDisplay</span> /&gt;)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#a6e22e">click</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;button&#39;</span>, { <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">/ask/i</span> }))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// Verify loading state
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">expect</span>(<span style="color:#66d9ef">await</span> <span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">findByRole</span>(<span style="color:#e6db74">&#39;status&#39;</span>)).<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// Verify final content
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">expect</span>(<span style="color:#66d9ef">await</span> <span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">findByText</span>(<span style="color:#e6db74">&#39;React is great.&#39;</span>)).<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// Verify input re-enabled after stream completes
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">waitFor</span>(() <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;button&#39;</span>, { <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">/ask/i</span> })).<span style="color:#a6e22e">not</span>.<span style="color:#a6e22e">toBeDisabled</span>()
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><h3 id="testing-cancellation">Testing Cancellation</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#a6e22e">test</span>(<span style="color:#e6db74">&#39;cancel button aborts stream and re-enables input&#39;</span>, <span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">use</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">post</span>(<span style="color:#e6db74">&#39;/api/stream&#39;</span>, <span style="color:#66d9ef">async</span> ({ <span style="color:#a6e22e">request</span> }) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// Simulate slow stream — test will cancel mid-way
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">stream</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">simulateReadableStream</span>({ <span style="color:#a6e22e">chunks</span>: <span style="color:#66d9ef">longChunks</span>, <span style="color:#a6e22e">chunkDelayInMs</span>: <span style="color:#66d9ef">200</span> })
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">HttpResponse</span>(<span style="color:#a6e22e">stream</span>, { <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> { <span style="color:#e6db74">&#39;Content-Type&#39;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;text/event-stream&#39;</span> } })
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">render</span>(&lt;<span style="color:#f92672">StreamingDisplay</span> /&gt;)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#a6e22e">click</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;button&#39;</span>, { <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">/ask/i</span> }))
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">findByRole</span>(<span style="color:#e6db74">&#39;status&#39;</span>) <span style="color:#75715e">// loading started
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#a6e22e">click</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;button&#39;</span>, { <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">/stop/i</span> }))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">waitFor</span>(() <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">queryByRole</span>(<span style="color:#e6db74">&#39;status&#39;</span>)).<span style="color:#a6e22e">not</span>.<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;button&#39;</span>, { <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">/ask/i</span> })).<span style="color:#a6e22e">not</span>.<span style="color:#a6e22e">toBeDisabled</span>()
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><h2 id="common-ai-component-test-patterns-chatbots-autocomplete-content-generation">Common AI Component Test Patterns (Chatbots, Autocomplete, Content Generation)</h2>
<p>The three most common AI-powered React components each have a distinct testing pattern. Chatbots (multi-turn conversation UI) need tests that verify message history accumulates correctly across turns, the input is disabled while a response streams, and error messages surface when the API fails. Autocomplete components powered by an LLM need tests that verify debounce behavior (no request fires until after N ms of idle input), the suggestion list renders with the correct number of items, and keyboard navigation works — RTL&rsquo;s <code>getByRole('listbox')</code> and <code>getByRole('option')</code> queries are the right tool here, not <code>querySelector</code>. Content generation components (blog writers, code explainers) need tests that verify the component starts in an empty state, transitions to a loading state on submit, and displays the final content — with particular attention to sanitization if the generated content is rendered as HTML. Accessibility-first querying is especially important for AI-generated UI content that may produce unpredictable DOM output; <code>getByRole</code> queries will catch a component that renders a button with no accessible name even if <code>getByTestId</code> would silently pass.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#75715e">// Chatbot: verify multi-turn conversation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">test</span>(<span style="color:#e6db74">&#39;accumulates messages across multiple turns&#39;</span>, <span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">callCount</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">use</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">post</span>(<span style="color:#e6db74">&#39;/api/chat&#39;</span>, () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">callCount</span><span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">makeMockStream</span>(<span style="color:#a6e22e">callCount</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">1</span> <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;First response&#39;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Second response&#39;</span>)
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">render</span>(&lt;<span style="color:#f92672">ChatBot</span> /&gt;)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#66d9ef">type</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByLabelText</span>(<span style="color:#e6db74">&#39;Message&#39;</span>), <span style="color:#e6db74">&#39;Turn 1&#39;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#a6e22e">click</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;button&#39;</span>, { <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">/send/i</span> }))
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">expect</span>(<span style="color:#66d9ef">await</span> <span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">findByText</span>(<span style="color:#e6db74">&#39;First response&#39;</span>)).<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#66d9ef">type</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByLabelText</span>(<span style="color:#e6db74">&#39;Message&#39;</span>), <span style="color:#e6db74">&#39;Turn 2&#39;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#a6e22e">click</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;button&#39;</span>, { <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">/send/i</span> }))
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">expect</span>(<span style="color:#66d9ef">await</span> <span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">findByText</span>(<span style="color:#e6db74">&#39;Second response&#39;</span>)).<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// Both messages still in DOM
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getAllByRole</span>(<span style="color:#e6db74">&#39;listitem&#39;</span>)).<span style="color:#a6e22e">toHaveLength</span>(<span style="color:#ae81ff">4</span>) <span style="color:#75715e">// 2 user + 2 AI
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Autocomplete: verify debounce and list rendering
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">test</span>(<span style="color:#e6db74">&#39;shows autocomplete suggestions after debounce&#39;</span>, <span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">vi</span>.<span style="color:#a6e22e">useFakeTimers</span>()
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">use</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">post</span>(<span style="color:#e6db74">&#39;/api/suggest&#39;</span>, () <span style="color:#f92672">=&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">HttpResponse</span>.<span style="color:#a6e22e">json</span>({ <span style="color:#a6e22e">suggestions</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;React Hooks&#39;</span>, <span style="color:#e6db74">&#39;React Server Components&#39;</span>, <span style="color:#e6db74">&#39;React 19&#39;</span>] })
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">render</span>(&lt;<span style="color:#f92672">AIAutocomplete</span> /&gt;)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#66d9ef">type</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;combobox&#39;</span>), <span style="color:#e6db74">&#39;React&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// Before debounce: no suggestions
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">queryByRole</span>(<span style="color:#e6db74">&#39;listbox&#39;</span>)).<span style="color:#a6e22e">not</span>.<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">vi</span>.<span style="color:#a6e22e">advanceTimersByTime</span>(<span style="color:#ae81ff">400</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">waitFor</span>(() <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;listbox&#39;</span>)).<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getAllByRole</span>(<span style="color:#e6db74">&#39;option&#39;</span>)).<span style="color:#a6e22e">toHaveLength</span>(<span style="color:#ae81ff">3</span>)
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">vi</span>.<span style="color:#a6e22e">useRealTimers</span>()
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><h3 id="content-generation-testing-html-output-safely">Content Generation: Testing HTML Output Safely</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#a6e22e">test</span>(<span style="color:#e6db74">&#39;renders generated markdown as safe HTML&#39;</span>, <span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">use</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">post</span>(<span style="color:#e6db74">&#39;/api/generate&#39;</span>, () <span style="color:#f92672">=&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">HttpResponse</span>.<span style="color:#a6e22e">json</span>({ <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;## Hello\n\nThis is **bold**.&#39;</span> })
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">render</span>(&lt;<span style="color:#f92672">ContentGenerator</span> /&gt;)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">userEvent</span>.<span style="color:#a6e22e">click</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByRole</span>(<span style="color:#e6db74">&#39;button&#39;</span>, { <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">/generate/i</span> }))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">heading</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">findByRole</span>(<span style="color:#e6db74">&#39;heading&#39;</span>, { <span style="color:#a6e22e">level</span>: <span style="color:#66d9ef">2</span>, <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Hello&#39;</span> })
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">heading</span>).<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// Verify no script injection
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">expect</span>(document.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;script[data-injected]&#39;</span>)).<span style="color:#a6e22e">not</span>.<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><h2 id="using-ai-coding-tools-to-generate-and-improve-your-react-tests">Using AI Coding Tools to Generate and Improve Your React Tests</h2>
<p>AI coding tools — Claude Code, Cursor, GitHub Copilot — can dramatically accelerate RTL test writing in 2026 when given the right context, but they produce garbage when the prompt omits the testing stack details. The pattern that works: open your component file and your <code>setupTests.ts</code> in context, then prompt with &ldquo;Write a Vitest + React Testing Library test for this component. Use MSW to mock the POST /api/chat endpoint. Use findBy queries for async assertions. Test the happy path, an error state, and the loading state.&rdquo; Without the stack context, AI tools default to Jest + <code>jest.fn()</code> mocks and synchronous <code>getBy</code> queries that fail on async AI components. With it, they produce near-complete tests that need only minor adjustment. Claude Code&rsquo;s file-aware context makes it particularly effective for this workflow — it can read your MSW handler file and generate a test that reuses your existing handler structure rather than inventing a new one. Use <code>@testing-library/user-event</code> for all interaction simulation: never use <code>fireEvent.click()</code> for AI component tests because it skips pointer events and focus management, which matters when testing that a chat input is disabled during streaming.</p>
<h3 id="effective-prompts-for-ai-assisted-test-generation">Effective Prompts for AI-Assisted Test Generation</h3>
<ul>
<li>&ldquo;Generate a Vitest test for <code>&lt;ChatComponent /&gt;</code>. It uses <code>useChat</code> from <code>ai/react</code>. Mock POST /api/chat with MSW to return a Vercel AI SDK data stream. Assert the response appears in a list item.&rdquo;</li>
<li>&ldquo;Add a test case where the API returns HTTP 500. The component should show an error message. Reuse the existing MSW server from <code>src/mocks/server.ts</code>.&rdquo;</li>
<li>&ldquo;Write a test that uses <code>vi.useFakeTimers()</code> to verify that my autocomplete component debounces input by 300ms before calling the AI API.&rdquo;</li>
</ul>
<h3 id="reviewing-ai-generated-tests">Reviewing AI-Generated Tests</h3>
<p>When an AI tool generates a test, check three things before committing: (1) does it use <code>findBy*</code> or <code>waitFor</code> for every assertion that follows an async action? (2) does it reset MSW handlers in <code>afterEach</code>? (3) does it avoid <code>act()</code> warnings by letting RTL&rsquo;s async utilities handle the event loop? A common AI-generated mistake is wrapping <code>userEvent</code> calls in <code>act()</code>, which is redundant since <code>@testing-library/user-event</code> v14 already wraps interactions in <code>act</code> internally.</p>
<h2 id="cicd-integration-and-best-practices-for-ai-component-testing">CI/CD Integration and Best Practices for AI Component Testing</h2>
<p>Integrating AI component tests into CI/CD requires three configuration decisions that greenfield guides skip: timeout tuning, parallelism limits, and environment variable management. AI component tests with streaming mocks can be slow because <code>simulateReadableStream</code> with a <code>chunkDelayInMs</code> of 50ms times out flaky assertions under heavy CI load — set <code>vi.setConfig({ testTimeout: 10000 })</code> globally in <code>setupTests.ts</code> and use <code>chunkDelayInMs: 0</code> in CI. Vitest&rsquo;s <code>--pool=forks</code> flag (not <code>threads</code>) is safer for tests that use <code>ReadableStream</code> and <code>TextEncoder</code>, which have subtle cross-thread behavior in Node 22. Never hardcode real API keys in test files; MSW intercepts all requests so no real key is needed, but test environments often accidentally forward <code>OPENAI_API_KEY</code> from <code>.env.local</code> — add <code>.env.test</code> with <code>OPENAI_API_KEY=test-key</code> to make the intent explicit and prevent accidental live calls if a handler is missing. CI configuration with GitHub Actions for a Vitest + RTL project looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># .github/workflows/test.yml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">Test</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>: [<span style="color:#ae81ff">push, pull_request]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">test</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-node@v4</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">node-version</span>: <span style="color:#e6db74">&#39;22&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">cache</span>: <span style="color:#e6db74">&#39;npm&#39;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm ci</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm test -- --run --reporter=verbose</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">OPENAI_API_KEY</span>: <span style="color:#ae81ff">test-key</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">CI</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><h3 id="test-coverage-for-ai-components">Test Coverage for AI Components</h3>
<p>Coverage reporting for AI component tests needs one extra step: Vitest&rsquo;s <code>v8</code> provider covers async code paths correctly, but streaming-heavy components need branch coverage targets lower than a typical CRUD component — 70% is a realistic threshold when 30% of branches are error paths that require specific network conditions to exercise. Add <code>coverage.thresholds</code> to <code>vitest.config.ts</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#a6e22e">test</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">coverage</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">provider</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;v8&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">thresholds</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">lines</span>: <span style="color:#66d9ef">80</span>, <span style="color:#a6e22e">branches</span>: <span style="color:#66d9ef">70</span>, <span style="color:#a6e22e">functions</span>: <span style="color:#66d9ef">85</span> },
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">exclude</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;src/mocks/**&#39;</span>, <span style="color:#e6db74">&#39;**/*.d.ts&#39;</span>],
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="separating-ai-tests-from-unit-tests">Separating AI Tests from Unit Tests</h3>
<p>Tag AI integration tests with a custom Vitest project to run them separately in CI when you want fast unit feedback without the streaming mock overhead:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#75715e">// vitest.config.ts
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#a6e22e">defineConfig</span>({
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">test</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">projects</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>      { <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;unit&#39;</span>, <span style="color:#a6e22e">include</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;src/**/*.test.ts&#39;</span>] },
</span></span><span style="display:flex;"><span>      { <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;ai-integration&#39;</span>, <span style="color:#a6e22e">include</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;src/**/*.ai-test.ts&#39;</span>], <span style="color:#a6e22e">testTimeout</span>: <span style="color:#66d9ef">15000</span> },
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p>Run unit tests on every commit (<code>vitest --project unit</code>) and AI integration tests only on PRs and main branch merges.</p>
<h2 id="faq">FAQ</h2>
<p><strong>Q: Do I need a real OpenAI API key to run React Testing Library tests for AI components?</strong></p>
<p>No. MSW intercepts all HTTP requests at the network layer before they leave the Node.js process, so tests never reach the real API. Use <code>OPENAI_API_KEY=test-key</code> in your <code>.env.test</code> to satisfy any client-side key validation in the SDK without making live calls.</p>
<p><strong>Q: What is the difference between using MSW and MockLanguageModelV1 for testing?</strong></p>
<p>MSW mocks at the HTTP layer and is the right choice for testing full React components that call an API route (<code>/api/chat</code>). <code>MockLanguageModelV1</code> from <code>ai/test</code> mocks at the SDK layer and is the right choice for unit testing custom hooks or utility functions that call the AI SDK directly without going through an API route.</p>
<p><strong>Q: Why does my RTL test get an act() warning when testing a streaming component?</strong></p>
<p>This usually means an async state update is happening outside of RTL&rsquo;s <code>waitFor</code> or <code>findBy</code> utilities. Replace bare <code>expect(screen.getByText(...))</code> assertions after async actions with <code>expect(await screen.findByText(...))</code>. If you have a <code>useEffect</code> that kicks off after streaming completes, wrap the final assertion in <code>waitFor</code>.</p>
<p><strong>Q: Should I use Jest or Vitest for testing AI-powered React components in 2026?</strong></p>
<p>Use Vitest for any project using Vite (including most new React projects, Remix, and Next.js with Turbopack). Vitest starts 3x faster than Jest on large codebases and has native ESM support, which the Vercel AI SDK and most AI client libraries require. Keep Jest only if you are on a large existing CRA or Webpack codebase where migration cost outweighs the speed benefit.</p>
<p><strong>Q: How do I test a React component that uses the Anthropic Claude API directly (not via Vercel AI SDK)?</strong></p>
<p>Use MSW to mock <code>https://api.anthropic.com/v1/messages</code>. Return a JSON response with the same shape as the Anthropic API response object (check the Anthropic API docs for the <code>Message</code> type). For streaming, return <code>Content-Type: text/event-stream</code> with properly formatted SSE lines. The MSW handler runs in Node and intercepts the fetch call made by the Anthropic SDK, so no real key or network connection is needed.</p>
]]></content:encoded></item></channel></rss>