"""196-1 B06 횡단 맨 위 알림 줄 — 칩 꼴 · 짧은 이름 + 수 · 긴 설명 = 호버 · 내용 칸 하나 공유. `B06_Section_UI_Cross_Notice_Chips.ts` 를 Node 로 그대로 돌림(작은 가짜 DOM · 호버는 받은 글만 기록). 줄 짜임(sticky · 열 수 칸 · 카드 scroll-margin)과 이름 · 설명 자리는 원문 대조. """ import json import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] TS = ROOT / "config" / "node_modules" / "typescript" B06 = ROOT / "B06_Section" MOD = B06 / "B06_Section_UI_Cross_Notice_Chips.ts" PRELUDE = f""" const fs = require("fs"); const ts = require({json.dumps(str(TS))}); const js = ts.transpileModule(fs.readFileSync({json.dumps(str(MOD))}, "utf8"), {{ compilerOptions: {{ module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }}, }}).outputText; class El {{ constructor(tag) {{ this.tag = tag; this.parentElement = null; this.children = []; this.on = {{}}; this.attrs = {{}}; this.dataset = {{}}; this.hidden = false; this.textContent = ""; this.cls = new Set(); const cls = this.cls; this.classList = {{ toggle: (c, on) => (on ? cls.add(c) : cls.delete(c)), contains: (c) => cls.has(c) }}; }} set className(v) {{ this.cls.clear(); v.split(/\\s+/).filter(Boolean).forEach((c) => this.cls.add(c)); }} get className() {{ return [...this.cls].join(" "); }} append(...cs) {{ cs.forEach((c) => {{ c.parentElement = this; this.children.push(c); }}); }} replaceChildren(...cs) {{ this.children = []; this.append(...cs); }} setAttribute(k, v) {{ this.attrs[k] = String(v); }} getAttribute(k) {{ return this.attrs[k] ?? null; }} addEventListener(t, f) {{ (this.on[t] ||= []).push(f); }} closest(sel) {{ for (let x = this; x; x = x.parentElement) if (sel.startsWith(".") ? x.cls.has(sel.slice(1)) : x.tag === sel) return x; return null; }} click() {{ const e = {{ type: "click", target: this }}; for (let x = this; x; x = x.parentElement) (x.on.click || []).forEach((f) => f(e)); }} }} const document = {{ createElement: (t) => new El(t) }}; const hovers = []; const fakeRequire = (name) => {{ if (name !== "@ui/ui_template_hover_info") throw new Error("뜻밖의 import " + name); return {{ attachHoverInfo: (root, text, sel) => hovers.push({{ root, text, sel }}) }}; }}; const mod = {{ exports: {{}} }}; new Function("exports", "module", "require", "document", js)(mod.exports, mod, fakeRequire, document); const picks = []; const bar = mod.exports.createNoticeChips((id) => picks.push(id)); const [chipsRow, panel] = bar.root.children; const chip = (key) => chipsRow.children.find((c) => c.dataset.noticeKey === key); const view = () => ({{ chips: chipsRow.children.map((c) => [c.textContent, c.getAttribute("aria-expanded"), c.className]), panelHidden: panel.hidden, panelInfo: panel.classList.contains("is-info"), rows: panel.children.map((b) => [b.textContent, b.dataset.stationId]), }}); const row = (id) => ({{ stationId: id, text: "STA " + id }}); const items = (fill, pave, berm) => [ {{ key: "fill", label: "성토 5m↑", hint: "긴 성토 설명", tone: "warn", rows: fill.map(row) }}, {{ key: "pave", label: "포장 권장", hint: "긴 포장 설명", tone: "warn", rows: pave.map(row) }}, {{ key: "berm", label: "소단 검토", hint: "긴 소단 설명", tone: "info", rows: berm.map(row) }}, ]; const out = {{}}; """ def _node(body: str): script = PRELUDE + body + "\nconsole.log(JSON.stringify(out));" got = subprocess.run( ["node", "-e", script], capture_output=True, text=True, encoding="utf-8", timeout=60 ) assert got.returncode == 0, got.stderr return json.loads(got.stdout) def test_칩_이름은_짧은_이름_더하기_측점_수_0이면_안_보임(): got = _node( """ bar.update(items(["a", "b", "c"], [], ["a", "d", "e", "f", "g"])); out.v = view(); out.bar = bar.root.className; out.tags = chipsRow.children.map((c) => [c.tag, c.type]); """ ) assert got["v"]["chips"] == [ ["성토 5m↑ 3", "false", "b06-section__notice-chip b06-section__notice-chip--warn"], ["소단 검토 5", "false", "b06-section__notice-chip b06-section__notice-chip--info"], ] assert got["tags"] == [["button", "button"], ["button", "button"]] assert got["v"]["panelHidden"] is True and got["v"]["rows"] == [] def test_긴_설명은_공용_호버로_칩마다(): got = _node( """ bar.update(items(["a"], ["b"], ["c"])); out.count = hovers.length; out.root = hovers[0].root === chipsRow; out.sel = hovers[0].sel; out.hints = chipsRow.children.map((c) => hovers[0].text(c)); out.other = hovers[0].text({ dataset: {} }) ?? null; """ ) assert got["count"] == 1 and got["root"] and got["sel"] == ".b06-section__notice-chip" assert got["hints"] == ["긴 성토 설명", "긴 포장 설명", "긴 소단 설명"] assert got["other"] is None def test_내용_칸_하나_한_번에_하나_다시_누르면_닫힘(): got = _node( """ bar.update(items(["a", "b"], ["p"], ["c"])); out.panels = bar.root.children.length; chip("fill").click(); out.open1 = view(); chip("berm").click(); out.open2 = view(); chip("berm").click(); out.closed = view(); """ ) assert got["panels"] == 2 # 칩 줄 + 내용 칸 하나 o1 = got["open1"] assert o1["panelHidden"] is False and o1["panelInfo"] is False assert o1["rows"] == [["STA a", "a"], ["STA b", "b"]] assert [c[1] for c in o1["chips"]] == ["true", "false", "false"] o2 = got["open2"] assert o2["rows"] == [["STA c", "c"]] and o2["panelInfo"] is True assert [c[1] for c in o2["chips"]] == ["false", "false", "true"] c = got["closed"] assert c["panelHidden"] is True and c["rows"] == [] assert [x[1] for x in c["chips"]] == ["false", "false", "false"] def test_측점_누르면_그_카드로_다시_그려도_연_칩_유지_0이면_닫힘(): got = _node( """ bar.update(items(["a", "b"], [], [])); chip("fill").click(); panel.children[1].click(); // 207-1 — 고르면 내용 칸 접힘 chipsRow.children[0].click(); chipsRow.children[0].click(); // 칩 누름은 측점 고름 아님 out.picks = picks.slice(); chip("fill").click(); bar.update(items(["a", "b", "x"], [], [])); out.kept = view(); bar.update(items([], ["p"], [])); out.gone = view(); """ ) assert got["picks"] == ["b"] assert got["kept"]["rows"] == [["STA a", "a"], ["STA b", "b"], ["STA x", "x"]] assert got["kept"]["chips"][0][:2] == ["성토 5m↑ 3", "true"] assert got["gone"]["panelHidden"] is True and got["gone"]["chips"][0][:2] == [ "포장 권장 1", "false", ] def test_알림_줄은_칩을_쓰고_긴_설명은_호버_짧은_이름은_사전(): src = (B06 / "B06_Section_UI_Cross_FillSlope_Notice.ts").read_text(encoding="utf-8") assert "