Files
Aislo/resources/tester/test_215_1_free_profile.py
T
eomsangdonandClaude Opus 5.5 04dd53e5e6 fix(B05): 367-4 자유지정 선 · 미리보기 · 점 녹색 · OS 자동 어두움에 success · danger 토큰
- freemark 선 · 점 stroke = --color-success · 넘음 빨강 · 범위 띠 · 경계 보라 그대로
- ui_template_theme OS 자동 어두움 블록에 --color-success · --color-danger 채움
- 시험 test_215_1 색 확인

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
2026-10-17 11:54:49 +09:00

208 lines
8.7 KiB
Python

# -*- 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_자유지정_되돌리기_다시하기(out):
# 367-3 ⑥ 점 둘 → ↶ → ↷ → 새 클릭이 ↷ 지움 · 큰 이력(apply · close) 안 건드림
assert out["undoEmpty"] == {"can": False, "backspace": False}
assert out["undo1"] == {"dots": [40, 80], "canRedo": True}
assert out["redo1"] == {"dots": [40, 80, 120], "canRedo": False}
assert out["newClick"] == {"dots": [40, 80, 100], "canRedo": False}
# 한 걸음 = 점 · 시작 · 범위 끝 · 범위 첫 · 같은 세로선 다시 누름은 안 쌓임
assert out["undoAll"] == ["drawing:2", "drawing:2", "armed:2", "range:1", "range:0"]
assert out["undoApplied"] == 0 and out["undoClosed"] == 0
assert out["redoTwice"] == {"phase": "armed", "band": [40, 160]}
assert out["resetClears"] == {"undo": False, "redo": False}
def test_적용_막히면_열린_채(out):
# 367-3 ⑦ Enter · 끝 세로선 둘 다 apply 시도(2) · close 없음 · 점 그대로
assert out["blocked"] == {"tries": 2, "phase": "drawing", "dots": 2, "canUndo": True}
def test_되돌리기_배선_자유지정_먼저():
panel = _read("B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts")
assert 'tools.mode() === "free" ? (free.undo(), freeView.redraw()) : inner.undo()' in panel
assert 'tools.mode() === "free" ? free.canUndo() : inner.canUndo()' in panel
keys = panel[panel.index('isCtrlKey(event, "z") || isCtrlKey(event, "y")') :]
free_at = keys.index('if (tools.mode() === "free") {')
assert (
free_at < keys.index("history.undo()")
and "return;" in keys[free_at : keys.index("flushHold();")]
)
profile = _read("A00_Common/design/design_profile.ts")
assert "function applyEdits(next: AlignmentEdits): boolean {" in profile
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 _css_rule(css: str, selector: str) -> str:
at = css.index(selector + " {")
return css[at : css.index("}", at)]
def test_자유지정_녹색_넘음_빨강_띠_보라():
# 367-4 선 · 미리 보기 · 점 = --color-success · 넘음 빨강 · 범위 띠 · 경계 보라 그대로
css = _read("B05_Profile/B05_Profile_UI_Style_Table.css")
assert "stroke: var(--color-success);" in _css_rule(css, ".b05-profile-freemark__line")
assert "stroke: var(--color-success);" in _css_rule(css, ".b05-profile-freemark__dot")
assert "var(--color-danger" in _css_rule(css, ".b05-profile-freemark__line.is-over")
assert "var(--color-danger" in _css_rule(css, ".b05-profile-freemark__grade.is-over")
assert "--color-primary" in _css_rule(css, ".b05-profile-freemark__band")
assert "--color-primary" in _css_rule(css, ".b05-profile-freemark__edge")
# 밝음 · 어두움(명시 · OS 자동) 모두 success · danger 토큰 있음
theme = _read("ui_template/ui_template_theme.css")
auto = theme[theme.index("@media (prefers-color-scheme: dark)") :]
auto = auto[: auto.index("\n}\n")]
for block in (_css_rule(theme, ":root"), _css_rule(theme, ':root[data-theme="dark"]'), auto):
assert "--color-success:" in block and "--color-danger:" in block
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)