- HIG(탭바 5~6·시트)·M3(바텀내비 3~5·48dp)·NN/g·CSS-Tricks 조사(research/mobile-app-ux-v1.md) 후 Nielsen 10 감사로 위반 도출 - 바텀 내비 5탭 + 더보기 바텀 시트(그룹 헤더), 뷰·다이얼로그 pushState/popstate — 안드로이드 백 키가 앱 내 뒤로 작동 - safe area(env+max)·입력 16px(iOS 확대 방지)·coarse 44px 타깃·tap-highlight·overscroll 위생, 인쇄에서 모바일 크롬 제거 - PWA: manifest + sharp 생성 아이콘(가온=장부 문항, 두레=시간표 블록) - tools/e2e.mjs L6 시나리오: RED 1,742 단언 318 실패 → GREEN 1,762/0, verify 체인 7계층 통과 - 실기기(안드로이드 에뮬레이터·Chrome) 텍스트 근거 검증 — 백 키·시트·탭 전환, iOS 는 WebKit(L4) 대체 - 스킬 내재화: mobile-app-ux.md, audit-gate 7계층·하니스 12~14, design-gate 터치타깃 검사, SKILL.md 규칙 7 - 도그푸드: 게이트가 죽은 선택자 8종·thresholds 병합 버그 적발 — CSS v22(양앱) 범프 후 전체 재검증
122 lines
6.8 KiB
JavaScript
122 lines
6.8 KiB
JavaScript
// L1 단위 테스트 — 계산 로직 경계값. 브라우저 없이 Node 에서 직접 실행.
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import url from "node:url";
|
|
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
const load = (rel) => {
|
|
const m = {};
|
|
new Function("module", fs.readFileSync(path.join(here, rel), "utf8"))(m);
|
|
return m.exports;
|
|
};
|
|
const GAON = load("../../public/work/gaon-lms/calc.js");
|
|
const DURE = load("../../public/work/dure-enrollment/calc.js");
|
|
|
|
let passed = 0, failed = 0;
|
|
const t = (name, fn) => {
|
|
try { fn(); passed++; console.log("PASS " + name); }
|
|
catch (e) { failed++; console.log("FAIL " + name + " — " + e.message); }
|
|
};
|
|
|
|
console.log("── 가온 ──");
|
|
t("attSummary: 전원 출석 → 100%", () => assert.equal(GAON.attSummary([2, 2, 2, 2, 2]), "100%"));
|
|
t("attSummary: 혼합 4/5 → 80%", () => assert.equal(GAON.attSummary([2, 2, 2, 2, 1]), "80%"));
|
|
t("attSummary: 빈 행 → 휴학", () => assert.equal(GAON.attSummary([]), "휴학"));
|
|
t("attSummary: null/undefined 무시", () => assert.equal(GAON.attSummary([2, null, 2, undefined, 0]), "67%"));
|
|
t("attSummary: 1/3 반올림 → 33%", () => assert.equal(GAON.attSummary([2, 0, 0]), "33%"));
|
|
|
|
t("dayTotal/dayPresent: 행 배열 목록 (0/3 사고 회귀)", () => {
|
|
const marks = [[2, 2, 2, 2, 2], [2, 1, 2, 2, 2], null];
|
|
assert.equal(GAON.dayTotal(marks, 0), 2);
|
|
assert.equal(GAON.dayPresent(marks, 0), 2);
|
|
assert.equal(GAON.dayPresent(marks, 1), 1); // 지각 1명은 출석 아님
|
|
});
|
|
t("dayPresent: 전원 결석일 → 0", () => assert.equal(GAON.dayPresent([[0, 0], [0, 0]], 0), 0));
|
|
t("dayTotal: 전부 null 행 → 0 (나눗셈 가드)", () => assert.equal(GAON.dayTotal([null, null], 0), 0));
|
|
|
|
t("counts: 집계 정합", () => {
|
|
const c = GAON.counts([[2, 1, 0], [2, 2, 2]]);
|
|
assert.deepEqual([c.att, c.late, c.abs, c.total, c.rate], [4, 1, 1, 6, 67]);
|
|
});
|
|
t("counts: 빈 입력 → total 0, rate 0 (NaN 아님)", () => {
|
|
const c = GAON.counts([]);
|
|
assert.equal(c.rate, 0); assert.equal(c.total, 0); assert.ok(!Number.isNaN(c.rate));
|
|
});
|
|
|
|
t("rosterStats: 0명 명단 → 0/NaN 없음", () => {
|
|
const s = GAON.rosterStats([]);
|
|
assert.deepEqual([s.total, s.active, s.avgAtt, s.hwRate], [0, 0, 0, 0]);
|
|
});
|
|
t("rosterStats: 재학만 통계에 포함", () => {
|
|
const s = GAON.rosterStats([
|
|
{ status: "재학", att: 90, hw: "8/8" },
|
|
{ status: "재학", att: 70, hw: "6/8" },
|
|
{ status: "휴학", att: 0, hw: "0/8" },
|
|
]);
|
|
assert.equal(s.active, 2);
|
|
assert.equal(s.avgAtt, 80);
|
|
assert.deepEqual([s.hwDone, s.hwTotal, s.hwRate], [14, 16, 88]);
|
|
});
|
|
t("rosterStats: 홀수 반올림", () => assert.equal(GAON.rosterStats([{ status: "재학", att: 91, hw: "1/3" }]).avgAtt, 91));
|
|
|
|
console.log("── 두레 ──");
|
|
t("isOverlap: 같은 요일 겹침 → true", () =>
|
|
assert.equal(DURE.isOverlap({ days: [1, 3], start: 10, end: 11.5 }, { days: [3], start: 11, end: 12 }), true));
|
|
t("isOverlap: 닿는 경계(끝==시작) → false", () =>
|
|
assert.equal(DURE.isOverlap({ days: [1], start: 10, end: 12 }, { days: [1], start: 12, end: 14 }), false));
|
|
t("isOverlap: 다른 요일 같은 시간 → false", () =>
|
|
assert.equal(DURE.isOverlap({ days: [1], start: 10, end: 12 }, { days: [2], start: 10, end: 12 }), false));
|
|
t("isOverlap: 동일 과목 → true", () =>
|
|
assert.equal(DURE.isOverlap({ days: [2, 4], start: 13, end: 14.5 }, { days: [4, 2], start: 13, end: 14.5 }), true));
|
|
t("isOverlap: 부분 겹침 → true", () =>
|
|
assert.equal(DURE.isOverlap({ days: [5], start: 9, end: 12 }, { days: [5], start: 11, end: 13 }), true));
|
|
|
|
t("credits: 합계", () => assert.equal(DURE.credits([{ credits: 3 }, { credits: 2 }]), 5));
|
|
t("credits: 빈 목록 → 0", () => assert.equal(DURE.credits([]), 0));
|
|
t("credits: credits 없는 항목 → 0 취급", () => assert.equal(DURE.credits([{}, { credits: 3 }]), 3));
|
|
|
|
t("wouldExceed: 15+3 ≤ 18 → false", () => assert.equal(DURE.wouldExceed(15, 3, 18), false));
|
|
t("wouldExceed: 16+3 > 18 → true (경계)", () => assert.equal(DURE.wouldExceed(16, 3, 18), true));
|
|
t("wouldExceed: 18+0 → false (경계)", () => assert.equal(DURE.wouldExceed(18, 0, 18), false));
|
|
|
|
t("seatsState: 만석 → 정원 마감", () => assert.deepEqual(DURE.seatsState({ seats: 0 }), { label: "정원 마감", closed: true, near: false }));
|
|
t("seatsState: 여석 5 → 임박", () => assert.ok(DURE.seatsState({ seats: 5 }).near));
|
|
t("seatsState: 여석 6 → 임박 아님 (경계)", () => assert.ok(!DURE.seatsState({ seats: 6 }).near));
|
|
|
|
t("remaining: 정상 잔여", () => assert.equal(DURE.remaining(130, 94, 3), 33));
|
|
t("remaining: 초과 → 음수", () => assert.equal(DURE.remaining(24, 21, 6), -3));
|
|
|
|
console.log("── 가온 v19: 채점 검증 ──");
|
|
t("validateScore: 정상 92 → ok·값 반환", () => assert.deepEqual(GAON.validateScore("92"), { ok: true, value: 92 }));
|
|
t("validateScore: 경계 0·100 → ok", () => {
|
|
assert.equal(GAON.validateScore("0").ok, true);
|
|
assert.equal(GAON.validateScore("100").ok, true);
|
|
});
|
|
t("validateScore: 경계 밖 101·-1 → range", () => {
|
|
assert.equal(GAON.validateScore("101").reason, "range");
|
|
assert.equal(GAON.validateScore("-1").reason, "range");
|
|
});
|
|
t("validateScore: 빈 칸·공백 → empty", () => {
|
|
assert.equal(GAON.validateScore("").reason, "empty");
|
|
assert.equal(GAON.validateScore(" ").reason, "empty");
|
|
});
|
|
t("validateScore: 숫자 아님 → nan", () => assert.equal(GAON.validateScore("9점").reason, "nan"));
|
|
t("validateScore: 소수 88.5 → ok", () => assert.equal(GAON.validateScore("88.5").value, 88.5));
|
|
|
|
console.log("── 두레 v19: 신청 판정 ──");
|
|
t("canEnroll: 정상 → ok", () =>
|
|
assert.deepEqual(DURE.canEnroll({ name: "논리와 토론", seats: 5, days: [2], start: 16, end: 18, credits: 2 }, [], 15, 18), { ok: true }));
|
|
t("canEnroll: 정원 마감 → full 이 우선(겹침보다 먼저)", () =>
|
|
assert.equal(DURE.canEnroll({ name: "드로잉", seats: 0, days: [3], start: 14, end: 17, credits: 3 }, [{ name: "생명과학", days: [3], start: 15, end: 16 }], 0, 18).reason, "full"));
|
|
t("canEnroll: 겹침 → 겹친 과목명 반환", () => {
|
|
const r = DURE.canEnroll({ name: "생명과학 산책", seats: 3, days: [1], start: 14, end: 16, credits: 2 }, [{ name: "미시경제학", days: [1], start: 13, end: 14.5 }], 0, 18);
|
|
assert.equal(r.reason, "overlap");
|
|
assert.deepEqual(r.names, ["미시경제학"]);
|
|
});
|
|
t("canEnroll: 학점 초과 경계 16+3 → credits", () =>
|
|
assert.equal(DURE.canEnroll({ name: "문학과 사회", seats: 9, days: [1, 3], start: 10, end: 11.5, credits: 3 }, [], 16, 18).reason, "credits"));
|
|
t("canEnroll: 15+3 → 통과", () =>
|
|
assert.equal(DURE.canEnroll({ name: "문학과 사회", seats: 9, days: [1, 3], start: 10, end: 11.5, credits: 3 }, [], 15, 18).ok, true));
|
|
|
|
console.log(`\n${failed ? `${failed} FAIL` : "UNIT ALL PASS"} (${passed} passed)`);
|
|
process.exit(failed ? 1 : 0);
|