Files
admin 3274dede7a
CI / CD Pipeline / build (push) Failing after 4m28s
CI / CD Pipeline / deploy (push) Has been skipped
690603:2041 ADR-034-134 #01
2026-06-03 20:41:42 +07:00

355 lines
9.3 KiB
Markdown

---
name: e2e-testing
description: Playwright E2E testing patterns, Page Object Model, configuration, CI/CD integration, artifact management, and flaky test strategies for LCBP3-DMS.
version: 1.9.0
scope: testing
depends-on: []
handoffs-to: [speckit-tester]
user-invocable: true
---
# E2E Testing Skill
Playwright E2E testing patterns adapted for LCBP3-DMS (NestJS + Next.js + MariaDB stack).
## LCBP3 Context
See [`_LCBP3-CONTEXT.md`](../_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)
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
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
```bash
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:**
```typescript
// 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:**
```typescript
// 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:**
```typescript
// 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
```typescript
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
```typescript
// In playwright.config.ts
use: {
trace: 'on-first-retry'
}
// View trace
npx playwright show-trace trace.zip
```
### Video
```typescript
// In playwright.config.ts
use: {
video: 'retain-on-failure',
videosPath: 'artifacts/videos/'
}
```
## CI/CD Integration
```yaml
# .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
```markdown
# 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
```typescript
// 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, never `parseInt()` (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`