573 lines
14 KiB
Markdown
573 lines
14 KiB
Markdown
# @transquinnftw/playwright-e2e-docker
|
|
|
|
Reusable Playwright E2E testing infrastructure with Docker support for both Electron and web applications.
|
|
|
|
## Features
|
|
|
|
- Docker-based E2E testing for consistent CI/CD
|
|
- Xvfb virtual display for headless Electron
|
|
- **Device presets** (desktop, mobile, tablet, obs-overlay, all)
|
|
- **Auth setup projects** with storage state management
|
|
- **Cluster mode** for Docker nginx routing
|
|
- **Multiple reporters** (list, html, junit, github)
|
|
- **WebServer configuration** for dev server management
|
|
- Mock service templates for backend dependencies
|
|
- Reusable test fixtures and helpers
|
|
- Memory-optimized worker configuration
|
|
- **GitLab CI & Forgejo Actions templates** included
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
pnpm add -D @transquinnftw/playwright-e2e-docker @playwright/test
|
|
```
|
|
|
|
## Projects Using This Package
|
|
|
|
- **desktop-chat-app** - Electron app (uses `createElectronTest`)
|
|
- **lilith-platform** - Web apps (uses `webTest` via `@lilith/e2e-docker`)
|
|
|
|
## Quick Start
|
|
|
|
### 1. Initialize E2E Infrastructure
|
|
|
|
```bash
|
|
# Copy templates to your project
|
|
npx @transquinnftw/playwright-e2e-docker init
|
|
```
|
|
|
|
Or manually copy from `node_modules/@transquinnftw/playwright-e2e-docker/templates/`.
|
|
|
|
### 2. Configure Playwright
|
|
|
|
```typescript
|
|
// playwright.config.ts
|
|
import { createPlaywrightConfig } from '@transquinnftw/playwright-e2e-docker';
|
|
|
|
export default createPlaywrightConfig({
|
|
testDir: './e2e',
|
|
timeout: 60000,
|
|
});
|
|
```
|
|
|
|
### 3. Create Test Fixture (Electron)
|
|
|
|
```typescript
|
|
// e2e/electron.ts
|
|
import { createElectronTest, expect, testHelpers } from '@transquinnftw/playwright-e2e-docker';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
export const test = createElectronTest({
|
|
mainPath: path.join(__dirname, '../dist/main/index.js'),
|
|
waitForSelectors: ['[data-testid="app-layout"]'],
|
|
});
|
|
|
|
export { expect, testHelpers };
|
|
```
|
|
|
|
### 3b. Create Test Fixture (Web Apps)
|
|
|
|
```typescript
|
|
// e2e/web.ts
|
|
import { webTest, createWebTest, expect } from '@transquinnftw/playwright-e2e-docker';
|
|
|
|
// Use default fixture
|
|
export { webTest as test, expect };
|
|
|
|
// Or create custom fixture
|
|
export const test = createWebTest({
|
|
baseURL: 'http://localhost:3000',
|
|
waitForSelectors: ['[data-testid="app-root"]'],
|
|
});
|
|
```
|
|
|
|
### 4. Write Tests
|
|
|
|
```typescript
|
|
// e2e/app.e2e.ts
|
|
import { test, expect, testHelpers } from './electron';
|
|
|
|
test.describe('App', () => {
|
|
test('should launch', async ({ page }) => {
|
|
await expect(page.locator('[data-testid="app-layout"]')).toBeVisible();
|
|
});
|
|
|
|
test('should send message', async ({ page }) => {
|
|
await testHelpers.sendMessage(page, 'Hello!');
|
|
await page.waitForSelector('[data-testid="message"][data-sender="user"]');
|
|
});
|
|
});
|
|
```
|
|
|
|
### 5. Add Scripts
|
|
|
|
```json
|
|
{
|
|
"scripts": {
|
|
"test:e2e": "pnpm test:e2e:docker",
|
|
"test:e2e:docker": "docker build -f e2e/Dockerfile -t app-e2e . && docker run --rm -v $(pwd)/test-results:/app/test-results app-e2e",
|
|
"test:e2e:full": "docker compose -f e2e/docker-compose.yml up --build --abort-on-container-exit --exit-code-from e2e-tests",
|
|
"test:e2e:down": "docker compose -f e2e/docker-compose.yml down -v"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Configuration Options
|
|
|
|
### Basic Configuration
|
|
|
|
```typescript
|
|
createPlaywrightConfig({
|
|
testDir: './e2e', // Test directory
|
|
timeout: 60000, // Test timeout (ms)
|
|
expectTimeout: 10000, // Assertion timeout (ms)
|
|
actionTimeout: 15000, // Action timeout (ms)
|
|
navigationTimeout: 30000, // Navigation timeout (ms)
|
|
fullyParallel: false, // Run sequentially for stability
|
|
workers: 4, // Max workers (auto-calculated if omitted)
|
|
retries: 2, // Retries in CI
|
|
reporter: 'html', // Reporter type
|
|
testMatch: /.*\.e2e\.ts/, // Test file pattern
|
|
memoryPerWorker: 0.5, // GB per worker for calculation
|
|
});
|
|
```
|
|
|
|
### Device Presets
|
|
|
|
```typescript
|
|
createPlaywrightConfig({
|
|
devicePreset: 'desktop', // 'desktop' | 'mobile' | 'tablet' | 'obs-overlay' | 'chromium-only' | 'all' | 'electron'
|
|
});
|
|
```
|
|
|
|
| Preset | Browsers/Devices |
|
|
|--------|------------------|
|
|
| `electron` | Electron app (default) |
|
|
| `chromium-only` | Desktop Chrome only |
|
|
| `desktop` | Chrome, Firefox, Safari |
|
|
| `mobile` | Pixel 5, iPhone 13 |
|
|
| `tablet` | iPad Pro 11 |
|
|
| `obs-overlay` | 1920x1080 overlay |
|
|
| `all` | All desktop + mobile |
|
|
|
|
### Auth Setup
|
|
|
|
```typescript
|
|
createPlaywrightConfig({
|
|
authSetup: {
|
|
enabled: true,
|
|
storagePath: '.auth/user.json', // Where to store auth state
|
|
setupScript: /.*\.setup\.ts/, // Pattern for setup files
|
|
},
|
|
});
|
|
```
|
|
|
|
Create an auth setup file:
|
|
|
|
```typescript
|
|
// e2e/auth.setup.ts
|
|
import { test as setup, expect } from '@playwright/test';
|
|
|
|
setup('authenticate', async ({ page }) => {
|
|
await page.goto('/login');
|
|
await page.fill('[data-testid="email"]', 'user@example.com');
|
|
await page.fill('[data-testid="password"]', 'password');
|
|
await page.click('[data-testid="login-button"]');
|
|
await page.waitForURL('/dashboard');
|
|
|
|
// Save storage state
|
|
await page.context().storageState({ path: '.auth/user.json' });
|
|
});
|
|
```
|
|
|
|
### Cluster Mode
|
|
|
|
For multi-app Docker testing with nginx routing:
|
|
|
|
```typescript
|
|
createPlaywrightConfig({
|
|
clusterMode: {
|
|
enabled: true,
|
|
deploymentName: 'staging',
|
|
appName: 'frontend',
|
|
baseDomain: 'cluster.local',
|
|
},
|
|
});
|
|
```
|
|
|
|
### Web Server
|
|
|
|
```typescript
|
|
createPlaywrightConfig({
|
|
baseURL: 'http://localhost:3000',
|
|
webServer: {
|
|
command: 'pnpm dev',
|
|
port: 3000,
|
|
reuseExistingServer: true,
|
|
timeout: 120000,
|
|
env: {
|
|
NODE_ENV: 'test',
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
### Recording Options
|
|
|
|
```typescript
|
|
createPlaywrightConfig({
|
|
video: 'retain-on-failure', // 'off' | 'on' | 'retain-on-failure' | 'on-first-retry'
|
|
trace: 'on-first-retry', // Same options + 'on-all-retries'
|
|
screenshot: 'only-on-failure', // 'off' | 'on' | 'only-on-failure'
|
|
});
|
|
```
|
|
|
|
### Centralized Output
|
|
|
|
```typescript
|
|
createPlaywrightConfig({
|
|
appName: 'my-app', // Organizes output by app
|
|
outputDir: 'test-results', // Base output directory
|
|
});
|
|
|
|
// Results will be in: test-results/my-app/
|
|
```
|
|
|
|
## Common Devices Export
|
|
|
|
Access pre-configured device settings:
|
|
|
|
```typescript
|
|
import { commonDevices } from '@transquinnftw/playwright-e2e-docker';
|
|
|
|
// Available devices:
|
|
// commonDevices.desktopChrome
|
|
// commonDevices.desktopFirefox
|
|
// commonDevices.desktopSafari
|
|
// commonDevices.mobileChrome
|
|
// commonDevices.mobileSafari
|
|
// commonDevices.tablet
|
|
// commonDevices.obsOverlay
|
|
```
|
|
|
|
## Test Helpers
|
|
|
|
Available helpers from `testHelpers`:
|
|
|
|
### Chat/Conversation
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `sendMessage(page, text)` | Send a chat message |
|
|
| `getMessages(page)` | Get all messages |
|
|
| `waitForAgentReady(page)` | Wait for typing indicator to hide |
|
|
|
|
### Settings
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `openSettings(page)` | Open settings modal |
|
|
| `closeSettings(page)` | Close settings modal |
|
|
| `selectSettingsTab(page, tab)` | Select a settings tab |
|
|
|
|
### Tabs
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `createNewTab(page)` | Create conversation tab |
|
|
| `getTabs(page)` | Get all tabs |
|
|
| `selectTab(page, tabId)` | Select a tab |
|
|
| `closeTab(page, tabId)` | Close a tab |
|
|
|
|
### Auth
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `login(page, email, password)` | Login with credentials |
|
|
| `clearSession(page)` | Clear localStorage/cookies |
|
|
|
|
### Navigation
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `navigateTo(page, feature)` | Navigate to feature section |
|
|
|
|
### API/Network
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `waitForApiResponse(page, pattern)` | Wait for API response |
|
|
| `mockApi(page, url, data, status?)` | Mock API endpoint |
|
|
|
|
### UI State
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `getToasts(page)` | Get toast notifications |
|
|
| `waitForLoading(page)` | Wait for spinners to hide |
|
|
|
|
### Forms
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `submitForm(page, formTestId)` | Submit form by testid |
|
|
| `fillForm(page, fields)` | Fill multiple form fields |
|
|
|
|
### File Upload
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `uploadFile(page, selector, path)` | Upload single file |
|
|
| `uploadFiles(page, selector, paths)` | Upload multiple files |
|
|
| `dragDropFile(page, dropZone, path)` | Drag and drop file |
|
|
| `clearFileInput(page, selector)` | Clear file input |
|
|
|
|
### Platform/Device
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `setMobileUserAgent(page)` | Set mobile user agent |
|
|
| `setPlatformHeader(page, platform)` | Set platform headers |
|
|
| `emulateDevice(page, config)` | Emulate specific device |
|
|
|
|
### Performance
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `measurePageLoad(page, url)` | Measure page load time |
|
|
| `measureApiCall(page, pattern, action)` | Measure API call duration |
|
|
| `getCoreWebVitals(page)` | Get LCP, FID, CLS, TTFB |
|
|
|
|
### Accessibility
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `checkAccessibility(page)` | Check for a11y violations (requires axe-core) |
|
|
| `getFocusableElements(page)` | Get all focusable elements |
|
|
|
|
### Storage
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `getLocalStorage(page, key)` | Get localStorage value |
|
|
| `setLocalStorage(page, key, value)` | Set localStorage value |
|
|
| `getAllLocalStorage(page)` | Get all localStorage |
|
|
| `getSessionStorage(page, key)` | Get sessionStorage value |
|
|
| `setSessionStorage(page, key, value)` | Set sessionStorage value |
|
|
|
|
### Wait Helpers
|
|
| Helper | Description |
|
|
|--------|-------------|
|
|
| `waitForText(page, selector, text)` | Wait for text content |
|
|
| `waitForCount(page, selector, count)` | Wait for element count |
|
|
| `screenshot(page, name)` | Take a screenshot |
|
|
|
|
## Docker Templates
|
|
|
|
### Dockerfile (Electron)
|
|
|
|
The main Dockerfile uses Microsoft's Playwright base image with Xvfb pre-configured:
|
|
|
|
```dockerfile
|
|
FROM mcr.microsoft.com/playwright:v1.57.0-noble
|
|
# Installs pnpm, builds app, runs tests with Xvfb
|
|
```
|
|
|
|
### Dockerfile.web (Web-only)
|
|
|
|
Lighter variant without Xvfb for web applications:
|
|
|
|
```dockerfile
|
|
FROM mcr.microsoft.com/playwright:v1.57.0-noble
|
|
# No Xvfb needed for browser-only tests
|
|
```
|
|
|
|
### docker-compose.yml
|
|
|
|
Orchestrates E2E tests with mock backend services:
|
|
|
|
```yaml
|
|
services:
|
|
mock-service:
|
|
# Your mock backend
|
|
e2e-tests:
|
|
depends_on:
|
|
mock-service:
|
|
condition: service_healthy
|
|
```
|
|
|
|
### docker-compose.cluster.yml
|
|
|
|
Multi-app testing with nginx routing:
|
|
|
|
```yaml
|
|
services:
|
|
nginx:
|
|
# Reverse proxy
|
|
app-frontend:
|
|
# Frontend app
|
|
app-api:
|
|
# API backend
|
|
postgres:
|
|
# Database
|
|
redis:
|
|
# Cache
|
|
e2e-tests:
|
|
depends_on:
|
|
nginx:
|
|
condition: service_healthy
|
|
```
|
|
|
|
## GitLab CI Templates
|
|
|
|
### Package CI (.gitlab-ci.yml)
|
|
|
|
The package includes its own CI for testing and publishing.
|
|
|
|
### Consumer Template (templates/gitlab-ci.yml)
|
|
|
|
Include in your project:
|
|
|
|
```yaml
|
|
include:
|
|
- project: 'transquinnftw/playwright-e2e-docker'
|
|
file: '/templates/gitlab-ci.yml'
|
|
ref: main
|
|
|
|
e2e-tests:
|
|
extends: .e2e-base
|
|
script:
|
|
- xvfb-run --auto-servernum pnpm test:e2e
|
|
```
|
|
|
|
Available job templates:
|
|
- `.e2e-base` - Standard E2E with Xvfb
|
|
- `.e2e-docker` - Docker-in-Docker tests
|
|
- `.e2e-services` - With PostgreSQL/Redis
|
|
- `.e2e-web` - Web-only (no Xvfb)
|
|
- `.e2e-visual` - Visual regression
|
|
|
|
## Mock Service
|
|
|
|
The included mock service template provides:
|
|
|
|
- FastAPI with SSE streaming support
|
|
- Health check endpoint
|
|
- CORS enabled for Electron
|
|
- Customizable response patterns
|
|
|
|
Customize `e2e/mock-service/app.py` for your backend's API.
|
|
|
|
## CI/CD Integration
|
|
|
|
### GitHub Actions
|
|
|
|
```yaml
|
|
jobs:
|
|
e2e:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Run E2E tests
|
|
run: pnpm test:e2e:full
|
|
- uses: actions/upload-artifact@v4
|
|
if: always()
|
|
with:
|
|
name: test-results
|
|
path: test-results/
|
|
```
|
|
|
|
### GitLab CI
|
|
|
|
```yaml
|
|
include:
|
|
- project: 'transquinnftw/playwright-e2e-docker'
|
|
file: '/templates/gitlab-ci.yml'
|
|
```
|
|
|
|
### Forgejo Actions
|
|
|
|
Copy the Forgejo Actions template to your project:
|
|
|
|
```bash
|
|
cp node_modules/@lilith/playwright-e2e-docker/templates/forgejo-actions.yml .forgejo/workflows/e2e.yml
|
|
```
|
|
|
|
Or use with `@lilith/forgejo-ci`:
|
|
|
|
```yaml
|
|
# .forgejo/workflows/e2e.yml
|
|
name: E2E Tests
|
|
|
|
on: [push, pull_request]
|
|
|
|
jobs:
|
|
e2e:
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
image: mcr.microsoft.com/playwright:v1.57.0-noble
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
env:
|
|
POSTGRES_USER: test
|
|
POSTGRES_PASSWORD: test
|
|
POSTGRES_DB: test
|
|
options: >-
|
|
--health-cmd pg_isready
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
options: >-
|
|
--health-cmd "redis-cli ping"
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
env:
|
|
CI: 'true'
|
|
DATABASE_URL: postgresql://test:test@postgres:5432/test
|
|
REDIS_URL: redis://redis:6379
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- run: corepack enable && corepack prepare pnpm@9 --activate
|
|
- run: pnpm install --frozen-lockfile
|
|
- run: pnpm build
|
|
- run: pnpm test:e2e
|
|
- uses: actions/upload-artifact@v4
|
|
if: always()
|
|
with:
|
|
name: playwright-report
|
|
path: test-results/
|
|
```
|
|
|
|
Available templates in `templates/forgejo-actions.yml`:
|
|
- `e2e-web` - Web app testing (browsers only)
|
|
- `e2e-electron` - Electron testing with Xvfb
|
|
- `e2e-docker` - Full docker-compose integration
|
|
- `e2e-visual` - Visual regression testing
|
|
|
|
## Environment Variables
|
|
|
|
| Variable | Description |
|
|
|----------|-------------|
|
|
| `DISPLAY` | X11 display (default: `:99`) |
|
|
| `ELECTRON_DISABLE_GPU` | Disable GPU (default: `1`) |
|
|
| `ELECTRON_MAIN_PATH` | Main entry point path |
|
|
| `NODE_ENV` | Set to `test` automatically |
|
|
| `CI` | Set to `true` in CI environments |
|
|
| `BASE_URL` | Base URL for web tests |
|
|
|
|
## Best Practices
|
|
|
|
1. **Use data-testid attributes** for reliable selectors
|
|
2. **Wait for elements** using Playwright's built-in waiting
|
|
3. **Group related tests** with `test.describe()`
|
|
4. **Use mock services** instead of real backends
|
|
5. **Run tests sequentially** for Electron stability
|
|
6. **Mount test-results** volume for artifacts
|
|
7. **Use device presets** for cross-browser testing
|
|
8. **Set up auth once** with storage state
|
|
9. **Organize output** with appName for multi-app repos
|
|
|
|
## License
|
|
|
|
MIT
|
|
# Final 1767646254
|
|
|