Back to Blog

How to Test an App You Vibe-Coded with Cursor or Claude Code

How to Test an App You Vibe-Coded with Cursor or Claude Code

You opened Cursor on a Sunday. You vibe-coded a whole feature. It works on your machine. Now it's Monday, you're about to push, and a small voice is asking: how do I actually know this thing won't break next week?

Here is the uncomfortable part. Industry surveys keep landing on the same split: developers reach for AI tools constantly, but trust the output far less often than they ship it. Code co-authored with an LLM tends to carry more defects than code a human wrote alone. That is not a reason to stop vibe coding. It is a reason to put a net under it.

This post is that net: a concrete, step-by-step workflow for how to test a Cursor or Claude Code app, without drowning in brittle selectors or burning tokens on every CI run.

Why the obvious approaches fail

Before we get to the workflow, let's kill the three tempting shortcuts. Each one seems reasonable. Each one hurts.

Just ask the LLM to test it

Pasting your diff into a chat and asking "does this look right?" feels like testing. It is not. The answer is non-deterministic (ask twice, get two answers), it costs tokens on every run, and the LLM is grading its own homework. It wrote the bug. It will not call itself out on the bug. A test you cannot run ten thousand times with identical results is not a test, it's a vibe check with extra steps.

Let the LLM write Playwright tests

This looks better on paper. The LLM generates actual test code, you run it in CI, done. In practice it usually takes several rounds of prompting before an end-to-end test runs clean, and once the tests exist the maintenance bill shows up: hand-rolled LLM-generated suites need ongoing hand-holding every time the UI moves. LLMs also have a predictable bias. They write tests where tests are easiest to write, which is the feature they just built while the context is fresh, not the dusty corners where regressions actually hide. And they only look at the DOM. A button can be present, clickable, correctly wired, and completely invisible because a CSS regression moved it behind a modal. Playwright-from-an-LLM will cheerfully mark that green.

Write the tests by hand

You could. You also could have written the feature by hand instead of vibe coding it. If you are going to spend two hours writing Playwright selectors for a feature you built in fifteen minutes, the math has stopped making sense.

The workflow that actually works to test a Cursor or Claude Code app

Here is the loop. Record once, let AI fill in the gaps, approve, replay forever. Five steps.

Five-step pipeline for testing a vibe-coded app: record one flow, AI expands coverage, approve the baseline, wire into CI, then replay forever at zero token cost. AI runs only in the first three steps; the CI replay loop is free.
AI does the creative work once in steps 1 to 3; from there CI replays the frozen Playwright forever for $0.

Step 1: Get Lastest running

Lastest is a free, open-source, self-hosted visual regression and E2E platform. One docker-compose command and you have a dashboard on localhost. No per-seat fees, no per-screenshot fees; on your own infra it stays $0 forever.

git clone https://github.com/las-team/lastest
cd lastest
docker compose up -d
# open http://localhost:3000

No signup, no API key, no credit card. Screenshots stay on your machine. License is FSL-1.1-ALv2 - read: free forever, source open, build on top of it.

Step 2: Point it at your app and record one flow

In the dashboard, create a project, drop in the URL of your local dev server, and hit record. A browser opens. Click through the happy path of whatever you just vibe-coded - sign in, open the new page, submit the form, land on the success state. Hit stop.

What Lastest just did: captured every click, input, and navigation and emitted a deterministic Playwright script. Every interactive step gets a screenshot. No one wrote a selector by hand. No one guessed at timing.

Step 3: Let AI expand coverage

One recorded flow is not a test suite. This is where the Play Agent earns its keep. It runs an eleven-step autonomous pipeline - route scan, plan, generate, run, fix up to three times, re-run, report - using sub-agents (Orchestrator, Planner, Scout, Diver, Generator, Healer) that crawl your app and generate tests for the routes you didn't think to click. The agent finds the empty states, the error paths, and the half-loaded modals you forgot about. You bring your own AI: Claude CLI, OpenRouter, the Anthropic API, OpenAI, or Ollama if you want everything local.

Step 4: Approve the baseline

This is the human-in-the-loop moment and skipping it is how visual regression projects die. Lastest shows you every first-run screenshot. You look at each one and say "yes, this is what correct looks like." That set becomes your baseline. From here on, every push is compared against it with three diff engines running in parallel: pixelmatch for pixel-exact checks, SSIM for structural similarity, and Butteraugli for perceptual diffs that match how humans actually see. A text-region-aware two-pass comparison handles anti-aliasing noise around fonts so you don't get paged at 2am because a glyph shifted a sub-pixel.

