Skip to content

Commit b940ee3

Browse files
committed
yeah
1 parent 10b7a3e commit b940ee3

4 files changed

Lines changed: 99 additions & 18 deletions

File tree

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,7 @@
1-
import { execSync } from 'child_process';
2-
import path from 'path';
3-
4-
const root = path.resolve(import.meta.dirname, '../../..');
5-
1+
// The integration docker stack is brought up by `scripts/dev-integration.mjs`
2+
// (the webServer command) because Playwright starts the webServer before
3+
// globalSetup, so Vite's SSR fetches would race the API container coming up.
4+
// Keep this hook in place for future cross-test setup work.
65
export default function globalSetup() {
7-
try {
8-
execSync('docker compose -f docker-compose.integration.yml up -d --wait', {
9-
cwd: root,
10-
stdio: 'inherit',
11-
});
12-
} catch (err) {
13-
console.error(
14-
'\n[integration] failed to start docker-compose stack - is Docker Desktop running?\n'
15-
);
16-
throw err;
17-
}
6+
// intentionally empty
187
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"test:e2e": "playwright test --config=playwright.e2e.config.ts",
1919
"test:unit": "vitest",
2020
"regen-api": "node scripts/regenerate-api.js",
21-
"dev:integration": "vite dev --mode integration"
21+
"dev:integration": "node scripts/dev-integration.mjs"
2222
},
2323
"devDependencies": {
2424
"@eslint/compat": "^2.0.5",

playwright.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ export default defineConfig({
2424
url: FRONTEND_URL,
2525
reuseExistingServer: !process.env.CI,
2626
ignoreHTTPSErrors: true,
27-
timeout: 120_000,
27+
// Cold CI runs pull docker images and wait for healthchecks before Vite
28+
// starts (see scripts/dev-integration.mjs). 10 minutes covers worst-case
29+
// cold pulls on the GitHub-hosted runner.
30+
timeout: 10 * 60 * 1000,
2831
},
2932
use: {
3033
baseURL: FRONTEND_URL,

scripts/dev-integration.mjs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env node
2+
// Brings up the integration backend (docker-compose) and waits for it to be
3+
// reachable before launching `vite dev --mode integration`. Playwright starts
4+
// the webServer command before globalSetup, so we cannot rely on globalSetup
5+
// to start docker — we have to do it here, otherwise Vite SSR fetches race
6+
// the API container coming up and Playwright times out the webServer probe.
7+
8+
import { spawn, spawnSync } from 'node:child_process';
9+
import { fileURLToPath } from 'node:url';
10+
import path from 'node:path';
11+
import { request as httpsRequest } from 'node:https';
12+
import { request as httpRequest } from 'node:http';
13+
14+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15+
const API_URL = process.env.VITE_API_PROXY_TARGET ?? 'https://localhost:5001';
16+
const TIMEOUT_MS = Number(process.env.INTEGRATION_BACKEND_TIMEOUT_MS ?? 10 * 60 * 1000);
17+
const POLL_INTERVAL_MS = 1500;
18+
19+
function probe(url) {
20+
return new Promise((resolve) => {
21+
const req = (url.startsWith('https:') ? httpsRequest : httpRequest)(
22+
url,
23+
{ method: 'GET', rejectUnauthorized: false, timeout: 2000 },
24+
(res) => {
25+
res.resume();
26+
// Any HTTP response (even 404) means the server is accepting connections.
27+
resolve(true);
28+
}
29+
);
30+
req.on('error', () => resolve(false));
31+
req.on('timeout', () => {
32+
req.destroy();
33+
resolve(false);
34+
});
35+
req.end();
36+
});
37+
}
38+
39+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
40+
41+
async function waitForBackend() {
42+
const deadline = Date.now() + TIMEOUT_MS;
43+
let attempts = 0;
44+
process.stdout.write(`[dev:integration] waiting for backend at ${API_URL} ...\n`);
45+
while (Date.now() < deadline) {
46+
if (await probe(API_URL)) {
47+
process.stdout.write(`[dev:integration] backend reachable after ${attempts} attempt(s)\n`);
48+
return;
49+
}
50+
attempts++;
51+
await sleep(POLL_INTERVAL_MS);
52+
}
53+
process.stderr.write(
54+
`[dev:integration] backend at ${API_URL} did not become reachable within ${TIMEOUT_MS}ms\n`
55+
);
56+
process.exit(1);
57+
}
58+
59+
function startDockerStack() {
60+
process.stdout.write('[dev:integration] starting docker-compose stack ...\n');
61+
const result = spawnSync(
62+
'docker',
63+
['compose', '-f', 'docker-compose.integration.yml', 'up', '-d', '--wait'],
64+
{ cwd: ROOT, stdio: 'inherit' }
65+
);
66+
if (result.status !== 0) {
67+
process.stderr.write(
68+
'[dev:integration] failed to start docker stack — is Docker Desktop running?\n'
69+
);
70+
process.exit(result.status ?? 1);
71+
}
72+
}
73+
74+
startDockerStack();
75+
await waitForBackend();
76+
77+
const child = spawn('pnpm', ['exec', 'vite', 'dev', '--mode', 'integration'], {
78+
stdio: 'inherit',
79+
shell: process.platform === 'win32',
80+
});
81+
82+
child.on('exit', (code, signal) => {
83+
if (signal) process.kill(process.pid, signal);
84+
else process.exit(code ?? 0);
85+
});
86+
87+
for (const sig of ['SIGINT', 'SIGTERM']) {
88+
process.on(sig, () => child.kill(sig));
89+
}

0 commit comments

Comments
 (0)