"""PLAN 164 — B06 [상세] 그림 줌 · 팬(164-1) · 표 강조 붉은 한 벌(164-2). 짝: `B06_Section/B06_Section_UI_Cross_Detail.ts`(`attachDetailZoom` · 칸 누름) · `_UI_Style_Cross_Detail.css`(칸 비율 · 붉은 강조). 실제 TS 를 `helper_164_detail_zoom.cjs` 가 Node 로 돌린다. """ from __future__ import annotations import json import os import subprocess import pytest HERE = os.path.dirname(__file__) ROOT = os.path.join(HERE, "..", "..") B06 = os.path.join(ROOT, "B06_Section") pytestmark = pytest.mark.skipif( not os.path.isdir(os.path.join(ROOT, "config", "node_modules")), reason="프론트엔드 의존성 미설치", ) def _read(name: str) -> str: with open(os.path.join(B06, name), encoding="utf-8") as f: return f.read().replace(chr(13), "") @pytest.fixture(scope="module") def out() -> dict: proc = subprocess.run( # noqa: S603 — 고정 실행 파일 ["node", os.path.join(HERE, "helper_164_detail_zoom.cjs")], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def test_감싸지_않음_비율(out): # 179-3 — 복제를 g 하나로 감싸지 않음(축 · 눈금 · 도형 한 장 → 도형 층만) assert out["wrapped"] is False and out["children"] == 4 assert out.get("ratio") is None # 173-4 — 칸 = viewBox(칸 크기로 새로 그림) · 비율 변수 없음 assert out["before"] is None assert out["ticksBefore"] > 0 def test_축_고정_도형_층만_움직임_눈금_다시(out): # 179-3 — 팬 뒤 도형 층만 transform · 축 층 · clip 그룹 제자리 · 눈금 글은 보이는 범위로 assert out["plotTransform"] == "translate(100 20) scale(1)" assert out["axesTransform"] is None and out["clipTransform"] is None assert out["ticksChanged"] is True and out["ticksAfter"] > 0 assert out["restoredTicksShifted"] is True # 다시 떠도 이어 간 범위로 # 확대 — 십자는 중심(319,142 · 칸 가운데)에서 배율 되돌림 assert out["centerTransform"].startswith("translate(319 142) scale(0.869") assert out["noAxesHandle"] is True # 축 옵션 없음 = 줌만 assert out["noLayer"] is None # 도형 층 없음 = 줌 없음 def test_휠_밀면_확대_커서_고정(out): assert out["wheelIn"]["scale"] == pytest.approx(1.15) assert out["cursorFixed"] == pytest.approx(300) # 커서 아래 svg x 300 이 제자리 assert out["wheelOut"] < out["wheelIn"]["scale"] assert out["max"] == 16 def test_가운데_단추_팬_좌클릭은_팬_아님(out): assert out["leftDrag"] == {"scale": 1, "tx": 0, "ty": 0} # 1배에서도 팬 · 화면 200·40 px = svg 100·20(칸 2 배) assert out["middlePan"] == {"scale": 1, "tx": 100, "ty": 20} assert out["panClickStopped"] is True def test_다시_떠도_배율_유지_더블클릭_원복(out): assert out["restored"] == "translate(100 20) scale(1)" assert out["reset"] == "translate(0 0) scale(1)" def test_모달_배선_단추_칸_비율(): src = _read("B06_Section_UI_Cross_Detail.ts") css = _read("B06_Section_UI_Style_Cross_Detail.css") assert "wheelZoom: true" in src and "attachSvgZoomPan({" in src assert "cad.replaceChildren(clone, ...(handle ? [buildZoomControls(handle)] : []))" in src flat = " ".join(src.split()) assert ( "attachDetailZoom( clone, axes, zoom, (next) => (zoom = next), drawn.homeScale, )" in flat ) assert "layer.append(...clone.childNodes)" not in src # 감싸기 지움(179-3) assert 'clone.querySelector("g.b06-chart__plot-layer")' in src assert "createCrossAxes(axisLayer, axes)" in src view = _read("B06_Section_UI_Section_View.ts") assert "const plot = fitted && crossPlotFromBase(fitted, heightPx);" in view assert "return { svg, axes, homeScale };" in view assert "position: relative;" in css # 173-4 — 그림 = 칸 크기로 새로 그림 · svg 칸 가득 assert ".b06-detail__cad > svg {\n display: block;\n width: 100%;\n height: 100%;" in css assert "--ratio" not in css and "--ratio" not in src def test_강조_붉은_한_벌_고름_hover_다름_빈칸(): css = _read("B06_Section_UI_Style_Cross_Detail.css") for selector in ( ".b06-detail__cad .b06-chart__area.is-active > polygon {\n fill: color-mix(in srgb, var(--color-danger)", ".b06-detail__cad .b06-chart__design-hit.is-active {\n stroke: color-mix(in srgb, var(--color-danger)", ".b06-detail__cad .b06-chart__center.is-active line {\n stroke: var(--color-danger);", ".b06-detail__cad .b06-chart__spoil-fill.is-active {\n stroke: var(--color-danger);", ".b06-detail__qtable .is-picked {\n background: color-mix(in srgb, var(--color-danger) 18%", ".b06-detail__qtable [data-key][data-empty] {\n cursor: default;", ): assert selector in css, selector # 259-3 — 줄 물들임 · 칸마다 테두리 없앰(묶음 바깥 한 줄) assert "tr:has(.is-picked)" not in css assert "inset 0 0 0 1px var(--color-danger)" not in css assert "royal-amethyst" not in css and "--color-warning);" not in css.split("/* 그림 강조")[1] assert ( ".b06-detail__qtable [data-key]:hover {\n background: color-mix(in srgb, var(--color-danger) 8%" in css ) def test_빈칸_누름_없음_이름_칸_첫_키(): src = _read("B06_Section_UI_Cross_Detail.ts") assert "!present.has(el.dataset.key) && !tablePick.graph(el.dataset.key)" in src assert '(!el.dataset.key || el.hasAttribute("data-empty"))) return;' in src # 179-4 — 묶음 이름 칸 = 하위 줄만큼(양식 2줄 · 더한 줄만큼 늘어남) · 그 줄 첫 값 칸 키 · # 259-3 덮는 줄 키 모두(data-keys) flat = " ".join(src.split()) assert "nameCell( groupL, { row: groupSpan(layout.left, index) }, keyL, id, 0," in flat assert "nameCell( groupM, { row: groupSpan(layout.middle, index) }, keyM, id, 1," in flat def test_표_글자_14px_이름_칸_머리색_빈칸_값칸만(): # 179-3 — 표 글자 14px · 이름 칸(th) 바탕 = 데이터 표 머리색 · 빈 칸 바탕 지움은 값 칸(td)만 css = _read("B06_Section_UI_Style_Cross_Detail.css") table = css.split(".b06-detail__qtable {", 1)[1].split("}", 1)[0] assert "font-size: var(--text-body-sm);" in table assert ( "--b06-detail-head: color-mix(in srgb, var(--color-accent) 6%, var(--color-surface));" in table ) head = css.split(".b06-detail__qtable th {", 1)[1].split("}", 1)[0] assert "background: var(--b06-detail-head);" in head and "color: var(--color-text);" in head empty = css.split(".b06-detail__qtable [data-key][data-empty] {", 1)[1].split("}", 1)[0] assert "background" not in empty assert ".b06-detail__qtable td[data-key][data-empty] {\n background: none;" in css assert ( ".b06-detail__qtable th[data-key][data-empty] {\n background: var(--b06-detail-head);" in css ) theme = open( os.path.join(ROOT, "ui_template", "ui_template_theme.css"), encoding="utf-8" ).read() assert "--text-body-sm: 14px;" in theme