Skip to content

Testing

Testing

Comprehensive end-to-end testing using Playwright for the Cbox Init Web UI.

Overview

The test suite provides:

  • 35 comprehensive tests across 3 test suites
  • Multi-browser support (Chromium, Firefox, WebKit)
  • Mobile testing (Pixel 5, iPhone 12)
  • Accessibility validation (WCAG 2.1 compliance)
  • API mocking for isolated testing
  • Visual regression with screenshots
  • 100% test coverage of critical paths

Quick Start

Install Dependencies

cd web
npm install
npx playwright install

Run Tests

# Run all tests
npm run test

# Run in specific browser
npm run test:chromium
npm run test:firefox
npm run test:webkit

# Run with UI mode (interactive)
npm run test:ui

# Run in headed mode (visible browser)
npm run test:headed

# Debug mode
npm run test:debug

# View HTML report
npm run test:report

Test Suites

1. Dashboard Tests (16 tests)

File: web/e2e/dashboard.spec.ts

Tests core dashboard functionality:

test('should display process stats', async ({ page }) => {
  await page.waitForTimeout(500);

  await expect(page.getByText('Total Processer')).toBeVisible();
  await expect(page.getByText('Kører')).toBeVisible();
  await expect(page.getByText('Stoppet')).toBeVisible();
  await expect(page.getByText('Fejlet')).toBeVisible();
});

Coverage:

  • Page rendering and layout
  • Health status display
  • Stats cards (total, running, stopped, failed)
  • Process cards and instances
  • Dark mode toggle
  • Refresh functionality
  • API error handling
  • Empty state handling
  • Mobile responsiveness

2. Process Actions Tests (11 tests)

File: web/e2e/process-actions.spec.ts

Tests process control functionality:

test('should start a stopped process', async ({ page }) => {
  let startActionCalled = false;

  await page.route('**/api/v1/processes/test-process', async (route) => {
    if (route.request().method() === 'POST') {
      const postData = route.request().postDataJSON();
      if (postData.action === 'start') {
        startActionCalled = true;
        await route.fulfill({
          status: 200,
          contentType: 'application/json',
          body: JSON.stringify({ success: true }),
        });
      }
    }
  });

  await page.goto('/');
  await page.waitForTimeout(500);

  const processCard = page.getByTestId('process-card-test-process');
  const startButton = processCard.locator('button').first();
  await startButton.click();

  await page.waitForTimeout(500);
  expect(startActionCalled).toBeTruthy();
});

Coverage:

  • Start process action
  • Stop process action
  • Restart process action
  • Error handling
  • Button states and visibility
  • Process state changes
  • Scale information
  • Instance count display
  • Hover effects

3. Accessibility Tests (12 tests)

File: web/e2e/accessibility.spec.ts

WCAG 2.1 compliance validation:

test('should not have automatically detectable accessibility issues', async ({ page }) => {
  await page.waitForTimeout(500);

  const buttons = page.locator('button');
  const buttonCount = await buttons.count();

  for (let i = 0; i < buttonCount; i++) {
    const button = buttons.nth(i);
    const ariaLabel = await button.getAttribute('aria-label');
    const title = await button.getAttribute('title');
    const textContent = await button.textContent();

    expect(
      ariaLabel || title || (textContent && textContent.trim().length > 0)
    ).toBeTruthy();
  }
});

Coverage:

  • Semantic HTML structure
  • ARIA labels for icon buttons
  • Keyboard navigation
  • Screen reader support
  • Color contrast ratios
  • Proper heading hierarchy
  • Form label associations
  • Focus indicators
  • Dark mode accessibility
  • Status messages

Configuration

Playwright Config

File: web/playwright.config.ts

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,

  use: {
    baseURL: 'http://localhost:5173',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
  ],

  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: !process.env.CI,
    timeout: 120 * 1000,
  },
});

API Mocking

All tests use API mocking for isolation:

async function mockAPIResponses(page: Page) {
  // Mock health endpoint
  await page.route('**/api/v1/health', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ status: 'healthy' }),
    });
  });

  // Mock processes list endpoint
  await page.route('**/api/v1/processes', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        processes: [
          {
            name: 'php-fpm',
            state: 'running',
            scale: 2,
            instances: [/* ... */],
          },
        ],
      }),
    });
  });
}

test.beforeEach(async ({ page }) => {
  await mockAPIResponses(page);
});

Test Patterns

Using Test IDs

For reliable element selection:

