- _RouteEdit_View.fitView(처음 열기 · [맞춤] 한 벌) · _RouteEdit_Cover.onCoverChange(덮인 높이 바뀜 알림 · 떠난 편집기는 뗌) - 시험: test_345_5b_plan_visible_area 3 · 관련 81 통과 · typecheck 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
151 lines
6.5 KiB
Python
151 lines
6.5 KiB
Python
"""345-5 덧 — 노선 편집 평면의 보이는 영역(뜬 종단 판 위) · 노선 맞춤 · 글판 · 곡선 바 자리.
|
||
|
||
`logic/B05_Profile_RouteEdit_Visible.ts` 를 tsc 로 옮겨 node 로 돌림 · 배선 · CSS 는 글로 봄.
|
||
./venv/Scripts/python.exe -m pytest resources/tester/test_345_5b_plan_visible_area.py -q
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import subprocess
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
B05 = ROOT / "B05_Profile"
|
||
TS_FILE = B05 / "logic" / "B05_Profile_RouteEdit_Visible.ts"
|
||
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||
|
||
# 도엽 1000 × 800 m · 칸 1200 × 900 px · 노선 가운데 (500, 400) · 크기 300 × 200 m(+여유 200 m)
|
||
_FIT = {
|
||
"mapWidthM": 1000,
|
||
"mapHeightM": 800,
|
||
"width": 1200,
|
||
"height": 900,
|
||
"centerXM": 500,
|
||
"centerYM": 400,
|
||
"wantWidthM": 700,
|
||
"wantHeightM": 600,
|
||
}
|
||
|
||
_RUNNER = """
|
||
import { writeFileSync } from "node:fs";
|
||
import * as visible from "./B05_Profile/logic/B05_Profile_RouteEdit_Visible.js";
|
||
const { coveredBottomPx, fitRouteToVisible } = visible;
|
||
const fit = JSON.parse(process.argv[3]);
|
||
const rect = { x: 37.5, y: 0, width: 1125, height: 900 };
|
||
const at = (coveredPx) => fitRouteToVisible({ ...fit, mapRect: rect, coveredPx });
|
||
writeFileSync(process.argv[2], JSON.stringify({
|
||
cover: [
|
||
coveredBottomPx({ top: 0, bottom: 900 }, null),
|
||
coveredBottomPx({ top: 0, bottom: 900 }, { top: 360, bottom: 900 }),
|
||
coveredBottomPx({ top: 0, bottom: 900 }, { top: 500, bottom: 900 }),
|
||
coveredBottomPx({ top: 0, bottom: 900 }, { top: 899, bottom: 900 }),
|
||
coveredBottomPx({ top: 0, bottom: 900 }, { top: -50, bottom: 900 }),
|
||
coveredBottomPx({ top: 0, bottom: 900 }, { top: 950, bottom: 1300 }),
|
||
],
|
||
none: at(0),
|
||
half: at(450),
|
||
most: at(880),
|
||
}));
|
||
"""
|
||
|
||
|
||
def _run(tmp_path: Path) -> dict:
|
||
out = tmp_path / "js"
|
||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||
[
|
||
"node",
|
||
str(TSC),
|
||
str(TS_FILE),
|
||
"--outDir",
|
||
str(out),
|
||
"--rootDir",
|
||
str(ROOT),
|
||
"--module",
|
||
"esnext",
|
||
"--target",
|
||
"es2022",
|
||
"--moduleResolution",
|
||
"bundler",
|
||
"--ignoreConfig",
|
||
],
|
||
cwd=str(ROOT),
|
||
check=True,
|
||
capture_output=True,
|
||
)
|
||
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
|
||
result = tmp_path / "out.json"
|
||
subprocess.run( # noqa: S603
|
||
["node", str(out / "runner.mjs"), str(result), json.dumps(_FIT)],
|
||
cwd=str(ROOT),
|
||
check=True,
|
||
capture_output=True,
|
||
)
|
||
return json.loads(result.read_text(encoding="utf-8"))
|
||
|
||
|
||
def _screen_center(fit: dict) -> tuple[float, float]:
|
||
"""노선 가운데의 화면 자리 — B04 `createMetricProjector` 와 같은 식."""
|
||
rect = {"x": 37.5, "y": 0, "width": 1125, "height": 900}
|
||
s = fit["scale"]
|
||
nx = _FIT["centerXM"] / _FIT["mapWidthM"]
|
||
ny = 1 - _FIT["centerYM"] / _FIT["mapHeightM"]
|
||
x = rect["x"] * s + _FIT["width"] / 2 * (1 - s) + fit["offsetX"] + nx * rect["width"] * s
|
||
y = rect["y"] * s + _FIT["height"] / 2 * (1 - s) + fit["offsetY"] + ny * rect["height"] * s
|
||
return x, y
|
||
|
||
|
||
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
|
||
def test_덮인_높이와_보이는_영역_맞춤(tmp_path: Path) -> None:
|
||
out = _run(tmp_path)
|
||
# 판 없음 · 판 높이 540 · 400 · 접힘(테두리만) · 칸보다 큼(칸 높이까지) · 칸 밖
|
||
assert out["cover"] == [0, 540, 400, 0, 900, 0]
|
||
# 덮인 높이 0 = B04 computeRouteView 와 같음(배율 = min(1000/700, 800/600) · 가운데 = 칸 가운데)
|
||
none = out["none"]
|
||
assert none["scale"] == pytest.approx(min(1000 / 700, 800 / 600))
|
||
assert _screen_center(none) == pytest.approx((600, 450))
|
||
# 아래 반을 덮음 — 노선 가운데 = 보이는 영역(0 ~ 450) 가운데 · 노선 높이가 그 영역 안
|
||
half = out["half"]
|
||
assert _screen_center(half) == pytest.approx((600, 225))
|
||
route_px = _FIT["wantHeightM"] * (900 / 800) * half["scale"]
|
||
assert route_px <= 450 + 1e-6
|
||
# 거의 다 덮음 — 남은 20 px 에 들게 줄임 · 가운데는 남은 20 px 가운데
|
||
most = out["most"]
|
||
assert most["scale"] == pytest.approx((800 / 600) * (20 / 900))
|
||
assert _screen_center(most)[1] == pytest.approx(10)
|
||
|
||
|
||
def test_배선과_글판_자리() -> None:
|
||
view = (B05 / "B05_Profile_UI_RouteEdit_View.ts").read_text(encoding="utf-8")
|
||
assert "coveredPx: coveredPx()," in view and "computeRouteView(" not in view
|
||
assert 'wrap.classList.contains("b05-routeedit__canvas-wrap")) watchCover(wrap);' in view
|
||
cover = (B05 / "B05_Profile_UI_RouteEdit_Cover.ts").read_text(encoding="utf-8")
|
||
assert 'querySelector<HTMLElement>(".b05-route-profile")' in cover
|
||
assert 'setProperty("--b05-routeedit-cover"' in cover
|
||
css = (B05 / "B05_Profile_UI_Style_RouteEdit.css").read_text(encoding="utf-8")
|
||
# 상태 · 범례 · 잰 값 글판 = 판 바로 위(접으면 덮인 높이 0 = 원래 자리)
|
||
assert css.count("bottom: calc(var(--spacing-12) + var(--b05-routeedit-cover, 0px));") == 2
|
||
# 접힌 판은 높이 0 — 덮인 높이도 0 이 됨
|
||
style = (B05 / "B05_Profile_UI_Style.css").read_text(encoding="utf-8")
|
||
assert ".b05-route-profile.is-collapsed {\n height: 0;" in style
|
||
|
||
|
||
def test_곡선_바는_판_위_맞춤_단추():
|
||
"""345-5 덧 2 — 곡선 바 아래 한계 = 판 바로 위 · 판 끌기 · 접기 따라감 · [맞춤] 단추."""
|
||
bar = (B05 / "B05_Profile_UI_RouteEdit_CurveBar.ts").read_text(encoding="utf-8")
|
||
assert "bottom: rect.bottom - 8 - coveredPx()," in bar
|
||
assert "onCoverChange(() => (params.container.isConnected ? (sync(), true) : false));" in bar
|
||
cover = (B05 / "B05_Profile_UI_RouteEdit_Cover.ts").read_text(encoding="utf-8")
|
||
assert "if (covered !== before)" in cover and "listeners.delete(listener)" in cover
|
||
chrome = (B05 / "B05_Profile_UI_RouteEdit_Chrome.ts").read_text(encoding="utf-8")
|
||
assert 'data-act="fit"' in chrome and ">맞춤</button>" in chrome
|
||
editor = (B05 / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
|
||
at = editor.index("""overlay.querySelector('[data-act="fit"]')!""")
|
||
handler = editor[at : at + 260]
|
||
assert "view = fitView(meta, view, planned);" in handler and "rotation.set(0);" in handler
|
||
assert len(editor.splitlines()) <= 700
|
||
view = (B05 / "B05_Profile_UI_RouteEdit_View.ts").read_text(encoding="utf-8")
|
||
assert "return { view: fitView(meta, view, nodes), rotationRad: 0 };" in view
|