/* 191-1 좌측 세부 컨테이너 누름 · 초점 = 형상 고름 — 실제 `wireStructureSelection` 을 그 자리에서 * 트랜스파일해 흉내 DOM 에서 돌림. 고름(select)이 폼을 다시 그리는 것까지 흉내. 짝 = `test_191_1_box_press_pick.py`. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); class FakeNode { constructor(tag, dataset = {}) { this.tagName = tag.toUpperCase(); this.dataset = dataset; this.children = []; this.parentElement = null; this._listeners = {}; } append(...kids) { kids.forEach((k) => { k.parentElement = this; this.children.push(k); }); } replaceChildren(...kids) { this.children.forEach((k) => (k.parentElement = null)); this.children = []; this.append(...kids); } addEventListener(type, fn) { (this._listeners[type] ??= []).push(fn); } matches(sel) { return sel.split(",").some((s) => { const m = s.trim().match(/^(\w+)?(?:\[data-([\w-]+)(?:="([^"]*)")?\])?$/); if (!m) return false; const [, tag, attr, value] = m; if (tag && this.tagName !== tag.toUpperCase()) return false; if (!attr) return true; const key = attr.replace(/-(\w)/g, (_, c) => c.toUpperCase()); return value === undefined ? this.dataset[key] !== undefined : this.dataset[key] === value; }); } closest(sel) { for (let n = this; n; n = n.parentElement) if (n.matches?.(sel)) return n; return null; } contains(node) { for (let n = node; n; n = n.parentElement) if (n === this) return true; return false; } get isConnected() { return panelRoot.contains(this); } querySelectorAll(sel) { const found = []; const walk = (n) => n.children.forEach((k) => { if (k.matches(sel)) found.push(k); walk(k); }); walk(this); return found; } focus() { globalThis.document.activeElement = this; } } // 흉내 이벤트 — 대상에서 뿌리까지 거품 function fire(target, type) { for (let n = target; n; n = n.parentElement) (n._listeners[type] ?? []).forEach((fn) => fn({ type, target })); } globalThis.document = { activeElement: null }; const timers = []; globalThis.setTimeout = (fn) => timers.push(fn); const flush = () => { while (timers.length) timers.shift()(); }; globalThis.queueMicrotask = (fn) => fn(); // 좌측 폼 — 상자 넷(유입 · 유출 · 관 · 흙막이 둘) · 칸마다 data-spec-item 입력 하나 const BOXES = [ ["inlet", undefined, "inlet.kind"], ["outlet", undefined, "outlet.tiers"], ["pipe", undefined, "pipe.diameter"], ["soil_add", "0", "soil.height"], ["soil_add", "1", "soil.height"], ]; const panelRoot = new FakeNode("div"); let paints = 0; function paint() { paints += 1; panelRoot.replaceChildren( ...BOXES.map(([id, index, item]) => { const box = new FakeNode("fieldset", { specContainer: id, specIndex: index }); const legend = new FakeNode("legend"); const row = new FakeNode("label", { specItem: item }); const input = new FakeNode("input"); row.append(input); box.append(legend, row); return box; }), ); } paint(); const boxOf = (id, index) => panelRoot .querySelectorAll("fieldset") .find((b) => b.dataset.specContainer === id && b.dataset.specIndex === index); const inputOf = (id, index) => boxOf(id, index).querySelectorAll("input")[0]; const CH = 120; const SECTION = { chainage_m: CH, culvert: {} }; let highlight = null; const log = []; const stationControls = { structureSpan: { ownerOf: () => SECTION }, inletStructure: { set: () => {}, valueFor: () => "auto" }, revetOffset: { select: (chainageM, key) => { highlight = key; log.push(["select", chainageM, key]); }, highlightFor: () => highlight, }, ford: { select: () => {} }, box: { select: () => {} }, }; const panel = { root: panelRoot, currentChainage: () => CH, showAtChainage: () => paint(), // 고름이 폼을 다시 그림 — 누른 칸이 사라짐 releasePipe: () => {}, facility: { onInletStructureChange: () => {}, setInletStructure: () => {} }, }; function loadSelection() { const file = path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Structures_Selection.ts"); const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; const stubs = { "./B06_Section_UI_Adjust_Dock": { refreshAdjustSlots: () => {} } }; new Function("exports", "module", "require", js)(box.exports, box, (r) => stubs[r]); return box.exports; } loadSelection().wireStructureSelection( stationControls, () => ({ cross_sections: [SECTION] }), panel, (chainageM, key) => log.push(["remember", chainageM, key]), (chainageM) => log.push(["card", chainageM]), (chainageM) => log.push(["reveal", chainageM]), ); const reset = () => { highlight = null; log.length = 0; document.activeElement = null; }; // 마우스 누름 — pointerdown · (칸이면 초점) · click function press(target, focus = false) { fire(target, "pointerdown"); if (focus) { target.focus(); fire(target, "focusin"); } fire(target, "click"); flush(); } const out = {}; // 값 바꿈(지금 길) — 기준 reset(); inputOf("inlet").focus(); fire(inputOf("inlet"), "change"); out.change = { highlight, log: [...log] }; // 빈 곳 · 제목 누름 = 같은 결과 reset(); press(boxOf("inlet")); out.pressBox = { highlight, log: [...log] }; reset(); press(boxOf("inlet").children[0]); out.pressLegend = { highlight, log: [...log] }; // 입력 칸 누름(초점) — 고름 + 다시 그린 새 칸에 초점 reset(); const before = paints; const old = inputOf("pipe"); press(old, true); out.pressInput = { highlight, repainted: paints > before, oldConnected: old.isConnected, focusSame: document.activeElement === inputOf("pipe"), }; // 키보드(Tab) 초점 = 고름 · 다른 상자 누르면 바뀜 reset(); const tabbed = inputOf("soil_add", "1"); fire(tabbed, "keydown"); tabbed.focus(); fire(tabbed, "focusin"); flush(); out.tab = { highlight, focusSame: document.activeElement === inputOf("soil_add", "1") }; press(boxOf("soil_add", "0")); out.switched = highlight; press(boxOf("outlet")); out.switched2 = highlight; // 누름이 준 초점은 click 전엔 안 고름(누른 칸을 click 전에 지우지 않음) reset(); const target = inputOf("pipe"); fire(target, "pointerdown"); target.focus(); fire(target, "focusin"); flush(); out.focusBeforeClick = { highlight, connected: target.isConnected }; // 단 수 칸 누름 = 값 바꿀 때처럼 안 고름 reset(); press(inputOf("outlet"), true); out.tiers = highlight; // 같은 상자 거듭 누름 = 한 번만 고름 · 한 번만 넘김 reset(); press(boxOf("inlet")); press(boxOf("inlet")); out.twice = log.filter((e) => e[0] === "select" || e[0] === "reveal").length; process.stdout.write(JSON.stringify(out));