Files
Aislo/resources/tester/helper_b09_group_cards.py
T

96 lines
4.1 KiB
Python

"""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 · 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;
};
"""
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.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; }
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)); }
}
globalThis.document = { createElement: (t) => new E(t) };
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)