Files
Aislo/resources/tester/test_196_1_b06_notice_chips.py
T
eomsangdonandClaude Opus 5.5 6e8692d133 feat(B06): 횡단 알림 줄 칩 꼴 · 내용 칸 하나 공유 · 긴 설명 = 호버 (196-1)
- 항목 = 짧은 이름 + 측점 수 칩(성토 5m↑ · 포장 권장 · 소단 검토) · 0 이면 숨김
- 펼침 = 줄 아래 내용 칸 하나(누른 칩 목록만 · 다시 누르면 닫힘 · 한 번에 하나)
- 긴 법령 설명 · 측점 없는 구조물 줄 까닭 글 = 공용 호버
- sticky · 열 수 칸 · 카드 scroll-margin 그대로 · 새 시험 test_196_1

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

196 lines
8.2 KiB
Python

"""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();
chipsRow.children[0].click(); chipsRow.children[0].click(); // 칩 누름은 측점 고름 아님
out.picks = picks.slice();
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 "<details" not in src and '"details"' not in src and "summary" not in src
assert "createNoticeChips(onPick)" in src
for label, hint in (
('L("B06_Notice_FillSlope")', "FILL_SLOPE_WARN_TEXT"),
('L("B06_Notice_PaveSuggested")', 'L("B06_Design_Paved_Suggested")'),
('L("B06_Notice_Berm")', "BERM_INFO_TEXT"),
):
assert f"label: {label}" in src and f"hint: {hint}" in src
loc = (ROOT / "ui_template" / "ui_template_locale_b2.ts").read_text(encoding="utf-8")
assert 'B06_Notice_FillSlope: ["성토 5m↑"' in loc
assert 'B06_Notice_PaveSuggested: ["포장 권장"' in loc
assert 'B06_Notice_Berm: ["소단 검토"' in loc
def test_sticky_열_수_칸_카드_scroll_margin_그대로():
view = (B06 / "B06_Section_UI_Section_View.ts").read_text(encoding="utf-8")
assert "fillSlopeNotice.root.append(columnsField.root)" in view
assert '"--b06-notice-h"' in view
css = (B06 / "B06_Section_UI_Style_Cross.css").read_text(encoding="utf-8")
bar = css[css.index(".b06-section__notice-bar {") :]
assert "position: sticky" in bar[: bar.index("}")]
assert "scroll-margin-top: calc(var(--b06-notice-h" in css
assert ".b06-section__notice-list[hidden]" in css and "max-height: 30vh" in css
def test_측점_없는_구조물_줄_긴_까닭은_호버():
src = (B06 / "B06_Section_UI_Missing_Stations.ts").read_text(encoding="utf-8")
assert '". 이 자리는' not in src
assert 'attachHoverInfo(text, "이 자리는 횡단도에도 안 서고 수량에서도 빠집니다.")' in src