Files
2025-11-29 18:45:50 +08:00

13 KiB

description
description
Generate Playwright E2E tests for Tanstack Start routes, server functions, and components

Playwright Test Generator Command

<command_purpose> Automatically generate comprehensive Playwright tests for Tanstack Start routes, server functions, and components with Cloudflare Workers-specific patterns. </command_purpose>

Introduction

Senior QA Engineer specializing in test generation for Tanstack Start applications

This command generates ready-to-use Playwright tests that cover:

  • TanStack Router route loading and navigation
  • Server function calls with Cloudflare bindings
  • Component interactions
  • Accessibility validation
  • Error handling
  • Loading states

Prerequisites

- Playwright installed (`/es-test-setup`) - Tanstack Start project - Route or component to test

Command Usage

/es-test-gen <target> [options]

Arguments:

  • <target>: What to generate tests for

    • Route path: /users/$id, /dashboard, /blog
    • Server function: src/lib/server-functions/createUser.ts
    • Component: src/components/UserCard.tsx
  • [options]: Optional flags:

    • --with-auth: Include authentication tests
    • --with-server-fn: Include server function tests
    • --with-a11y: Include accessibility tests (default: true)
    • --output <path>: Custom output path

Examples:

# Generate tests for a route
/es-test-gen /users/$id

# Generate tests for server function
/es-test-gen src/lib/server-functions/createUser.ts --with-auth

# Generate tests for component
/es-test-gen src/components/UserCard.tsx --with-a11y

Main Tasks

1. Analyze Target

