/* 481-2 B08 · B09 단계 이동 = 저장 성공 뒤 — 진짜 `B08_Quantity_UI_Page.ts` · `B09_Estimation_UI_Shell.ts` 를 * 트랜스파일해 가짜 DOM · 가짜 의존 위에서 돌리고 값을 JSON 으로 냄. * 의존은 기록용 가짜 — 지정 안 한 import 는 무엇이든 받아 주는 빈 값(any). */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); // 무엇이든 받아 주는 빈 값 — 부르면 또 빈 값 · 펼치면 비어 있음 · 글자로는 "" function any() { const store = {}; return new Proxy(function () {}, { get(_t, k) { if (k in store) return store[k]; if (k === "then") return undefined; if (k === Symbol.iterator) return function* () {}; if (k === Symbol.toPrimitive) return () => ""; return (store[k] = any()); }, set(_t, k, v) { store[k] = v; return true; }, apply: () => any(), construct: () => any(), }); } // 가짜 요소 — isConnected = 뿌리까지 부모 줄이 이어짐 class Node { constructor(tag = "div") { this.tagName = tag.toUpperCase(); this.children = []; this.parent = null; this.dataset = {}; this.style = {}; this.classList = { add() {}, toggle() {}, remove() {}, contains: () => false }; } get isConnected() { let n = this; while (n.parent) n = n.parent; return n.isRoot === true; } append(...kids) { kids.forEach((k) => { if (k instanceof Node) k.parent = this; this.children.push(k); }); } replaceChildren(...kids) { this.children.forEach((k) => k instanceof Node && (k.parent = null)); this.children = []; this.append(...kids); } setAttribute() {} querySelector() { return null; } querySelectorAll() { return []; } addEventListener() {} } global.document = { createElement: (tag) => new Node(tag) }; function load(rel, stubs) { let src = fs .readFileSync(path.join(ROOT, rel), "utf8") .replace(/import\.meta\.glob\(/g, "(() => ({}))("); const js = ts.transpileModule(src, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const mod = { exports: {} }; const req = (id) => (id in stubs ? stubs[id] : id.endsWith(".css") ? {} : any()); new Function("exports", "module", "require", js)(mod.exports, mod, req); return mod.exports; } const flush = () => new Promise((r) => setTimeout(r, 0)); const calls = []; let layoutOpts = null; const store = new Map(); // 가짜 세션 캐시 — 이름 → 값 const common = { "@ui/ui_template_workflow_layout": { createWorkflowLayout: (opts) => { layoutOpts = opts; const root = new Node(); root.append(opts.leftPanel, opts.mainContent); return { root }; }, }, "@ui/ui_template_elements": { createButton: (o) => Object.assign(new Node("button"), { onClick: o.onClick }), showToast: (text, kind) => calls.push(`toast:${kind}`), showLoadingOverlay() {}, hideLoadingOverlay() {}, showConfirmDialog: async () => false, }, "@ui/ui_template_locale": { ui_locales: any(), currentLanguageIndex: 0 }, "@config/config_frontend": { CURRENT_PROJECT_ID_KEY: "pid", API_BASE_URL: "/api" }, "../A00_Common/b_page_scaffold": { workflowSteps: () => [] }, "../A00_Common/b_workflow_nav": { WORKFLOW_STEP_ROUTES: ["b03", "b04", "b05", "b06", "b07", "b08", "b09"], goToWorkflowStage: (_p, route) => calls.push(`go:${route}`), }, "../A00_Common/b_page_state": { readState: (name) => store.get(name) ?? null, writeState: (name, v) => store.set(name, v), clearState: (name) => store.delete(name), }, }; global.localStorage = { getItem: () => "P1" }; async function b08() { let optionsOk = true; let tabOk = true; let registerMaterial = null; let registerHydro = null; const m = load("B08_Quantity/B08_Quantity_UI_Page.ts", { ...common, "./B08_Quantity_UI_Options": { createOptionsPanel: () => ({ panel: new Node(), save: async () => (calls.push("save:options"), optionsOk), }), }, "./B08_Quantity_UI_Material": { renderMaterialSheet: (_h, _p, _s, onSave) => (registerMaterial = onSave), }, "./B08_Quantity_UI_Hydro": { renderHydro: (_h, _p, onSave) => (registerHydro = onSave), request: async (url) => calls.push(`req:${url.split("/").pop()}`), // 298-2 B09 쪽 = 단계 5 완료 }, }); const root = Object.assign(new Node(), { isRoot: true }); m.renderB08Quantity(root); const body = layoutOpts.mainContent; const tabBar = body.children[0]; const clickTab = (id) => tabBar.children.find((b) => b.dataset.b08Tab === id).onClick(); const step = async (i) => { calls.length = 0; layoutOpts.onStepClick(i); await flush(); await flush(); return [...calls]; }; const tabSave = (tag) => async () => (calls.push(`save:${tag}`), tabOk); const out = {}; out.summaryOk = await step(6); optionsOk = false; out.optionsFail = await step(6); optionsOk = true; clickTab("material"); registerMaterial(tabSave("material")); out.materialOk = await step(4); tabOk = false; out.materialFail = await step(4); optionsOk = false; out.optionsFailSkipsTab = await step(4); optionsOk = true; tabOk = false; clickTab("hydrology"); registerHydro(tabSave("hydro")); out.hydroFail = await step(6); clickTab("earthwork"); // 수리 탭 떠남 — 떨어진 탭 저장은 안 부름 out.leftTab = await step(6); return out; } async function b09() { let fail = false; global.fetch = async (url, init) => { calls.push(`fetch:${init?.method ?? "GET"}:${url}`); if (fail && init?.method === "PUT") return { ok: false, status: 500, json: async () => ({}) }; return { ok: true, status: 200, json: async () => any(), text: async () => "{}" }; }; const m = load("B09_Estimation/B09_Estimation_UI_Shell.ts", common); calls.length = 0; m.renderB09Estimation(Object.assign(new Node(), { isRoot: true })); const step = async (i) => { calls.length = 0; layoutOpts.onStepClick(i); for (let k = 0; k < 5; k++) await flush(); return calls.filter((c) => !c.startsWith("toast")); }; const out = {}; out.beforeLoad = await (async () => { store.set("b09-estimation", { x: 1 }); return step(5); // 불러오기 전 — 저장 없이 이동 · 캐시는 남음 })(); for (let k = 0; k < 5; k++) await flush(); // GET 응답 반영 store.delete("b09-estimation"); out.clean = await step(5); store.set("b09-estimation", { x: 1 }); fail = true; out.fail = await step(5); fail = false; out.ok = await step(5); return out; } // 진짜 좌측 옵션 `save` — 캐시 없으면 요청 없이 참 · 1본 빈칸 = 거짓 · 요청 실패 = 거짓 async function options() { let fail = false; const m = load("B08_Quantity/B08_Quantity_UI_Options.ts", { ...common, "@ui/ui_template_elements": { ...common["@ui/ui_template_elements"], createInputField: () => ({ input: new Node("input"), root: new Node() }), }, "@ui/ui_template_collapsible": { attachCollapsible() {}, createPanelCard: () => new Node() }, "./B08_Quantity_UI_Hydro": { request: async (url, init) => { calls.push(`req:${init?.method ?? "GET"}`); if (fail && init?.method === "PUT") throw new Error("500"); return { one_pipe_length_m: 6, rock_ratios_pct: { 연암: 90 }, material_surcharge_pct: {} }; }, }, "./B08_Quantity_UI_Milestone_State": { saveMilestone: async () => calls.push("req:milestone"), }, }); const panel = m.createOptionsPanel("P1", () => calls.push("redraw")); await flush(); const run = async () => { calls.length = 0; const ok = await panel.save(); return [ok, calls.filter((c) => !c.startsWith("toast"))]; }; const out = {}; store.clear(); out.clean = await run(); store.set("b08-options", { soft_rock_pct: 80 }); out.changed = await run(); out.after = store.has("b08-options"); store.set("b08-milestone", { x: 1 }); out.milestoneOnly = await run(); store.set("b08-options", { one_pipe_length_m: null }); out.blankPipe = await run(); store.set("b08-options", { soft_rock_pct: 70 }); fail = true; out.fail = await run(); out.keptOnFail = store.has("b08-options"); store.clear(); return out; } (async () => { const r = { b08: await b08(), b09: await b09(), options: await options() }; console.log(JSON.stringify(r)); })().catch((e) => { console.error(e && e.stack ? e.stack : e); process.exit(1); });