ed image that includes compiled assets and production dependencies.
// src/components/NotificationPanel.tsx
import { useState, useEffect } from 'react';
interface AlertMessage {
id: string;
type: 'success' | 'warning' | 'error';
content: string;
dismissible?: boolean;
}
interface NotificationPanelProps {
endpoint: string;
pollingInterval?: number;
}
export function NotificationPanel({ endpoint, pollingInterval = 5000 }: NotificationPanelProps) {
const [alerts, setAlerts] = useState<AlertMessage[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchAlerts = async () => {
try {
const response = await fetch(endpoint);
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
setAlerts(data);
} catch (err) {
console.error('Failed to fetch notifications:', err);
} finally {
setIsLoading(false);
}
};
fetchAlerts();
const timer = setInterval(fetchAlerts, pollingInterval);
return () => clearInterval(timer);
}, [endpoint, pollingInterval]);
if (isLoading) return <div data-testid="loading-indicator" aria-live="polite">Fetching updates...</div>;
return (
<section data-testid="notification-panel" role="region" aria-label="System notifications">
{alerts.length === 0 ? (
<p data-testid="empty-state">No new notifications</p>
) : (
<ul>
{alerts.map((alert) => (
<li key={alert.id} data-testid={`alert-${alert.type}`}>
<span className={`badge ${alert.type}`}>{alert.type}</span>
<span>{alert.content}</span>
</li>
))}
</ul>
)}
</section>
);
}
Use official browser automation images that bundle system dependencies and browser binaries. This eliminates the need to install Chromium, Firefox, or WebKit manually on CI runners or developer machines.
# docker-compose.validation.yml
services:
frontend-runtime:
build:
context: .
dockerfile: Dockerfile.app
ports:
- "8080:8080"
environment:
NODE_ENV: integration
API_BASE_URL: http://mock-api:3000
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"]
interval: 4s
timeout: 2s
retries: 8
start_period: 10s
qa-runner:
image: mcr.microsoft.com/playwright:v1.56.1-noble
working_dir: /workspace
depends_on:
frontend-runtime:
condition: service_healthy
environment:
PLAYWRIGHT_BASE_URL: http://frontend-runtime:8080
CI: "true"
volumes:
- ./tests/e2e:/workspace/tests
- ./playwright.config.ts:/workspace/playwright.config.ts
command: npx playwright test --reporter=list
Step 3: Implement Synchronization & Network Routing
Docker Compose creates an internal DNS network. Services communicate using container names, not localhost. The healthcheck ensures the frontend server is actively accepting HTTP connections before the test runner executes. depends_on with condition: service_healthy guarantees startup order and readiness.
Step 4: Write Targeted E2E Specifications
Focus tests on user outcomes, not implementation details. Validate rendering, accessibility attributes, network error handling, and responsive behavior.
// tests/e2e/notification-flow.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Notification Panel Integration', () => {
test('renders empty state when no alerts exist', async ({ page }) => {
await page.route('**/api/notifications', (route) =>
route.fulfill({ status: 200, body: '[]' })
);
await page.goto('/dashboard');
const panel = page.getByTestId('notification-panel');
await expect(panel).toBeVisible();
await expect(page.getByTestId('empty-state')).toHaveText('No new notifications');
});
test('handles network failure gracefully', async ({ page }) => {
await page.route('**/api/notifications', (route) =>
route.abort('failed')
);
await page.goto('/dashboard');
await expect(page.getByTestId('loading-indicator')).not.toBeVisible();
await expect(page.getByTestId('notification-panel')).toBeVisible();
});
test('maintains layout integrity on narrow viewports', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/dashboard');
const panel = page.getByTestId('notification-panel');
await expect(panel).toBeInViewport();
await expect(panel.locator('ul')).not.toBeEmpty();
});
});
Architecture Rationale
- Separation of Concerns: The app container handles compilation and serving. The test container handles browser automation. This prevents dependency conflicts and keeps images lean.
- Deterministic Browser Binaries: Official Playwright/Cypress images pin browser versions. This eliminates "works on my machine" discrepancies caused by local browser updates.
- Health-Driven Startup:
depends_on alone only waits for the process to spawn. HTTP health checks verify the server is actually routing requests, preventing race conditions that cause flaky test failures.
- Volume Mounting Strategy: Only test files and configuration are mounted into the runner. Application code is baked into the
frontend-runtime image, ensuring tests run against the exact artifact that will ship to production.
Pitfall Guide
1. Relying on depends_on Without Health Checks
Explanation: Docker Compose starts dependent services immediately after the primary container's process launches, not when it's ready to serve traffic. Frontend dev servers often take 5β15 seconds to compile and bind to ports.
Fix: Always define a healthcheck with wget or curl, and use condition: service_healthy in depends_on.
2. Mounting Host node_modules Into Containers
Explanation: Native binaries compiled for macOS or Windows will crash inside Linux containers. This is a leading cause of silent test failures.
Fix: Use a named volume or Docker layer to install dependencies inside the container. Never bind-mount node_modules from the host.
3. Hardcoding localhost or 127.0.0.1 in Test Configs
Explanation: Inside Docker Compose, localhost refers to the container itself, not the host or other services. Tests will fail to connect to the frontend server.
Fix: Use the service name defined in docker-compose.yml (e.g., http://frontend-runtime:8080) as the base URL.
4. Over-Constraining Resources in Development
Explanation: Applying strict mem_limit or cpus constraints during local development can cause Vite/Webpack to OOM-kill during hot module replacement, creating false negatives.
Fix: Use resource limits only in CI or staging environments. Keep local development unconstrained for faster iteration.
5. Testing Implementation Details Instead of User Flows
Explanation: Asserting on CSS classes, internal state variables, or framework-specific attributes breaks when refactoring. AI-generated code frequently changes structure without changing behavior.
Fix: Use semantic selectors (data-testid, role, aria-label) and assert on visible text, network responses, and accessibility tree states.
6. Ignoring Browser State Persistence Across Runs
Explanation: Containers are ephemeral, but browser storage (localStorage, cookies) can persist if volumes are misconfigured or if tests don't clean up. This causes cascading failures in parallel test suites.
Fix: Use test.use({ storageState: undefined }) or explicitly clear storage in beforeEach hooks. Never mount browser profile directories unless intentionally debugging.
7. Skipping Parallelization in CI
Explanation: Running E2E tests sequentially in CI wastes compute time and delays feedback. AI-generated PRs often touch multiple components, making fast validation critical.
Fix: Configure Playwright/Cypress to shard tests across multiple containers. Use --workers=auto and split specs by route or feature domain.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small team, rapid prototyping | Local Docker Compose + Playwright | Fast feedback loop, minimal CI overhead | Low infrastructure cost |
| Enterprise CI/CD, strict compliance | GitHub Actions + Docker Compose + Cypress | Audit trails, parallel execution, official support | Medium compute cost, high reliability |
| Micro-frontend architecture | Containerized E2E per sub-app + contract tests | Isolates breaking changes, prevents cross-team flakiness | Higher pipeline complexity, lower regression rate |
| Legacy codebase migration | Dockerized E2E + visual regression (Playwright) | Catches layout shifts from AI refactoring, preserves UX | Additional storage for snapshots |
Configuration Template
# docker-compose.ci.yml
services:
app-server:
build:
context: .
dockerfile: Dockerfile.production
ports:
- "3000:3000"
environment:
NODE_ENV: test
PORT: 3000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/healthz"]
interval: 3s
timeout: 2s
retries: 10
start_period: 15s
e2e-runner:
image: mcr.microsoft.com/playwright:v1.56.1-noble
working_dir: /project
depends_on:
app-server:
condition: service_healthy
environment:
PLAYWRIGHT_BASE_URL: http://app-server:3000
CI: "true"
volumes:
- ./tests/integration:/project/tests
- ./playwright.config.ts:/project/playwright.config.ts
command: npx playwright test --reporter=github --shard=${SHARD_INDEX:-1}/${SHARD_TOTAL:-1}
# .github/workflows/validate-ai-changes.yml
name: Containerized E2E Validation
on:
pull_request:
paths:
- 'src/**'
- 'components/**'
- 'pages/**'
- 'docker-compose.ci.yml'
jobs:
validate:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3]
env:
SHARD_INDEX: ${{ matrix.shard }}
SHARD_TOTAL: 3
steps:
- uses: actions/checkout@v4
- name: Build application image
run: docker compose -f docker-compose.ci.yml build app-server
- name: Execute E2E suite
run: docker compose -f docker-compose.ci.yml run --rm e2e-runner
- name: Upload test artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report-shard-${{ matrix.shard }}
path: test-results/
Quick Start Guide
- Create a production-ready Dockerfile: Ensure your frontend builds static assets and serves them via a lightweight HTTP server. Expose port
3000 or 8080.
- Add a health endpoint: Implement a lightweight
/health or /healthz route that returns 200 OK when the server is ready to accept traffic.
- Initialize the compose file: Copy the
docker-compose.ci.yml template, adjust service names and ports to match your stack, and mount your test directory.
- Configure the test runner: Set
PLAYWRIGHT_BASE_URL to http://<app-service-name>:<port>. Ensure CI=true is passed to disable interactive UI and enable headless execution.
- Execute and iterate: Run
docker compose -f docker-compose.ci.yml up --build. Verify tests pass against the containerized app. Integrate the workflow into your PR pipeline to enforce validation before merge.