9.3 KiB
9.3 KiB
name, description, version, scope, depends-on, handoffs-to, user-invocable
| name | description | version | scope | depends-on | handoffs-to | user-invocable | |
|---|---|---|---|---|---|---|---|
| e2e-testing | Playwright E2E testing patterns, Page Object Model, configuration, CI/CD integration, artifact management, and flaky test strategies for LCBP3-DMS. | 1.9.0 | testing |
|
true |
E2E Testing Skill
Playwright E2E testing patterns adapted for LCBP3-DMS (NestJS + Next.js + MariaDB stack).
LCBP3 Context
See _LCBP3-CONTEXT.md for project-specific testing requirements:
- Backend: Jest (Unit + Integration + E2E)
- Frontend: Vitest (Unit) + Playwright (E2E)
- E2E test location:
frontend/e2e/workflow-adr021.spec.ts - Coverage goals: Backend 70%+, Business Logic 80%+
When to Use
Invoke this skill when:
- Creating new E2E tests for frontend features
- Debugging flaky Playwright tests
- Setting up CI/CD integration for E2E tests
- Optimizing test performance and reliability
- Implementing Page Object Model (POM) patterns
Test File Organization
frontend/
├── e2e/
│ ├── auth/
│ │ ├── login.spec.ts
│ │ └── logout.spec.ts
│ ├── correspondence/
│ │ ├── create.spec.ts
│ │ └── workflow.spec.ts
│ ├── transmittals/
│ │ ├── create.spec.ts
│ │ └── submit.spec.ts
│ ├── circulation/
│ │ ├── routing.spec.ts
│ │ └── approval.spec.ts
│ └── workflow-adr021.spec.ts # Existing ADR-021 integration test
├── playwright.config.ts
└── tests/
└── fixtures/
├── auth.ts
└── data.ts
Page Object Model (POM)
// frontend/e2e/pages/CorrespondencePage.ts
import { Page, Locator } from '@playwright/test'
export class CorrespondencePage {
readonly page: Page
readonly createButton: Locator
readonly subjectInput: Locator
readonly recipientSelect: Locator
readonly submitButton: Locator
readonly successMessage: Locator
constructor(page: Page) {
this.page = page
this.createButton = page.getByTestId('create-correspondence')
this.subjectInput = page.getByTestId('subject-input')
this.recipientSelect = page.getByTestId('recipient-select')
this.submitButton = page.getByTestId('submit-button')
this.successMessage = page.getByTestId('success-message')
}
async goto() {
await this.page.goto('/admin/doc-control/correspondences')
await this.page.waitForLoadState('networkidle')
}
async createCorrespondence(data: {
subject: string
recipientId: string
}) {
await this.createButton.click()
await this.subjectInput.fill(data.subject)
await this.recipientSelect.selectOption(data.recipientId)
await this.submitButton.click()
}
async verifySuccess() {
await expect(this.successMessage).toBeVisible()
}
}
Test Structure
// frontend/e2e/correspondence/create.spec.ts
import { test, expect } from '@playwright/test'
import { CorrespondencePage } from '../pages/CorrespondencePage'
test.describe('Correspondence Creation', () => {
let correspondencePage: CorrespondencePage
test.beforeEach(async ({ page }) => {
correspondencePage = new CorrespondencePage(page)
await correspondencePage.goto()
})
test('should create correspondence successfully', async ({ page }) => {
await correspondencePage.createCorrespondence({
subject: 'Test Correspondence',
recipientId: 'test-recipient-id'
})
await correspondencePage.verifySuccess()
await page.screenshot({ path: 'artifacts/correspondence-created.png' })
})
test('should validate required fields', async ({ page }) => {
await correspondencePage.createButton.click()
await correspondencePage.submitButton.click()
await expect(page.getByTestId('subject-error')).toBeVisible()
await expect(page.getByTestId('recipient-error')).toBeVisible()
})
})
Playwright Configuration
// frontend/playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { outputFolder: 'playwright-report' }],
['junit', { outputFile: 'playwright-results.xml' }],
['json', { outputFile: 'playwright-results.json' }]
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
actionTimeout: 10000,
navigationTimeout: 30000,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
webServer: {
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
})
Flaky Test Patterns
Quarantine
test('flaky: complex workflow', async ({ page }) => {
test.fixme(true, 'Flaky - Issue #123')
// test code...
})
test('conditional skip', async ({ page }) => {
test.skip(process.env.CI, 'Flaky in CI - Issue #123')
// test code...
})
Identify Flakiness
cd frontend
npx playwright test e2e/correspondence/create.spec.ts --repeat-each=10
npx playwright test e2e/correspondence/create.spec.ts --retries=3
Common Causes & Fixes
Race conditions:
// Bad: assumes element is ready
await page.click('[data-testid="submit-button"]')
// Good: auto-wait locator
await page.locator('[data-testid="submit-button"]').click()
Network timing:
// Bad: arbitrary timeout
await page.waitForTimeout(5000)
// Good: wait for specific condition
await page.waitForResponse(resp =>
resp.url().includes('/api/correspondences') && resp.status() === 201
)
Animation timing:
// Bad: click during animation
await page.click('[data-testid="menu-item"]')
// Good: wait for stability
await page.locator('[data-testid="menu-item"]').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')
await page.locator('[data-testid="menu-item"]').click()
Artifact Management
Screenshots
await page.screenshot({ path: 'artifacts/after-login.png' })
await page.screenshot({ path: 'artifacts/full-page.png', fullPage: true })
await page.locator('[data-testid="workflow-banner"]').screenshot({
path: 'artifacts/workflow-banner.png'
})
Traces
// In playwright.config.ts
use: {
trace: 'on-first-retry'
}
// View trace
npx playwright show-trace trace.zip
Video
// In playwright.config.ts
use: {
video: 'retain-on-failure',
videosPath: 'artifacts/videos/'
}
CI/CD Integration
# .github/workflows/e2e.yml
name: E2E Tests
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: pnpm install
- run: cd frontend && npx playwright install --with-deps
- run: cd frontend && npx playwright test
env:
BASE_URL: ${{ vars.STAGING_URL }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: frontend/playwright-report/
retention-days: 30
Test Report Template
# E2E Test Report
**Date:** YYYY-MM-DD HH:MM
**Duration:** Xm Ys
**Status:** PASSING / FAILING
## Summary
- Total: X | Passed: Y (Z%) | Failed: A | Flaky: B | Skipped: C
## Failed Tests
### correspondence-create
**File:** `frontend/e2e/correspondence/create.spec.ts:45`
**Error:** Expected element to be visible
**Screenshot:** artifacts/failed.png
**Recommended Fix:** Add waitForLoadState after form submission
## Artifacts
- HTML Report: frontend/playwright-report/index.html
- Screenshots: frontend/artifacts/*.png
- Videos: frontend/artifacts/videos/*.webm
- Traces: frontend/artifacts/*.zip
Critical Flow Testing
// frontend/e2e/workflow/adr021.spec.ts
test('workflow: correspondence → rfa → approval', async ({ page }) => {
// Create correspondence
await createCorrespondence(page)
await expect(page.getByTestId('correspondence-created')).toBeVisible()
// Submit for RFA
await page.getByTestId('submit-rfa').click()
await expect(page.getByTestId('rfa-submitted')).toBeVisible()
// Approve RFA
await page.goto('/admin/doc-control/rfa/123')
await page.getByTestId('approve-button').click()
await expect(page.getByTestId('approval-success')).toBeVisible()
// Verify workflow state
await expect(page.getByTestId('workflow-state')).toContainText('APPROVED')
})
LCBP3-Specific Considerations
- UUID Handling: Use
publicId(string UUID) in E2E tests, neverparseInt()(ADR-019) - Authentication: Mock auth tokens for E2E tests to avoid real auth flows
- Workflow States: Test ADR-021 workflow transitions (DRAFT → PENDING → APPROVED)
- i18n: Test with Thai language to verify i18n key resolution
- RBAC: Test different user roles (admin, user, reviewer) for permission checks
References
- LCBP3 Testing Strategy:
specs/05-Engineering-Guidelines/05-04-testing-strategy.md - ADR-021 Workflow Context:
specs/06-Decision-Records/ADR-021-workflow-context.md - Existing E2E test:
frontend/e2e/workflow-adr021.spec.ts