A checkout bug shipped on a Friday because the pull request looked clean.
The diff was small. A loading state moved. A form component was refactored. A couple of hooks changed order. The AI assistant that helped write it produced tidy React code, the naming was sensible, TypeScript passed, unit tests stayed green, and CI/CD reported success. Two reviewers approved it in under twenty minutes.
By Monday, support had a pattern: users on Safari could add items to the cart, enter shipping details, and click Continue, but nothing happened. No crash. No obvious error. Just a disabled button that never re-enabled because one async state transition behaved differently in that browser when autofill kicked in.
Nobody missed the syntax. Nobody misunderstood the intent. The failure was simpler and more dangerous: the team treated the diff as evidence of behavior.
That is the PR review trap.
As AI generates more UI code, this trap gets worse. Not because AI always writes bad code, but because it writes plausible code. It produces changes that read cleanly in review, satisfy local style norms, and often preserve enough structure to feel safe. Reviewers can inspect the logic and still fail to validate what matters: whether a real user can still complete the workflow.
This is the new reliability gap in frontend engineering. Code review catches certain categories of mistakes well. It is good at syntax, rough intent, architecture smells, naming, and obvious regressions. It is not good at validating temporal behavior, state coordination, browser quirks, rendering races, async timing, focus management, autofill interactions, or multi-step journeys that span components and backend responses.
If your process still assumes that visual inspection of a diff plus green CI means the UI change is safe, you are operating with false confidence.
The problem is not bad reviewers. The problem is the interface.
Most teams talk about review quality as if the issue is reviewer discipline. “We need more careful reviews.” “People should pull the branch and test locally.” “QA will catch it later.” Those responses miss the deeper point.
A pull request diff is a terrible interface for validating behavior.
A diff shows textual change, not executed reality. It shows what lines were added and removed, but not how a user journey unfolds across time. It cannot naturally express questions like:
- What happens when the API returns slowly?
- Does this field become enabled before the cached validation state is ready?
- Does mobile Safari preserve focus differently?
- What if the user clicks twice?
- What if the browser autofills after initial render?
- Does a debounced state update race with navigation?
- Does this modal trap focus correctly when rendered in a portal?
- Does the optimistic update rollback if the request fails after route transition?
All of those are behavior questions. Most production UI failures are behavior failures.
Yet PR review is optimized around reading text. That mismatch is survivable when the codebase changes slowly and experienced engineers write small, carefully reasoned patches. It becomes dangerous when AI can generate larger volumes of “reasonable-looking” UI changes at speed.
AI accelerates code production. It does not automatically improve behavioral verification. In many teams, it does the opposite: more change reaches review with less human execution of the workflow.
The result is familiar. Reviewers skim a polished diff, recognize patterns they have seen before, and approve based on plausibility. CI passes because it validated what was easy to automate: linting, unit assertions, build integrity, maybe a few broad integration tests. The workflow itself remains largely unproven.
Why AI-written UI changes are especially deceptive in review
There is a specific reason AI-generated frontend code increases review risk: it often looks more consistent than the underlying behavior deserves.
Human-written rushed code carries clues. You see awkward branching, a suspicious conditional, duplicate event handlers, or an obvious smell that triggers deeper scrutiny. AI often smooths those surfaces. It produces code that is idiomatic enough to pass social review.
That means the old reviewer heuristic—“messy code deserves more skepticism”—fails more often.
AI-generated code commonly has these properties:
- It uses familiar framework patterns.
- It includes comments or naming that explain intent convincingly.
- It satisfies type contracts.
- It updates tests just enough to preserve local correctness.
- It keeps diffs mechanically tidy.
- It appears complete while quietly missing environmental or temporal edge cases.
For example, an AI assistant might refactor a submit flow from a direct callback into a sequence of derived state changes. In the diff, that can look cleaner: fewer imperative steps, more reusable hooks, better separation. But now the workflow depends on timing between validation, render, and network completion. The reviewer sees cleaner code. The user sees a button that occasionally deadlocks.
This is not “AI is bad at coding.” It is a systems problem. Reviewers are being asked to verify runtime behavior through a textual abstraction that hides the failure modes most likely to matter.
What pull requests catch well, and what they consistently miss
Code review still matters. The mistake is treating it as a product behavior gate.
PR review is strong at catching:
- incorrect business intent visible in code
- API misuse that is textually obvious
- naming, duplication, and maintainability problems
- unsafe dependencies or architectural drift
- straightforward logic mistakes
- missing null handling in places the reviewer can reason about statically
- patterns that violate team conventions
PR review is weak at catching:
- async sequencing bugs
- browser-specific event ordering
- race conditions between state updates and navigation
- animation or transition timing issues
- rendering and hydration mismatches
- focus, keyboard, and accessibility regressions in real interaction
- stale cache behavior
- retries, backoff, and partial failure handling
- long-tail workflow regressions across multiple screens
- production-only integration issues hidden behind mocks
These weaknesses are not moral failings. They come from the fact that many frontend bugs are emergent. They appear only when code runs in an environment with timing, state, input devices, browser rules, network variability, and actual user behavior.
A diff cannot show emergence.
Green CI often means “the wrong things were tested successfully”
Teams frequently say, “But CI passed.” That statement matters less than people think.
CI/CD pipelines are only as valuable as the checks they run. In many UI-heavy stacks, CI heavily validates implementation artifacts rather than user actions.
A common pipeline looks like this:
- lint
- typecheck
- unit tests
- build
- maybe a snapshot suite
- maybe a shallow component test layer
That pipeline is useful, but it does not tell you whether a user can complete sign-up, checkout, onboarding, or password reset.
Worse, AI-generated code can excel at satisfying these checks because they are narrow and local. It can preserve interfaces, appease typings, and keep tests green while changing the actual coordination of the workflow.
Here is a toy React example that looks harmless in diff review and can easily pass unit tests.
tsxfunction CheckoutButton({ submitOrder, isValid }: { submitOrder: () => Promise<void> isValid: boolean }) { const [loading, setLoading] = useState(false) const [ready, setReady] = useState(false) useEffect(() => { if (isValid) { const timer = setTimeout(() => setReady(true), 150) return () => clearTimeout(timer) } setReady(false) }, [isValid]) const onClick = async () => { if (!ready || loading) return setLoading(true) try { await submitOrder() } finally { setLoading(false) } } return ( <button disabled={!ready || loading} onClick={onClick}> {loading ? 'Submitting...' : 'Continue'} </button> ) }
Nothing here looks outrageous. The code is readable. Intent is clear. Unit tests might confirm that the button disables during submission and re-enables after success. But now correctness depends on timing. If browser autofill updates validity after focus transitions in a different order than expected, ready may lag. If the user clicks during that transition, the action is ignored. If there is no visual explanation, the workflow feels broken.
The bug is behavioral, not syntactic.
A unit test can miss it because the test controls the component too directly:
tsxit('submits when clicked', async () => { const submitOrder = vi.fn().mockResolvedValue(undefined) render(<CheckoutButton submitOrder={submitOrder} isValid={true} />) await waitFor(() => expect(screen.getByRole('button')).toBeEnabled()) await userEvent.click(screen.getByRole('button', { name: /continue/i })) expect(submitOrder).toHaveBeenCalled() })
This proves the component can work in a controlled scenario. It does not prove the workflow works when form completion, browser autofill, validation, and navigation all interact.
That is the distinction many CI/CD setups fail to make.
State, timing, and browser behavior are where reviewers lose the plot
The hardest UI bugs usually hide in three dimensions that diffs compress away: state, time, and environment.
State
Modern interfaces are not simple request-response pages. They are collections of local state, server state, derived state, optimistic state, validation state, cached state, and URL state. A small refactor can alter when one state source becomes authoritative.
Reviewers can read each line and still miss the transition graph.
For example:
- a disabled button now depends on both form validity and async pricing state
- a route transition clears component state before an in-flight save resolves
- an optimistic update masks a backend validation failure until after navigation
- a modal close resets form values that were supposed to persist on retry
These are not obvious from a localized diff unless the reviewer mentally simulates the entire workflow.
Time
UI behavior unfolds over time, not in static snapshots. Diffs flatten time into code structure.
A change can introduce:
- debounce delays that make fast interactions flaky
- re-render ordering differences after splitting hooks
- effects that run one tick later than before
- duplicate submissions under double click or touch behavior
- subtle loading-state dead zones
Reviewers rarely execute these timing scenarios mentally with precision, especially under normal PR volume.
Environment
What works in Chromium on a laptop may fail in Safari, WebView, Firefox, low-power mobile mode, or under latency. Browser event ordering, autofill behavior, layout timing, and focus semantics differ enough to break real workflows.
A diff cannot tell you how Safari handles input, change, blur, or autofill in your exact component composition.
You need execution, not inference.
Why manual QA does not close the gap reliably
When engineering teams realize review and unit tests are insufficient, they often point to QA. But traditional QA is not a stable answer either.
Manual QA has real value for exploratory debugging and release confidence. It is also constrained by time, coverage, and repeatability.
The typical problems are predictable:
- QA validates the happy path, not every branch touched by the PR
- test environments differ from production enough to hide timing issues
- browser/device coverage is limited
- the same workflow is not exercised on every pull request
- failures found late are expensive and politically harder to fix
- institutional knowledge stays with individual testers instead of becoming executable checks
Most importantly, manual QA is not attached tightly enough to the PR itself. By the time someone clicks through the app, the review context is gone, the author has moved on, and multiple changes may already be stacked.
If the team wants reliable developer productivity, behavioral validation has to move closer to code review, not live downstream as a best-effort ritual.
The core insight: validate actions, not just code paths
The solution is not “more tests” in the abstract. It is a different testing target.
Teams should attach action-level checks to every pull request.
That means for any UI change that affects user behavior, the PR should be accompanied by executable verification of the user actions that matter:
- sign in
- search
- add to cart
- apply coupon
- save settings
- invite teammate
- upload file
- complete checkout
- reset password
- retry failed payment
This is not the same thing as broad end-to-end coverage of your entire application. You do not need a giant brittle suite that tries to model every possible path. You need focused workflow checks that prove the changed behavior still works in a realistic browser context.
The key shift is from asking, “Does the code look correct?” to asking, “Can a user still complete the action?”
That is a much better reliability gate for AI-assisted UI development.
What action-level verification looks like in practice
Suppose a PR changes checkout form composition, validation timing, and submit button logic. Instead of relying on reviewer intuition, attach browser-based tests that execute the journey.
Here is a Playwright example:
tsimport { test, expect } from '@playwright/test' test('user can complete checkout after address autofill-like interaction', async ({ page }) => { await page.goto('/checkout') await page.getByLabel('Email').fill('user@example.com') await page.getByLabel('Address').fill('123 Market Street') await page.getByLabel('City').fill('San Francisco') await page.getByLabel('Postal Code').fill('94103') await page.getByRole('button', { name: 'Continue' }).click() await expect(page.getByText('Payment')).toBeVisible() }) test('continue button eventually enables after validation settles', async ({ page }) => { await page.goto('/checkout') await page.getByLabel('Email').fill('user@example.com') await page.getByLabel('Address').fill('123 Market Street') await page.getByLabel('City').fill('San Francisco') await page.getByLabel('Postal Code').fill('94103') const continueButton = page.getByRole('button', { name: 'Continue' }) await expect(continueButton).toBeEnabled() })
These tests are not glamorous. That is the point. Reliability comes from checking actual workflows in a browser, not admiring the elegance of component internals.
If the app has known risk around latency or async transitions, make that explicit.
tstest('checkout still submits under slow network conditions', async ({ page }) => { await page.route('**/api/checkout', async route => { await new Promise(resolve => setTimeout(resolve, 1200)) await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) }) }) await page.goto('/checkout') await page.getByLabel('Email').fill('user@example.com') await page.getByLabel('Address').fill('123 Market Street') await page.getByLabel('City').fill('San Francisco') await page.getByLabel('Postal Code').fill('94103') await page.getByRole('button', { name: 'Continue' }).click() await expect(page.getByText('Submitting...')).toBeVisible() await expect(page.getByText('Payment')).toBeVisible() })
If you want this tied directly to PRs, wire it into CI/CD instead of making it optional.
yamlname: pr-workflow-checks on: pull_request: branches: [main] jobs: ui-workflows: 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 run test:workflows
This is where CI/CD becomes meaningful again: not because it is green, but because it is green for checks that reflect user reality.
Add traces and artifacts so reviewers can inspect behavior, not just text
A strong pattern is to attach execution artifacts to the PR: traces, videos, screenshots, step logs. If diffs are a poor interface for behavior, give reviewers a better one.
With Playwright, that can be built in:
tsimport { defineConfig } from '@playwright/test' export default defineConfig({ use: { trace: 'on-first-retry', video: 'retain-on-failure', screenshot: 'only-on-failure' } })
Now a reviewer can inspect an actual failed interaction rather than debate code style.
The same pattern works in Python for teams using pytest with browser automation:
pythonfrom playwright.sync_api import sync_playwright def test_user_can_save_settings(): with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.goto('http://localhost:3000/settings') page.get_by_label('Display name').fill('New Name') page.get_by_role('button', name='Save').click() assert page.get_by_text('Settings saved').is_visible() browser.close()
Again, the goal is not to replace all other testing. The goal is to put executable workflow evidence in the same decision loop as the pull request.
A realistic testing stack for AI-heavy UI development
If teams accept that diffs cannot validate behavior, the next question is how to rebalance the stack.
Here is a practical model.
1. Keep unit tests, but narrow their job
Unit tests are still useful. They should validate local logic, branching, parsing, formatting, reducers, utilities, and deterministic component behavior. They are fast and good for debugging implementation details.
But do not expect them to guarantee workflow safety.
2. Add workflow tests for core user actions
For each revenue-critical or retention-critical journey, have browser-based tests that run in CI on every relevant PR. These should be stable, focused, and written in the language of user actions.
Think less “assert internal state” and more “fill form, click button, observe outcome.”
3. Run targeted checks based on changed areas
Not every PR needs the entire end-to-end suite. Map routes, components, or domains to workflow checks. If checkout files change, run checkout workflows. If auth changes, run sign-in and reset-password workflows.
This keeps CI/CD useful without turning it into a two-hour bottleneck.
4. Preserve exploratory QA for unknown unknowns
Manual QA still matters for weird interactions, accessibility nuance, and release candidate exploration. But it should complement executable checks, not substitute for them.
5. Capture production signals
Even good pre-merge testing will miss some failures. Instrument user journeys with meaningful observability: action failure rates, frontend errors by route, conversion drop alerts, synthetic probes for critical workflows.
Reliability is a chain, not a single gate.
Tool comparison: what each layer is actually good for
Here is the blunt version teams need.
Code review
Good for:
- intent validation
- maintainability
- architecture
- readability
- obvious misuse
Bad for:
- proving workflows still function
- catching timing and browser issues
- validating multi-step user behavior
Unit tests
Good for:
- pure logic
- deterministic branches
- utility functions
- component-level invariants
- fast feedback during debugging
Bad for:
- browser semantics
- end-to-end state coordination
- production-like user interaction
Snapshot or visual diff tests
Good for:
- obvious rendering regressions
- UI drift detection
Bad for:
- interaction correctness
- hidden disabled states
- async failures after input
- navigation or backend coordination
Manual QA
Good for:
- exploratory testing
- edge-case discovery
- accessibility checks with human judgment
- release sanity checks
Bad for:
- repeatability
- PR-by-PR consistency
- comprehensive coverage at scale
Browser workflow tests
Good for:
- validating real user journeys
- detecting interaction regressions
- cross-browser verification
- attaching executable evidence to CI/CD
Bad for:
- replacing all lower-level tests
- broad uncontrolled sprawl if not curated
The right question is not which tool wins. It is whether your stack includes a layer that tests user actions before merge. Many teams still do not.
Practical rules for reviewing AI-written UI changes
If AI is involved in generating frontend code, teams should update review expectations immediately.
Rule 1: Treat plausibility as suspicious, not reassuring
When a change looks tidy and conventional, do not mistake that for proof. AI-generated code often passes the “seems reasonable” filter while still failing under realistic interaction.
Rule 2: Ask for workflow evidence, not just code explanation
A strong PR description should include:
- what user journey changed
- what workflow checks were run
- links to traces/videos/screenshots where relevant
- affected browsers or devices
- failure modes considered
Rule 3: Review state transitions explicitly
If the change touches UI state, loading flags, validation, navigation, effects, or caching, require the author to describe transition behavior. What enables the button? What disables it? What happens on retry? What happens on slow responses?
Rule 4: Prefer browser tests over increased reviewer effort
Do not solve behavioral uncertainty by demanding heroic code review. Human reviewers are expensive and inconsistent. Executable checks scale better.
Rule 5: Put critical workflows on the merge path
If a broken flow would impact revenue, onboarding, or support volume, it should be validated automatically before merge. Not eventually. Not only before release.
How to choose which workflows must be attached to every PR
Start with business pain, not test taxonomy.
Pick workflows that meet one or more of these criteria:
- directly tied to revenue
- central to activation or retention
- historically flaky
- sensitive to browser differences
- recently refactored heavily
- commonly touched by AI-generated code
- expensive when broken but easy to automate
Typical examples:
- sign up and email verification
- login and MFA
- checkout and payment confirmation
- invite teammate
- save billing settings
- upload and process a file
- search and open result
- create, edit, and publish core content
Do not try to automate everything first. Build a reliable action-level safety net around the flows that matter most.
A debugging mindset that matches how failures really happen
One reason teams underinvest in workflow checks is that they still think of bugs as local code defects. But many modern UI failures are distributed events.
A user clicks. The browser emits events in a specific order. A library batches state updates. Validation runs async. A network request races with navigation. A component unmounts. Cached state rehydrates. A button stays disabled. CI says green. Support gets the ticket.
That is a debugging problem, not just a coding problem.
And debugging distributed UI behavior requires evidence from execution: traces, logs, network timing, browser context, and reproducible action sequences. Diffs are useful inputs, but they are not the reality you are trying to debug.
The best teams align their testing strategy with this fact. They do not confuse implementation review with behavioral proof.
What a better PR process looks like
A mature process for AI-assisted UI changes is straightforward:
- Author describes the changed user workflow.
- Relevant browser workflow checks run automatically in CI/CD.
- Failures produce traces, screenshots, and videos attached to the PR.
- Reviewer inspects both the diff and the behavioral evidence.
- Critical workflows are blocked from merge if checks fail.
- Production observability validates that real users remain healthy after deploy.
Notice what changed: the PR is no longer only a text review artifact. It becomes a decision point supported by executable proof.
That is the process shift AI code generation demands. More generated code means more plausible diffs and less confidence that reviewers can infer runtime behavior correctly. The answer is not panic and it is not banning AI. The answer is raising the fidelity of what counts as evidence.
Conclusion
The PR review trap is simple: AI-written UI changes look safe because diffs are optimized for reading code, not validating behavior.
A reviewer can approve a clean refactor, green tests can pass, CI/CD can report success, and the product can still break in exactly the places users care about: filling forms, clicking buttons, completing flows, recovering from delays, switching browsers.
That blind spot is now larger because AI produces code that is increasingly plausible on inspection. Traditional review catches syntax and intent. It does not prove a workflow survives state transitions, timing quirks, and browser reality.
Teams that care about reliability and developer productivity need to stop treating green PRs as equivalent to working software. Keep code review. Keep unit tests. Keep QA. But add the missing layer: action-level checks attached to every pull request for the workflows that matter.
If a user action is important, it deserves executable proof before merge.
That is how you avoid approving a broken experience that looked perfectly safe in the diff.
