Files
Aislo/resources/tester/helper_191_1_box_press_pick.cjs
T
eomsangdonandClaude Opus 5.5 50b45314a8 feat(B06): 좌측 세부 컨테이너 누름 · 초점 = 그 구조물 형상 고름(191-1)
- 값 바꿀 때 고름 길(pickAt)을 click · 키보드 초점에도 씀 — 칸 제 동작 뒤 setTimeout
- 고름이 폼을 다시 그려 초점 칸이 사라지면 같은 이름 새 칸에 초점 되살림
- 시험 test_191_1_box_press_pick + 흉내 DOM 헬퍼

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
2026-10-10 13:22:45 +09:00

234 lines
7.0 KiB
JavaScript

/* 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));