| 1 |
#!/usr/bin/env node |
| 2 |
import { spawn } from 'node:child_process'; |
| 3 |
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; |
| 4 |
import { dirname, join, relative, resolve, sep } from 'node:path'; |
| 5 |
import { fileURLToPath } from 'node:url'; |
| 6 |
|
| 7 |
const __filename = fileURLToPath(import.meta.url); |
| 8 |
const ROOT = resolve(dirname(__filename), '..', '..', '..'); |
| 9 |
const TEST_DIR = resolve(ROOT, 'tests/playwright'); |
| 10 |
const MAX_ATTEMPTS = Number(process.env.MAX_ATTEMPTS ?? 2); |
| 11 |
|
| 12 |
const walkSpecs = (dir) => { |
| 13 |
const out = []; |
| 14 |
for (const entry of readdirSync(dir)) { |
| 15 |
const full = join(dir, entry); |
| 16 |
const stat = statSync(full); |
| 17 |
if (stat.isDirectory()) { |
| 18 |
out.push(...walkSpecs(full)); |
| 19 |
continue; |
| 20 |
} |
| 21 |
if (entry.endsWith('.spec.ts')) out.push(full); |
| 22 |
} |
| 23 |
return out; |
| 24 |
}; |
| 25 |
|
| 26 |
const hasBlueprint = (specPath) => { |
| 27 |
let dir = dirname(specPath); |
| 28 |
while (dir.startsWith(TEST_DIR) || dir === TEST_DIR) { |
| 29 |
if (existsSync(join(dir, 'blueprint.json'))) return true; |
| 30 |
if (dir === TEST_DIR) return false; |
| 31 |
dir = dirname(dir); |
| 32 |
} |
| 33 |
return false; |
| 34 |
}; |
| 35 |
|
| 36 |
const projectName = (specPath) => |
| 37 |
relative(TEST_DIR, specPath) |
| 38 |
.replace(/\.spec\.ts$/, '') |
| 39 |
.split(sep) |
| 40 |
.join('/'); |
| 41 |
|
| 42 |
// First-line `// e2e:disabled` marker — see ./discover-playwright-projects.mjs. |
| 43 |
const isDisabled = (spec) => |
| 44 |
readFileSync(spec, 'utf8').split('\n', 1)[0].includes('e2e:disabled'); |
| 45 |
|
| 46 |
const projects = existsSync(TEST_DIR) |
| 47 |
? walkSpecs(TEST_DIR) |
| 48 |
.filter(hasBlueprint) |
| 49 |
.filter((s) => !isDisabled(s)) |
| 50 |
.map(projectName) |
| 51 |
: []; |
| 52 |
|
| 53 |
const explicit = process.env.RUN_PROJECT; |
| 54 |
const toRun = explicit ? [explicit] : projects; |
| 55 |
|
| 56 |
if (toRun.length === 0) { |
| 57 |
console.error('No Playwright projects discovered under tests/playwright/'); |
| 58 |
process.exit(1); |
| 59 |
} |
| 60 |
|
| 61 |
const runOnce = (project) => |
| 62 |
new Promise((resolveFn) => { |
| 63 |
const child = spawn('npx', ['playwright', 'test', `--project=${project}`], { |
| 64 |
stdio: 'inherit', |
| 65 |
cwd: ROOT, |
| 66 |
env: { ...process.env, RUN_PROJECT: project }, |
| 67 |
}); |
| 68 |
child.on('exit', (code) => resolveFn(code ?? 1)); |
| 69 |
}); |
| 70 |
|
| 71 |
const failures = []; |
| 72 |
for (const project of toRun) { |
| 73 |
let code = 1; |
| 74 |
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { |
| 75 |
console.log(`\n→ ${project} (attempt ${attempt}/${MAX_ATTEMPTS})`); |
| 76 |
code = await runOnce(project); |
| 77 |
if (code === 0) break; |
| 78 |
} |
| 79 |
if (code !== 0) failures.push(project); |
| 80 |
} |
| 81 |
|
| 82 |
if (failures.length) { |
| 83 |
console.error(`\nFailed projects: ${failures.join(', ')}`); |
| 84 |
process.exit(1); |
| 85 |
} |
| 86 |
|