import { describe, it, expect } from "vitest"; import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; /** * Guards against the "two Zavřít buttons" bug class. * * The shared (src/admin/ui/Modal.tsx) ALWAYS renders a primary submit * button (text = `submitText`) and — unless `hideCancel` is set — a secondary * cancel button (text = `cancelText`, default "Zrušit"). A close-only modal * therefore sets `submitText="Zavřít"`; if it ALSO leaves the cancel button on, * the user sees two buttons that both just close the dialog — and when * `cancelText` is "Zavřít" too, two literally-identical "Zavřít" buttons * (the production bug found on the "paid received invoice" detail modal). * * Rule enforced: any whose `submitText` is a "Zavřít…" label (i.e. its * only action is to close) MUST pass `hideCancel`. Close-only modals get a * single button. See PlanCellModal / PlanCategoriesModal for the correct shape. */ const ADMIN_DIR = join(__dirname, "..", "admin"); function tsxFiles(dir: string): string[] { const out: string[] = []; for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = join(dir, entry.name); if (entry.isDirectory()) out.push(...tsxFiles(full)); else if (entry.name.endsWith(".tsx")) out.push(full); } return out; } /** * Extract every `` opening-tag substring from a source string. * Brace- and string-aware so arrow functions (`=>`) and conditional props * (`{cond ? "a" : "b"}`) inside the tag don't terminate it early. */ function modalOpeningTags(src: string): string[] { const tags: string[] = []; let i = 0; while ((i = src.indexOf(" component, not . if (after && !/[\s>/]/.test(after)) { i += 6; continue; } let depth = 0; let quote: string | null = null; let j = i + 6; for (; j < src.length; j++) { const c = src[j]; if (quote) { if (c === quote && src[j - 1] !== "\\") quote = null; continue; } if (c === '"' || c === "'" || c === "`") quote = c; else if (c === "{") depth++; else if (c === "}") depth--; else if (c === ">" && depth === 0) break; } tags.push(src.slice(i, j + 1)); i = j + 1; } return tags; } /** Value of a JSX prop: `name="..."`, `name={...}` (balanced), or boolean. */ function propValue(tag: string, name: string): string | null { const re = new RegExp(`\\b${name}\\b`); const m = re.exec(tag); if (!m) return null; let k = m.index + m[0].length; while (k < tag.length && /\s/.test(tag[k])) k++; if (tag[k] !== "=") return ""; // boolean prop (e.g. `hideCancel`) k++; while (k < tag.length && /\s/.test(tag[k])) k++; if (tag[k] === '"' || tag[k] === "'") { const q = tag[k]; const end = tag.indexOf(q, k + 1); return tag.slice(k + 1, end); } if (tag[k] === "{") { let depth = 0; for (let p = k; p < tag.length; p++) { if (tag[p] === "{") depth++; else if (tag[p] === "}" && --depth === 0) return tag.slice(k, p + 1); } } return null; } describe("Modal close-only buttons", () => { const offenders: string[] = []; for (const file of tsxFiles(ADMIN_DIR)) { const src = readFileSync(file, "utf8"); for (const tag of modalOpeningTags(src)) { const submit = propValue(tag, "submitText"); const isCloseOnly = submit != null && submit.includes("Zavřít"); const hasHideCancel = propValue(tag, "hideCancel") != null; if (isCloseOnly && !hasHideCancel) { const title = propValue(tag, "title") ?? "(no title)"; offenders.push(`${file.replace(ADMIN_DIR, "admin")} — ${title}`); } } } it("close-only modals (submitText='Zavřít') set hideCancel — no duplicate close button", () => { expect( offenders, `These s render a close-only 'Zavřít' submit alongside a redundant ` + `cancel button (two buttons that both just close). Add hideCancel:\n` + offenders.map((o) => ` • ${o}`).join("\n"), ).toEqual([]); }); });