The difference between mediocre AI-generated tests and production-ready ones isn’t the AI model — it’s the prompt. I’ve seen the same AI tool produce brittle, unmaintainable tests from vague prompts and excellent, well-structured tests from specific ones. The prompt is the new skill.

This post is your playbook. Five patterns that consistently produce good test code, a review checklist for AI output, the most common mistakes, and reusable templates you can copy and customize.

Pattern 1: Explore First, Generate Second

The most important pattern. Never ask AI to write tests without first understanding the page.

Bad Prompt ❌

Write Playwright tests for the blog page.

Result: The AI guesses at selectors, invents features that don’t exist, and uses brittle CSS class selectors.

Good Prompt ✅

First, use Playwright MCP to navigate to http://localhost:4321/blog.
Explore the page:
- What interactive elements are there?
- What does the search do?
- What are the tag filters?
- How many posts are visible?
- What happens when you click a post?

DO NOT write any code yet. Report what you find.

Then, after the AI reports back:

Based on what you found, write Playwright tests using:
- Page Object Model in tests/pages/BlogPage.ts
- Tests in tests/e2e/blog.spec.ts
- Import { test, expect } from '../fixtures/base.fixture'
- Use getByRole() and getByText() locators only

Why this works: The AI has real data about the page. It uses accurate selectors from the accessibility tree instead of guessing.

Pattern 2: Specify the Architecture You Want

AI tools default to the simplest approach — inline tests with hardcoded selectors. You need to tell them the pattern you want.

Bad Prompt ❌

Write a test for the login page.

Result:

test('login', async ({ page }) => {
  await page.goto('/login');
  await page.locator('#email').fill('test@test.com');
  await page.locator('#password').fill('pass');
  await page.locator('.submit-btn').click();
  await page.waitForTimeout(2000);
  expect(page.url()).toContain('dashboard');
});

Problems: CSS selectors, hardcoded wait, no Page Object, no proper assertions.

Good Prompt ✅

Write Playwright tests for the login page following these conventions:

ARCHITECTURE:
- Create a LoginPage class in tests/pages/LoginPage.ts
- Tests in tests/e2e/auth.spec.ts
- Import { test, expect } from '../fixtures/base.fixture'

LOCATORS (in priority order):
1. getByRole() — for buttons, links, headings
2. getByLabel() — for form inputs
3. getByPlaceholder() — for search fields
4. getByText() — for static text
5. getByTestId() — last resort

ASSERTIONS:
- Use expect(page).toHaveURL() for navigation checks
- Use expect(locator).toBeVisible() for visibility
- Use expect(locator).toContainText() for content
- NEVER use page.waitForTimeout()
- NEVER use expect with raw string comparison

TEST CASES:
1. Successful login with valid credentials → redirects to /dashboard
2. Invalid password → shows error message
3. Empty email → shows validation error
4. Empty password → shows validation error
5. Login button is disabled while request is in progress

Each test must be independent — no test should depend on another.

Why this works: You’ve defined the architecture, locator strategy, assertion patterns, and specific test cases. The AI has clear constraints to follow.

Pattern 3: Provide Edge Case Context

AI generates happy-path tests naturally. The edge cases — the ones that catch real bugs — come from your domain knowledge.

Prompt Template

Write tests for the [FEATURE] feature.

HAPPY PATH:
- [describe the normal flow]

EDGE CASES (from my experience as QC):
- What happens when [edge case 1]?
- What happens when [edge case 2]?
- What happens when [edge case 3]?

ERROR STATES:
- Use page.route() to mock API failures
- Test: API returns 500 → error message visible
- Test: API returns empty data → empty state visible
- Test: API times out (10s delay) → loading state then timeout message

BOUNDARY VALUES:
- Test with empty string input
- Test with maximum length input (500 characters)
- Test with special characters: <script>alert('xss')</script>
- Test with unicode characters: 日本語テスト
Write tests for the product search feature.

HAPPY PATH:
- User types "laptop" in search, sees laptop results

EDGE CASES (from my QC experience):
- User pastes a very long string (500+ characters) into search
- User types special characters: @#$%^&*()
- User searches while previous search is still loading
- User clears search after filtering by tag
- User presses Enter with empty search field
- User searches with leading/trailing spaces: "  laptop  "

ERROR STATES (use page.route to mock):
- Search API returns 500 → "Search is temporarily unavailable" message
- Search API returns empty array → "No products found" with suggestion to try different keywords
- Search API takes >5 seconds → loading spinner appears

MOBILE CONSIDERATIONS:
- Search on mobile viewport (375x667)
- Virtual keyboard doesn't cover results

Why this works: You’re combining your domain knowledge (the edge cases you know about from manual testing) with AI’s ability to quickly write the code for each scenario.

Pattern 4: Iterate with Failure Context

When AI-generated tests fail, don’t rewrite them manually. Feed the error back to the AI with context.

The Iteration Loop

Step 1: Run the tests

npx playwright test --project=e2e

Step 2: Copy the failure

The test "user can filter by tag" failed:

Error: Timed out 5000ms waiting for expect(locator).toBeVisible()

  Locator: getByRole('button', { name: 'ai' })
  Expected: visible
  Received: <element not found>

  Call log:
    - waiting for getByRole('button', { name: 'ai' })

Test ran on: http://localhost:4321/blog

Step 3: Ask for a fix

This Playwright test failed. The error says it can't find 
a button with name 'ai'. 

I checked the page manually — the tag filters are actually 
rendered as <span> elements with class "tag-pill", not buttons.
The text is lowercase "ai".

Fix the locator and update the Page Object accordingly.

Step 4: Verify and repeat

npx playwright test --project=e2e

Pro Tip: Copy as Prompt from Playwright

