Keep gate copies aligned for release
This commit is contained in:
parent
45ba92ea82
commit
69e5cc1efd
2 changed files with 86 additions and 14 deletions
|
|
@ -26,7 +26,6 @@ const DEFAULT_CFG = {
|
||||||
viewAttribute: "data-view",
|
viewAttribute: "data-view",
|
||||||
views: ["main"],
|
views: ["main"],
|
||||||
h1: "h1",
|
h1: "h1",
|
||||||
h1MaxLines: 1,
|
|
||||||
brand: ".brand",
|
brand: ".brand",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -233,6 +232,53 @@ const UTILS = `
|
||||||
const text = [...el.childNodes].find((node) => node.nodeType === 3 && node.textContent.trim());
|
const text = [...el.childNodes].find((node) => node.nodeType === 3 && node.textContent.trim());
|
||||||
return window.__lines(text || el);
|
return window.__lines(text || el);
|
||||||
};
|
};
|
||||||
|
window.__heading = (sel) => {
|
||||||
|
let el;
|
||||||
|
try { el = document.querySelector(sel); }
|
||||||
|
catch { return { state: "invalid" }; }
|
||||||
|
if (!el) return { state: "missing" };
|
||||||
|
if (!el.textContent.trim()) return { state: "empty" };
|
||||||
|
const box = el.getBoundingClientRect();
|
||||||
|
const hiddenAncestor = (() => {
|
||||||
|
for (let node = el; node && node !== document.documentElement; node = node.parentElement) {
|
||||||
|
const style = getComputedStyle(node);
|
||||||
|
if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse" || style.contentVisibility === "hidden" || parseFloat(style.opacity) <= 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
if (hiddenAncestor || box.width <= 0 || box.height <= 0) {
|
||||||
|
return { state: "hidden" };
|
||||||
|
}
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(el);
|
||||||
|
const rects = [...range.getClientRects()].filter((r) => r.width > 1 && r.height > 4);
|
||||||
|
const tops = rects.map((r) => r.top).sort((a, b) => a - b);
|
||||||
|
const lines = [];
|
||||||
|
for (const top of tops) if (!lines.length || top - lines[lines.length - 1] > 5) lines.push(top);
|
||||||
|
return { state: "visible", lines: lines.length, rects: rects.map((r) => ({ left: r.left, right: r.right, top: r.top, bottom: r.bottom })) };
|
||||||
|
};
|
||||||
|
window.__headingScaleAudit = (sel) => {
|
||||||
|
const heading = window.__heading(sel);
|
||||||
|
if (heading.state !== "visible") return heading;
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
const clipping = [];
|
||||||
|
const viewportWidth = document.documentElement.clientWidth;
|
||||||
|
for (const rect of heading.rects) {
|
||||||
|
if (rect.left < -1 || rect.right > viewportWidth + 1) clipping.push("viewport");
|
||||||
|
}
|
||||||
|
for (let ancestor = el; ancestor && ancestor !== document.documentElement; ancestor = ancestor.parentElement) {
|
||||||
|
const cs = getComputedStyle(ancestor);
|
||||||
|
const box = ancestor.getBoundingClientRect();
|
||||||
|
const name = ancestor.className ? "." + String(ancestor.className).trim().split(/\\s+/)[0] : ancestor.tagName.toLowerCase();
|
||||||
|
if (/^(hidden|clip|auto|scroll)$/.test(cs.overflowX) && heading.rects.some((rect) => rect.left < box.left - 1 || rect.right > box.right + 1)) clipping.push(name + "(x)");
|
||||||
|
if (/^(hidden|clip|auto|scroll)$/.test(cs.overflowY) && heading.rects.some((rect) => rect.top < box.top - 1 || rect.bottom > box.bottom + 1)) clipping.push(name + "(y)");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...heading,
|
||||||
|
documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||||
|
clipping: [...new Set(clipping)],
|
||||||
|
};
|
||||||
|
};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const browser = await puppeteer.default.launch({ executablePath: CHROME, headless: "new", args: ["--force-device-scale-factor=1"] });
|
const browser = await puppeteer.default.launch({ executablePath: CHROME, headless: "new", args: ["--force-device-scale-factor=1"] });
|
||||||
|
|
@ -326,14 +372,19 @@ for (const p of CFG.pages) {
|
||||||
}
|
}
|
||||||
overs.length ? fail(`${p.name}/${v} 오버플로`, overs.join(" ")) : pass(`${p.name}/${v} 오버플로`, CFG.widths.join("/"));
|
overs.length ? fail(`${p.name}/${v} 오버플로`, overs.join(" ")) : pass(`${p.name}/${v} 오버플로`, CFG.widths.join("/"));
|
||||||
|
|
||||||
// h1 줄 수 — 앱 화면은 기본 1줄, 마케팅 헤드라인은 페이지별로 최대 2줄까지 명시할 수 있다.
|
// 제목은 항상 존재·가시성을 확인하고, 줄 수는 페이지가 명시한 계약일 때만 검사한다.
|
||||||
await page.setViewport({ width: 1440, height: 900 });
|
await page.setViewport({ width: 1440, height: 900 });
|
||||||
const h1l = await page.evaluate(`__lines(${JSON.stringify(p.h1 || "h1")})`);
|
const h1Selector = p.h1 || "h1";
|
||||||
const h1MaxLines = Number.isInteger(p.h1MaxLines) && p.h1MaxLines > 0 ? p.h1MaxLines : 1;
|
const h1 = await page.evaluate((sel) => window.__heading(sel), h1Selector);
|
||||||
const h1Rule = h1MaxLines === 1 ? "h1 1줄" : `h1 최대 ${h1MaxLines}줄`;
|
const h1MaxLines = Number.isInteger(p.h1MaxLines) && p.h1MaxLines > 0 ? p.h1MaxLines : null;
|
||||||
h1l >= 1 && h1l <= h1MaxLines
|
if (h1.state === "missing") fail(`${p.name}/${v} h1`, `선택자 없음 (${h1Selector})`);
|
||||||
? pass(`${p.name}/${v} ${h1Rule}`)
|
else if (h1.state === "empty") fail(`${p.name}/${v} h1`, "텍스트 없음");
|
||||||
: (h1l > h1MaxLines ? fail(`${p.name}/${v} h1 ${h1l}줄(최대 ${h1MaxLines}줄)`) : skip(`${p.name}/${v} h1`, "요소 없음"));
|
else if (h1.state === "hidden") fail(`${p.name}/${v} h1`, `선택자가 숨김 (${h1Selector})`);
|
||||||
|
else if (h1.state === "invalid") fail(`${p.name}/${v} h1`, `잘못된 선택자 (${h1Selector})`);
|
||||||
|
else if (h1.lines < 1) fail(`${p.name}/${v} h1`, "텍스트 없음");
|
||||||
|
else if (h1MaxLines !== null && h1.lines > h1MaxLines) fail(`${p.name}/${v} h1`, `${h1.lines}줄(최대 ${h1MaxLines}줄)`);
|
||||||
|
else if (h1MaxLines !== null) pass(`${p.name}/${v} h1 최대 ${h1MaxLines}줄`, `${h1.lines}줄`);
|
||||||
|
else pass(`${p.name}/${v} h1 보임`, `${h1.lines}줄(줄수 계약 없음)`);
|
||||||
|
|
||||||
// 세로 쌓임(수축) — 짧은 라벨 3줄 이상
|
// 세로 쌓임(수축) — 짧은 라벨 3줄 이상
|
||||||
if (CFG.checks.stack) {
|
if (CFG.checks.stack) {
|
||||||
|
|
@ -430,21 +481,34 @@ for (const p of CFG.pages) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 스케일×폭 h1 행렬 (안드로이드 글꼴 확대 재현) ──
|
// ── 스케일×폭 제목 행렬 (글꼴 확대에서 줄 수 증가와 잘림을 분리한다) ──
|
||||||
if (CFG.checks.scaleMatrix) {
|
if (CFG.checks.scaleMatrix) {
|
||||||
const bad = [];
|
const bad = [];
|
||||||
const h1MaxLines = Number.isInteger(p.h1MaxLines) && p.h1MaxLines > 0 ? p.h1MaxLines : 1;
|
const measurements = [];
|
||||||
for (const w of [375, 390]) {
|
for (const w of [375, 390]) {
|
||||||
for (const s of CFG.fontScales) {
|
for (const s of CFG.fontScales) {
|
||||||
await page.setViewport({ width: w, height: 800, isMobile: true, hasTouch: true });
|
await page.setViewport({ width: w, height: 800, isMobile: true, hasTouch: true });
|
||||||
await page.goto(href, { waitUntil: "networkidle0" });
|
await page.goto(href, { waitUntil: "networkidle0" });
|
||||||
await page.evaluate(UTILS);
|
await page.evaluate(UTILS);
|
||||||
await page.addStyleTag({ content: `html { font-size: ${16 * s}px !important; }` });
|
const baseRootSize = await page.evaluate(() => parseFloat(getComputedStyle(document.documentElement).fontSize));
|
||||||
const l = await page.evaluate(`__lines(${JSON.stringify(p.h1 || "h1")})`);
|
if (!Number.isFinite(baseRootSize) || baseRootSize <= 0) {
|
||||||
if (l > h1MaxLines) bad.push(`${w}@${s}x:${l}줄(최대 ${h1MaxLines}줄)`);
|
bad.push(`${w}@${s}x:root font-size 측정 실패`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await page.addStyleTag({ content: `html { font-size: ${baseRootSize * s}px !important; }` });
|
||||||
|
const h1 = await page.evaluate((sel) => window.__headingScaleAudit(sel), p.h1 || "h1");
|
||||||
|
const point = `${w}@${s}x`;
|
||||||
|
if (h1.state === "missing") { bad.push(`${point}:h1 선택자 없음 (${p.h1 || "h1"})`); continue; }
|
||||||
|
if (h1.state === "empty") { bad.push(`${point}:h1 텍스트 없음`); continue; }
|
||||||
|
if (h1.state === "hidden") { bad.push(`${point}:h1 선택자가 숨김 (${p.h1 || "h1"})`); continue; }
|
||||||
|
if (h1.state === "invalid") { bad.push(`${point}:h1 잘못된 선택자 (${p.h1 || "h1"})`); continue; }
|
||||||
|
if (h1.lines < 1) { bad.push(`${point}:h1 텍스트 없음`); continue; }
|
||||||
|
measurements.push(`${point}:${h1.lines}줄`);
|
||||||
|
if (s > 1 && h1.documentOverflow > 1) bad.push(`${point}:문서 가로오버플로 +${h1.documentOverflow}px`);
|
||||||
|
if (s > 1 && h1.clipping.length) bad.push(`${point}:제목 텍스트 클리핑 ${h1.clipping.join(",")}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bad.length ? fail(`${p.name} 스케일×폭 h1`, bad.join(",")) : pass(`${p.name} 스케일×폭 h1`, `최대 ${h1MaxLines}줄 · scales ${CFG.fontScales.join("/")}`);
|
bad.length ? fail(`${p.name} 스케일×폭 h1`, bad.join(",")) : pass(`${p.name} 스케일×폭 h1`, `줄수 정보 ${measurements.join(", ")}`);
|
||||||
// 브랜드 1줄
|
// 브랜드 1줄
|
||||||
await page.setViewport({ width: 390, height: 844, isMobile: true, hasTouch: true });
|
await page.setViewport({ width: 390, height: 844, isMobile: true, hasTouch: true });
|
||||||
await page.goto(href, { waitUntil: "networkidle0" });
|
await page.goto(href, { waitUntil: "networkidle0" });
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,16 @@ import url from "node:url";
|
||||||
|
|
||||||
const repo = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "../../..");
|
const repo = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "../../..");
|
||||||
const gate = path.join(repo, "apps/site/tools/design-gate.mjs");
|
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 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 = {}) => ({
|
const configFor = (page, extra = {}) => ({
|
||||||
pages: [{ name: "fixture", path: page, h1: "h1", ...extra }],
|
pages: [{ name: "fixture", path: page, h1: "h1", ...extra }],
|
||||||
widths: [375, 390],
|
widths: [375, 390],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue