"""PLAN 505-2 B05 · B06 저장 · 고름 흠(요구 309-3 · C1 1 · 2b · 3 · 4d · 7 · 8 · 9). 못박는 것 - 1 확정 · 노선 아닌 가림막 저장 도중 고침도 조각 정리 건너뜀 + 「저장 중 고친 값」 알림 (키는 안 막음 · B07~B09 안 번짐). - 2b 옛 세션 초안 간격 0.1 · 1 m = null — 서버 `load_station_snap_step` 과 거울. - 3 추가 측점 = 측점 묶음 `data-plus` — 고른 뒤(선 클래스 --selected)에도 B05 누름 판정이 읽음. - 4d 숨은 동안 고른 카드 = 다음 다시 그림 끝에 밂 · 사용자 휠 · 누름이면 버림. - 7 횡단 · 종단 보기가 건 set 넷 + 여는 이 = 풀개를 onDispose 에 — 자기 값일 때만 비움. - 8 저장 중 [초기화] = [저장]과 같은 알림 + 막음 · 서버 /route/reset = 프로젝트 잠금 안. - 9 = `test_398_1_reveal_user_pick.py` busyPick 1. ./venv/Scripts/python.exe -m pytest resources/tester/test_505_2_save_pick.py -q """ from __future__ import annotations import asyncio import json import subprocess import sys from pathlib import Path from uuid import uuid4 import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) HELPER = Path(__file__).resolve().parent / "helper_505_2_save_pick.cjs" DESIGN = "A00_Common/design/" def _read(rel: str) -> str: return (ROOT / rel).read_text(encoding="utf-8") @pytest.fixture(scope="module") def out() -> dict: run = subprocess.run( # noqa: S603, S607 — 고정 실행 파일 ["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", timeout=60 ) assert run.returncode == 0, run.stderr return json.loads(run.stdout) def _server_step(tmp_path: Path, step: float | None) -> float | None: from B05_Profile.B05_Profile_Engine_Sections import load_station_snap_step from common_util.common_util_route_geometry import planned_route_working_path path = planned_route_working_path(tmp_path) path.parent.mkdir(parents=True, exist_ok=True) path.write_text("x,y\n0,0\n10,0\n", encoding="utf-8") curves = {"curves": [], "station_snap": True, "station_snap_step": step} path.with_name(f"{path.stem}_curves.json").write_text(json.dumps(curves), encoding="utf-8") return load_station_snap_step(tmp_path) def test_2b_초안_간격_서버와_거울(out, tmp_path) -> None: steps = [10, 0.1, 1, 20, None] assert ( out["draft"] == [_server_step(tmp_path, s) for s in steps] == [10, None, None, None, None] ) assert out["saved"] == [10, None, None, None] assert out["snapKept"] is True # 스냅 켬은 그대로 · 간격만 측점 No. def test_7_풀개는_자기_값만(out) -> None: assert out["afterRelease"] == 0 assert out["newKept"] == {"a": 0, "b": 1} assert out["sinkOpened"] == 1 and out["sinkReleasedOpened"] == 2 cross = _read(DESIGN + "design_view_cross.ts") for call in ("setPipeExtraCountSource(", "setPipeInletSource(", "setPipeRevetChoiceBridge("): assert f"host.onDispose(\n {call}" in cross or f"host.onDispose({call}" in cross, call assert "const releaseSink = setPipeExtraCountSink(" in cross assert "host.onDispose(releaseSink);" in cross profile = _read(DESIGN + "design_view_profile.ts") assert 'host.onDispose(setPipeCrossOpener(() => shared.ensureView("cross")));' in profile def test_1_가림막_저장도_도중_고침_검사() -> None: save = _read(DESIGN + "design_save.ts") assert "const touched = !route && designSignature(ctx.projectId) !== sent;" in save assert "if (touched && locked)" in save and "저장 중 고친 값은 아직 저장 안 됨" in save assert "!locked && designSignature" not in save elements = _read("ui_template/ui_template_elements.ts") assert "keydown" not in elements[elements.index("function ensureOverlay") :][:1200] def test_8_저장_중_초기화_막음() -> None: reset = _read(DESIGN + "design_reset.ts") body = reset[reset.index("export async function resetDesign(") :] assert body.index("saveBusy()") < body.index("showConfirmDialog(") assert "if (saveBusy()) return;" in body # 확인 창 동안 뜬 뒤 저장도 assert "designSaveBusy(projectId)" in body and "SAVE_BUSY_TEXT" in body save = _read(DESIGN + "design_save.ts") assert save.count("showToast(SAVE_BUSY_TEXT") == 1 # [저장]과 같은 글 def test_8_서버_초기화는_프로젝트_잠금_안(monkeypatch) -> None: from B05_Profile import B05_Profile_Router_Lifecycle as lifecycle from common_util import common_util_save_guard as guard pid = uuid4() seen: list[bool] = [] async def body(project_id, session): seen.append(guard.project_lock(str(project_id)).locked()) return "ok" monkeypatch.setattr(lifecycle, "_reset_route_design", body) assert asyncio.run(lifecycle.reset_route_design(pid, None)) == "ok" assert seen == [True] assert not guard.project_lock(str(pid)).locked() src = _read("B05_Profile/B05_Profile_Router_Lifecycle.py") inner = src[src.index("async def _reset_route_design(") :] assert "project_lock" not in inner # 안에서 같은 잠금 다시 안 잡음(교착) def test_3_추가_측점은_data_plus() -> None: longi = _read("B06_Section/B06_Section_UI_Longitudinal.ts") assert 'if (station.plus) marker.setAttribute("data-plus", "1");' in longi modal = _read("B05_Profile/B05_Profile_UI_Profile_CrossModal.ts") render = _read("B05_Profile/B05_Profile_UI_Profile_Render.ts") assert 'marker?.hasAttribute("data-plus")' in modal assert '!marker.hasAttribute("data-plus")' in render assert "station-line--plus" not in modal + render def test_4d_숨은_동안_고름은_다음_그림에_밂() -> None: view = _read("B06_Section/B06_Section_UI_Section_View.ts") assert len(view.splitlines()) <= 700 reveal = view[view.index("const revealCard = ") :][:300] assert "pendingReveal = stationId;" in reveal draw = view[view.index("cards.scrollTop = keepScrollTop;") :][:200] assert 'if (pendingReveal) revealCard(pendingReveal, "auto");' in draw assert "pendingReveal = null;" in draw assert "cards.onwheel = cards.onpointerdown = () => (pendingReveal = null);" in view def test_파이프라인_페이지_안_늘림() -> None: assert len(_read(DESIGN + "design_pipeline_page.ts").splitlines()) <= 699