Files
Charon/tests/README.md
akanealw eec8c28fb3
Some checks are pending
Go Benchmark / Performance Regression Check (push) Waiting to run
Cerberus Integration / Cerberus Security Stack Integration (push) Waiting to run
Upload Coverage to Codecov / Backend Codecov Upload (push) Waiting to run
Upload Coverage to Codecov / Frontend Codecov Upload (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (go) (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Waiting to run
CrowdSec Integration / CrowdSec Bouncer Integration (push) Waiting to run
Docker Build, Publish & Test / build-and-push (push) Waiting to run
Docker Build, Publish & Test / Security Scan PR Image (push) Blocked by required conditions
Quality Checks / Auth Route Protection Contract (push) Waiting to run
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Waiting to run
Quality Checks / Backend (Go) (push) Waiting to run
Quality Checks / Frontend (React) (push) Waiting to run
Rate Limit integration / Rate Limiting Integration (push) Waiting to run
Security Scan (PR) / Trivy Binary Scan (push) Waiting to run
Supply Chain Verification (PR) / Verify Supply Chain (push) Waiting to run
WAF integration / Coraza WAF Integration (push) Waiting to run
changed perms
2026-04-22 18:19:14 +00:00

471 lines
13 KiB
Markdown
Executable File

# Charon E2E Test Suite
**Playwright-based end-to-end tests for the Charon management interface.**
Quick Links:
- 📖 [Complete Testing Documentation](../docs/testing/)
- 📝 [E2E Test Writing Guide](../docs/testing/e2e-test-writing-guide.md)
- 🐛 [Debugging Guide](../docs/testing/debugging-guide.md)
---
## Running Tests
```bash
# All tests (Chromium only)
npm run e2e
# All browsers (Chromium, Firefox, WebKit)
npm run e2e:all
# Headed mode (visible browser)
npm run e2e:headed
# Single test file
npx playwright test tests/settings/system-settings.spec.ts
# Specific test by name
npx playwright test --grep "Enable Cerberus"
# Debug mode with inspector
npx playwright test --debug
# Generate code (record interactions)
npx playwright codegen http://localhost:8080
```
---
## Project Structure
```
tests/
├── core/ # Core application tests
├── dns-provider-crud.spec.ts # DNS provider CRUD tests
├── dns-provider-types.spec.ts # DNS provider type-specific tests
├── emergency-server/ # Emergency API tests
├── manual-dns-provider.spec.ts # Manual DNS provider tests
├── monitoring/ # Uptime monitoring tests
├── security/ # Security dashboard tests
├── security-enforcement/ # ACL, WAF, Rate Limiting enforcement tests
├── settings/ # Settings page tests
│ └── system-settings.spec.ts # Feature flag toggle tests
├── tasks/ # Async task tests
├── utils/ # Test helper utilities
│ ├── debug-logger.ts # Structured logging
│ ├── test-steps.ts # Step and assertion helpers
│ ├── ui-helpers.ts # UI interaction helpers (switches, toasts, forms)
│ └── wait-helpers.ts # Wait/polling utilities (feature flags, API)
├── fixtures/ # Shared test fixtures
├── reporters/ # Custom Playwright reporters
├── auth.setup.ts # Global authentication setup
└── global-setup.ts # Global test initialization
```
---
## Available Helper Functions
### UI Interaction Helpers (`utils/ui-helpers.ts`)
#### Switch/Toggle Components
```typescript
import { clickSwitch, expectSwitchState, toggleSwitch } from './utils/ui-helpers';
// Click a switch reliably (handles hidden input pattern)
await clickSwitch(page.getByRole('switch', { name: /cerberus/i }));
// Assert switch state
await expectSwitchState(switchLocator, true); // Checked
await expectSwitchState(switchLocator, false); // Unchecked
// Toggle and get new state
const newState = await toggleSwitch(switchLocator);
console.log(`Switch is now ${newState ? 'enabled' : 'disabled'}`);
```
**Why**: Switch components use a hidden `<input>` with styled siblings. Direct clicks fail in WebKit/Firefox.
#### Cross-Browser Form Field Locators (Phase 2)
```typescript
import { getFormFieldByLabel } from './utils/ui-helpers';
// Basic usage
const nameInput = getFormFieldByLabel(page, /name/i);
await nameInput.fill('John Doe');
// With fallbacks for robust cross-browser support
const scriptPath = getFormFieldByLabel(
page,
/script.*path/i,
{
placeholder: /dns-challenge\.sh/i,
fieldId: 'field-script_path'
}
);
await scriptPath.fill('/usr/local/bin/dns-challenge.sh');
```
**Why**: Browsers handle label association differently. This helper provides 4-tier fallback:
1. `getByLabel()` — Standard label association
2. `getByPlaceholder()` — Fallback to placeholder text
3. `locator('#id')` — Fallback to direct ID
4. `getByRole()` with proximity — Fallback to role + nearby label text
**Impact**: Prevents timeout errors in Firefox/WebKit.
#### Toast Notifications
```typescript
import { waitForToast, getToastLocator } from './utils/ui-helpers';
// Wait for toast with text
await waitForToast(page, /success/i, { type: 'success', timeout: 5000 });
// Get toast locator for custom assertions
const toast = getToastLocator(page, /error/i, { type: 'error' });
await expect(toast).toBeVisible();
```
### Wait/Polling Helpers (`utils/wait-helpers.ts`)
#### Feature Flag Propagation (Phase 2 Optimized)
```typescript
import { waitForFeatureFlagPropagation } from './utils/wait-helpers';
// Wait for feature flag to propagate after toggle
await clickSwitch(cerberusToggle);
await waitForFeatureFlagPropagation(page, {
'cerberus.enabled': true
});
// Wait for multiple flags
await waitForFeatureFlagPropagation(page, {
'cerberus.enabled': false,
'crowdsec.enabled': false
}, { timeout: 60000 });
```
**Performance**: Includes conditional skip optimization — exits immediately if flags already match.
#### API Responses
```typescript
import { clickAndWaitForResponse, waitForAPIResponse } from './utils/wait-helpers';
// Click and wait for response atomically (prevents race conditions)
const response = await clickAndWaitForResponse(
page,
saveButton,
/\/api\/v1\/proxy-hosts/,
{ status: 200 }
);
expect(response.ok()).toBeTruthy();
// Wait for response without interaction
const response = await waitForAPIResponse(page, /\/api\/v1\/feature-flags/, {
status: 200,
timeout: 10000
});
```
#### Retry with Exponential Backoff
```typescript
import { retryAction } from './utils/wait-helpers';
// Retry action with backoff (2s, 4s, 8s)
await retryAction(async () => {
await clickSwitch(toggle);
await waitForFeatureFlagPropagation(page, { 'flag': true });
}, { maxAttempts: 3, baseDelay: 2000 });
```
#### Other Wait Utilities
```typescript
// Wait for loading to complete
await waitForLoadingComplete(page, { timeout: 10000 });
// Wait for modal dialog
const modal = await waitForModal(page, /edit.*host/i);
await modal.getByLabel(/domain/i).fill('example.com');
// Wait for table rows
await waitForTableLoad(page, 'table', { minRows: 5 });
// Wait for WebSocket connection
await waitForWebSocketConnection(page, /\/ws\/logs/);
```
### Debug Helpers (`utils/debug-logger.ts`)
```typescript
import { DebugLogger } from './utils/debug-logger';
const logger = new DebugLogger('test-name');
logger.step('Navigate to settings');
logger.network({ method: 'GET', url: '/api/v1/feature-flags', status: 200, elapsedMs: 123 });
logger.assertion('Cerberus toggle is visible', true);
logger.error('Failed to load settings', new Error('Network timeout'));
```
---
## Performance Best Practices (Phase 2)
### 1. Only Poll When State Changes
**Before (Inefficient)**:
```typescript
test.beforeEach(async ({ page }) => {
// Polls even if flags already correct
await waitForFeatureFlagPropagation(page, { 'cerberus.enabled': false });
});
test('Test', async ({ page }) => {
await clickSwitch(toggle);
await waitForFeatureFlagPropagation(page, { 'cerberus.enabled': true });
});
```
**After (Optimized)**:
```typescript
test.afterEach(async ({ request }) => {
// Restore defaults once at end
await request.post('/api/v1/settings/restore', {
data: { module: 'system', defaults: true }
});
});
test('Test', async ({ page }) => {
// Test starts from defaults (no beforeEach poll needed)
await clickSwitch(toggle);
// Only poll when state changes
await waitForFeatureFlagPropagation(page, { 'cerberus.enabled': true });
});
```
**Impact**: Removed ~90% of unnecessary API calls.
### 2. Use Conditional Skip Optimization
The helper automatically checks if flags are already in the expected state:
```typescript
// If flags match, exits immediately (no polling)
await waitForFeatureFlagPropagation(page, { 'cerberus.enabled': false });
// Console: "[POLL] Already in expected state - skipping poll"
```
**Impact**: ~50% reduction in polling iterations.
### 3. Request Coalescing for Parallel Workers
Tests running in parallel share in-flight requests:
```typescript
// Worker 0: Waits for {cerberus: false}
// Worker 1: Waits for {cerberus: false}
// Result: 1 polling loop per worker (cached promise), not 2 separate loops
```
**Cache Key Format**: `[worker_index]:[sorted_flags_json]`
---
## Test Isolation Pattern
Always clean up in `afterEach`, not `beforeEach`:
```typescript
test.describe('Feature', () => {
test.afterEach(async ({ request }) => {
// Restore defaults after each test
await request.post('/api/v1/settings/restore', {
data: { module: 'system', defaults: true }
});
});
test('Test A', async ({ page }) => {
// Starts from defaults (restored by previous test)
// ...test logic...
// Cleanup happens in afterEach
});
test('Test B', async ({ page }) => {
// Also starts from defaults
});
});
```
**Why**: Prevents test pollution and ensures each test starts from known state.
---
## Common Patterns
### Toggle Feature Flag
```typescript
test('Enable feature', async ({ page }) => {
const toggle = page.getByRole('switch', { name: /feature/i });
await test.step('Toggle on', async () => {
await clickSwitch(toggle);
await waitForFeatureFlagPropagation(page, { 'feature.enabled': true });
});
await test.step('Verify UI', async () => {
await expectSwitchState(toggle, true);
});
});
```
### Create Resource via API, Verify in UI
```typescript
test('Create proxy host', async ({ page, testData }) => {
await test.step('Create via API', async () => {
const host = await testData.createProxyHost({
domain: 'example.com',
forward_host: '192.168.1.100'
});
});
await test.step('Verify in UI', async () => {
await page.goto('/proxy-hosts');
await waitForResourceInUI(page, 'example.com');
});
});
```
### Wait for Async Task
```typescript
test('Start long task', async ({ page }) => {
await page.getByRole('button', { name: /start/i }).click();
// Wait for progress bar
await waitForProgressComplete(page, { timeout: 30000 });
// Verify completion
await expect(page.getByText(/complete/i)).toBeVisible();
});
```
---
## Cross-Browser Compatibility
| Strategy | Purpose | Supported Browsers |
|----------|---------|-------------------|
| `getFormFieldByLabel()` | Form field location | ✅ Chromium ✅ Firefox ✅ WebKit |
| `clickSwitch()` | Switch interaction | ✅ Chromium ✅ Firefox ✅ WebKit |
| `getByRole()` | Semantic locators | ✅ Chromium ✅ Firefox ✅ WebKit |
**Avoid**:
- CSS selectors (brittle, browser-specific)
- `{ force: true }` clicks (bypasses real user behavior)
- `waitForTimeout()` (non-deterministic)
---
## Troubleshooting
### Test Fails in Firefox/WebKit Only
**Symptom**: `TimeoutError: locator.fill: Timeout exceeded`
**Cause**: Label matching differs between browsers.
**Fix**: Use `getFormFieldByLabel()` with fallbacks:
```typescript
const field = getFormFieldByLabel(page, /field name/i, {
placeholder: /enter value/i
});
```
### Feature Flag Polling Times Out
**Symptom**: `Feature flag propagation timeout after 120 attempts`
**Causes**:
1. Config reload overlay stuck visible
2. Backend not updating flags
3. Database transaction not committed
**Fix**:
1. Check backend logs for PUT `/api/v1/feature-flags` errors
2. Check if overlay is stuck: `page.locator('[data-testid="config-reload-overlay"]').isVisible()`
3. Add retry wrapper:
```typescript
await retryAction(async () => {
await clickSwitch(toggle);
await waitForFeatureFlagPropagation(page, { 'flag': true });
});
```
### Switch Click Intercepted
**Symptom**: `click intercepted by overlay`
**Cause**: Config reload overlay or sticky header blocking interaction.
**Fix**: Use `clickSwitch()` (handles overlay automatically):
```typescript
await clickSwitch(page.getByRole('switch', { name: /feature/i }));
```
---
## Test Execution Metrics (Phase 2)
| Metric | Before Phase 2 | After Phase 2 | Improvement |
|--------|----------------|---------------|-------------|
| System Settings Tests | 23 minutes | 16 minutes | 31% faster |
| Feature Flag API Calls | ~300 calls | ~30 calls | 90% reduction |
| Polling Iterations (avg) | 60 per test | 30 per test | 50% reduction |
| Cross-Browser Pass Rate | 96% (Firefox flaky) | 100% (all browsers) | +4% |
---
## Documentation
- **[Testing README](../docs/testing/README.md)** — Quick reference, debugging, VS Code tasks
- **[E2E Test Writing Guide](../docs/testing/e2e-test-writing-guide.md)** — Comprehensive best practices
- **[Debugging Guide](../docs/testing/debugging-guide.md)** — Troubleshooting guide
- **[Security Helpers](../docs/testing/security-helpers.md)** — ACL/WAF/CrowdSec test utilities
---
## CI/CD Integration
Tests run on every PR and push:
- **Browsers**: Chromium, Firefox, WebKit
- **Sharding**: 4 parallel workers per browser
- **Artifacts**: Videos (on failure), traces, screenshots, logs
- **Reports**: HTML report, GitHub Job Summary
See [`.github/workflows/playwright.yml`](../.github/workflows/playwright.yml) for full CI configuration.
---
**Questions?** See [docs/testing/](../docs/testing/) or open an issue.