- 핵심 규칙 6: 완료의 정의는 게이트 통과. 사용자를 QA로 쓰지 않는다 - 5단계 기계 검사 4번째: design-gate 실행 의무화 - references/audit-gate.md: 사고-검사 매핑, SEO/meta 체크리스트, OS/브라우저 특성, 하니스 규칙 - tools/design-gate.mjs: 범용 게이트(메타/SEO·대비·수축·리듬·트랙·스케일×폭 + 옵션 L0/L3/L4, checks 깊은 병합) - 리듬·트랙 불변식: 숫자 라벨 등폭·빈 셀 트랙 균일·등간격 — 시간표 자동배치 결함 재현 픽스처 검출, 앱 15뷰 통과(오탐 0) feat(site): 관리 앱 2종 프로덕션 콘솔(v6→v18) - 가온 학적부 9뷰·두레 수강신청 6뷰: shadcn 문법, Pretendard/Noto Serif/IBM Plex 폰트 전략, 볼드 금지(400/500/600), WCAG AA 대비 전면 교정, 한글 keep-all 조판 - LMS 필수 요소(알림 센터·공지·진도·평가 유형·출결 사유·학점 경고), 12명 기준 데이터 정합, SEO 구조(h1 유일·OG/twitter·og.png) - 시간표 자동배치 결함 수리(명시적 격자 좌표) - QA 게이트: L0 stylelint/html-validate · L1 단위 30 · L2 감사 61+불변식 · L3 시각회귀 30화면 · L4 WebKit · L5 키보드 탐색 — npm run verify 실패 시 배포 금지 체인
This commit is contained in:
parent
78ddc8b61a
commit
72337b7ee0
226 changed files with 18212 additions and 380 deletions
82
apps/site/tools/webkit.mjs
Normal file
82
apps/site/tools/webkit.mjs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// L4 — WebKit(사파리 엔진) 크로스 브라우저 검증.
|
||||
// 핵심 불변식(h1 줄수·브랜드 1줄·수축 탐지·콘텐츠 정합·오버플로)을 WebKit 에서 재실행한다.
|
||||
import { webkit } from "playwright";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
|
||||
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(here, "../public/work");
|
||||
const results = [];
|
||||
const pass = (name, detail = "") => results.push({ ok: true, name, detail });
|
||||
const fail = (name, detail = "") => results.push({ ok: false, name, detail });
|
||||
|
||||
const APPS = {
|
||||
"gaon-lms": ["dashboard", "students", "attendance", "grades", "notice"],
|
||||
"dure-enrollment": ["catalog", "starred", "enrollments", "credits"],
|
||||
};
|
||||
|
||||
const UTILS = `
|
||||
window.__lines = (sel) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return -1;
|
||||
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 ls = [];
|
||||
for (const t of tops) if (!ls.length || t - ls[ls.length - 1] > 5) ls.push(t);
|
||||
return ls.length;
|
||||
};
|
||||
`;
|
||||
|
||||
const browser = await webkit.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
|
||||
for (const [app, views] of Object.entries(APPS)) {
|
||||
const appUrl = url.pathToFileURL(path.join(ROOT, app, "index.html")).href;
|
||||
await page.goto(appUrl, { waitUntil: "networkidle" });
|
||||
await page.evaluate(UTILS);
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
|
||||
// 브랜드 1줄 + h1 1줄 + 페이지 오버플로
|
||||
const brand = await page.evaluate(`__lines('.brand')`);
|
||||
if (brand > 1) fail(`${app} WebKit 브랜드 세로`, brand + "줄"); else pass(`${app} WebKit 브랜드`);
|
||||
const h1 = await page.evaluate(`__lines('h1')`);
|
||||
if (h1 > 1) fail(`${app} WebKit h1 여러 줄`, h1 + "줄"); else pass(`${app} WebKit h1`);
|
||||
|
||||
for (const v of views) {
|
||||
await page.click(`[data-view="${v}"]`);
|
||||
await page.waitForTimeout(120);
|
||||
const over = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||||
if (over > 0) fail(`${app}/${v} WebKit 오버플로`, `+${over}px`);
|
||||
const vh1 = await page.evaluate(`__lines('.view.is-on h1, h1')`);
|
||||
if (vh1 > 1) fail(`${app}/${v} WebKit h1`, vh1 + "줄");
|
||||
}
|
||||
pass(`${app} WebKit 전 뷰 오버플로·h1`);
|
||||
|
||||
// 콘텐츠 정합 (JS 계산 동일성)
|
||||
const cons = await page.evaluate(() => {
|
||||
const out = [];
|
||||
if (window.GAON && document.getElementById("rep-stats")) {
|
||||
const s = GAON.rosterStats(STUDENTS);
|
||||
const txt = document.getElementById("rep-stats").textContent;
|
||||
if (!txt.includes(s.active + "명")) out.push("재학 " + s.active);
|
||||
if (!txt.includes(s.avgAtt + "%")) out.push("평균 " + s.avgAtt);
|
||||
}
|
||||
if (window.DURE && document.getElementById("en-credits")) {
|
||||
const list = [...enrolled].map((id) => COURSES.find((c) => c.id === id));
|
||||
const want = String(DURE.credits(list));
|
||||
const got = document.getElementById("en-credits").textContent.trim();
|
||||
if (got !== want) out.push("학점 " + got + "≠" + want);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
if (cons.length) fail(`${app} WebKit 콘텐츠 정합`, cons.join(",")); else pass(`${app} WebKit 콘텐츠 정합`);
|
||||
}
|
||||
await browser.close();
|
||||
|
||||
const fails = results.filter((r) => !r.ok).length;
|
||||
for (const r of results) console.log((r.ok ? "PASS" : "FAIL") + " " + r.name + (r.detail ? ` — ${r.detail}` : ""));
|
||||
console.log(fails ? `\nWEBKIT: ${fails} FAIL` : "\nWEBKIT ALL PASS");
|
||||
process.exit(fails ? 1 : 0);
|
||||
Loading…
Add table
Add a link
Reference in a new issue