Parse the target to understand what type of tests to generate.
# Determine target type
if [[ "$TARGET" == /* ]]; then
  TYPE="route"
elif [[ "$TARGET" == *server-functions* ]]; then
  TYPE="server-function"
elif [[ "$TARGET" == *components* ]]; then
  TYPE="component"
fi

2. Generate Route Tests

For route: /users/$id

Task playwright-testing-specialist(analyze route and generate tests):

  • Identify dynamic parameters
  • Detect loaders and data dependencies
  • Check for authentication requirements
  • Generate test cases

Output: e2e/routes/users.$id.spec.ts

import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'

test.describe('User Profile Page', () => {
  const testUserId = '123'

  test('loads user profile successfully', async ({ page }) => {
    await page.goto(`/users/${testUserId}`)

    // Wait for loader to complete
    await page.waitForSelector('[data-testid="user-profile"]')

    // Verify user data displayed
    await expect(page.locator('h1')).toBeVisible()
    await expect(page.locator('[data-testid="user-email"]')).toBeVisible()
  })

  test('shows loading state during navigation', async ({ page }) => {
    await page.goto('/')

    // Navigate to user profile
    await page.click(`a[href="/users/${testUserId}"]`)

    // Verify loading indicator
    await expect(page.locator('[data-testid="loading"]')).toBeVisible()

    // Wait for content to load
    await expect(page.locator('[data-testid="user-profile"]')).toBeVisible()
  })

  test('handles non-existent user (404)', async ({ page }) => {
    const response = await page.goto('/users/999999')

    // Verify error state
    await expect(page.locator('text=/user not found/i')).toBeVisible()
  })

  test('has no accessibility violations', async ({ page }) => {
    await page.goto(`/users/${testUserId}`)

    const accessibilityScanResults = await new AxeBuilder({ page })
      .analyze()

    expect(accessibilityScanResults.violations).toEqual([])
  })

  test('navigates back correctly', async ({ page }) => {
    await page.goto(`/users/${testUserId}`)

    // Go back
    await page.goBack()

    // Verify we're back at previous page
    await expect(page).toHaveURL('/')
  })
})

3. Generate Server Function Tests

For: src/lib/server-functions/createUser.ts

Output: e2e/server-functions/create-user.spec.ts

import { test, expect } from '@playwright/test'

test.describe('Create User Server Function', () => {
  test('creates user successfully', async ({ page }) => {
    await page.goto('/users/new')

    // Fill form
    await page.fill('[name="name"]', 'Test User')
    await page.fill('[name="email"]', 'test@example.com')

    // Submit (calls server function)
    await page.click('button[type="submit"]')

    // Wait for redirect
    await page.waitForURL(/\/users\/\d+/)

    // Verify user created
    await expect(page.locator('h1')).toContainText('Test User')
  })

  test('validates required fields', async ({ page }) => {
    await page.goto('/users/new')

    // Submit empty form
    await page.click('button[type="submit"]')

    // Verify validation errors
    await expect(page.locator('[data-testid="name-error"]'))
      .toContainText(/required/i)
  })

  test('shows loading state during submission', async ({ page }) => {
    await page.goto('/users/new')

    await page.fill('[name="name"]', 'Test User')
    await page.fill('[name="email"]', 'test@example.com')

    // Start submission
    await page.click('button[type="submit"]')

    // Verify loading indicator
    await expect(page.locator('button[type="submit"]')).toBeDisabled()
    await expect(page.locator('[data-testid="loading"]')).toBeVisible()
  })

  test('handles server errors gracefully', async ({ page }) => {
    await page.goto('/users/new')

    // Simulate server error by using invalid data
    await page.fill('[name="email"]', 'invalid-email')

    await page.click('button[type="submit"]')

    // Verify error message
    await expect(page.locator('[data-testid="error"]')).toBeVisible()
  })

  test('stores data in Cloudflare D1', async ({ page, request }) => {
    await page.goto('/users/new')

    const testEmail = `test-${Date.now()}@example.com`

    await page.fill('[name="name"]', 'D1 Test User')
    await page.fill('[name="email"]', testEmail)

    await page.click('button[type="submit"]')

    // Wait for creation
    await page.waitForURL(/\/users\/\d+/)

    // Verify data persisted (reload page)
    await page.reload()

    await expect(page.locator('[data-testid="user-email"]'))
      .toContainText(testEmail)
  })
})

4. Generate Component Tests

For: src/components/UserCard.tsx

Output: e2e/components/user-card.spec.ts

import { test, expect } from '@playwright/test'

test.describe('UserCard Component', () => {
  test.beforeEach(async ({ page }) => {
    // Navigate to component demo/storybook page
    await page.goto('/components/user-card-demo')
  })

  test('renders user information correctly', async ({ page }) => {
    await expect(page.locator('[data-testid="user-card"]')).toBeVisible()
    await expect(page.locator('[data-testid="user-name"]')).toBeVisible()
    await expect(page.locator('[data-testid="user-email"]')).toBeVisible()
  })

  test('handles click interactions', async ({ page }) => {
    await page.click('[data-testid="user-card"]')

    // Verify click handler triggered
    await expect(page).toHaveURL(/\/users\/\d+/)
  })

  test('displays avatar image', async ({ page }) => {
    const avatar = page.locator('[data-testid="user-avatar"]')

    await expect(avatar).toBeVisible()

    // Verify image loaded
    await expect(avatar).toHaveJSProperty('complete', true)
  })

  test('has no accessibility violations', async ({ page }) => {
    const accessibilityScanResults = await new AxeBuilder({ page })
      .include('[data-testid="user-card"]')
      .analyze()

    expect(accessibilityScanResults.violations).toEqual([])
  })

  test('keyboard navigation works', async ({ page }) => {
    // Tab to card
    await page.keyboard.press('Tab')

    // Verify focus
    await expect(page.locator('[data-testid="user-card"]')).toBeFocused()

    // Press Enter
    await page.keyboard.press('Enter')

    // Verify navigation
    await expect(page).toHaveURL(/\/users\/\d+/)
  })

  test('matches visual snapshot', async ({ page }) => {
    await expect(page.locator('[data-testid="user-card"]'))
      .toHaveScreenshot('user-card.png')
  })
})

5. Generate Authentication Tests (--with-auth)

Output: e2e/auth/protected-route.spec.ts

import { test, expect } from '@playwright/test'

test.describe('Protected Route - /users/$id', () => {
  test('redirects to login when unauthenticated', async ({ page }) => {
    await page.goto('/users/123')

    // Should redirect to login
    await page.waitForURL(/\/login/)

    // Verify redirect query param
    expect(page.url()).toContain('redirect=%2Fusers%2F123')
  })

  test('allows access when authenticated', async ({ page }) => {
    // Login first
    await page.goto('/login')
    await page.fill('[name="email"]', 'test@example.com')
    await page.fill('[name="password"]', 'password123')
    await page.click('button[type="submit"]')

    // Navigate to protected route
    await page.goto('/users/123')

    // Should not redirect
    await expect(page).toHaveURL('/users/123')
    await expect(page.locator('[data-testid="user-profile"]')).toBeVisible()
  })

  test('redirects to original destination after login', async ({ page }) => {
    // Try to access protected route
    await page.goto('/users/123')

    // Should be on login page
    await page.waitForURL(/\/login/)

    // Login
    await page.fill('[name="email"]', 'test@example.com')
    await page.fill('[name="password"]', 'password123')
    await page.click('button[type="submit"]')

    // Should redirect back to original destination
    await expect(page).toHaveURL('/users/123')
  })
})

6. Update Test Metadata

Add test to suite configuration:

// e2e/test-registry.ts (auto-generated)
export const testRegistry = {
  routes: [
    'e2e/routes/users.$id.spec.ts',
    // ... other routes
  ],
  serverFunctions: [
    'e2e/server-functions/create-user.spec.ts',
    // ... other server functions
  ],
  components: [
    'e2e/components/user-card.spec.ts',
    // ... other components
  ],
}

7. Generate Test Documentation

Output: e2e/routes/users.$id.README.md

# User Profile Route Tests

## Test Coverage

- ✅ Route loading with valid user ID
- ✅ Loading state during navigation
- ✅ 404 handling for non-existent users
- ✅ Accessibility (zero violations)
- ✅ Back navigation

## Running Tests

```bash
# Run all tests for this route
pnpm test:e2e e2e/routes/users.$id.spec.ts

# Run specific test
pnpm test:e2e e2e/routes/users.$id.spec.ts -g "loads user profile"

# Debug mode
pnpm test:e2e:debug e2e/routes/users.$id.spec.ts

Test Data

Uses test user ID: 123 (configured in test fixtures)

Dependencies

  • Requires D1 database with test data
  • Requires user with ID 123 to exist

## Test Generation Patterns

### Pattern: Dynamic Route Parameters

For `/blog/$category/$slug`:

```typescript
test.describe('Blog Post Page', () => {
  const testCategory = 'tech'
  const testSlug = 'tanstack-start-guide'

  test('loads blog post successfully', async ({ page }) => {
    await page.goto(`/blog/${testCategory}/${testSlug}`)

    await expect(page.locator('article')).toBeVisible()
    await expect(page.locator('h1')).toBeVisible()
  })
})

Pattern: Search Params

For /users?page=2&sort=name:

test.describe('Users List with Search Params', () => {
  test('paginates users correctly', async ({ page }) => {
    await page.goto('/users?page=2')

    // Verify page 2 content
    await expect(page.locator('[data-testid="pagination"]'))
      .toContainText('Page 2')
  })

  test('sorts users by name', async ({ page }) => {
    await page.goto('/users?sort=name')

    const userNames = await page.locator('[data-testid="user-name"]').allTextContents()

    // Verify sorted
    const sorted = [...userNames].sort()
    expect(userNames).toEqual(sorted)
  })
})

Validation

After generating tests:

  1. Syntax check: Verify TypeScript compiles
  2. Dry run: Run tests without executing
  3. Coverage: Ensure critical paths covered
# Check syntax
npx tsc --noEmit

# Dry run
pnpm test:e2e --list

# Run generated tests
pnpm test:e2e e2e/routes/users.$id.spec.ts

Success Criteria

Tests generated for target All tests pass on first run Accessibility tests included Error handling covered Loading states tested Documentation generated Test registered in test suite

Resources