# -*- coding: utf-8 -*- """215-1 B05 종단 [자유지정] — 범위 고르고 클릭으로 계획선 잇기. 계산 · 상태(`A00_Common/design/logic/design_profile_free.ts`)는 Node 헬퍼 (`helper_215_1_free_profile.cjs`)가 트랜스파일해 돌리고, 화면 배선(도구줄 단추 · 오버레이 · 키 · 적용 길)은 소스로 본다. """ import json import os import subprocess from pathlib import Path import pytest HERE = Path(__file__).resolve().parent ROOT = HERE.parent.parent def _read(relative: str) -> str: return (ROOT / relative).read_text(encoding="utf-8") @pytest.fixture(scope="module") def out(): proc = subprocess.run( ["node", os.path.join(HERE, "helper_215_1_free_profile.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): assert out["yAt103_2"] == pytest.approx(121.6) assert out["zBack"] == pytest.approx(103.2) assert out["parsed"] == {"scale": 1.2, "offset": -20} assert out["parsedNone"] == {"scale": 1, "offset": 0} def test_세로선_붙음_범위_안만(out): assert out["snap79"] == 80 assert out["snapOutside"] == 160 def test_범위_고르기_강조(out): assert out["pendingEdges"] == [40] assert out["rangePhase"] == "armed" assert out["band"] == [40, 160] assert out["stillArmed"] == "armed" # 가운데 클릭은 시작이 아님 def test_시작_이음_지금_계획고(out): assert out["startPhase"] == "drawing" assert out["startDot"] == {"chainage_m": 40, "elevation_m": pytest.approx(100.6)} def test_클릭_점_세로선_붙음_높이(out): assert out["point1"] == {"chainage_m": 80, "elevation_m": pytest.approx(103.46)} assert out["dotsAfterBack"] == 2 # 뒤로 가는 클릭은 무시 def test_점선_미리_보기_기울기_글(out): first, last = out["preview"] assert (first["preview"], last["preview"]) == (False, True) assert last["to"] == {"chainage_m": 120, "elevation_m": pytest.approx(101.2)} assert first["grade"] == pytest.approx(7.15) assert last["grade"] == pytest.approx(-5.65) assert not first["over"] and not last["over"] def test_기준_넘음_빨강(out): assert out["steep"]["over"] is True assert abs(out["steep"]["grade"]) > 14 def test_끝_이음과_적용_편집(out): assert out["finishPreview"] == {"chainage_m": 160, "elevation_m": pytest.approx(102.4)} assert out["appliedCount"] == 1 and out["closed"] == 1 and out["afterPhase"] == "range" assert out["offsetKeys"] == ["100.000", "120.000", "140.000", "60.000", "80.000"] pvi = out["pvi"] assert pvi["40"] == pytest.approx(out["ground"]["40"]) # 양 끝 그대로 assert pvi["160"] == pytest.approx(out["ground"]["160"]) assert pvi["80"] == pytest.approx(103.46) assert pvi["120"] == pytest.approx(101.2) assert pvi["60"] == pytest.approx((100.6 + 103.46) / 2) # 점 사이 = 직선 assert pvi["140"] == pytest.approx((101.2 + 102.4) / 2) assert out["outsideUntouched"] is True def test_종단곡선_자동(out): assert out["radiiKeys"] == ["120.000", "80.000"] assert out["r80"] == pytest.approx(out["expectR80"]) assert out["curve80L"] == pytest.approx(20) # 정책 기본 길이 L def test_Enter_끝_거꾸로_시작(out): assert out["reverseAnchor"] == 160 assert out["enter"] is True and out["enterApplied"] == 1 pvi = out["enterPvi"] assert pvi["40"] == pytest.approx(100.6) and pvi["160"] == pytest.approx(102.4) assert pvi["100"] == pytest.approx(104) assert pvi["60"] == pytest.approx(100.6 + (104 - 100.6) / 3, abs=1e-5) def test_Backspace_와_Esc_취소(out): assert out["afterBackspace"] == [40, 80] assert out["backspaceToArmed"] == "armed" assert out["escape"] is True assert out["escApplied"] == 0 and out["escClosed"] == 1 assert out["escPhase"] == "range" and out["escDots"] == 0 assert out["enterOutsideDrawing"] is False def test_도구줄_직선화_오른쪽_자유지정(): tools = _read("B05_Profile/B05_Profile_UI_Profile_Tools.ts") assert '"자유지정"' in tools assert "wrap.append(reset, undo, redo, straighten, free, shift, up, down)" in tools assert 'className = "b05-route-profile__tool"' in tools # 같은 단추 양식 def test_적용은_직선화와_같은_길_키_배선(): panel = _read("B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts") assert "apply: (next) => ctx.applyEdits(next)" in panel assert 'tools.mode() === "free" && free.key(key)' in panel profile = _read("A00_Common/design/design_profile.ts") # applyEdits = 캐시(store.replace) + 되돌리기 기록 — 자동저장 없음 assert "store.replace(next);\n history.record();" in profile render = _read("B05_Profile/B05_Profile_UI_Profile_Render.ts") assert "ctx.tools.mountOverlay(" in render def test_오버레이_점선_빨강_글(): view = _read("A00_Common/design/design_profile_free_view.ts") assert "is-preview" in view and "is-over" in view assert "grade_pct.toFixed(1)}%" in view css = _read("B05_Profile/B05_Profile_UI_Style_Table.css") assert ".b05-profile-freemark__line {" in css and "stroke-dasharray: 6 4" in css assert ".b05-profile-freemark__line.is-over" in css def test_줄_수_늘리지_않음(): for relative in ( "A00_Common/design/design_profile.ts", "A00_Common/design/logic/design_profile_free.ts", "A00_Common/design/design_profile_free_view.ts", "A00_Common/design/design_profile_overlays.ts", "A00_Common/design/design_profile_viewport.ts", ): assert len(_read(relative).splitlines()) <= 700 # 소스 700줄 초과 금지(213-3)