import fs from "node:fs"; import path from "node:path"; import fg from "fast-glob"; import type { CheckError, SyncCheckResult } from "../../lib/sync-check/shared"; import { appE2eCategory } from "./categories"; /** * E2E sync check. * * Each business flow (`docs/business-flow//README.md`) must have a matching * spec file (`frontend/e2e/tests/.spec.ts`), and each spec file must match a * business flow. Spec content is not inspected — whether the spec holds a journey * that covers the flow's `## Flow Diagram` main path is a review concern. */ export async function runE2eSyncCheck(appRoot: string, cwd: string): Promise { const errors: CheckError[] = []; const config = appE2eCategory(appRoot); const flowReadmes = await fg(config.docPattern, { cwd }); const flowNames = new Set(flowReadmes.map((p) => path.basename(path.dirname(p)))); for (const readmePath of flowReadmes) { const flow = path.basename(path.dirname(readmePath)); const specPath = `${appRoot}/${config.testDir}/${flow}.spec.ts`; if (!fs.existsSync(path.join(cwd, specPath))) { errors.push({ type: "missing-test-file", category: config.name, docPath: readmePath, sourcePath: specPath, expectedBasename: `${flow}.spec.ts`, }); } } // A spec is valid only directly at tests/.spec.ts. Playwright still runs // nested specs, so anything in a subdirectory is an orphan regardless of name. const allSpecFiles = await fg(`${appRoot}/${config.testDir}/**/*.spec.ts`, { cwd }); for (const specFile of allSpecFiles) { const rel = path.posix.relative(`${appRoot}/${config.testDir}`, specFile); const base = path.basename(specFile, ".spec.ts"); if (rel.includes("/") || !flowNames.has(base)) { errors.push({ type: "orphaned-spec", category: config.name, sourcePath: specFile, expectedBasename: path.basename(specFile), }); } } return { errors, filesChecked: flowReadmes.length + allSpecFiles.length }; }