"""171 묶음 카드 시험 도우미 — TS 소스를 이어 붙여 Node(타입 지움)로 돌림 · 작은 가짜 DOM. 시험 파일 아님(test_ 머리 없음) — test_171_2 · test_171_3 · test_157_2 가 씀. """ import json import re import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) COMMON = "ui_template/ui_template_group_cards.ts" TABS = "B09_Estimation/B09_Estimation_UI_Tabs.ts" GROUPS = "B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts" # 기억(b_page_state) 대신 Map · 단추(createButton) 대신 가짜 button · 호버(attachHoverInfo) = 부른 자리 기록 · 157-3 원문번호 풍선(raw) 짝 STUBS = r""" const kept = new Map(); const readState = (name) => (kept.has(name) ? JSON.parse(kept.get(name)) : null); const writeState = (name, value) => kept.set(name, JSON.stringify(value)); const createButton = (o) => { const b = document.createElement("button"); b.className = `ui-btn ui-btn--${o.variant}`; b.textContent = o.label; if (o.onClick) b.addEventListener("click", o.onClick); return b; }; const hoverCalls = []; const attachHoverInfo = (root, text, selector) => { hoverCalls.push({ root, text, selector }); return () => {}; }; """ RAW = r""" const raw = (shown, codes) => (codes && codes !== shown ? `원문번호 ${String(codes)}` : null); const SOURCES = []; const openQuotes = () => {}; """ FAKE_DOM = r""" class E { constructor(tag) { this.tagName = tag.toUpperCase(); this.children = []; this.parent = null; this.dataset = {}; this.attrs = {}; this.cls = new Set(); this.textContent = ""; this.title = ""; this.on = {}; const props = {}; this.style = { props, setProperty: (k, v) => { props[k] = v; } }; const self = this; this.classList = { add: (...c) => c.forEach((x) => self.cls.add(x)), remove: (...c) => c.forEach((x) => self.cls.delete(x)), contains: (c) => self.cls.has(c), toggle: (c, force) => { const on = force === undefined ? !self.cls.has(c) : force; on ? self.cls.add(c) : self.cls.delete(c); return on; }, }; } get className() { return [...this.cls].join(" "); } set className(v) { this.cls = new Set(String(v).split(/\s+/).filter(Boolean)); } append(...xs) { for (const x of xs) { x.parent = this; this.children.push(x); } } replaceChildren(...xs) { this.children = []; this.append(...xs); } prepend(...xs) { for (const x of xs.reverse()) { x.parent = this; this.children.unshift(x); } } addEventListener(t, f) { (this.on[t] ??= []).push(f); } setAttribute(k, v) { this.attrs[k] = String(v); } scrollIntoView() { this.scrolled = true; } matches(sel) { if (sel.includes(",")) return sel.split(",").some((s) => this.matches(s.trim())); const tag = sel.match(/^([a-z]+)([.[].*)$/); // tr.is-pick · tr[data-i="3"] if (tag) return this.matches(tag[1]) && this.matches(tag[2]); if (sel.startsWith(".")) return this.cls.has(sel.slice(1)); const m = sel.match(/^\[data-([\w-]+)(?:="(.*)")?\]$/); const k = m ? m[1].replace(/-(\w)/g, (_, c) => c.toUpperCase()) : ""; if (m) return m[2] === undefined ? k in this.dataset : this.dataset[k] === m[2]; return this.tagName === sel.toUpperCase(); } closest(sel) { for (let e = this; e; e = e.parent) if (e.matches(sel)) return e; return null; } all(sel, out = []) { for (const c of this.children) { if (c.matches(sel)) out.push(c); c.all(sel, out); } return out; } querySelector(sel) { return this.all(sel)[0] ?? null; } querySelectorAll(sel) { return this.all(sel); } get isConnected() { return true; } contains(x) { for (let e = x; e; e = e.parent) if (e === this) return true; return false; } get text() { return this.textContent + this.children.map((c) => c.text).join(""); } click() { const event = { target: this }; for (let e = this; e; e = e.parent) (e.on.click ?? []).forEach((f) => f(event)); } } const docKeys = []; // 248-1 문서 keydown — key(이름, 대상) 로 부름 globalThis.document = { createElement: (t) => new E(t), addEventListener: (t, f) => t === "keydown" && docKeys.push(f) }; const key = (k, target = null) => docKeys.forEach((f) => f({ key: k, target })); globalThis.requestAnimationFrame = (f) => f(); """ def run(files, js_body, prelude=""): """`files` 차례로 이어 붙인 소스(import 지움) + 가짜 DOM + `js_body` → stdout JSON.""" src = "".join(IMPORTS.sub("", (ROOT / f).read_text("utf-8")) for f in files) result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript"], input=FAKE_DOM + STUBS + prelude + src + js_body, capture_output=True, text=True, encoding="utf-8", timeout=60, ) # fmt: skip assert result.returncode == 0, result.stderr return json.loads(result.stdout)