Generate Playwright Tests Automatically from Any URL
Manually writing Playwright tests for every page and interaction is slow. Learn how to auto-generate a full Playwright TypeScript test suite from any URL in minutes, with CI/CD integration included.
Playwright is the best browser automation framework available today. It's fast, reliable, multi-browser, and its TypeScript API is expressive enough to handle anything from simple navigation checks to complex multi-step user flows.
The problem isn't Playwright itself. It's the labor cost of writing tests. A non-trivial web application has hundreds of interactive states. Manually writing a test for each one — getting the selectors right, handling async state, adding assertions — takes days or weeks even for experienced engineers.
This tutorial shows how to generate a production-ready Playwright TypeScript test suite automatically from any URL, what the output looks like, and how to integrate it into your CI/CD pipeline.
How Automatic Playwright Test Generation Works
The process has three stages: crawl, generate, export.
Stage 1: Crawl
A headless browser visits your URL and systematically explores the application. Unlike a simple web spider that follows links, a full crawler also:
- Clicks buttons, toggles, tabs, and accordions
- Fills and submits forms
- Scrolls to trigger lazy-loaded content
- Detects navigation events and follows them to new pages
- Records the DOM state at each point
The crawler uses Playwright internally — the same browser engine your tests will use. That means what the crawler sees is exactly what your tests will see.
Stage 2: Generate Test Cases
From the discovered states, AI generates test cases. Each test is a reproducible path from a known entry point to a specific UI state, with assertions that verify the expected state.
The generator uses stable, semantic selectors:
getByRole()targeting ARIA rolesgetByLabel()for form fieldsgetByText()for contentgetByTestId()for elements withdata-testidattributes
CSS class selectors and XPath are used only as fallbacks when semantic options don't exist.
Stage 3: Export as Playwright TypeScript
The output is standard .spec.ts files. No proprietary runtime, no vendor-specific API. You can drop the files into any existing Playwright project and run them with npx playwright test.
What the Generated Output Looks Like
Navigation Test
import { test, expect } from '@playwright/test';
test.describe('Primary navigation', () => {
test('Pricing page loads and displays plan comparison', async ({ page }) => {
await page.goto('https://example.com');
await page.getByRole('navigation').getByRole('link', { name: 'Pricing' }).click();
await expect(page).toHaveURL(/.*\/pricing/);
await expect(page.getByRole('heading', { name: 'Choose your plan' })).toBeVisible();
await expect(page.getByRole('table')).toBeVisible();
});
});
Form Interaction Test
import { test, expect } from '@playwright/test';
test.describe('Contact form', () => {
test('Submitting empty form shows validation errors', async ({ page }) => {
await page.goto('https://example.com/contact');
await page.getByRole('button', { name: 'Send Message' }).click();
await expect(page.getByText('Name is required')).toBeVisible();
await expect(page.getByText('Email is required')).toBeVisible();
});
test('Valid submission shows success confirmation', async ({ page }) => {
await page.goto('https://example.com/contact');
await page.getByLabel('Name').fill('Jane Smith');
await page.getByLabel('Email').fill('jane@example.com');
await page.getByLabel('Message').fill('Test inquiry.');
await page.getByRole('button', { name: 'Send Message' }).click();
await expect(page.getByRole('alert')).toContainText('Message sent');
});
});
Modal and Overlay Test
import { test, expect } from '@playwright/test';
test('Product image gallery opens full-screen on click', async ({ page }) => {
await page.goto('https://example.com/products/widget-pro');
await page.getByRole('img', { name: 'Widget Pro front view' }).click();
await expect(page.getByRole('dialog')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click();
await expect(page.getByRole('dialog')).not.toBeVisible();
});
Playwright Configuration for Generated Tests
The generator also outputs a playwright.config.ts preconfigured for multi-browser runs:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [
['html'],
['junit', { outputFile: 'results/junit.xml' }],
],
use: {
baseURL: process.env.BASE_URL || 'https://example.com',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
Note the BASE_URL environment variable — point the same test suite at different environments without modifying test files.
CI/CD Integration
GitHub Actions
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
env:
BASE_URL: ${{ vars.STAGING_URL }}
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
GitLab CI
playwright:
image: mcr.microsoft.com/playwright:v1.52.0-noble
stage: test
script:
- npm ci
- npx playwright test
variables:
BASE_URL: $STAGING_URL
artifacts:
when: always
paths:
- playwright-report/
reports:
junit: results/junit.xml
Handling Authentication
Playwright's storageState feature handles authentication efficiently — authenticate once, reuse the session across all tests:
// auth.setup.ts
import { test as setup } from '@playwright/test';
import path from 'path';
const authFile = path.join(__dirname, '../.auth/user.json');
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: authFile });
});
The AegisRunner crawler uses the same session-reuse approach when crawling authenticated apps.
Keeping Generated Tests Up to Date
After a significant release, run a new crawl against your updated staging environment. The tool detects new states that don't have existing tests and generates tests for them. Existing tests for states that haven't changed are preserved.
For visual regression baselines, you review and accept changes in a PR-style workflow: changed screenshots are flagged for review, you accept the intentional changes, and reject the bugs.
What Manual Playwright Tests Are Still Better For
Generated tests cover breadth — every discoverable UI state. Handwritten tests remain the right tool for:
- Complex business logic assertions: Verifying a discount calculation is mathematically correct
- API response validation: Assertions about network request payloads
- Performance benchmarks: Load time assertions with specific thresholds
- Data-driven tests: Running the same flow with dozens of input variations
Use generated tests for broad UI regression coverage, write targeted manual tests for logic that requires domain knowledge.
Getting Started
You need: a URL and a few minutes.
- Sign up for AegisRunner's free tier at aegisrunner.com
- Create a project and enter your URL
- Run the crawl — free tier covers up to 50 pages
- Review discovered states and export as Playwright TypeScript
- Drop the
.spec.tsfiles into your repo and runnpx playwright test
For a React, Vue, Next.js, or Nuxt application, the full process from zero to a running CI pipeline takes under an hour. No Playwright expertise required — though the exported tests are clean enough that you'll want to read them anyway.