Files
dbx-main/tests/js/box_calc_ui.test.js
T
king 26beab1ae7 feat(cupang): 전체 배분 완료 시 빨간 도장
모든 박스(제품별 + 혼합)를 센터에 담아 미배분 잔여가 0 이 되면 ② 요약의
"센터 배분" 칸에 빨간 "전체 배분 완료" 도장을 찍는다. 처음 완료될 때만
찍히는 애니메이션이 돌고, 하나라도 빼면 도장이 사라진다. 재계산하면
기록도 초기화된다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 04:04:19 +09:00

242 lines
14 KiB
JavaScript

// 박스 계산 화면(cupang/box_calc.html) UI 테스트 — jsdom.
// 실행 전 `python tests/js/render_box_calc.py` 로 템플릿을 렌더해 둔다.
const fs = require("fs");
const path = require("path");
const { JSDOM } = require("jsdom");
const RENDER = path.join(__dirname, ".out", "box_calc_render.html");
const html = fs.readFileSync(RENDER, "utf8");
// 서버 응답 고정: 미라네 1호 18개(2박스+자투리2), 미라네 4호 12개(1박스+자투리4)
const apiResponse = {
results: [
{product_code:"MS-1001", product_name:"미라네 1호 세트", box_name:"쿠팡박스", units_per_box:8,
quantity:18, configured:true, full_boxes:2, remainder_units:2, required_boxes:3},
{product_code:"MS-1004", product_name:"미라네 4호 세트", box_name:"쿠팡박스", units_per_box:8,
quantity:12, configured:true, full_boxes:1, remainder_units:4, required_boxes:2},
],
totals: [],
mixes: [{box_name:"쿠팡박스", box_count:1, leftover_units:6, boxes:[
{items:[{product_code:"MS-1004",product_name:"미라네 4호 세트",quantity:4},
{product_code:"MS-1001",product_name:"미라네 1호 세트",quantity:2}], fill_percent:75.0}]}],
grand_total_boxes: 4,
};
const dom = new JSDOM(html, { runScripts: "dangerously", pretendToBeVisual: true });
const w = dom.window, d = w.document;
w.fetch = () => Promise.resolve({ ok: true, json: () => Promise.resolve(apiResponse) });
const fails = [];
function check(name, cond, extra) {
if (cond) console.log("PASS " + name);
else { console.log("FAIL " + name + (extra ? " → " + extra : "")); fails.push(name); }
}
const $ = (s) => d.querySelector(s);
const $$ = (s) => Array.from(d.querySelectorAll(s));
const text = (s) => ($(s) ? $(s).textContent.replace(/\s+/g, " ").trim() : "<none>");
const fire = (el, type) => el.dispatchEvent(new w.Event(type, { bubbles: true }));
// 마우스 드래그 시뮬레이션.
// jsdom 은 레이아웃이 없어 elementFromPoint 가 항상 null 이므로 드롭 대상을 스텁한다.
function mouse(el, type, x, y) {
el.dispatchEvent(new w.MouseEvent(type, {
bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0,
}));
}
function dragCardTo(card, target) {
d.elementFromPoint = () => target;
mouse(card, "mousedown", 10, 10);
mouse(d, "mousemove", 60, 60); // 임계값(4px) 초과 → 드래그 시작
mouse(d, "mousemove", 200, 200);
mouse(d, "mouseup", 200, 200);
d.elementFromPoint = () => null;
}
(async () => {
// ── ① 입력 → 계산 ──────────────────────────────────
const row1 = $(".cpg-calc-row");
row1.querySelector(".cpg-calc-name").value = "MS-1001";
row1.querySelector(".cpg-calc-qty").value = "18";
const ke = new w.KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
row1.querySelector(".cpg-calc-qty").dispatchEvent(ke);
check("Tab 이 새 행을 만든다", $$(".cpg-calc-row").length === 2, "행수=" + $$(".cpg-calc-row").length);
check("Tab 후 포커스가 새 행 수량", d.activeElement === $$(".cpg-calc-row")[1].querySelector(".cpg-calc-qty"));
const row2 = $$(".cpg-calc-row")[1];
row2.querySelector(".cpg-calc-name").value = "MS-1004";
row2.querySelector(".cpg-calc-qty").value = "12";
$("#cpg-calc-run").click();
await new Promise(r => setTimeout(r, 20));
check("요약이 표시된다", $("#cpg-sum-body").hidden === false);
check("제품 카드 2장", $$(".cpg-sum-prod").length === 2, String($$(".cpg-sum-prod").length));
check("혼합 박스 1장", $$(".cpg-box-card").length === 1);
check("제품별 잔여 = 2박스(16개)", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("2박스16개"),
text(".cpg-sum-prod .cpg-sum-prod-nums"));
check("KPI 총 박스 4", text(".cpg-kpi .cpg-kpi-value") === "4박스", text(".cpg-kpi .cpg-kpi-value"));
// ── ③ 센터는 추가한 것만 표시 ─────────────────────
check("초기 센터 패널 없음", $$(".cpg-dist-center").length === 0);
$("#cpg-center-pick").value = "1";
fire($("#cpg-center-pick"), "change");
check("센터 추가 후 1개 표시", $$(".cpg-dist-center").length === 1, String($$(".cpg-dist-center").length));
check("추가한 센터 이름", text(".cpg-dist-center strong") === "대구3", text(".cpg-dist-center strong"));
// ── 마우스 드래그 → 드롭 → 대화상자 ───────────────
const card = $(".cpg-sum-prod.is-draggable");
d.elementFromPoint = () => $(".cpg-dist-center");
mouse(card, "mousedown", 10, 10);
mouse(d, "mousemove", 60, 60);
check("4px 이상 움직이면 드래그 시작", card.classList.contains("is-dragging"));
check("고스트(복제 카드) 생성", !!$(".cpg-drag-ghost"));
check("드래그 중 대상 센터 강조", $(".cpg-dist-center").classList.contains("is-over"));
mouse(d, "mouseup", 200, 200);
d.elementFromPoint = () => null;
check("드롭 후 고스트 제거", !$(".cpg-drag-ghost"));
check("드롭하면 대화상자가 열린다", $("#cpg-dlg").hidden === false);
check("대화상자에 잔여/개수 표시", text("#cpg-dlg-item").includes("잔여 2박스 (16개)"), text("#cpg-dlg-item"));
check("대화상자에 센터 버튼 표시", $$(".cpg-dlg-center-btn").length === 1, String($$(".cpg-dlg-center-btn").length));
check("드롭한 센터 버튼이 선택됨",
$(".cpg-dlg-center-btn[aria-pressed='true']").getAttribute("data-center") === "1",
$(".cpg-dlg-center-btn[aria-pressed='true']").getAttribute("data-center"));
// 1박스만 담기
$("#cpg-dlg-qty").value = "1";
$("#cpg-dlg-ok").click();
check("대화상자가 닫힌다", $("#cpg-dlg").hidden === true);
check("센터에 항목 1줄", $$(".cpg-dist-item").length === 1);
check("센터 항목에 박스·개수 표시", text(".cpg-dist-item .cpg-dist-unit") === "박스 · 8개",
text(".cpg-dist-item .cpg-dist-unit"));
check("센터 합계 배지 1박스/8개", text(".cpg-dist-sum").replace(/\s/g, "").startsWith("1박스8개"),
text(".cpg-dist-sum"));
check("요약 잔여가 1박스로 줄어든다", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("1박스8개"),
text(".cpg-sum-prod .cpg-sum-prod-nums"));
check("KPI 센터 배분 1박스", $$(".cpg-kpi-value")[1].textContent === "1박스", $$(".cpg-kpi-value")[1].textContent);
// ── 클릭 대체 경로 + 잔여 전부 담기 ───────────────
$$(".cpg-sum-prod")[0].click();
check("클릭으로도 대화상자가 열린다", $("#cpg-dlg").hidden === false);
$("#cpg-dlg-all").click();
check("전량 담기 후 잔여 0", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("0박스0개"),
text(".cpg-sum-prod .cpg-sum-prod-nums"));
check("잔여 0 카드는 드래그 불가", $$(".cpg-sum-prod")[0].classList.contains("is-done"));
check("배분 완료 도장 표시", !!$$(".cpg-sum-prod")[0].querySelector(".cpg-stamp"));
check("막 완료된 도장은 애니메이션", $$(".cpg-sum-prod")[0].querySelector(".cpg-stamp").classList.contains("is-new"));
check("제품 카드에 박스명 배지 없음", !$$(".cpg-sum-prod")[0].querySelector(".erp-badge"));
check("센터 합계 2박스/16개", text(".cpg-dist-sum").replace(/\s/g, "").startsWith("2박스16개"),
text(".cpg-dist-sum"));
// 다른 카드를 담아 다시 렌더해도, 이미 찍힌 도장은 애니메이션을 반복하지 않는다
$$(".cpg-sum-prod")[1].click();
$("#cpg-dlg-all").click();
check("기존 도장은 애니메이션 반복 없음",
!$$(".cpg-sum-prod")[0].querySelector(".cpg-stamp").classList.contains("is-new"));
check("새로 완료된 도장만 애니메이션",
$$(".cpg-sum-prod")[1].querySelector(".cpg-stamp").classList.contains("is-new"));
// 되돌려서 뒷 시나리오에 영향 없게
$$(".cpg-dist-del")[1].click();
// ── 수량 직접 수정 (상한 = 잔여 + 자기 수량) ──────
const qty = $(".cpg-dist-item .cpg-dist-qty");
qty.value = "5"; fire(qty, "input");
check("잔여 초과 입력은 최대치로 잘림", qty.value === "2", qty.value);
qty.value = "1"; fire(qty, "input");
check("수량 줄이면 개수 표시 갱신", text(".cpg-dist-item .cpg-dist-unit") === "박스 · 8개",
text(".cpg-dist-item .cpg-dist-unit"));
check("수량 줄이면 요약 잔여 복귀", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("1박스8개"),
text(".cpg-sum-prod .cpg-sum-prod-nums"));
// ── 혼합 박스 담기 ────────────────────────────────
dragCardTo($(".cpg-box-card.is-draggable"), $(".cpg-dist-center"));
check("혼합 박스도 대화상자가 열린다", $("#cpg-dlg").hidden === false);
check("혼합 박스 잔여 1박스(6개)", text("#cpg-dlg-item").includes("잔여 1박스 (6개)"), text("#cpg-dlg-item"));
$("#cpg-dlg-ok").click();
check("혼합 박스가 센터에 담긴다", $$(".cpg-dist-item").length === 2);
check("혼합 담은 뒤 배분됨 표시", $(".cpg-box-card").className.includes("is-done"));
check("혼합 박스에도 도장", !!$(".cpg-box-card .cpg-stamp"));
check("혼합 박스 아래 내용물 트리", $$(".cpg-dist-tree li").length === 2,
String($$(".cpg-dist-tree li").length));
check("트리에 제품명·개수", text(".cpg-dist-tree li").includes("미라네 4호 세트") &&
text(".cpg-dist-tree li").includes("4개"), text(".cpg-dist-tree li"));
check("단일 제품 항목에는 트리 없음",
!$$(".cpg-dist-item")[0].querySelector(".cpg-dist-tree"));
// ── 항목 빼기 / 센터 빼기 ─────────────────────────
$$(".cpg-dist-del")[1].click();
check("항목 빼면 줄이 사라진다", $$(".cpg-dist-item").length === 1);
check("항목 빼면 혼합 박스 복귀", !$(".cpg-box-card").className.includes("is-done"));
$(".cpg-dist-close").click();
check("센터 빼기", $$(".cpg-dist-center").length === 0);
check("센터 빼면 전량 잔여 복귀", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("2박스16개"),
text(".cpg-sum-prod .cpg-sum-prod-nums"));
// ── 드롭 실패 경로 방어 ───────────────────────────
$("#cpg-center-pick").value = "2";
fire($("#cpg-center-pick"), "change");
check("센터 다시 추가", $$(".cpg-dist-center").length === 1, String($$(".cpg-dist-center").length));
// (1) 패널이 아닌 ③ 카드 여백에 놓아도 센터가 하나면 그 센터로
dragCardTo($(".cpg-sum-prod.is-draggable"), $(".cpg-dist-card"));
check("여백에 놓아도 대화상자가 열린다", $("#cpg-dlg").hidden === false);
$("#cpg-dlg-qty").value = "1";
$("#cpg-dlg-ok").click();
check("여백 드롭도 센터에 담긴다", $$(".cpg-dist-item").length === 1, String($$(".cpg-dist-item").length));
// (2) ③ 밖에 놓으면 담기지 않고 안내만 뜬다
$(".cpg-dist-del").click();
dragCardTo($(".cpg-sum-prod.is-draggable"), $(".cpg-calc-card"));
check("③ 밖에 놓으면 담기지 않는다", $("#cpg-dlg").hidden === true && $$(".cpg-dist-item").length === 0);
check("③ 밖 드롭 안내 메시지", text("#cpg-calc-msg").includes("센터 위에 놓아야"), text("#cpg-calc-msg"));
// (3) 대화상자에서 센터 버튼으로 대상 변경
$("#cpg-center-pick").value = "1";
fire($("#cpg-center-pick"), "change");
check("센터 2개 표시", $$(".cpg-dist-center").length === 2, String($$(".cpg-dist-center").length));
$(".cpg-sum-prod.is-draggable").click();
check("센터 2개 → 버튼 2개", $$(".cpg-dlg-center-btn").length === 2);
const other = $$(".cpg-dlg-center-btn").filter(b => b.getAttribute("aria-pressed") !== "true")[0];
const otherId = other.getAttribute("data-center");
other.click();
check("버튼 클릭으로 센터 변경",
$(".cpg-dlg-center-btn[aria-pressed='true']").getAttribute("data-center") === otherId);
$("#cpg-dlg-qty").value = "1";
$("#cpg-dlg-ok").click();
const target = $('[data-drop-for="' + otherId + '"]');
check("선택한 센터에 담긴다", target.querySelectorAll(".cpg-dist-item").length === 1);
target.querySelector(".cpg-dist-del").click();
$(".cpg-dist-close").click();
// (3) 드래그 없이 클릭한 경우
$(".cpg-sum-prod.is-draggable").click();
check("클릭 대체 경로 동작", $("#cpg-dlg").hidden === false);
$("#cpg-dlg-qty").value = "1";
$("#cpg-dlg-ok").click();
check("클릭으로 담긴다", $$(".cpg-dist-item").length === 1);
// ── 전체 배분 완료 도장 ───────────────────────────
check("잔여가 남아 있으면 전체 도장 없음", !$(".cpg-stamp-all"));
while ($(".cpg-sum-prod.is-draggable")) { // 제품 박스 전량 담기
$(".cpg-sum-prod.is-draggable").click();
$("#cpg-dlg-all").click();
}
while ($(".cpg-box-card.is-draggable")) { // 혼합 박스까지 담기
$(".cpg-box-card.is-draggable").click();
$("#cpg-dlg-all").click();
}
check("미배분 잔여 0", $$(".cpg-kpi")[2].querySelector(".cpg-kpi-value").textContent === "0박스",
$$(".cpg-kpi")[2].querySelector(".cpg-kpi-value").textContent);
check("전체 배분 완료 도장 표시", !!$(".cpg-stamp-all"));
check("전체 도장 위치 = 센터 배분 칸", !!$$(".cpg-kpi")[1].querySelector(".cpg-stamp-all"));
check("전체 도장 첫 등장은 애니메이션", $(".cpg-stamp-all").classList.contains("is-new"));
$(".cpg-dist-del").click(); // 하나 빼면
check("잔여가 생기면 전체 도장 제거", !$(".cpg-stamp-all"));
console.log("\n" + (fails.length ? "FAILED: " + fails.join(" | ") : "ALL PASS"));
process.exit(fails.length ? 1 : 0);
})();