July 23, 2026
Testing AI Chat UIs for Streaming Responses, Partial Renders, and Cancel Actions
A practical tutorial for testing AI chat UIs, covering streaming responses, partial renders, cancel behavior, interrupted requests, and state recovery in production-like conditions.
AI chat interfaces look simple on the surface, but they combine several tricky systems at once: live text streaming, asynchronous network behavior, evolving UI state, and user actions that can interrupt a request halfway through. The result is that the failures teams care about are usually not the obvious ones. The real issues are partial messages that duplicate or vanish, layout shifts that make text unreadable, cancel buttons that stop the UI but not the backend, and recovery flows that leave the conversation in a confusing state.
This tutorial is about testing AI chat UIs for streaming responses in a way that reflects how production systems actually fail. It focuses on the behaviors that matter to frontend engineers, QA engineers, and AI product teams: unfinished chunks, incremental UI updates, partial render testing, interrupted requests, and what should happen after cancellation.
What makes AI chat UIs different from ordinary chat UIs
A traditional chat UI mostly renders complete messages that arrive as discrete events. An AI chat UI often behaves more like a live transcript. Tokens, chunks, or deltas arrive over time, and the frontend updates the current assistant message repeatedly before the response is complete.
That means the UI is not just rendering text, it is managing a stream lifecycle:
- request starts
- placeholder message appears
- streamed content arrives in chunks
- text is appended or patched
- the request completes, errors, or is canceled
- the UI reconciles final state with user-visible state
This is a classic software testing problem, but with more concurrency and more user-visible intermediate states than many teams expect. A test that only checks the final answer can miss the most important defects, because users often interact with the UI before the answer is done.
A streaming chat interface should be judged by the integrity of its intermediate states, not just whether the final message looks correct.
The failure modes worth testing first
Not every edge case deserves the same attention. Start with the ones that break trust, confuse users, or make support tickets expensive.
1. Unfinished chunks and broken message assembly
Streaming systems often deliver partial text in chunks that are not aligned to word boundaries or sentence boundaries. A chunk can end in the middle of a Markdown list, a code fence, or an HTML entity.
Common failure modes:
- duplicated text when deltas are appended twice
- missing text when a chunk is dropped or overwritten
- malformed Markdown that renders incorrectly until the final chunk arrives
- code blocks that remain open and distort the rest of the page
- cursor indicators or spinners that never clear after completion
For testing, the key question is not “does the final string match,” but “does the UI preserve a coherent state when the message is incomplete?”
2. Partial renders that trigger layout instability
Streaming text changes the size of the assistant message repeatedly. That can cause layout shifts, scroll jumps, and reflow problems, especially if the page auto-scrolls to the bottom as new chunks arrive.
Common failure modes:
- the viewport jumps upward when the browser recalculates height
- the input box becomes obscured by a growing message
- scroll position resets when the framework re-renders the conversation list
- images, citations, or expandable blocks appear after the text and push content unexpectedly
If the page feels unstable while the response streams, users often interpret that as the model being unreliable, even when the backend is functioning correctly.
3. Cancel actions that stop only part of the system
Cancel is one of the most misunderstood actions in AI chat UIs. A good cancel should stop visible generation, update UI state, and leave the conversation in a recoverable state. But many implementations only stop one layer.
Typical failure modes:
- the spinner stops, but the backend request keeps running
- the UI shows “canceled,” but the partial message is left in an editable or ambiguous state
- a new prompt is sent while the canceled response still streams in the background
- the cancel action is ignored during a race window right after submit
- memory or conversation context includes partially generated content that should not have been committed
4. Interrupted requests and retry behavior
Network interruptions are not unusual. A streaming connection can end because of a proxy, tab sleep, browser throttling, timeout, server restart, or user navigation. The UI should distinguish between completed, canceled, and interrupted responses.
Common failure modes:
- the conversation shows a complete-looking answer that was actually cut off
- retry creates a duplicate assistant message instead of replacing the partial one
- a reconnect resumes from the wrong offset or replays chunks incorrectly
- the app silently swallows the error and leaves the chat stuck in loading state
5. State recovery after cancellation or failure
After a failure, the user usually wants to know three things: what happened, whether their prompt was preserved, and whether they can continue.
A good recovery flow usually needs to preserve:
- the user prompt
- the partial assistant output, if your product design keeps it
- the request state, such as canceled, failed, or completed
- the ability to retry without duplicate entries
A bad recovery flow leaves the conversation in a contradictory state, such as a disabled input box with no visible error, or a retry button that generates a second assistant reply below a half-finished original.
Define expected behavior before you automate anything
Streaming UI testing becomes much easier when the team writes down the state model first. Without that, you end up testing implementation details instead of observable behavior.
A practical state model might look like this:
idle, no active requestsubmitting, request created but no chunks received yetstreaming, partial assistant response visiblecompleted, final chunk received and response finalizedcanceled, user stopped generationfailed, request ended unexpectedly
For each state, define what the user should see:
- whether the cancel button is visible
- whether the input field is enabled
- whether partial text is editable
- whether auto-scroll is active
- whether retry is offered
- whether the assistant message gets a final timestamp or label
This is where context-driven testing helps. Instead of asking whether the UI is “correct,” ask whether it is useful, honest, and recoverable for the user in that state.
What to test in the DOM, not just in screenshots
Visual checks are useful, but streaming bugs often show up in state and timing, not just pixels. For test automation, you want assertions that observe the live behavior of the page.
Focus on these DOM-level concerns:
- the assistant message container updates incrementally without replacing the whole conversation
- the streaming indicator appears and disappears in the right order
aria-busyor similar accessibility state reflects active generation- cancel removes or freezes only the current request, not the entire chat
- the final message state is different from the in-progress state
A useful trick is to add explicit test hooks or stable selectors for message IDs and stream status. For example, a message can expose data-state="streaming" while it is active, then switch to data-state="complete" when finalized.
A Playwright strategy for streaming responses
Playwright is a practical choice here because it can watch network activity, observe DOM updates, and coordinate timing-sensitive assertions. The point is not to test every token. The point is to verify transitions and failure recovery.
Here is a small example of how to test a streamed response and ensure the message grows over time:
import { test, expect } from '@playwright/test';
test('assistant response streams incrementally', async ({ page }) => {
await page.goto('/chat');
await page.getByRole('textbox', { name: 'Message' }).fill('Explain partial renders');
await page.getByRole('button', { name: 'Send' }).click();
const assistant = page.locator(‘[data-testid=”assistant-message”]’).last(); await expect(assistant).toHaveAttribute(‘data-state’, ‘streaming’); await expect(assistant).toContainText(/Explain|partial/);
await expect(assistant).toHaveAttribute(‘data-state’, ‘complete’, { timeout: 15000 }); });
This test checks a meaningful contract: the UI starts streaming, shows partial content, then finalizes. It does not assume a specific token sequence, which makes it less brittle than asserting exact chunk-by-chunk output.
Testing cancel behavior as a state transition
Cancel testing should verify both visible UI changes and invisible network consequences. If your frontend only turns off the spinner, the test should catch that. If your backend keeps generating after cancellation, that is a product and cost issue, not just a UI issue.
A useful pattern is to stub the streaming endpoint so you can control when chunks arrive and when the user cancels.
import { test, expect } from '@playwright/test';
test('cancel stops streaming and preserves the prompt', async ({ page }) => {
await page.route('**/api/chat', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ stream: true })
});
});
await page.goto(‘/chat’); await page.getByRole(‘textbox’, { name: ‘Message’ }).fill(‘Draft a reply’); await page.getByRole(‘button’, { name: ‘Send’ }).click(); await page.getByRole(‘button’, { name: ‘Cancel generation’ }).click();
await expect(page.getByTestId(‘generation-status’)).toHaveText(‘Canceled’); await expect(page.getByRole(‘textbox’, { name: ‘Message’ })).toHaveValue(‘Draft a reply’); });
In practice, a stronger test also verifies that no further chunks are rendered after cancellation. If your app uses an AbortController, confirm that cancel calls abort() and that the request lifecycle moves into a terminal state.
Guarding against race conditions
Streaming UIs often fail at the edges between states. A user can cancel at almost the same moment the first chunks arrive. They can press Enter twice. They can navigate away while the response is still in flight. These are race conditions, and they are worth testing explicitly.
Useful scenarios include:
- cancel immediately after submit, before the first chunk
- cancel after several chunks are visible
- send a second prompt while the first response is still streaming
- refresh the page mid-stream and verify rehydration rules
- switch tabs or background the browser and see whether the UI resumes cleanly
The purpose of these tests is not to force the app into every possible bad state. It is to verify that the app fails predictably, with a state model that the user can understand.
Partial render testing for Markdown, code, and rich content
AI chat UIs often render more than plain text. They show Markdown, citations, tables, callouts, or code fences. Streaming makes these structures fragile because the markup may be incomplete for several seconds.
A partial render test should check behavior while content is incomplete, not just after completion.
Watch for:
- unclosed code fences causing the rest of the page to render as code
- lists that temporarily lose numbering or indentation
- links that appear before their label is complete
- tables that collapse during incremental updates
- syntax highlighting that thrashes the DOM on every chunk
One practical safeguard is to separate the raw streaming text from the final rich rendering. For example, display a plain text stream during generation, then render Markdown only after completion. That avoids many broken intermediate states, although it trades away some formatting fidelity during the stream.
The tradeoff is clear, users may prefer immediate readable text over pretty but unstable formatting.
Accessibility checks that often get skipped
Streaming and cancellation affect accessibility more than many teams expect. If the UI is changing constantly, assistive technology needs meaningful signals about what is happening.
Consider checking:
- the streaming region has an appropriate live region strategy
- the cancel button is keyboard accessible and reachable while the response streams
- focus does not jump unexpectedly when new chunks arrive
- state changes, such as canceled or failed, are announced clearly
- the input returns focus after cancel or completion, if that is your design
The browser may render the response correctly, but if the user cannot tell whether generation is still active, the UI is not truly usable.
A minimal CI setup for streaming UI tests
Streaming tests are valuable in CI, but they should be selected carefully because they can be timing-sensitive. Run a small set on every pull request, and keep deeper failure-recovery scenarios in a broader suite.
A simple GitHub Actions job might look like this:
name: ui-tests
on: [push, pull_request]
jobs:
playwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npm test -- --grep "streaming|cancel"
If your suite becomes flaky, do not automatically blame the framework. Look at the assertions first. Tests that wait for exact timing or exact intermediate text tend to be fragile because real streams are not deterministic. It is usually better to assert state transitions and terminal outcomes than specific chunk timing.
What good metrics look like, and what to avoid
It is tempting to measure success by the number of automated tests, the speed of the suite, or the volume of UI events captured. Those numbers can be useful, but they can also be vanity metrics if they do not correlate with risk.
More useful questions are:
- Do tests cover the states users actually experience?
- Can the team tell cancel, fail, and complete apart in the UI?
- Do failures produce actionable evidence, such as logs, state snapshots, or network traces?
- Are the tests stable enough that engineers trust them?
- Does the suite detect regressions in partial render behavior before release?
That is a better lens than chasing quantity. In practice, a small set of well-chosen tests that cover the stream lifecycle is more valuable than a large suite that only confirms happy-path rendering.
A checklist for production-grade AI frontend QA
Use this as a practical starting point for your own test plan:
- verify the assistant message appears before the first chunk
- verify partial content is visible and readable during streaming
- verify the UI does not duplicate chunks during re-renders
- verify cancel halts visible generation and terminates request state
- verify interrupted requests move into a clear failed state
- verify retry does not duplicate the prior message
- verify the conversation can recover after cancellation
- verify layout remains stable while the message grows
- verify keyboard and accessibility behavior during active generation
- verify rich text and code blocks do not break the page during partial renders
If the application supports message regeneration, branching threads, citations, or tool calls, extend the checklist to cover those states too. Each added feature increases the number of transitions you need to validate.
Final practical advice
Testing AI chat UIs is less about proving that the model can answer and more about proving that the interface behaves honestly under uncertainty. Streaming responses are inherently partial. Cancellation is inherently disruptive. Network failures are inherently messy. Your tests should reflect that reality.
If you design the UI state model carefully, expose stable test hooks, and write automation around observable transitions rather than exact timing, you can catch the failures that actually frustrate users: unfinished chunks, reflow during streaming, interrupted requests, and broken recovery after cancel.
That is the core of effective AI frontend QA, and it is where product quality becomes visible to the user.