Playwright’s HTML report has a “Copy as Prompt” button on every failed test. This copies the failure context in a format optimized for AI tools:

npx playwright show-report

Click the failing test → click “Copy as Prompt” → paste into Claude or Copilot Chat. The AI gets the full error context and fixes the test accurately.

Pattern 5: Generate Data-Driven Tests

This pattern is perfect for scenarios with many input variations — the kind that take hours to test manually.

Prompt Template

Generate a data-driven Playwright test for the [FEATURE].

Test the same flow with these inputs:
| Input | Expected Result |
|-------|----------------|
| [input 1] | [result 1] |
| [input 2] | [result 2] |
| [input 3] | [result 3] |
| ...       | ...         |

Use a scenarios array and iterate with for...of.
Each row should be a separate test with a descriptive name.
Follow the Page Object Model pattern.

Real Example

Generate a data-driven test for the registration form.

Test these scenarios:
| Email | Password | Confirm | Expected |
|-------|----------|---------|----------|
| valid@email.com | Strong123! | Strong123! | Success redirect to /welcome |
| | password | password | "Email is required" error |
| not-an-email | password | password | "Invalid email format" error |
| valid@email.com | short | short | "Password must be at least 8 characters" |
| valid@email.com | password | different | "Passwords do not match" |
| existing@email.com | Strong123! | Strong123! | "Email already registered" |
| valid@email.com | NoSpecial1 | NoSpecial1 | "Password must contain a special character" |

Structure the scenarios array with clear types.
Use LoginPage page object for the form interactions.

Controlling AI Output Quality

The Review Checklist

Before committing any AI-generated test, check these:

## AI Test Review Checklist

### Locators
- [ ] Uses getByRole() or getByLabel() — NOT CSS selectors
- [ ] No hardcoded IDs unless using data-testid
- [ ] Locators are specific enough (won't match multiple elements)

### Assertions
- [ ] Uses expect() with auto-retrying matchers (toBeVisible, toHaveURL, etc.)
- [ ] NO page.waitForTimeout() calls
- [ ] Assertions are meaningful (not just "element exists")
- [ ] Negative cases tested (error states, empty states)

### Structure
- [ ] Test is independent — doesn't depend on other tests
- [ ] Uses beforeEach for navigation, not beforeAll
- [ ] No shared mutable state between tests
- [ ] Descriptive test names that explain the scenario

### Page Objects
- [ ] All selectors live in Page Object, not in test file
- [ ] No assertions inside Page Objects
- [ ] Methods describe user actions, not implementation details

### Maintenance
- [ ] Would I understand this test in 6 months?
- [ ] If the UI changes, how many files need updating? (Should be 1)
- [ ] Is there unnecessary duplication?

Common AI Mistakes to Fix

AI MistakeHow to SpotFix
CSS selectors.btn-primary, #email-inputReplace with getByRole(), getByLabel()
Hardcoded waitswaitForTimeout(3000)Replace with expect().toBeVisible()
Non-specific locatorspage.locator('button')Add { name: 'Submit' }
Tests depend on orderTest 2 assumes Test 1’s stateAdd proper setup in beforeEach
Missing error handlingOnly happy path testedAdd error state tests with page.route()
Duplicate selectorsSame selector in multiple testsCreate a Page Object
String assertionsexpect(text).toBe('exact match')Use toContainText() for flexibility

Reusable Prompt Templates

Template: Page Object + Tests

Create a Playwright Page Object and tests for the [PAGE_NAME] page
at [URL].

Page Object (tests/pages/[PageName]Page.ts):
- Locators for all interactive elements
- Methods for common user actions
- No assertions

Tests (tests/e2e/[page-name].spec.ts):
- Import test from '../fixtures/base.fixture'
- Use test.describe('[Page Name]') grouping
- beforeEach navigates to the page
- Test these scenarios:
  1. [Happy path scenario]
  2. [Validation scenario]
  3. [Error state scenario]
  4. [Edge case scenario]

Use getByRole/getByLabel/getByText locators only.

Template: BDD Feature File + Steps

Write a Cucumber BDD feature file and step definitions for [FEATURE].

Feature file (tests/features/[feature].feature):
- Write in Given/When/Then format
- Include a Background section for common setup
- Include Scenario Outline with Examples table for data variations
- Use @smoke tag for critical scenarios

Step definitions (tests/steps/[feature].steps.ts):
- Import CustomWorld from '../support/world'
- Use existing Page Objects from tests/pages/
- Use Playwright assertions (expect from @playwright/test)

Template: API Test Suite

Write Playwright API tests for the [ENDPOINT] endpoint.

File: tests/api/[endpoint].spec.ts

Test these HTTP scenarios:
- GET [path] → 200 with expected response shape
- GET [path] with filters → filtered results
- POST [path] with valid data → 201 with created resource
- POST [path] with invalid data → 400 with error message
- POST [path] without auth → 401
- DELETE [path]/:id → 204
- DELETE [path]/:nonexistent → 404

Use Playwright's request fixture.
Assert response status, content-type, and body structure.

How to Build Your Prompt Library

Create a folder in your test project for prompt templates:

tests/
├── prompts/                ← Your team's prompt library
│   ├── page-object.md      ← Template for generating Page Objects
│   ├── e2e-test.md         ← Template for E2E tests
│   ├── api-test.md         ← Template for API tests
│   ├── bdd-feature.md      ← Template for BDD feature files
│   └── debug-fix.md        ← Template for debugging failing tests

When anyone on the team needs to generate tests with AI, they grab the template, fill in the specifics, and get consistent, high-quality output every time.

Series Navigation

In Part 8, we’ll tackle the collaboration challenge — how Dev and QC teams share test automation work effectively, including repo structure, code reviews, task splitting, and the Definition of Done.

Export for reading

Comments