June 7, 2026
How to Debug AI-Generated Playwright Tests
A practical tutorial for debugging AI-generated Playwright tests, covering locators, waits, assertions, test structure, CI failures, and when Endtest is the simpler alternative.
AI-generated Playwright tests can save time, but they also introduce a very specific kind of debugging work. The test often looks plausible, the syntax is usually valid, and yet it fails in ways that are annoyingly subtle: a locator is a little too broad, a wait hides a race, the assertion verifies the wrong state, or the generated flow assumes a page structure that does not exist in your app.
I see this pattern a lot when teams use Claude, ChatGPT, or another assistant to draft Playwright tests. The first draft is rarely the hard part. The hard part is making the generated test behave like something you would actually trust in CI.
If you are trying to debug AI-generated Playwright tests, the goal is not just to fix one failure. You want a repeatable way to decide whether the issue is the app, the prompt, the locator strategy, the test design, or the AI-generated code itself. That is the difference between a one-off patch and a stable automation workflow.
Start by classifying the failure
Before editing code, identify what kind of failure you are dealing with. Most AI generated test debugging work falls into one of these buckets:
- Locator failure - the test cannot find the element, or finds the wrong one.
- Timing failure - the element exists, but not yet in the state the test expects.
- Assertion failure - the test reaches the right screen but checks the wrong condition.
- Flow failure - the AI generated the wrong user journey.
- Environment failure - data, authentication, CI, or browser differences are breaking the run.
- Framework misuse - the generated code uses Playwright in a brittle or unidiomatic way.
That classification matters because each failure type has a different fix. If you treat every failure as a locator issue, you will add random waits and make the test worse.
A flaky AI-generated test is often not “too smart”, it is just too confident about app behavior it did not actually observe.
Inspect the generated test like a reviewer, not like a passenger
A lot of people paste the AI output into the repo and wait for the red build to tell them what is wrong. That is backwards. Read the test line by line and ask the same questions you would ask a teammate’s PR.
Questions I ask immediately
- Does the test use user-visible actions, or does it jump into internals?
- Are selectors based on stable attributes, or on generated CSS paths and nth-child chains?
- Does the test assert meaningful outcomes, or just that a button was clicked?
- Is the test coupled to exact copy that changes often?
- Does it assume the page loads synchronously?
- Does it include extra steps that are not part of the user journey?
If the answer to any of those is “not great”, fix that before chasing the runtime error.
Common problem 1, bad locators
The most common issue in AI-generated Playwright tests is locator quality. AI tools often produce selectors that are syntactically valid but operationally fragile, for example:
typescript
await page.locator('div:nth-child(3) > button').click();
That kind of selector may work today and fail tomorrow after a layout change.
Prefer semantic locators
Playwright works best when you lean on its accessible locators and role-based queries. The official Playwright docs are worth revisiting here, because much of the framework’s reliability comes from locator strategy, not just syntax.
typescript
await page.getByRole('button', { name: 'Sign in' }).click();
await page.getByLabel('Email').fill('user@example.com');
This is easier to debug because the intent is obvious. When it fails, the error often tells you what changed in the UI.
What to check when a locator fails
- Is there more than one matching element?
- Is the accessible name different from what the AI guessed?
- Did the button text change because of localization or A/B testing?
- Is the input inside an iframe or shadow DOM?
- Is the element present but hidden?
In practice, I usually open Playwright’s codegen output, then replace brittle selectors with role, label, placeholder, or test id locators. If your app does not have good accessibility semantics, AI-generated test debugging gets much harder because the model has fewer stable signals to work with.
Common problem 2, waits that hide the real issue
AI-generated tests often overuse waitForTimeout because it is the easiest way for a model to make a failure disappear. It is also one of the fastest ways to create flaky tests.
typescript
await page.waitForTimeout(3000);
That line does not prove the app is ready. It proves you waited three seconds.
Use state-based waits instead
A better pattern is to wait for the actual condition that matters.
typescript
await page.getByRole('heading', { name: 'Dashboard' }).waitFor();
await expect(page.getByTestId('success-toast')).toBeVisible();
If the AI-generated test fails after a timing-related change, do not just add more waiting. Find the signal the app gives you, then wait for that signal.
Debug timing by asking what actually changed
A useful debugging question is:
- Is the element missing because the app is slow, or because the navigation never happened?
- Is the UI still animating, or is the app stuck on a network call?
- Did the route change occur before the expected content rendered?
If the app uses client-side rendering, hydration, background fetches, or websockets, then visible content may lag behind navigation. AI models often assume a simpler synchronous flow than your app actually has.
Common problem 3, assertions are too weak or too strong
A lot of generated tests assert something that is technically true but not useful, such as checking that a button exists after clicking it. That does not verify the workflow. On the other hand, some AI-generated tests assert exact text that is unstable across locales, feature flags, or content edits.
Weak assertion example
typescript
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible();
If the test clicked Save, that assertion may not tell you anything.
Better assertion example
typescript
await expect(page.getByText('Profile updated')).toBeVisible();
await expect(page.getByLabel('Display name')).toHaveValue('Alex Carter');
Now you are checking an outcome, not just a control.
Avoid brittle text checks when possible
If your product copy changes often, use more durable assertions where available:
- URL changes
- visible state of a specific component
- form values
- record creation in a backend API
- stable status messages or test ids
For AI generated test debugging, assertions are where the test becomes meaningful. If the model created a “click and hope” script, make the assertions user-centric and outcome-driven.
Common problem 4, the generated flow is not the real user flow
Sometimes the code is fine, but the test is simply testing the wrong thing. That happens when the model infers a generic signup or checkout path instead of the actual business flow in your app.
This is especially common when generating tests from short prompts like:
- “Test login”
- “Verify checkout”
- “Create a new project”
The model may produce a happy path that skips real app constraints, such as MFA, feature gates, onboarding modals, or pre-seeded data.
Fix by making the scenario explicit
When prompting Claude Playwright tests or any other AI model, include:
- starting state
- test data
- expected screen names
- required authentication method
- post-condition
- any known modals or banners
For example, instead of “test user signup”, say:
Sign up as a new user, verify the email step is shown, complete the profile, and confirm the dashboard loads with the welcome banner.
That makes it much easier to see whether the generated flow matches your app.
Common problem 5, the AI uses Playwright APIs in a brittle way
Some generated tests are syntactically clean but structurally poor. A common example is using page.waitForNavigation() in places where Playwright can already infer navigation, or chaining too many operations without assertions.
typescript
await Promise.all([
page.waitForNavigation(),
page.getByRole('button', { name: 'Continue' }).click()
]);
This pattern can work, but AI often overuses it without understanding whether a navigation actually occurs.
Prefer simpler, intention-revealing code
typescript
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page).toHaveURL(/checkout/);
This is easier to read and easier to debug.
Look for these smells in generated code
- repeated locators that should be extracted into helper functions
- nested
Promise.allblocks with no clear reason - excessive
page.locator()chains whengetByRole()would do - assertions that do not correspond to user value
- tests that are too long, trying to cover multiple unrelated behaviors
In my experience, a cleanly structured Playwright test is easier to debug than a clever one. AI output tends to optimize for completeness, not maintainability.
Debugging with Playwright traces, screenshots, and videos
When a generated test fails in CI, do not start by editing the code blindly. Use Playwright’s debugging artifacts.
Enable tracing in your config or CI run
import { defineConfig } from '@playwright/test';
export default defineConfig({ use: { trace: ‘on-first-retry’, screenshot: ‘only-on-failure’, video: ‘retain-on-failure’ } });
Trace files are one of the best ways to debug AI-generated Playwright tests because they show exactly what the browser saw and what the script did.
What I look for in a trace
- Was the click targeted at the right element?
- Did the page navigate where expected?
- Did a modal appear and block the action?
- Was the expected network request actually sent?
- Did a different element steal focus?
Sometimes the code looks wrong. Other times the code is fine, but the app state is not what the generated prompt assumed.
Debugging CI failures versus local failures
A test that passes locally but fails in CI is a classic automation problem, but AI-generated tests make it more common because they often include hidden assumptions.
Common CI-specific causes
- slower environments
- different browser versions
- missing test data
- parallel execution conflicts
- rate limits or external dependencies
- auth sessions expiring faster than expected
If a test uses shared data, hardcoded email addresses, or a fixed user account, it may pass alone and fail under parallel load.
A simple GitHub Actions pattern
name: playwright
on: [push, pull_request]
jobs:
test:
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: npx playwright test
If your AI-generated test fails only in CI, compare the local and CI runtime conditions before changing the locator strategy. The problem may be data setup, not code quality.
Debugging prompts themselves
One mistake I see is treating the prompt as disposable. But the prompt is part of the test artifact. If your AI keeps generating fragile tests, the prompt is probably too vague.
A better prompt includes constraints
Ask the model to:
- use role-based locators first
- avoid
waitForTimeout - assert on visible outcomes
- keep the test short
- prefer stable selectors and test ids
- explain any assumption it had to make
That gives you a better first draft and a better debugging starting point.
Example prompt shape
Generate a Playwright test for the signup flow. Use accessible locators, avoid arbitrary waits, assert that the user reaches the onboarding screen, and keep the test focused on the happy path only.
That kind of instruction reduces the amount of repair work later.
When generated code is the wrong abstraction
At some point, you should ask whether AI-generated Playwright tests are the right format for the team at all. If non-developers need to author or maintain these tests, code may be the wrong abstraction layer.
This is where a platform-native approach can be simpler. Endtest takes a different route, it uses agentic AI to generate editable, platform-native test steps instead of handing you a code snippet that needs framework maintenance. That matters when the real problem is not “how do I fix this selector”, but “why is the team spending debugging time on generated code at all?”.
Endtest’s AI Test Creation Agent generates tests as regular steps inside the platform, so they can be inspected and edited without managing a Playwright codebase, runner setup, browser drivers, or custom framework glue. For teams that want AI-assisted creation but do not want to debug code output every time the page changes, that is a much simpler maintenance model.
If your team wants generated tests to be easier to review and edit, platform-native steps are often less fragile than code snippets that still need framework expertise.
A practical debugging checklist I use
When I get a failing AI-generated Playwright test, I walk through this sequence:
1. Reproduce the failure locally
Confirm the failure is real and not a stale artifact.
2. Inspect the trace or video
See whether the app state matches the test’s assumptions.
3. Review selectors first
Replace brittle CSS chains with role, label, or test id locators.
4. Remove arbitrary waits
Replace them with assertions or wait conditions tied to visible state.
5. Check the test data
Make sure the account, record, or fixture really exists.
6. Validate the user flow
Make sure the generated steps match the actual app.
7. Tighten assertions
Verify outcomes, not just element existence.
8. Simplify the structure
Split long tests into smaller scenarios if the AI produced a monolithic script.
A small example of fixing a generated test
Suppose the AI creates this:
typescript
await page.goto('https://app.example.com');
await page.locator('input[type="email"]').fill('user@example.com');
await page.locator('input[type="password"]').fill('secret123');
await page.locator('button').click();
await page.waitForTimeout(5000);
await expect(page.locator('h1')).toContainText('Dashboard');
Problems:
- the button locator is ambiguous
- there is an arbitrary wait
- the assertion is too generic
- the test relies on implied page behavior
A better version might be:
typescript
await page.goto('https://app.example.com/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('secret123');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
That is shorter, clearer, and much easier to debug.
When to stop fixing and regenerate
Not every AI-generated test is worth salvaging. I usually regenerate when:
- the flow is completely wrong
- the locators are all based on unstable layout structure
- the test covers multiple unrelated behaviors
- the model hallucinated fields or buttons that do not exist
- the output requires so much repair that writing it manually would be faster
If the test is more than half broken, regeneration with a better prompt is often the better investment.
Final take
Debugging AI-generated Playwright tests is mostly about discipline. The AI can draft quickly, but you still need to verify locators, state transitions, assertions, and assumptions. The most reliable tests are the ones that stay close to what a user actually sees and does, and the most debuggable tests are the ones that make those expectations explicit.
If you are working in code and want the flexibility of Playwright, keep the generated output small, semantic, and traceable. If you want a simpler maintenance path for a broader team, a platform-native approach like Endtest can reduce the amount of code debugging altogether, because the generated output lives as editable steps inside the platform rather than as fragile snippets.
Either way, the core rule is the same: do not trust generated tests just because they run once. Trust them when you can explain, inspect, and repair every step.