The five minutes you spend clicking approve here are what separates "I have tests" from "I have tests I trust."

Step 5: Wire it into your push loop

Add a GitHub Action that calls your Lastest instance on every PR. Replays are deterministic - same script, same browser, same viewport - so they cost exactly zero tokens. The LLM already did its job back in Step 3. From now on, CI just runs the code it produced, forever, for free.

Because replays are deterministic and selectors self-heal, the recurring maintenance that hand-rolled LLM-generated suites demand mostly disappears. The bill you used to pay every CI run, in tokens and in babysitting, collapses to near zero. See the real cost of LLM-driven test runs for the full breakdown.

Letting Claude Code drive the whole thing

If you live inside Claude Code or Cursor, you don't have to leave the chat to use any of this. Lastest ships an MCP server that exposes around 20 tools to any MCP-compatible agent. Claude Code can run your suite, read the failures, look at the diffs, and approve baselines, all from the same conversation where you built the feature.

you: I just shipped the new checkout flow. run the tests.
claude: calling lastest.run_tests(project="web")...
        3 passed, 1 visual diff on /checkout/confirm
        calling lastest.review_diff(test_id="checkout-confirm")...
        the success banner moved 4px down because the
        padding change in Button.tsx. looks intentional.
        want me to approve the new baseline?
you: yes
claude: calling lastest.approve_baseline(test_id="checkout-confirm")...
        done. pushed.

That is the whole loop. You never opened a Playwright file. You never wrote a selector. Claude Code picked up the diff, explained it in plain English, and updated the baseline when you confirmed.

What breaks - and how Lastest heals it

Tests break. That is the job. The question is whether they break in a way that wastes your afternoon or in a way that fixes itself.

When a selector drifts - because you renamed a class, swapped a component, or Cursor refactored a div - Lastest walks a seven-layer fallback chain to find the element: data-testid, then id, then ARIA role, then aria-label, then text content, then CSS, and finally OCR on the screenshot itself. The test keeps running. You get a note that the selector healed, not a red build. (We go deep on the mechanism in how self-healing selectors work.)

The seven-layer selector fallback chain Lastest tries in order when a selector breaks: data-testid, id, ARIA role, aria-label, text content, CSS selector, and finally OCR on the screenshot. When one layer fails the test falls through to the next instead of going red.
A refactored div does not break the test; Lastest falls through seven layers until it finds the element, ending at OCR on the screenshot.

When something does fail for real, an AI triage pass classifies it: is this an actual regression, a flaky test, an environment issue, or a test that just needs updating for a legitimate UI change? If it's something the agent thinks it can fix, it will try - up to three automatic repair attempts - before bothering you. You show up to a dashboard that already separated the noise from the signal.

Why this beats "just ask the LLM"

The core move here is separating two things that LLMs are bad at separating: generating a test and running a test. You let the LLM do the creative part once, with full context, while the feature is fresh. Then you freeze that work into deterministic Playwright and never pay for it again. Visual diffs catch the bugs the DOM hides. Self-healing selectors absorb the refactors. Claude Code drives the whole thing from chat when you want it to.

Two-column comparison. Asking the LLM to test on every run is non-deterministic, burns tokens each run, and grades its own DOM-only homework. The Lastest loop replays deterministic Playwright, costs zero tokens per replay, and adds a human verdict plus visual diff engines.
Re-asking the LLM pays the generation cost on every run; the Lastest loop pays it once and replays free.

You get the speed of vibe coding and the confidence of a real test suite. Those two things were supposed to be in tension. They are not, if you structure the workflow right.

Get started

One command and you're testing in five minutes:

git clone https://github.com/las-team/lastest && cd lastest && docker compose up -d

Self-host it free, forever, on your own infra (FSL-1.1, source open), or skip the ops and run it on Lastest Cloud for a flat $299/month with no per-seat or per-screenshot fees. Source, issues, and docs live on github.com/las-team/lastest; the docs have the GitHub Action and MCP setup. If you want a deeper take on this workflow, the vibe coders page walks through how Lastest fits a Cursor or Claude Code loop end to end. Vibe code the feature. Record one flow. Let the agent do the rest. Push.