/** * -------------------------------------------------------------------- * docmd : the zero-config documentation engine. * * Shared test helpers — extracted from `tests/feature-integration.test.js` so the * categorised tests under `tests/` can share a single source of truth * for the project-fixture utilities (setup, writeFile, build, * readSite, exitCodeOf, assert). * * Each test file in `tests/` calls `runTestFile({ name, run })` from * the runner — the runner owns the pass/fail counter, the TUI section * output, and the per-file result aggregation. This file owns ONLY * the stateless helpers. * * Pattern: * import { setup, writeFile, assert, build, readSite, exitCodeOf, * runTestFile, DOCMD } from '../shared.js'; * * runTestFile({ * name: 'Exit-code contract', * emoji: '🚦', * run: ({ assert, setup, writeFile, exitCodeOf }) => { * // ... assertions ... * } * }); * -------------------------------------------------------------------- */ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; export const DOCMD = path.resolve( import.meta.dirname, '..', 'packages/core/dist/bin/docmd.js' ); export const TEST_ROOT = '/tmp/docmd-brute-tests'; export const PASS = '✅'; export const FAIL = '❌'; /** * Create an empty test directory under TEST_ROOT and return its path. * The directory is wiped clean on every call (no test-pollution). */ export function setup(name) { const dir = path.join(TEST_ROOT, name); fs.rmSync(dir, { recursive: true, force: true }); fs.mkdirSync(dir, { recursive: true }); return dir; } /** * Run `node ${DOCMD} build` in `dir`. Returns `{ ok, output }` where * `ok` is true on exit 0 and `output` is the captured stdout/stderr. * Set `expectFail = true` to invert the meaning of `ok` (the caller * is testing a documented failure path). */ export function build(dir, expectFail = false) { try { const out = execSync(`node ${DOCMD} build`, { cwd: dir, stdio: 'pipe', encoding: 'utf8' }); if (expectFail) return { ok: false, output: out }; return { ok: true, output: out }; } catch (e) { if (expectFail) return { ok: true, output: e.stderr || e.stdout || '' }; return { ok: false, output: e.stderr || e.stdout || '' }; } } /** * Run a command in `dir` and return its numeric exit code. * 0 = success, 1+ = documented failure, -1 = killed by signal. */ export function exitCodeOf(cmd, cwd) { try { execSync(cmd, { cwd, stdio: 'pipe' }); return 0; } catch (e) { return e.status == null ? -1 : e.status; } } /** * Write `content` to `/`. Parent directories are * created automatically. */ export function writeFile(dir, filePath, content) { const full = path.join(dir, filePath); fs.mkdirSync(path.dirname(full), { recursive: true }); fs.writeFileSync(full, content); } /** * Read `/site/`. Returns `null` if the file does * not exist (test fixtures use this to assert "build did not * produce output X"). */ export function readSite(dir, filePath) { const full = path.join(dir, 'site', filePath); if (!fs.existsSync(full)) return null; return fs.readFileSync(full, 'utf8'); } /** * `true` if `/site/` exists. Convenience wrapper * for the common "did the build produce this asset?" check. */ export function siteExists(dir, filePath) { return fs.existsSync(path.join(dir, 'site', filePath)); } /** * Run a test file's `run` callback inside a tracked TUI section. * The runner reads the per-file results and aggregates them into * the global pass/fail counts. * * @param {object} opts * @param {string} opts.name — section title (e.g. "Exit-code contract") * @param {string} opts.emoji — single emoji (e.g. "🚦") * @param {Function} opts.run — `({ assert }) => void | Promise` * Receives the runner's assert helper so * the test's pass/fail count feeds into * the global counter. */ export function runTestFile(opts) { if (typeof opts !== 'object' || !opts || typeof opts.run !== 'function') { throw new TypeError('runTestFile({ name, emoji, run }) — run must be a function'); } return { name: opts.name, emoji: opts.emoji || '📦', run: opts.run }; }