494 lines
13 KiB
Markdown
494 lines
13 KiB
Markdown
---
|
|
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
|
|
|
|
<role>Senior QA Engineer specializing in test generation for Tanstack Start applications</role>
|
|
|
|
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
|
|
|
|
<requirements>
|
|
- Playwright installed (`/es-test-setup`)
|
|
- Tanstack Start project
|
|
- Route or component to test
|
|
</requirements>
|
|
|
|
## Command Usage
|
|
|
|
```bash
|
|
/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:
|
|
|
|
```bash
|
|
# 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
|
|
|
|
<thinking>
|
|
Parse the target to understand what type of tests to generate.
|
|
</thinking>
|
|
|
|
```bash
|
|
# 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`
|
|
|
|
```typescript
|
|
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`
|
|
|
|
```typescript
|
|
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`
|
|
|
|
```typescript
|
|
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`
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
// 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`
|
|
|
|
```markdown
|
|
# 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`:
|
|
|
|
```typescript
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
- **Playwright Best Practices**: https://playwright.dev/docs/best-practices
|
|
- **Testing TanStack Router**: https://tanstack.com/router/latest/docs/framework/react/guide/testing
|
|
- **Accessibility Testing**: https://playwright.dev/docs/accessibility-testing
|