- 항목 = 짧은 이름 + 측점 수 칩(성토 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
196 lines
8.2 KiB
Python
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
|