Skip to content

Last updated:

E2E Testing

End-to-end (E2E) testing is not yet configured in the CPR project. This document outlines the recommended strategy, tool choices, and critical user flows that should be covered when E2E testing is implemented.

Playwright is the recommended E2E testing framework for CPR because:

  • First-class support for modern web applications (SPA, Vite-based)
  • Built-in auto-waiting reduces flaky tests
  • Multi-browser support (Chromium, Firefox, WebKit)
  • Excellent TypeScript support
  • Built-in test runner with parallel execution
  • Good integration with Vitest via @vitest/browser

Alternative: Cypress

Cypress is also a viable option if the team prefers its interactive test runner and dashboard. However, Playwright's speed and multi-browser support make it the stronger choice for a medical application that needs thorough cross-browser coverage.

Setup (Future)

bash
# Install Playwright
npm install -D @playwright/test

# Initialize and download browsers
npx playwright install

Example playwright.config.ts:

ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  baseURL: 'http://localhost:3000',
  use: {
    headless: true,
    screenshot: 'only-on-failure',
    trace: 'on-first-retry',
  },
  webServer: {
    command: 'npm run dev',
    port: 3000,
    reuseExistingServer: true,
  },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
  ],
});

Critical User Flows to Test

1. Authentication

The login flow is the gateway to the entire application and must work reliably.

ts
// e2e/auth/login.spec.ts
import { test, expect } from '@playwright/test';

test('user can log in with valid credentials', async ({ page }) => {
  await page.goto('/login');

  await page.fill('[data-testid="username-input"]', 'doctor');
  await page.fill('[data-testid="password-input"]', 'password123');
  await page.click('[data-testid="login-button"]');

  // Should redirect to dashboard after login
  await expect(page).toHaveURL('/');
  await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
});

test('shows error for invalid credentials', async ({ page }) => {
  await page.goto('/login');

  await page.fill('[data-testid="username-input"]', 'wrong');
  await page.fill('[data-testid="password-input"]', 'wrong');
  await page.click('[data-testid="login-button"]');

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

test('redirects unauthenticated users to login', async ({ page }) => {
  await page.goto('/patients');
  await expect(page).toHaveURL('/login');
});

2. Password Reset Flow

The multi-step password reset flow uses route guards enforced by the reset-password middleware. Each step must be reached in sequence.

ts
// e2e/auth/reset-password.spec.ts
test('completes full password reset flow', async ({ page }) => {
  await page.goto('/reset-password');

  // Step 1: Enter email
  await page.fill('[data-testid="email-input"]', 'doctor@clinic.com');
  await page.click('[data-testid="send-code-button"]');

  // Step 2: Enter verification code
  await expect(page).toHaveURL('/reset-password/enter-code');
  await page.fill('[data-testid="code-input"]', '123456');
  await page.click('[data-testid="verify-button"]');

  // Step 3: Set new password
  await expect(page).toHaveURL('/reset-password/new-password');
  await page.fill('[data-testid="password-input"]', 'NewSecurePass123');
  await page.fill('[data-testid="confirm-password-input"]', 'NewSecurePass123');
  await page.click('[data-testid="reset-button"]');

  // Step 4: Success confirmation
  await expect(page).toHaveURL('/reset-password/success-state');
});

test('cannot skip to step 3 directly', async ({ page }) => {
  await page.goto('/reset-password/new-password');
  // Middleware redirects to step 1
  await expect(page).toHaveURL('/reset-password');
});

3. Patient Registration

ts
// e2e/patients/registration.spec.ts
test('registers a new patient', async ({ page }) => {
  await page.goto('/patients/create');

  await page.fill('[data-testid="last-name"]', 'Dela Cruz');
  await page.fill('[data-testid="first-name"]', 'Juan');
  await page.fill('[data-testid="birthdate"]', '1985-03-15');
  await page.selectOption('[data-testid="sex"]', 'Male');

  await page.click('[data-testid="save-button"]');

  // Should show success snackbar
  await expect(page.locator('.bg-green-600')).toBeVisible();
});

4. Queue Management

ts
// e2e/queue/management.spec.ts
test('adds patient to queue', async ({ page }) => {
  await page.goto('/queue');

  await page.click('[data-testid="add-to-queue"]');
  await page.fill('[data-testid="patient-search"]', 'Dela Cruz');
  await page.click('[data-testid="patient-result"]');

  await expect(page.locator('text=Dela Cruz')).toBeVisible();
});

5. Pharmacy Transactions

ts
// e2e/pharmacy/transaction.spec.ts
test('searches and views pharmacy transactions', async ({ page }) => {
  await page.goto('/transactions');

  await page.fill('[data-testid="search-input"]', 'Paracetamol');
  await expect(page.locator('text=Paracetamol')).toBeVisible();
});

6. Branch Switching

ts
// e2e/auth/branch-switching.spec.ts
test('switches between clinic branches', async ({ page }) => {
  await page.goto('/');

  await page.click('[data-testid="branch-selector"]');
  await page.click('text=Main Clinic');

  await expect(page.locator('[data-testid="current-branch"]')).toHaveText(
    'Main Clinic'
  );
});

Authentication Setup for Tests

Use Playwright's storageState to avoid repeating login in every test:

ts
// e2e/setup/auth.setup.ts
import { test as setup, expect } from '@playwright/test';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[data-testid="username-input"]', 'test-doctor');
  await page.fill('[data-testid="password-input"]', 'test-password');
  await page.click('[data-testid="login-button"]');
  await expect(page).toHaveURL('/');

  // Save sessionStorage auth state for reuse
  await page.context().storageState({ path: 'e2e/.auth/user.json' });
});

Test Data Strategy

For E2E tests in a medical application:

  1. Use a dedicated test database -- Never run E2E tests against production data.
  2. Seed known test data -- Create a seeder that sets up patients, medicines, and users before each test run.
  3. Use API fixtures -- Consider using Playwright's route.fulfill() to mock API responses for faster, more reliable tests.
  4. Clean up after tests -- Remove test-created records to prevent test pollution.

Implementation Priority

When implementing E2E tests, prioritize in this order:

PriorityFlowReason
1Login and authEverything depends on this
2Patient lookup and registrationCore medical workflow
3Queue managementDaily operational flow
4Billing and transactionsFinancial accuracy is critical
5Password resetSecurity-critical multi-step flow
6Branch switchingMulti-location functionality
7Pharmacy and stock managementInventory integrity

CPR - Clinical Patient Records