designpaca/packages/skill/tools/design-gate-contract.test.mjs
Yun Chan 69e5cc1efd
All checks were successful
ci / build (push) Successful in 36s
release / release (push) Successful in 54s
Keep gate copies aligned for release
2026-09-12 15:31:12 +09:00

80 lines
3.7 KiB
JavaScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import url from "node:url";
const repo = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "../../..");
const gate = path.join(repo, "apps/site/tools/design-gate.mjs");
const packagedGate = path.join(repo, "packages/skill/tools/design-gate.mjs");
const work = fs.mkdtempSync(path.join(os.tmpdir(), "design-gate-contract-"));
const normalizedSource = (file) => fs.readFileSync(file, "utf8").replace(/\r\n/g, "\n");
assert.equal(
normalizedSource(gate),
normalizedSource(packagedGate),
"apps/site/tools/design-gate.mjs must match packages/skill/tools/design-gate.mjs",
);
const configFor = (page, extra = {}) => ({
pages: [{ name: "fixture", path: page, h1: "h1", ...extra }],
widths: [375, 390],
fontScales: [1, 1.3],
checks: {
meta: false,
contrast: false,
stack: false,
rhythm: false,
scaleMatrix: true,
tapTargets: false,
leftInset: false,
deadCss: false,
cssHygiene: false,
},
});
const run = (name, html, extra = {}) => {
const page = path.join(work, `${name}.html`);
const config = path.join(work, `${name}.json`);
fs.writeFileSync(page, html.replace("<!doctype html>", "<!doctype html><style>html { overflow-x: clip; } body { margin: 0; }</style>"));
fs.writeFileSync(config, JSON.stringify(configFor(page, extra)));
return spawnSync(process.execPath, [gate, `--config=${config}`], { cwd: work, encoding: "utf8" });
};
const outputOf = (result) => result.stdout + result.stderr;
try {
const flexible = run("flexible", `<!doctype html><style>h1 { width: 9ch; font-size: 2rem; }</style><h1>A flexible display heading</h1>`);
assert.equal(flexible.status, 0, outputOf(flexible));
assert.match(outputOf(flexible), /h1 보임 — [2-9]줄\(줄수 계약 없음\)/);
const capped = run("capped", `<!doctype html><style>h1 { width: 100px; font-size: 2rem; }</style><h1>Heading that must wrap</h1>`, { h1MaxLines: 1 });
assert.equal(capped.status, 1, outputOf(capped));
assert.match(outputOf(capped), /h1 — \d+줄\(최대 1줄\)/);
const expanded = run("expanded", `<!doctype html><style>h1 { width: 260px; font-size: 1.5rem; }</style><h1>Scale tolerant design</h1>`, { h1MaxLines: 1 });
assert.equal(expanded.status, 0, outputOf(expanded));
assert.match(outputOf(expanded), /375@1x:1줄/);
assert.match(outputOf(expanded), /375@1\.3x:[2-9]줄/);
const clipped = run("clipped", `<!doctype html><style>.clip { width: 150px; overflow: hidden; } h1 { white-space: nowrap; font-size: 1.5rem; }</style><div class="clip"><h1>Heading that is clipped when enlarged</h1></div>`);
assert.equal(clipped.status, 1, outputOf(clipped));
assert.match(outputOf(clipped), /제목 텍스트 클리핑/);
const verticallyClipped = run("vertically-clipped", `<!doctype html><style>.clip { width: 300px; height: 32px; overflow-y: hidden; } h1 { margin: 0; font-size: 1.5rem; }</style><div class="clip"><h1>Title that becomes vertically clipped</h1></div>`);
assert.equal(verticallyClipped.status, 1, outputOf(verticallyClipped));
assert.match(outputOf(verticallyClipped), /제목 텍스트 클리핑 .+\(y\)/);
const missing = run("missing", `<!doctype html><h1>Existing heading</h1>`, { h1: ".missing" });
assert.equal(missing.status, 1, outputOf(missing));
assert.match(outputOf(missing), /h1 선택자 없음 \(\.missing\)/);
const empty = run("empty", `<!doctype html><h1></h1>`);
assert.equal(empty.status, 1, outputOf(empty));
assert.match(outputOf(empty), /h1 — 텍스트 없음/);
console.log("design-gate-contract: 7 passed");
} finally {
fs.rmSync(work, { recursive: true, force: true });
}