376 lines
21 KiB
JavaScript
376 lines
21 KiB
JavaScript
// 확장된 두 앱 감사: 신규 뷰 전부 + 기존 기능 회귀 + 오버플로
|
|
import puppeteer from "puppeteer-core";
|
|
import path from "node:path";
|
|
|
|
const root = "D:/workspace/designpaca/apps/site";
|
|
const chrome = "C:/Program Files/Google/Chrome/Application/chrome.exe";
|
|
const results = [];
|
|
const pass = (name, ok, detail = "") =>
|
|
results.push(`${ok ? "PASS" : "FAIL"} ${name}${detail ? ` — ${detail}` : ""}`);
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
const browser = await puppeteer.launch({
|
|
executablePath: chrome,
|
|
headless: "new",
|
|
args: ["--no-sandbox", "--disable-gpu", "--force-device-scale-factor=1"],
|
|
defaultViewport: { width: 1440, height: 900 },
|
|
});
|
|
const page = await browser.newPage();
|
|
page.on("pageerror", (e) => results.push(`FAIL pageerror — ${e.message}`));
|
|
const jclick = (sel) => page.evaluate((s) => { const el = document.querySelector(s); if (!el) throw new Error("no " + s); el.click(); }, sel);
|
|
const over = () => page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
const gotoFile = async (app) =>
|
|
page.goto(`file:///${path.join(root, "public/work", app, "index.html")}`, { waitUntil: "networkidle0" });
|
|
const gotoWorkFile = async (...parts) =>
|
|
page.goto(`file:///${path.join(root, "public/work", ...parts)}`, { waitUntil: "networkidle0" });
|
|
const centeredDialog = (selector) => page.$eval(selector, (dialog) => {
|
|
const rect = dialog.getBoundingClientRect();
|
|
return {
|
|
deltaX: Math.abs(rect.left + rect.width / 2 - innerWidth / 2),
|
|
deltaY: Math.abs(rect.top + rect.height / 2 - innerHeight / 2),
|
|
inViewport: rect.left >= 0 && rect.top >= 0 && rect.right <= innerWidth && rect.bottom <= innerHeight,
|
|
};
|
|
});
|
|
|
|
// ══════════ 가온 학적부 (8 뷰) ══════════
|
|
await gotoFile("gaon-lms");
|
|
|
|
for (const v of ["dashboard","students","attendance","grades","files","calendar","counsel","report","notice"]) {
|
|
await jclick(`[data-view="${v}"]`);
|
|
pass(`가온 뷰 ${v}`, await page.$eval(`#view-${v}`, (el) => el.classList.contains("is-on")));
|
|
}
|
|
pass("가온 aria-current(공지)", await page.$eval('[data-view="notice"]', (el) => el.getAttribute("aria-current") === "page"));
|
|
|
|
// 사이드바 그룹 헤더 3개
|
|
pass("가온 사이드 그룹 3개", (await page.$$eval(".side-group", (n) => n.length)) === 3);
|
|
|
|
// 출석부: 셀 순환 + 주 전환 + 반 전환
|
|
await jclick('[data-view="attendance"]');
|
|
const cellBefore = await page.$eval(".att-btn", (b) => b.textContent);
|
|
await jclick(".att-btn");
|
|
const cellAfter = await page.$eval(".att-btn", (b) => b.textContent);
|
|
pass("가온 출석 순환 ○→△", cellBefore === "○" && cellAfter === "△");
|
|
await jclick("#att-prev");
|
|
pass("가온 주 전환(1주)", (await page.$eval("#att-week", (el) => el.textContent)) === "3월 1주");
|
|
await jclick("#att-next");
|
|
await page.evaluate(() => document.querySelector("#att-class [data-sub='코딩 입문']").click());
|
|
await sleep(150);
|
|
const codingRows = await page.$$eval("#att-table tbody tr", (rows) => rows.map((r) => r.textContent.slice(0, 12)));
|
|
pass("가온 반 전환(코딩 입문 3행, 휴학 포함)", codingRows.length === 3, codingRows.join("|"));
|
|
|
|
// 자료실: 필터 + 배부 토글
|
|
await jclick('[data-view="files"]');
|
|
await page.evaluate(() => document.querySelector('#file-chips [data-kind="PDF"]').click());
|
|
await sleep(150);
|
|
pass("가온 자료 필터 PDF", (await page.$$("#file-body tr")).length === 2);
|
|
const giveBefore = await page.$eval("[data-file]", (b) => b.getAttribute("aria-pressed"));
|
|
await jclick("[data-file]");
|
|
pass("가온 배부 토글", (await page.$eval("[data-file]", (b) => b.getAttribute("aria-pressed"))) !== giveBefore);
|
|
|
|
// 달력: 42칸 + 오늘 1 + 예정 7
|
|
await jclick('[data-view="calendar"]');
|
|
pass("가온 달력 42칸·오늘 1", (await page.$$("#cal-table td")).length === 42 && (await page.$$(".is-today")).length === 1);
|
|
pass("가온 다가오는 일정 7건", (await page.$$("#up-list li")).length === 7);
|
|
|
|
// 상담: 필터 + 카드→학생 모달
|
|
await jclick('[data-view="counsel"]');
|
|
await page.evaluate(() => document.querySelector('#co-chips [data-sub="국어 논술 A"]').click());
|
|
await sleep(150);
|
|
pass("가온 상담 필터(국어 2건)", (await page.$$(".counsel-card")).length === 2);
|
|
await jclick(".counsel-card");
|
|
pass("가온 상담 카드→학생 모달", await page.$eval("#stu-dialog", (d) => d.open));
|
|
await page.keyboard.press("Escape");
|
|
|
|
// 리포트: 4행 + 수학 행 계산값
|
|
await jclick('[data-view="report"]');
|
|
pass("가온 리포트 4행", (await page.$$("#report-table tbody tr")).length === 4);
|
|
const repMath = await page.$eval("#report-table tbody tr", (tr) => tr.textContent.replace(/\s+/g, " "));
|
|
pass("가온 리포트 수학 행 실계산", repMath.includes("3명") && repMath.includes("91%") && repMath.includes("96%") && repMath.includes("84.4점"));
|
|
|
|
// 회귀: 검색/칩/학생 모달/메모/채점
|
|
await jclick('[data-view="students"]');
|
|
await page.type("#q", "박");
|
|
await sleep(200);
|
|
pass("가온 검색 축소(회귀)", (await page.$$("#view-students [data-stu]")).length === 1);
|
|
await page.$eval("#q", (el) => { el.value = ""; el.dispatchEvent(new Event("input", { bubbles: true })); });
|
|
await jclick("#view-students [data-stu]");
|
|
await page.waitForSelector("#stu-dialog[open]");
|
|
await page.type("#note-input", "확정 회귀 검사");
|
|
await jclick("#note-form button[type=submit]");
|
|
await sleep(150);
|
|
pass("가온 메모 추가(회귀)", (await page.$$("#stu-note-list li")).length === 2);
|
|
await jclick("#stu-dialog .modal-close");
|
|
await jclick('[data-view="grades"]');
|
|
await page.evaluate(() => document.querySelector('[data-assign="a2"]').click());
|
|
await page.waitForSelector("#as-dialog[open]");
|
|
await jclick("#as-save");
|
|
await sleep(150);
|
|
pass("가온 채점 상태 갱신(회귀)", (await page.$eval('#as-table .row-open[data-assign="a2"]', (b) => b.closest("tr").querySelector(".state").textContent)) === "채점 완료");
|
|
await page.keyboard.press("Escape");
|
|
pass("가온 1440 오버플로", (await over()) <= 0, `+${await over()}px`);
|
|
|
|
// ── 뷰별 프로덕션 요소 (v10) ──
|
|
await jclick('[data-view="students"]');
|
|
pass("가온 수강생 보호자 열", await page.$eval("#stu-table thead", (t) => t.textContent.includes("보호자")));
|
|
pass("가온 수강생 요약 스트립", (await page.$$eval("#stu-stats > div", (n) => n.length)) === 5);
|
|
await jclick('[data-view="attendance"]');
|
|
pass("가온 출석 일별 합계", (await page.$eval("#att-foot", (f) => f.textContent.includes("/"))));
|
|
await jclick('[data-view="grades"]');
|
|
await page.evaluate(() => document.querySelector('[data-as="과학 실험"]').click());
|
|
pass("가온 성적 과목 필터", (await page.$$eval("#as-table tbody tr:not([hidden])", (n) => n.length)) === 1);
|
|
await page.evaluate(() => document.querySelector('[data-as="all"]').click());
|
|
await jclick('[data-view="calendar"]');
|
|
pass("가온 일정 유형 배지·범례", (await page.$$eval(".cal-ev.is-exam", (n) => n.length)) === 2 && (await page.$$eval(".cal-legend .key", (n) => n.length)) === 4);
|
|
pass("가온 일정 D-day", (await page.$eval("#up-list", (u) => u.textContent.includes("D-"))));
|
|
await jclick('[data-view="counsel"]');
|
|
pass("가온 예정 상담 3건", (await page.$$eval('section[aria-labelledby="co-next-h"] .due-row', (n) => n.length)) === 3);
|
|
await jclick('[data-view="report"]');
|
|
pass("가온 학기 요약 카드", (await page.$$eval("#rep-stats > div", (n) => n.length)) === 4);
|
|
await jclick('[data-view="notice"]');
|
|
await page.evaluate(() => document.querySelector('[data-nt="소통"]').click());
|
|
pass("가온 공지 분류 필터", (await page.$$eval(".notice-row:not([hidden])", (n) => n.length)) === 3);
|
|
await page.evaluate(() => document.querySelector('[data-nt="전체"]').click());
|
|
|
|
// ══════════ 두레대학 (6 탭) ══════════
|
|
await gotoFile("dure-enrollment");
|
|
|
|
for (const v of ["catalog","starred","enrollments","credits","archive","notices"]) {
|
|
await jclick(`.tab[data-view="${v}"]`);
|
|
pass(`두레 탭 ${v}`, await page.$eval(`#view-${v}`, (el) => el.classList.contains("is-on")));
|
|
}
|
|
pass("두레 aria-current(공지)", await page.$eval('.tab[data-view="notices"]', (el) => el.getAttribute("aria-current") === "page"));
|
|
pass("두레 빈 상태 2종", await page.$eval("#en-empty", (el) => !el.hidden));
|
|
|
|
// 관심: 모달에서 남기기 → 관심 뷰 1행
|
|
await jclick('.tab[data-view="catalog"]');
|
|
await jclick('[data-course="phil115"]');
|
|
await jclick("#cd-star");
|
|
await jclick("#course-dialog .modal-close");
|
|
await jclick('.tab[data-view="starred"]');
|
|
pass("두레 관심 1행 + 목록 ★", (await page.$$("#starred-list .course-row")).length === 1 && (await page.$$("#starred-list .c-star")).length === 1);
|
|
|
|
// 신청 → 내역·이수 반영 → 총람 소계 → 공지
|
|
await jclick('.tab[data-view="catalog"]');
|
|
await jclick('[data-course="econ210"]');
|
|
await jclick("#cd-apply");
|
|
await sleep(200);
|
|
pass("두레 신청 블록 2(회귀)", (await page.$$(".tt-block")).length === 2);
|
|
await jclick('.tab[data-view="enrollments"]');
|
|
pass("두레 내역 1행·3학점", (await page.$$("#en-table tbody tr")).length === 1 && (await page.$eval("#en-credits", (el) => el.textContent)) === "3");
|
|
await jclick('.tab[data-view="credits"]');
|
|
const crMajor = await page.$eval("#cr-table tbody tr", (tr) => tr.textContent.replace(/\s+/g, " "));
|
|
pass("두레 이수 전공 반영", crMajor.includes("+ 3") && crMajor.includes("48"), crMajor);
|
|
await jclick('.tab[data-view="archive"]');
|
|
pass("두레 총람 12행(10+소계2)", (await page.$$("#ar-table tbody tr")).length === 12);
|
|
pass("두레 총람 여석 임박 표시", (await page.$$("#ar-table .low-seats")).length >= 1);
|
|
await jclick('.tab[data-view="notices"]');
|
|
pass("두레 공지 6건", (await page.$$("#notice-list li")).length === 6);
|
|
|
|
// 겹침 회귀: bio105 차단
|
|
await jclick('.tab[data-view="catalog"]');
|
|
await jclick('[data-course="econ210"]'); // 이미 신청됨 → 취소 버튼
|
|
await page.waitForSelector("#course-dialog[open]");
|
|
await jclick("#cd-cancel");
|
|
await sleep(150);
|
|
await jclick('[data-course="econ210"]');
|
|
await jclick("#cd-apply");
|
|
await sleep(150);
|
|
await jclick('[data-course="bio105"]');
|
|
await page.waitForSelector("#course-dialog[open]");
|
|
pass("두레 겹침 차단(회귀)", (await page.$eval("#cd-warn", (el) => !el.hidden && el.textContent.includes("겹칩니다"))) && (await page.$eval("#cd-apply", (b) => b.disabled)));
|
|
await jclick("#course-dialog .modal-close");
|
|
await jclick('.tab[data-view="enrollments"]');
|
|
await jclick("[data-cancel]");
|
|
pass("두레 내역 화면 취소", (await page.$$("#en-table tbody tr")).length === 0);
|
|
pass("두레 1440 오버플로", (await over()) <= 0, `+${await over()}px`);
|
|
|
|
// ══════════ 프로덕션 마감(v19): 검증 · 실행취소 · 단축키 · 인쇄 ══════════
|
|
await gotoFile("gaon-lms");
|
|
|
|
// 채점 검증: 범위 밖 점수는 이유를 말하고 저장을 막는다. 정상 복귀하면 푼다
|
|
await jclick('[data-view="grades"]');
|
|
await page.evaluate(() => document.querySelector('[data-assign="a2"]').click());
|
|
await page.waitForSelector("#as-dialog[open]");
|
|
await page.$eval("#g1", (el) => { el.value = "150"; el.dispatchEvent(new Event("input", { bubbles: true })); });
|
|
pass(
|
|
"가온 채점 범위 검증(150 차단)",
|
|
(await page.$eval("#as-warn", (w) => !w.hidden && w.textContent.includes("0에서 100"))) &&
|
|
(await page.$eval("#as-save", (b) => b.disabled)) &&
|
|
(await page.$eval("#g1", (i) => i.getAttribute("aria-invalid") === "true"))
|
|
);
|
|
await page.$eval("#g1", (el) => { el.value = "92"; el.dispatchEvent(new Event("input", { bubbles: true })); });
|
|
pass(
|
|
"가온 채점 정상 복귀(차단 해제)",
|
|
(await page.$eval("#as-warn", (w) => w.hidden)) && !(await page.$eval("#as-save", (b) => b.disabled))
|
|
);
|
|
await page.keyboard.press("Escape");
|
|
|
|
// 단축키: 숫자키 뷰 전환, / 검색 포커스
|
|
await page.focus("body");
|
|
await page.keyboard.press("3");
|
|
pass("가온 단축키 3(출석부)", await page.$eval("#view-attendance", (el) => el.classList.contains("is-on")));
|
|
await page.keyboard.press("/");
|
|
pass(
|
|
"가온 단축키 /(수강생 검색)",
|
|
(await page.$eval("#view-students", (el) => el.classList.contains("is-on"))) &&
|
|
(await page.evaluate(() => document.activeElement.id === "q"))
|
|
);
|
|
|
|
// 알림 센터 Esc 닫힘
|
|
await page.evaluate(() => document.getElementById("bell").click());
|
|
await page.keyboard.press("Escape");
|
|
pass("가온 알림 Esc 닫힘", await page.$eval("#bell-menu", (m) => !m.classList.contains("is-open")));
|
|
pass("가온 인쇄 스타일", fs2.readFileSync(path.join(root, "public/work/gaon-lms/styles/main.css"), "utf8").includes("@media print"));
|
|
|
|
await gotoFile("dure-enrollment");
|
|
|
|
// 실행취소: 신청 → 취소 → 되돌리기 → 원상 복구
|
|
await jclick('.tab[data-view="catalog"]');
|
|
await jclick('[data-course="econ210"]');
|
|
await page.waitForSelector("#course-dialog[open]");
|
|
await jclick("#cd-apply");
|
|
await sleep(150);
|
|
await jclick('.tab[data-view="enrollments"]');
|
|
await jclick("[data-cancel]");
|
|
pass("두레 취소 안내·실행취소 버튼", await page.$eval("#en-status", (s) => !!s.querySelector(".undo-btn")));
|
|
await page.evaluate(() => document.querySelector("#en-status .undo-btn").click());
|
|
await sleep(150);
|
|
pass(
|
|
"두레 실행취소 복원",
|
|
(await page.$$("#en-table tbody tr")).length === 1 && (await page.$eval("#credit-now", (el) => el.textContent)) === "3"
|
|
);
|
|
|
|
// 단축키: 숫자키 탭 전환, / 검색 포커스
|
|
await page.focus("body");
|
|
await page.keyboard.press("2");
|
|
pass("두레 단축키 2(관심 과목)", await page.$eval("#view-starred", (el) => el.classList.contains("is-on")));
|
|
await page.keyboard.press("/");
|
|
pass(
|
|
"두레 단축키 /(과목 검색)",
|
|
(await page.$eval("#view-catalog", (el) => el.classList.contains("is-on"))) &&
|
|
(await page.evaluate(() => document.activeElement.id === "q"))
|
|
);
|
|
pass("두레 인쇄 스타일", fs2.readFileSync(path.join(root, "public/work/dure-enrollment/styles/main.css"), "utf8").includes("@media print"));
|
|
|
|
// ══════════ 좁은 폭 ══════════
|
|
for (const w of [320, 390]) {
|
|
await page.setViewport({ width: w, height: 800 });
|
|
await gotoFile("gaon-lms");
|
|
const og = await over();
|
|
pass(`가온 ${w}px`, og <= 0, `+${og}px`);
|
|
await gotoFile("dure-enrollment");
|
|
const od = await over();
|
|
pass(`두레 ${w}px`, od <= 0, `+${od}px`);
|
|
}
|
|
|
|
// ── 사용자 보고 회귀: 팝업·모달은 좌상단에 붙거나 화면 밖으로 잘리면 안 된다 ──
|
|
await page.setViewport({ width: 390, height: 320 });
|
|
await gotoFile("gaon-lms");
|
|
await jclick("#bell");
|
|
const bellRect = await page.$eval("#bell-menu", (menu) => {
|
|
const r = menu.getBoundingClientRect();
|
|
return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, clientHeight: menu.clientHeight, scrollHeight: menu.scrollHeight };
|
|
});
|
|
pass("가온 짧은 화면 알림판 뷰포트·스크롤", bellRect.left >= 0 && bellRect.top >= 0 && bellRect.right <= 390 && bellRect.bottom <= 320 && bellRect.clientHeight > 0 && bellRect.scrollHeight >= bellRect.clientHeight, JSON.stringify(bellRect));
|
|
|
|
for (const viewport of [{ width: 390, height: 844 }, { width: 1440, height: 900 }, { width: 2560, height: 900 }]) {
|
|
await page.setViewport(viewport);
|
|
await gotoFile("materia-interior");
|
|
await jclick('[onclick^="openProjectModal"]');
|
|
const projectPosition = await centeredDialog("#projectModal");
|
|
pass(`마테리아 프로젝트 모달 중앙 ${viewport.width}px`, projectPosition.inViewport && projectPosition.deltaX <= 1 && projectPosition.deltaY <= 1, JSON.stringify(projectPosition));
|
|
const projectScrollLock = await page.$eval("html", (root) => getComputedStyle(root).overflow);
|
|
pass(`마테리아 프로젝트 모달 배경 잠금 ${viewport.width}px`, projectScrollLock === "hidden", projectScrollLock);
|
|
await page.keyboard.press("Escape");
|
|
|
|
await gotoWorkFile("materia-interior", "projects", "hannam-duplex", "index.html");
|
|
await page.waitForSelector(".gallery-card-action");
|
|
await jclick(".gallery-card-action");
|
|
const lightboxPosition = await centeredDialog("#lightboxDialog");
|
|
pass(`마테리아 갤러리 모달 중앙 ${viewport.width}px`, lightboxPosition.inViewport && lightboxPosition.deltaX <= 1 && lightboxPosition.deltaY <= 1, JSON.stringify(lightboxPosition));
|
|
const lightboxScrollLock = await page.$eval("html", (root) => getComputedStyle(root).overflow);
|
|
pass(`마테리아 갤러리 모달 배경 잠금 ${viewport.width}px`, lightboxScrollLock === "hidden", lightboxScrollLock);
|
|
await page.keyboard.press("Escape");
|
|
}
|
|
|
|
await page.setViewport({ width: 390, height: 320 });
|
|
await gotoFile("hannam-jari");
|
|
await jclick(".menu-button");
|
|
const mobileNav = await page.$eval(".site-nav", (nav) => {
|
|
const r = nav.getBoundingClientRect();
|
|
return { top: r.top, bottom: r.bottom, clientHeight: nav.clientHeight, scrollHeight: nav.scrollHeight };
|
|
});
|
|
pass("한남자리 짧은 화면 메뉴 뷰포트·스크롤", mobileNav.top >= 0 && mobileNav.bottom <= 320 && mobileNav.clientHeight > 0 && mobileNav.scrollHeight >= mobileNav.clientHeight, JSON.stringify(mobileNav));
|
|
|
|
await gotoWorkFile("hannam-jari", "admin", "index.html");
|
|
await jclick("[data-select-row]");
|
|
const inspectorScroll = await page.$eval(".inspector-scroll", (panel) => ({ clientHeight: panel.clientHeight, scrollHeight: panel.scrollHeight }));
|
|
pass("한남자리 짧은 화면 조사관 내부 스크롤", inspectorScroll.clientHeight > 0 && inspectorScroll.scrollHeight >= inspectorScroll.clientHeight, JSON.stringify(inspectorScroll));
|
|
|
|
// ── 사용자 보고 회귀: 2560px에서 사진 한 장이 본문 밀도를 압도하지 않는다 ──
|
|
await page.setViewport({ width: 2560, height: 900 });
|
|
await gotoFile("griddle-club");
|
|
const griddleWide = await page.$eval(".hero", (hero) => {
|
|
const rect = (selector, scope = hero) => {
|
|
const r = scope.querySelector(selector).getBoundingClientRect();
|
|
return { left: r.left, width: r.width, right: r.right };
|
|
};
|
|
const visualLineCount = (node) => {
|
|
const range = document.createRange();
|
|
range.selectNodeContents(node);
|
|
return new Set([...range.getClientRects()].map((r) => Math.round(r.top))).size;
|
|
};
|
|
const heroRect = hero.getBoundingClientRect();
|
|
const rail = document.querySelector(".menu-grid").getBoundingClientRect();
|
|
const process = document.querySelector(".process-grid");
|
|
const processRect = process.getBoundingClientRect();
|
|
const processHeading = process.querySelector(".process-copy h2");
|
|
return {
|
|
hero: { left: heroRect.left, width: heroRect.width, right: heroRect.right },
|
|
copy: rect(".hero-copy"),
|
|
media: rect(".hero-media"),
|
|
contentRail: { left: rail.left, width: rail.width, right: rail.right },
|
|
process: {
|
|
rail: { left: processRect.left, width: processRect.width, right: processRect.right },
|
|
copy: rect(".process-copy", process),
|
|
media: rect(".process-media", process),
|
|
heading: rect(".process-copy h2", process),
|
|
headingLines: visualLineCount(processHeading),
|
|
},
|
|
};
|
|
});
|
|
const heroCentered = Math.abs(griddleWide.hero.left + griddleWide.hero.width / 2 - 1280) <= 1;
|
|
pass("철판클럽 2560px 히어로·이미지 비율", heroCentered
|
|
&& griddleWide.hero.width >= 2140
|
|
&& griddleWide.hero.width <= 2162
|
|
&& griddleWide.copy.width >= 840
|
|
&& griddleWide.copy.width <= 902
|
|
&& griddleWide.media.width >= 1050
|
|
&& griddleWide.media.width <= 1121
|
|
&& griddleWide.media.width <= griddleWide.copy.width * 1.3, JSON.stringify(griddleWide));
|
|
pass("철판클럽 2560px 후속 콘텐츠 레일", griddleWide.contentRail.width >= 1200 && griddleWide.contentRail.width <= 1482 && griddleWide.contentRail.left >= 0 && griddleWide.contentRail.right <= 2560, JSON.stringify(griddleWide.contentRail));
|
|
const processCentered = Math.abs(griddleWide.process.rail.left + griddleWide.process.rail.width / 2 - 1280) <= 1;
|
|
pass("철판클럽 2560px 과정 텍스트·이미지 분할", processCentered
|
|
&& griddleWide.process.rail.width >= 1678
|
|
&& griddleWide.process.rail.width <= 1682
|
|
&& griddleWide.process.copy.width >= 810
|
|
&& griddleWide.process.copy.width <= 835
|
|
&& griddleWide.process.media.width >= 840
|
|
&& griddleWide.process.media.width <= 870
|
|
&& griddleWide.process.heading.width >= 620
|
|
&& griddleWide.process.headingLines <= 2, JSON.stringify(griddleWide.process));
|
|
|
|
// ── CSS 중괄호 균형 (v11 교훈) ──
|
|
import fs2 from "node:fs";
|
|
for (const f of ["gaon-lms/styles/main.css", "dure-enrollment/styles/main.css", "gaon-lms/styles/tokens.css", "dure-enrollment/styles/tokens.css"]) {
|
|
const c = fs2.readFileSync(path.join(root, "public/work", f), "utf8");
|
|
let depth = 0;
|
|
for (const ch of c) { if (ch === "{") depth++; if (ch === "}") depth--; }
|
|
pass(`CSS 중괄호 균형 ${f}`, depth === 0, `depth ${depth}`);
|
|
}
|
|
|
|
await browser.close();
|
|
console.log(results.join("\n"));
|
|
const fails = results.filter((r) => r.startsWith("FAIL")).length;
|
|
console.log(fails === 0 ? "\nALL PASS" : `\n${fails} FAIL`);
|
|
process.exit(fails === 0 ? 0 : 1);
|