// Component with data-testid
<div data-testid={`process-card-${process.name}`}>
  {/* ... */}
</div>

// Test selector
const processCard = page.getByTestId('process-card-test-process');

Role-Based Selectors

For semantic element selection:

await expect(
  page.getByRole('heading', { name: 'php-fpm', exact: true })
).toBeVisible();

CSS Selectors

For specific styling-based selection:

const runningBadges = page.locator('span.text-green-500.px-2', {
  hasText: 'running'
});
await expect(runningBadges).toHaveCount(3);

Waiting Strategies

// Wait for timeout (use sparingly)
await page.waitForTimeout(500);

// Wait for specific text
await page.waitForSelector('text=Process started');

// Wait for network response
await page.waitForResponse('**/api/v1/processes');

CI/CD Integration

GitHub Actions

name: E2E Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: Install dependencies
        run: |
          cd web
          npm ci
          npx playwright install --with-deps

      - name: Run tests
        run: npm run test --prefix web

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report
          path: web/playwright-report/

Docker Testing

FROM mcr.microsoft.com/playwright:v1.56.1-focal

WORKDIR /app
COPY web/package*.json ./
RUN npm ci

COPY web/ ./
RUN npm run build

CMD ["npm", "run", "test"]

Debugging Tests

Interactive UI Mode

npm run test:ui

Features:

  • Visual test execution
  • Step through tests
  • Time travel debugging
  • DOM snapshots
  • Network logs

Debug Mode

npm run test:debug

Opens headed browser with:

  • Playwright Inspector
  • Step-by-step execution
  • Selector playground
  • Console logs

Screenshots on Failure

Automatic screenshot capture:

test-results/
├── dashboard-test-name-chromium/
│   ├── test-failed-1.png
│   └── error-context.md

Trace Files

Enable trace recording:

use: {
  trace: 'on-first-retry',
}

View traces:

npx playwright show-trace test-results/trace.zip

Best Practices

1. Use Reliable Selectors

// ✅ Good: Test IDs
page.getByTestId('process-card-nginx')

// ✅ Good: Role-based
page.getByRole('button', { name: 'Start' })

// ❌ Bad: Text content (can match multiple)
page.locator('text=nginx')

2. Avoid Hard Waits

// ❌ Bad: Hard timeout
await page.waitForTimeout(1000);

// ✅ Good: Wait for condition
await expect(page.getByText('Process started')).toBeVisible();

3. Mock External Dependencies

Always mock API calls:

// ✅ Good: Mocked
await page.route('**/api/**', mockHandler);

// ❌ Bad: Real API (flaky, slow)
// (no mocking)

4. Clean Test Isolation

test.beforeEach(async ({ page }) => {
  await mockAPIResponses(page);
  await page.goto('/');
});

test.afterEach(async ({ page }) => {
  await page.close();
});

5. Descriptive Test Names

// ✅ Good
test('should display error message when API returns 500', async ({ page }) => {
  // ...
});

// ❌ Bad
test('error handling', async ({ page }) => {
  // ...
});

Performance

Parallel Execution

Tests run in parallel by default (8 workers):

fullyParallel: true,
workers: process.env.CI ? 1 : undefined,

Test Sharding

Split tests across multiple machines:

npx playwright test --shard=1/3
npx playwright test --shard=2/3
npx playwright test --shard=3/3

Selective Test Running

# Run specific file
npx playwright test dashboard.spec.ts

# Run specific test
npx playwright test -g "should start a stopped process"

# Run by tag
npx playwright test --grep @smoke

Coverage Metrics

Current test coverage:

  • 35/35 tests passing (100%)
  • 3 test suites (Dashboard, Actions, Accessibility)
  • 5 browser configurations (Desktop + Mobile)
  • All critical paths covered

Troubleshooting

Tests Failing Locally

  1. Check Node version: node --version (20+)
  2. Reinstall browsers: npx playwright install
  3. Clear cache: rm -rf node_modules && npm install
  4. Check port availability: lsof -i :5173

Timeout Errors

Increase timeout:

test('slow operation', async ({ page }) => {
  test.setTimeout(60000); // 60 seconds
  // ...
});

Selector Issues

Use Playwright Inspector:

PWDEBUG=1 npm run test

Click "Pick locator" to test selectors.

Future Enhancements

Planned improvements:

  • Visual regression testing
  • Performance benchmarks
  • API contract testing
  • Load testing scenarios
  • Mutation testing
  • Code coverage integration