const fs = require("fs"); const { JSDOM } = require("jsdom"); const path = require("path"); const RENDER = path.join(__dirname, ".out", "box_calc_render.html"); const html = fs.readFileSync(RENDER, "utf8"); // 먼저 render_box_calc.py 실행 // 서버 응답 고정: 미라네 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() : ""); function fire(el, type, init) { el.dispatchEvent(new w.Event(type, Object.assign({bubbles:true}, init))); } (async () => { // ── ① 입력 → 계산 ────────────────────────────────── const row1 = $(".cpg-calc-row"); row1.querySelector(".cpg-calc-name").value = "MS-1001"; row1.querySelector(".cpg-calc-qty").value = "18"; // Tab 으로 다음 행 생성 + 포커스 이동 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"; $("#cpg-center-add").click(); 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[draggable='true']"); const dt = { data: {}, setData(t, v) { this.data[t] = v; }, getData(t) { return this.data[t]; } }; const ds = new w.Event("dragstart", { bubbles: true }); ds.dataTransfer = dt; card.dispatchEvent(ds); check("dragstart 가 key 를 싣는다", dt.getData("text/plain") === "p:MS-1001", dt.getData("text/plain")); const zone = $(".cpg-dist-center"); const dov = new w.Event("dragover", { bubbles: true, cancelable: true }); dov.dataTransfer = dt; zone.dispatchEvent(dov); check("dragover 가 기본동작을 막는다(드롭 허용)", dov.defaultPrevented); const drop = new w.Event("drop", { bubbles: true, cancelable: true }); drop.dataTransfer = dt; zone.dispatchEvent(drop); check("드롭하면 대화상자가 열린다", $("#cpg-dlg").hidden === false); check("대화상자에 잔여/개수 표시", text("#cpg-dlg-item").includes("잔여 2박스 (16개)"), text("#cpg-dlg-item")); check("대화상자 센터 = 드롭한 센터", $("#cpg-dlg-center").value === "1"); // 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").startsWith("1박스 8개✕") || text(".cpg-dist-sum").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].getAttribute("draggable") === null); check("센터 합계 2박스/16개", text(".cpg-dist-sum").replace(/\s/g,"").startsWith("2박스16개"), text(".cpg-dist-sum")); // ── 수량 직접 수정 (상한 = 잔여+자기수량) ───────── 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")); // ── 혼합 박스 담기 ──────────────────────────────── const mix = $(".cpg-box-card[draggable='true']"); const dt2 = { data:{}, setData(t,v){this.data[t]=v;}, getData(t){return this.data[t];} }; const ds2 = new w.Event("dragstart", { bubbles: true }); ds2.dataTransfer = dt2; mix.dispatchEvent(ds2); const drop2 = new w.Event("drop", { bubbles: true, cancelable: true }); drop2.dataTransfer = dt2; $(".cpg-dist-center").dispatchEvent(drop2); 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")); // ── 항목 빼기 / 센터 빼기 ───────────────────────── $$(".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"; $("#cpg-center-add").click(); check("센터 다시 추가", $$(".cpg-dist-center").length === 1, String($$(".cpg-dist-center").length)); // (1) 패널이 아닌 여백(리스트 자체)에 놓아도, 센터가 하나면 그 센터로 담긴다 const card2 = $(".cpg-sum-prod[draggable='true']"); const dt3 = { data: {}, setData(t, v) { this.data[t] = v; }, getData(t) { return this.data[t] || ""; }, types: ["text/plain"] }; const ds3 = new w.Event("dragstart", { bubbles: true }); ds3.dataTransfer = dt3; card2.dispatchEvent(ds3); const den = new w.Event("dragenter", { bubbles: true, cancelable: true }); den.dataTransfer = dt3; $(".cpg-dist-card").dispatchEvent(den); check("dragenter 도 기본동작을 막는다", den.defaultPrevented); const drop3 = new w.Event("drop", { bubbles: true, cancelable: true }); drop3.dataTransfer = dt3; $("#cpg-dist-list").dispatchEvent(drop3); check("여백에 놓아도 대화상자가 열린다", $("#cpg-dlg").hidden === false); $("#cpg-dlg-ok").click(); check("여백 드롭도 센터에 담긴다", $$(".cpg-dist-item").length === 1, String($$(".cpg-dist-item").length)); // (2) setData 를 막는 브라우저: dataTransfer 가 비어도 dragKey 로 담긴다 $(".cpg-dist-del").click(); const card3 = $(".cpg-sum-prod[draggable='true']"); const dtBlocked = { setData() { throw new Error("blocked"); }, getData() { return ""; }, types: [] }; const ds4 = new w.Event("dragstart", { bubbles: true }); ds4.dataTransfer = dtBlocked; card3.dispatchEvent(ds4); const drop4 = new w.Event("drop", { bubbles: true, cancelable: true }); drop4.dataTransfer = dtBlocked; $(".cpg-dist-center").dispatchEvent(drop4); check("setData 가 막혀도 드롭이 동작한다", $("#cpg-dlg").hidden === false, text("#cpg-calc-msg")); $("#cpg-dlg-ok").click(); check("setData 차단 환경에서도 담긴다", $$(".cpg-dist-item").length === 1); console.log("\n" + (fails.length ? "FAILED: " + fails.join(" | ") : "ALL PASS")); process.exit(fails.length ? 1 : 0); })();