diff --git a/.github/workflows/gated-pipeline.yml b/.github/workflows/gated-pipeline.yml index aeaa51ff0..7be65a4fb 100644 --- a/.github/workflows/gated-pipeline.yml +++ b/.github/workflows/gated-pipeline.yml @@ -777,6 +777,7 @@ jobs: else echo "::warning::No playwright.config.ts found — E2E tests not configured" fi + timeout-minutes: 10 - name: Upload test results if: always() diff --git a/e2e/deployment-smoke.spec.ts b/e2e/deployment-smoke.spec.ts index 587aa072f..f27da07b1 100644 --- a/e2e/deployment-smoke.spec.ts +++ b/e2e/deployment-smoke.spec.ts @@ -4,10 +4,10 @@ import { test, expect } from '@playwright/test' * Deployment smoke tests — validates all services are reachable * through the nginx gateway on port 80. * - * These tests require the full Docker stack to be running. - * They are NOT run in CI (excluded via playwright.config.ts testIgnore). + * CI: The smoke stack (docker-compose.smoke.yml) provides nginx + admin tools. + * Playwright dev servers bind to 0.0.0.0 so nginx can proxy via host.docker.internal. * - * To run locally: + * Local: * cd deployment && docker compose -f docker-compose.stack.yml up -d * PLAYWRIGHT_BASE_URL=http://localhost/workflowui/ npx playwright test deployment-smoke */ diff --git a/e2e/global.setup.ts b/e2e/global.setup.ts index b1343130a..0e2b1826f 100644 --- a/e2e/global.setup.ts +++ b/e2e/global.setup.ts @@ -1,30 +1,50 @@ /** * Playwright global setup - * Runs before all tests to seed the database with package data + * + * 1. Starts the smoke-stack Docker containers via Testcontainers + * (nginx gateway + MySQL + MongoDB + Redis + admin tools) + * 2. Seeds the database via the /api/setup endpoint */ +import { DockerComposeEnvironment, Wait } from 'testcontainers' +import { resolve } from 'path' + +let environment: Awaited> | undefined async function globalSetup() { - // Wait a bit for the server to start + // ── 1. Start smoke stack via Testcontainers ────────────────────────────── + console.log('[setup] Starting smoke stack via Testcontainers...') + const composeDir = resolve(__dirname, '../deployment') + + environment = await new DockerComposeEnvironment(composeDir, 'docker-compose.smoke.yml') + .withWaitStrategy('nginx', Wait.forHealthCheck()) + .withWaitStrategy('phpmyadmin', Wait.forHealthCheck()) + .withWaitStrategy('mongo-express', Wait.forHealthCheck()) + .withWaitStrategy('redisinsight', Wait.forHealthCheck()) + .withStartupTimeout(180_000) + .up() + + console.log('[setup] Smoke stack healthy') + + // Store ref for teardown + ;(globalThis as Record).__TESTCONTAINERS_ENV__ = environment + + // ── 2. Wait for dev servers (started by Playwright webServer config) ───── await new Promise(resolve => setTimeout(resolve, 2000)) + // ── 3. Seed database ──────────────────────────────────────────────────── const setupUrl = process.env.PLAYWRIGHT_BASE_URL ? new URL('/api/setup', process.env.PLAYWRIGHT_BASE_URL.replace(/\/workflowui\/?$/, '')).href : 'http://localhost:3000/api/setup' try { - // Seed database with package data - const response = await fetch(setupUrl, { - method: 'POST', - }) - + const response = await fetch(setupUrl, { method: 'POST' }) if (!response.ok) { - console.error('Failed to seed database:', response.status, response.statusText) + console.error('[setup] Failed to seed database:', response.status, response.statusText) } else { - console.log('Database seeded successfully') + console.log('[setup] Database seeded successfully') } } catch (error) { - // Setup endpoint may not exist in all environments (e.g. CI smoke stack) - console.warn('Setup endpoint not available (non-fatal):', (error as Error).message) + console.warn('[setup] Setup endpoint not available (non-fatal):', (error as Error).message) } } diff --git a/e2e/global.teardown.ts b/e2e/global.teardown.ts new file mode 100644 index 000000000..93098cfce --- /dev/null +++ b/e2e/global.teardown.ts @@ -0,0 +1,18 @@ +/** + * Playwright global teardown + * Stops the Testcontainers smoke stack started in global.setup.ts + */ + +async function globalTeardown() { + const environment = (globalThis as Record).__TESTCONTAINERS_ENV__ as + | { down: () => Promise } + | undefined + + if (environment) { + console.log('[teardown] Stopping smoke stack...') + await environment.down() + console.log('[teardown] Smoke stack stopped') + } +} + +export default globalTeardown diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 00bec1550..1cb0f8e40 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -15,13 +15,8 @@ const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:3000/workfl export default defineConfig({ testDir: './', testMatch: '**/*.spec.ts', - /* Exclude smoke/debug/screenshot tests in CI — they require the full Docker - stack on port 80 and are not compatible with the dev-server webServer config */ - testIgnore: process.env.CI ? [ - '**/deployment-smoke.spec.ts', - '**/settings-debug.spec.ts', - '**/screenshot-pastebin.spec.ts', - ] : [], + globalSetup: './global.setup.ts', + globalTeardown: './global.teardown.ts', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, @@ -33,10 +28,13 @@ export default defineConfig({ screenshot: 'only-on-failure', }, - /* Start dev servers automatically when not running against a live Docker stack */ + /* Start dev servers automatically when not running against a live Docker stack. + * In CI the smoke stack's nginx (in Docker) proxies to the host dev servers via + * host.docker.internal. On Linux this resolves to the Docker bridge gateway IP + * (e.g. 172.17.0.1), NOT 127.0.0.1 — so dev servers MUST listen on 0.0.0.0. */ webServer: process.env.PLAYWRIGHT_BASE_URL ? undefined : [ { - command: 'npm run dev -w workflowui', + command: `npm run dev -w workflowui -- --hostname ${process.env.CI ? '0.0.0.0' : 'localhost'}`, url: 'http://localhost:3000/workflowui/', reuseExistingServer: !process.env.CI, timeout: 120_000, @@ -48,7 +46,7 @@ export default defineConfig({ }, }, { - command: 'PORT=3001 npm run dev -w codesnippet', + command: `PORT=3001 npm run dev -w codesnippet -- --hostname ${process.env.CI ? '0.0.0.0' : 'localhost'}`, url: 'http://localhost:3001/pastebin/', reuseExistingServer: !process.env.CI, timeout: 120_000, diff --git a/package.json b/package.json index 219107a43..1bb689b6b 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "eslint-plugin-react-refresh": "^0.5.0", "jest": "^30.2.0", "jsdom": "^28.1.0", - "npm-run-all2": "8.0.4" + "npm-run-all2": "8.0.4", + "testcontainers": "^11.12.0" }, "dependencies": { "@hookform/resolvers": "^5.2.2",