# Testing Strategy --- title: 'Testing Strategy' version: 1.5.0 status: first-draft owner: Nattanin Peancharoen last_updated: 2025-11-30 related: - specs/03-implementation/backend-guidelines.md - specs/03-implementation/frontend-guidelines.md - specs/02-architecture/system-architecture.md --- ## 📋 Overview āđ€āļ­āļāļŠāļēāļĢāļ™āļĩāđ‰āļāļģāļŦāļ™āļ”āļāļĨāļĒāļļāļ—āļ˜āđŒāļāļēāļĢāļ—āļ”āļŠāļ­āļšāļŠāļģāļŦāļĢāļąāļšāđ‚āļ›āļĢāđ€āļˆāļāļ•āđŒ LCBP3-DMS āđ€āļžāļ·āđˆāļ­āđƒāļŦāđ‰āļĄāļąāđˆāļ™āđƒāļˆāđƒāļ™āļ„āļļāļ“āļ āļēāļž āļ„āļ§āļēāļĄāļ–āļđāļāļ•āđ‰āļ­āļ‡ āđāļĨāļ°āļ„āļ§āļēāļĄāļ›āļĨāļ­āļ”āļ āļąāļĒāļ‚āļ­āļ‡āļĢāļ°āļšāļš ### Testing Philosophy > **"Quality First, Test Everything That Matters"** āđ€āļĢāļēāđ€āļ™āđ‰āļ™āļāļēāļĢāļ—āļ”āļŠāļ­āļšāļ—āļĩāđˆāļĄāļĩāļ„āļ§āļēāļĄāļŦāļĄāļēāļĒ āđ„āļĄāđˆāđƒāļŠāđˆāđāļ„āđˆāđ€āļžāļīāđˆāļĄāļ•āļąāļ§āđ€āļĨāļ‚ Coverage āđāļ•āđˆāđ€āļ›āđ‡āļ™āļāļēāļĢāļ—āļ”āļŠāļ­āļšāļ—āļĩāđˆāļŠāđˆāļ§āļĒāļ›āđ‰āļ­āļ‡āļāļąāļ™āļšāļąāđŠāļāļˆāļĢāļīāļ‡ āđāļĨāļ°āđƒāļŦāđ‰āļ„āļ§āļēāļĄāļĄāļąāđˆāļ™āđƒāļˆāļ§āđˆāļēāļĢāļ°āļšāļšāļ—āļģāļ‡āļēāļ™āļ–āļđāļāļ•āđ‰āļ­āļ‡ ### Key Principles 1. **Test Early, Test Often** - āđ€āļ‚āļĩāļĒāļ™ Test āļ„āļ§āļšāļ„āļđāđˆāļāļąāļš Code 2. **Critical Path First** - āļ—āļ”āļŠāļ­āļš Business Logic āļŠāļģāļ„āļąāļāļāđˆāļ­āļ™ 3. **Automated Testing** - Automate āļ—āļļāļāļ­āļĒāđˆāļēāļ‡āļ—āļĩāđˆāļ—āļģāļ‹āđ‰āļģāđ„āļ”āđ‰ 4. **Fast Feedback** - Unit Tests āļ•āđ‰āļ­āļ‡āļĢāļ§āļ”āđ€āļĢāđ‡āļ§ (< 5 āļ§āļīāļ™āļēāļ—āļĩ) 5. **Real-World Scenarios** - E2E Tests āļ•āđ‰āļ­āļ‡āđƒāļāļĨāđ‰āđ€āļ„āļĩāļĒāļ‡āļāļąāļšāļāļēāļĢāđƒāļŠāđ‰āļ‡āļēāļ™āļˆāļĢāļīāļ‡ --- ## ðŸŽŊ Testing Pyramid ``` /\ / \ / E2E \ ~10% /--------\ / \ / Integration \ ~30% /--------------\ / \ / Unit Tests \ ~60% /--------------------\ ``` ### Test Distribution | Level | Coverage | Speed | Purpose | | ----------- | -------- | ---------- | ------------------------------- | | Unit | 60% | Fast (ms) | āļ—āļ”āļŠāļ­āļšāļ•āļĢāļĢāļāļ°āđāļ•āđˆāļĨāļ° Function | | Integration | 30% | Medium (s) | āļ—āļ”āļŠāļ­āļšāļāļēāļĢāļ—āļģāļ‡āļēāļ™āļĢāđˆāļ§āļĄāļāļąāļ™āļ‚āļ­āļ‡ Modules | | E2E | 10% | Slow (m) | āļ—āļ”āļŠāļ­āļš User Journey āļ—āļąāđ‰āļ‡āļŦāļĄāļ” | --- ## 🧊 Backend Testing (NestJS) ### 1. Unit Testing **Tools:** Jest, @nestjs/testing **Target Coverage:** 80% āļŠāļģāļŦāļĢāļąāļš Services, Utilities, Guards #### 1.1 Service Unit Tests ```typescript // File: src/modules/correspondence/services/correspondence.service.spec.ts import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { CorrespondenceService } from './correspondence.service'; import { Correspondence } from '../entities/correspondence.entity'; import { Repository } from 'typeorm'; describe('CorrespondenceService', () => { let service: CorrespondenceService; let repository: Repository; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ CorrespondenceService, { provide: getRepositoryToken(Correspondence), useClass: Repository, }, ], }).compile(); service = module.get(CorrespondenceService); repository = module.get>( getRepositoryToken(Correspondence) ); }); describe('findAll', () => { it('should return an array of correspondences', async () => { const mockCorrespondences = [ { id: '1', title: 'Test 1' }, { id: '2', title: 'Test 2' }, ]; jest .spyOn(repository, 'find') .mockResolvedValue(mockCorrespondences as any); const result = await service.findAll(); expect(result).toEqual(mockCorrespondences); expect(repository.find).toHaveBeenCalled(); }); it('should throw error when repository fails', async () => { jest.spyOn(repository, 'find').mockRejectedValue(new Error('DB Error')); await expect(service.findAll()).rejects.toThrow('DB Error'); }); }); }); ``` #### 1.2 Guard/Interceptor Unit Tests ```typescript // File: src/common/guards/rbac.guard.spec.ts describe('RBACGuard', () => { let guard: RBACGuard; let abilityFactory: AbilityFactory; beforeEach(() => { abilityFactory = new AbilityFactory(); guard = new RBACGuard(abilityFactory); }); it('should allow access when user has permission', () => { const context = createMockExecutionContext({ user: { role: 'admin', permissions: ['correspondence.create'] }, handler: createMockHandler({ permission: 'correspondence.create' }), }); expect(guard.canActivate(context)).toBe(true); }); it('should deny access when user lacks permission', () => { const context = createMockExecutionContext({ user: { role: 'viewer', permissions: ['correspondence.view'] }, handler: createMockHandler({ permission: 'correspondence.create' }), }); expect(guard.canActivate(context)).toBe(false); }); }); ``` #### 1.3 Critical Business Logic Tests **Document Numbering Concurrency Test:** ```typescript // File: src/modules/document-numbering/services/numbering.service.spec.ts describe('DocumentNumberingService - Concurrency', () => { it('should generate unique numbers for concurrent requests', async () => { const context = { projectId: '1', organizationId: '1', typeId: '1', year: 2025, }; // āļˆāļģāļĨāļ­āļ‡ 100 requests āļžāļĢāđ‰āļ­āļĄāļāļąāļ™ const promises = Array(100) .fill(null) .map(() => service.generateNextNumber(context)); const results = await Promise.all(promises); // āļ•āļĢāļ§āļˆāļŠāļ­āļšāļ§āđˆāļēāđ„āļĄāđˆāļĄāļĩāđ€āļĨāļ‚āļ‹āđ‰āļģ const uniqueNumbers = new Set(results); expect(uniqueNumbers.size).toBe(100); // āļ•āļĢāļ§āļˆāļŠāļ­āļšāļ§āđˆāļēāđ€āļĢāļĩāļĒāļ‡āļĨāļģāļ”āļąāļšāļ–āļđāļāļ•āđ‰āļ­āļ‡ const sorted = [...results].sort(); expect(results).toEqual(sorted); }); }); ``` **Idempotency Test:** ```typescript describe('IdempotencyInterceptor', () => { it('should return cached result for duplicate request', async () => { const idempotencyKey = 'test-key-123'; const mockResponse = { id: '1', title: 'Test' }; // Request 1 const result1 = await invokeInterceptor(idempotencyKey, mockResponse); expect(result1).toEqual(mockResponse); // Request 2 (same key) const result2 = await invokeInterceptor(idempotencyKey, mockResponse); expect(result2).toEqual(mockResponse); // Verify business logic only executed once expect(mockBusinessLogic).toHaveBeenCalledTimes(1); }); }); ``` --- ### 2. Integration Testing **Tools:** Jest, Supertest, In-Memory Database **Target:** āļ—āļ”āļŠāļ­āļš API Endpoints āļžāļĢāđ‰āļ­āļĄ Database interactions #### 2.1 API Integration Test ```typescript // File: test/correspondence.e2e-spec.ts import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from './../src/app.module'; import { v4 as uuidv4 } from 'uuid'; describe('Correspondence API (e2e)', () => { let app: INestApplication; let authToken: string; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); // Login to get token const loginResponse = await request(app.getHttpServer()) .post('/auth/login') .send({ username: 'testuser', password: 'password123' }); authToken = loginResponse.body.access_token; }); afterAll(async () => { await app.close(); }); describe('POST /correspondences', () => { it('should create correspondence with valid data', async () => { const createDto = { title: 'E2E Test Correspondence', project_id: '1', correspondence_type_id: '1', }; const response = await request(app.getHttpServer()) .post('/correspondences') .set('Authorization', `Bearer ${authToken}`) .send(createDto) .expect(201); expect(response.body).toMatchObject({ id: expect.any(String), title: createDto.title, correspondence_number: expect.stringMatching(/^TEAM-RFA-\d{4}-\d{4}$/), }); }); it('should enforce idempotency', async () => { const idempotencyKey = uuidv4(); const createDto = { title: 'Idempotency Test', project_id: '1', correspondence_type_id: '1', }; // Request 1 const response1 = await request(app.getHttpServer()) .post('/correspondences') .set('Authorization', `Bearer ${authToken}`) .set('Idempotency-Key', idempotencyKey) .send(createDto) .expect(201); // Request 2 (same key) const response2 = await request(app.getHttpServer()) .post('/correspondences') .set('Authorization', `Bearer ${authToken}`) .set('Idempotency-Key', idempotencyKey) .send(createDto) .expect(201); // Should return same entity expect(response1.body.id).toBe(response2.body.id); }); it('should validate input data', async () => { const invalidDto = { title: '', // Empty title }; await request(app.getHttpServer()) .post('/correspondences') .set('Authorization', `Bearer ${authToken}`) .send(invalidDto) .expect(400); }); it('should enforce RBAC permissions', async () => { // Login as viewer (no create permission) const viewerResponse = await request(app.getHttpServer()) .post('/auth/login') .send({ username: 'viewer', password: 'password123' }); const createDto = { title: 'Test', project_id: '1', correspondence_type_id: '1', }; await request(app.getHttpServer()) .post('/correspondences') .set('Authorization', `Bearer ${viewerResponse.body.access_token}`) .send(createDto) .expect(403); }); }); describe('File Upload Flow', () => { it('should complete two-phase file upload', async () => { // Phase 1: Upload to temp const uploadResponse = await request(app.getHttpServer()) .post('/attachments/upload') .set('Authorization', `Bearer ${authToken}`) .attach('file', Buffer.from('test file content'), 'test.pdf') .expect(201); const tempId = uploadResponse.body.temp_id; expect(tempId).toBeDefined(); // Phase 2: Commit with correspondence const createResponse = await request(app.getHttpServer()) .post('/correspondences') .set('Authorization', `Bearer ${authToken}`) .send({ title: 'Test with Attachment', project_id: '1', correspondence_type_id: '1', temp_file_ids: [tempId], }) .expect(201); // Verify attachment is committed const attachments = await request(app.getHttpServer()) .get(`/correspondences/${createResponse.body.id}/attachments`) .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(attachments.body).toHaveLength(1); expect(attachments.body[0].is_temporary).toBe(false); }); }); }); ``` --- ### 3. Database Testing #### 3.1 Migration Tests ```typescript // File: test/migrations/migration.spec.ts describe('Database Migrations', () => { it('should run all migrations without error', async () => { const dataSource = await createTestDataSource(); await expect(dataSource.runMigrations()).resolves.not.toThrow(); await dataSource.destroy(); }); it('should rollback migrations successfully', async () => { const dataSource = await createTestDataSource(); await dataSource.runMigrations(); await expect(dataSource.undoLastMigration()).resolves.not.toThrow(); await dataSource.destroy(); }); }); ``` #### 3.2 Transaction Tests ```typescript describe('Transaction Handling', () => { it('should rollback on error', async () => { const initialCount = await correspondenceRepo.count(); await expect( service.createWithError({ /* invalid data */ }) ).rejects.toThrow(); const finalCount = await correspondenceRepo.count(); expect(finalCount).toBe(initialCount); // No change }); }); ``` --- ### 4. Performance Testing **Tools:** Artillery, k6, Jest with timing #### 4.1 Load Testing Configuration ```yaml # File: test/load/correspondence-load.yml config: target: 'http://localhost:3000' phases: - duration: 60 arrivalRate: 10 # 10 requests/second - duration: 120 arrivalRate: 50 # Ramp up to 50 requests/second processor: './load-test-processor.js' scenarios: - name: 'Create Correspondence' weight: 40 flow: - post: url: '/correspondences' headers: Authorization: 'Bearer {{ authToken }}' Idempotency-Key: '{{ $uuid }}' json: title: 'Load Test {{ $randomString() }}' project_id: '{{ projectId }}' correspondence_type_id: '{{ typeId }}' - name: 'Search Correspondences' weight: 60 flow: - get: url: '/correspondences?project_id={{ projectId }}' headers: Authorization: 'Bearer {{ authToken }}' ``` #### 4.2 Query Performance Tests ```typescript describe('Performance - Database Queries', () => { it('should fetch 1000 correspondences in < 100ms', async () => { const startTime = Date.now(); await service.findAll({ limit: 1000 }); const duration = Date.now() - startTime; expect(duration).toBeLessThan(100); }); it('should use proper indexes for search', async () => { const queryPlan = await repository.query( `EXPLAIN SELECT * FROM correspondences WHERE project_id = ? AND deleted_at IS NULL`, ['1'] ); expect(queryPlan[0].key).toBe('idx_project_deleted'); }); }); ``` --- ## ðŸŽĻ Frontend Testing (Next.js) ### 1. Component Testing **Tools:** Vitest, React Testing Library **Target Coverage:** 70% āļŠāļģāļŦāļĢāļąāļš Shared Components #### 1.1 UI Component Tests ```tsx // File: components/forms/correspondence-form.spec.tsx import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { CorrespondenceForm } from './correspondence-form'; describe('CorrespondenceForm', () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); it('should render all required fields', () => { render(, { wrapper }); expect(screen.getByLabelText('āļŦāļąāļ§āđ€āļĢāļ·āđˆāļ­āļ‡')).toBeInTheDocument(); expect(screen.getByLabelText('āđ‚āļ›āļĢāđ€āļˆāļāļ•āđŒ')).toBeInTheDocument(); expect(screen.getByLabelText('āļ›āļĢāļ°āđ€āļ āļ—āđ€āļ­āļāļŠāļēāļĢ')).toBeInTheDocument(); }); it('should show validation errors on submit without data', async () => { const onSubmit = vi.fn(); render(, { wrapper }); const submitButton = screen.getByRole('button', { name: /āļšāļąāļ™āļ—āļķāļ/i }); fireEvent.click(submitButton); expect(await screen.findByText('āļāļĢāļļāļ“āļēāļĢāļ°āļšāļļāļŦāļąāļ§āđ€āļĢāļ·āđˆāļ­āļ‡')).toBeInTheDocument(); expect(onSubmit).not.toHaveBeenCalled(); }); it('should submit form with valid data', async () => { const onSubmit = vi.fn(); render(, { wrapper }); // Fill form const titleInput = screen.getByLabelText('āļŦāļąāļ§āđ€āļĢāļ·āđˆāļ­āļ‡'); fireEvent.change(titleInput, { target: { value: 'Test Title' } }); const projectSelect = screen.getByLabelText('āđ‚āļ›āļĢāđ€āļˆāļāļ•āđŒ'); fireEvent.change(projectSelect, { target: { value: 'project-1' } }); const typeSelect = screen.getByLabelText('āļ›āļĢāļ°āđ€āļ āļ—āđ€āļ­āļāļŠāļēāļĢ'); fireEvent.change(typeSelect, { target: { value: 'type-1' } }); // Submit const submitButton = screen.getByRole('button', { name: /āļšāļąāļ™āļ—āļķāļ/i }); fireEvent.click(submitButton); await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith( expect.objectContaining({ title: 'Test Title', project_id: 'project-1', correspondence_type_id: 'type-1', }) ); }); }); it('should auto-save draft every 30 seconds', async () => { vi.useFakeTimers(); const saveDraft = vi.fn(); render(, { wrapper }); const titleInput = screen.getByLabelText('āļŦāļąāļ§āđ€āļĢāļ·āđˆāļ­āļ‡'); fireEvent.change(titleInput, { target: { value: 'Draft Test' } }); // Fast-forward 30 seconds vi.advanceTimersByTime(30000); await waitFor(() => { expect(saveDraft).toHaveBeenCalledWith( expect.objectContaining({ title: 'Draft Test' }) ); }); vi.useRealTimers(); }); }); ``` #### 1.2 Custom Hook Tests ```tsx // File: lib/hooks/use-correspondence.spec.ts import { renderHook, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useCorrespondences } from './use-correspondence'; import { correspondenceService } from '@/lib/services/correspondence.service'; vi.mock('@/lib/services/correspondence.service'); describe('useCorrespondences', () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); it('should fetch correspondences successfully', async () => { const mockData = [ { id: '1', title: 'Test 1' }, { id: '2', title: 'Test 2' }, ]; vi.mocked(correspondenceService.getAll).mockResolvedValue(mockData); const { result } = renderHook(() => useCorrespondences('project-1'), { wrapper, }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data).toEqual(mockData); }); it('should handle error state', async () => { vi.mocked(correspondenceService.getAll).mockRejectedValue( new Error('API Error') ); const { result } = renderHook(() => useCorrespondences('project-1'), { wrapper, }); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toBeDefined(); }); }); ``` --- ### 2. E2E Testing **Tools:** Playwright **Target:** āļ—āļ”āļŠāļ­āļš Critical User Journeys #### 2.1 E2E Test Configuration ```typescript // File: 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', use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] }, }, ], webServer: { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, }); ``` #### 2.2 Critical User Journey Tests ```typescript // File: e2e/correspondence-workflow.spec.ts import { test, expect } from '@playwright/test'; test.describe('Correspondence Complete Workflow', () => { test.beforeEach(async ({ page }) => { // Login await page.goto('/login'); await page.fill('input[name="username"]', 'testuser'); await page.fill('input[name="password"]', 'password123'); await page.click('button[type="submit"]'); await page.waitForURL('/dashboard'); }); test('should create, edit, and submit correspondence', async ({ page }) => { // 1. Navigate to create page await page.click('text=āļŠāļĢāđ‰āļēāļ‡āđ€āļ­āļāļŠāļēāļĢ'); await page.waitForURL('/correspondences/new'); // 2. Fill form await page.fill('input[name="title"]', 'E2E Test Correspondence'); await page.selectOption('select[name="project_id"]', { index: 1 }); await page.selectOption('select[name="type_id"]', { index: 1 }); await page.fill('textarea[name="description"]', 'E2E Test Description'); // 3. Upload attachment const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles('test-fixtures/sample.pdf'); await expect(page.locator('text=sample.pdf')).toBeVisible(); // 4. Save as draft await page.click('button:has-text("āļšāļąāļ™āļ—āļķāļāđāļšāļšāļĢāđˆāļēāļ‡")'); await expect(page.locator('text=āļšāļąāļ™āļ—āļķāļāđāļšāļšāļĢāđˆāļēāļ‡āļŠāļģāđ€āļĢāđ‡āļˆ')).toBeVisible(); // 5. Edit draft const docId = page.url().match(/correspondences\/([^/]+)/)?.[1]; await page.goto(`/correspondences/${docId}/edit`); await page.fill('input[name="title"]', 'E2E Test Correspondence (Edited)'); // 6. Submit for approval await page.click('button:has-text("āļŠāđˆāļ‡āļ­āļ™āļļāļĄāļąāļ•āļī")'); await page.waitForSelector('text=āļĒāļ·āļ™āļĒāļąāļ™āļāļēāļĢāļŠāđˆāļ‡āđ€āļ­āļāļŠāļēāļĢ'); await page.click('button:has-text("āļĒāļ·āļ™āļĒāļąāļ™")'); // 7. Verify status changed await expect(page.locator('text=āļĢāļ­āļāļēāļĢāļ­āļ™āļļāļĄāļąāļ•āļī')).toBeVisible(); }); test('should support offline draft save', async ({ page, context }) => { await page.goto('/correspondences/new'); // Fill form await page.fill('input[name="title"]', 'Offline Test'); // Go offline await context.setOffline(true); // Trigger auto-save await page.waitForTimeout(31000); // Wait for auto-save (30s) // Verify draft saved to localStorage const localStorage = await page.evaluate(() => window.localStorage); expect(localStorage).toHaveProperty('correspondence-drafts'); // Go back online await context.setOffline(false); // Reload page await page.reload(); // Verify draft restored const titleValue = await page.inputValue('input[name="title"]'); expect(titleValue).toBe('Offline Test'); }); test('should handle responsive design', async ({ page }) => { await page.goto('/correspondences'); // Desktop view - should show table await page.setViewportSize({ width: 1280, height: 720 }); await expect(page.locator('table')).toBeVisible(); // Mobile view - should show card layout await page.setViewportSize({ width: 375, height: 667 }); await expect(page.locator('table')).not.toBeVisible(); await expect(page.locator('[data-testid="card-view"]')).toBeVisible(); }); }); ``` --- ## 🔒 Security Testing ### 1. OWASP Top 10 Testing #### 1.1 SQL Injection Protection ```typescript describe('Security - SQL Injection', () => { it('should prevent SQL injection in search', async () => { const maliciousInput = "'; DROP TABLE correspondences; --"; const response = await request(app.getHttpServer()) .get(`/correspondences/search?title=${maliciousInput}`) .set('Authorization', `Bearer ${authToken}`) .expect(200); // Should not execute malicious SQL const tableExists = await repository.query( `SHOW TABLES LIKE 'correspondences'` ); expect(tableExists).toHaveLength(1); }); }); ``` #### 1.2 XSS Protection ```typescript test('should sanitize user input to prevent XSS', async ({ page }) => { await page.goto('/correspondences/new'); const xssPayload = ''; await page.fill('input[name="title"]', xssPayload); await page.click('button[type="submit"]'); // Verify script not executed const alerts = []; page.on('dialog', (dialog) => { alerts.push(dialog.message()); dialog.dismiss(); }); await page.waitForTimeout(1000); expect(alerts).toHaveLength(0); // Verify content escaped const displayedTitle = await page.textContent('h1'); expect(displayedTitle).not.toContain('