265 lines
11 KiB
Python
265 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""280-5 · 280-6 · 281 B07 좌측 ⚙ 설정 · 확정 단추 지움 · 저장 후 이동.
|
|
|
|
못박는 것
|
|
- ⚙ = 카드 제목(평면도 · 횡단도 · 구조물 · 전개도) · 한 장 단추 안(종단 1장 · 토적) · 표지 없음.
|
|
- ⚙ 누름 = 카드 접힘 그대로 · 도면 안 엶(Enter 도) · 제목 누름 · 단추 누름은 그대로.
|
|
- 모달 — 횡단 = 배치 칸(지금 장 값) + 표 배율 + 안내선 · 평면도 = 평면도 관리 모달 · 구조물 = 축척 없음 줄 ·
|
|
머리 [원본에서 다시 그리기] = 그 묶음 도면을 연 때만 켬(표준 횡단면도 = 횡단 묶음).
|
|
- 저장 묶음 — 고친 탭(●) + 지금 탭만 confirm · 고친 것 없으면 요청 0 · 실패 / 도각 편집 = 거짓(머묾) ·
|
|
원본 되돌림 = 물음 → invalidate → 다시 실음 · [취소] = 그대로.
|
|
- 페이지 — [현재 도면 확정] · [수정] · 자동 B08 이동 · 「모든 횡단 확정」 잠금 없음 · 읽기 전용 안 묶음 ·
|
|
☰ · 단계 판 · [<] [>] = 저장 → (B08 · B09) 단계 4 완료 → 이동 · 탭 줄 겹판.
|
|
- 서버 `design-drawings-complete` = 단계 4 완료.
|
|
|
|
Node 헬퍼 `helper_280_5_b07_settings.cjs` 가 화면 그릇 흉내로 돌림.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
from uuid import UUID
|
|
|
|
import pytest
|
|
|
|
import B07_DesignDetail.B07_DesignDetail_Router as router
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
ROOT = Path(HERE).parents[1]
|
|
B07 = ROOT / "B07_DesignDetail"
|
|
|
|
|
|
def _read(path: Path) -> str:
|
|
return path.read_text(encoding="utf-8").replace("\r\n", "\n")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def out():
|
|
proc = subprocess.run(
|
|
["node", os.path.join(HERE, "helper_280_5_b07_settings.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["cardGears"] == ["plan", "cross", "structure", "development"]
|
|
assert out["cardGearInTitle"] is True
|
|
assert out["buttonGears"] == ["longitudinal", "mass_haul"]
|
|
assert out["buttonGearRole"] == [["SPAN", "button", 0]] * 2 # 단추 안 단추 금지
|
|
assert out["coverGear"] == 0
|
|
|
|
|
|
def test_설정_누름은_접힘_도면_열기_안_일으킴(out):
|
|
assert out["collapsedSame"] is True
|
|
assert out["selectedAfterGear"] == []
|
|
assert out["enterStops"] is True
|
|
assert out["settingsCalls"] == [
|
|
"plan",
|
|
"cross",
|
|
"structure",
|
|
"development",
|
|
"longitudinal",
|
|
"mass_haul",
|
|
]
|
|
assert out["titleClickToggles"] is True and out["buttonClickOpens"] is True
|
|
|
|
|
|
def test_횡단_모달은_배치_칸_표_배율_안내선(out):
|
|
cross = out["crossModal"]
|
|
assert cross["title"] == "횡단도 설정"
|
|
assert cross["labels"] == ["배치 열", "행", "표 배율"]
|
|
assert cross["values"] == ["3", "2", "1.2"] # 지금 장(1장) 값 · 표 배율
|
|
assert [b[0] for b in cross["buttons"]] == ["이 장", "모든 장", "배율 적용", "안내선 숨기기"]
|
|
assert cross["guideLast"] is True
|
|
assert cross["head"] == [["원본에서 다시 그리기", False]]
|
|
assert cross["notes"] == ["지금 도면: cross_s1"]
|
|
assert cross["scaleTitle"] == "축척 — 횡단도 장(표준 횡단면도 뺌)"
|
|
saved = out["crossSave"]
|
|
assert saved["closed"] is True
|
|
assert saved["saved"] == [[{"default": [4, 4], "sheets": {"1": [2, 2]}, "table_scale": 1.2}, 1]]
|
|
|
|
|
|
def test_구조물_종단_평면도_모달(out):
|
|
structure = out["structureModal"]
|
|
assert structure["title"] == "구조물 도면 설정"
|
|
assert structure["notes"] == ["M02 그림 — 축척 없음"]
|
|
assert structure["head"][0][1] is True # 다른 묶음 도면을 연 중 = 꺼짐
|
|
assert out["longModal"] == {"title": "종단도 설정", "bodyKids": 1} # 축척 칸(485-2)
|
|
assert out["planModal"] == {"viaManager": True, "head": [["원본에서 다시 그리기", False]]}
|
|
assert out["planRedraw"] == {"redraws": 1, "closed": True}
|
|
assert out["standardInCross"] is True
|
|
|
|
|
|
def test_저장은_고친_탭만_실패면_머묾(out):
|
|
assert out["noEdits"] == {"ok": True, "calls": []}
|
|
saved = out["saveEdited"]
|
|
assert saved["ok"] is True
|
|
assert saved["confirms"] == [
|
|
["confirm", "cross_s1", "live", {"q": 1}], # 지금 탭 = CAD 에서 · 수량표 같이
|
|
["confirm", "longitudinal", "stash", None], # 다른 고친 탭 = 받아 둔 편집본
|
|
]
|
|
assert saved["clean"] == [False, False, False]
|
|
assert sorted(saved["confirmed"]) == [
|
|
"cross_s1",
|
|
"longitudinal",
|
|
] # 안 고친 mass_haul 은 안 보냄
|
|
assert out["saveFail"] == {"ok": False, "toasts": ["error"]}
|
|
assert out["frameEditing"] == {"ok": False, "calls": 0}
|
|
|
|
|
|
def test_원본에서_다시_그리기(out):
|
|
assert out["redrawable"] is True
|
|
assert out["redraw"] == {
|
|
"ok": True,
|
|
"calls": ["ask", "invalidate", "forget", "reload"],
|
|
"confirmed": False,
|
|
}
|
|
assert out["pristine"] is False # 원본 그대로 = 꺼짐
|
|
assert out["redrawCancel"] == {"ok": False, "invalidates": 0}
|
|
|
|
|
|
def test_페이지_확정_단추_잠금_자동이동_없음():
|
|
page = _read(B07 / "B07_DesignDetail_UI_Page.ts")
|
|
for gone in (
|
|
"현재 도면 확정",
|
|
'"수정"',
|
|
"all_confirmed",
|
|
"allDrawingsConfirmed",
|
|
"confirmDesignDrawing",
|
|
"invalidateDesignDrawing",
|
|
"crossLayoutSlot",
|
|
):
|
|
assert gone not in page, gone
|
|
assert "confirmed: false, // 잠금 없음(요구 281)" in page
|
|
assert "actionRow.append(frameEditor.button);" in page
|
|
assert "onStepClick: (stepIndex) => void moveToStage(stepIndex)," in page
|
|
move = page[
|
|
page.index("const moveToStage = async") : page.index("const layout = createWorkflowLayout")
|
|
]
|
|
assert move.index("saveEditedDrawings(store)") < move.index("completeDesignStage(projectId)")
|
|
assert move.index("completeDesignStage(projectId)") < move.index("goToWorkflowStage(")
|
|
assert "if (stepIndex > 4 && drawings.length > 0)" in move
|
|
assert "currentStage: 5," in page
|
|
assert len(page.splitlines()) <= 700
|
|
|
|
|
|
def test_탭_줄_겹판_약속():
|
|
page = _read(B07 / "B07_DesignDetail_UI_Page.ts")
|
|
assert 'cad.element.style.getPropertyValue("--cad-tab-slot-left") !== ""' in page
|
|
assert "if (slotted) cad.element.append(tabs.element);" in page
|
|
assert "else cadStack.prepend(tabs.element);" in page
|
|
css = _read(B07 / "B07_DesignDetail_UI_Style.css")
|
|
block = css[css.index(".b07-cad-stack .cad-host > .cad-tabs {") :]
|
|
block = block[: block.index("}")]
|
|
assert "right: var(--cad-tab-slot-right, 0px);" in block
|
|
assert "left: var(--cad-tab-slot-left, 0px);" in block
|
|
assert re.search(r"height:\s*25px", block)
|
|
|
|
|
|
class _Cursor:
|
|
async def __aenter__(self) -> _Cursor:
|
|
return self
|
|
|
|
async def __aexit__(self, *_: object) -> bool:
|
|
return False
|
|
|
|
|
|
class _Connection:
|
|
async def begin(self) -> None: ...
|
|
|
|
async def commit(self) -> None: ...
|
|
|
|
async def rollback(self) -> None: ...
|
|
|
|
def cursor(self) -> _Cursor:
|
|
return _Cursor()
|
|
|
|
|
|
class _Acquire:
|
|
async def __aenter__(self) -> _Connection:
|
|
return _Connection()
|
|
|
|
async def __aexit__(self, *_: object) -> bool:
|
|
return False
|
|
|
|
|
|
def test_서버_단계_완료(monkeypatch, tmp_path: Path) -> None:
|
|
stages: list[tuple[str, int]] = []
|
|
|
|
async def source(_project_id: UUID) -> tuple[int, Path, Path, bool]:
|
|
return 1, tmp_path, tmp_path / "longitudinal.json", True
|
|
|
|
async def complete(project_id: str, stage_no: int, action: str, _cursor: object) -> None:
|
|
stages.append((project_id, stage_no, action))
|
|
|
|
monkeypatch.setattr(router, "_confirmed_source", source)
|
|
monkeypatch.setattr(router, "mark_stage", complete)
|
|
monkeypatch.setattr(
|
|
router, "get_db_pool", lambda: type("P", (), {"acquire": lambda self: _Acquire()})()
|
|
)
|
|
project = UUID("8b98ca75-c572-4002-8468-2adae4323414")
|
|
result = asyncio.run(router.complete_design_stage(project))
|
|
assert result == {"status": "success", "project_id": str(project)}
|
|
assert stages == [(str(project), 4, "next")] # 298-2 [>] = 4 완료 · 5 진행 중
|
|
|
|
async def blocked(_project_id: UUID) -> tuple[int, Path, Path, bool]:
|
|
raise PermissionError("B06 종·횡단 확정 후 상세 설계를 진행할 수 있습니다.")
|
|
|
|
monkeypatch.setattr(router, "_confirmed_source", blocked)
|
|
assert asyncio.run(router.complete_design_stage(project)).status_code == 409
|
|
|
|
|
|
RANGE = "1/100~1/10,000 사이 정수"
|
|
|
|
|
|
def test_485_2_축척_칸_판정(out):
|
|
assert out["readAxis"] == [1000, RANGE, RANGE, RANGE, RANGE, RANGE, None] # 자동 = null
|
|
assert out["toSend"] == [None, 500, None] # 기본과 같으면 null
|
|
|
|
|
|
def test_485_2_종단_축척_저장(out):
|
|
scale = out["scale"]
|
|
assert scale["longLabels"] == ["X(가로) 1/", "Y(세로) 1/"]
|
|
assert scale["longStart"]["values"] == ["1000", "200"]
|
|
assert scale["longStart"]["autos"] == [] # 종단 = 자동 없음
|
|
for bad in ("longLow", "longFrac"): # 범위 밖 · 소수 = 붉음 + 저장 꺼짐
|
|
assert scale[bad]["errors"] == [RANGE, None]
|
|
assert scale[bad]["save"] is False
|
|
assert scale["longOk"]["errors"] == [None, None] and scale["longOk"]["save"] is True
|
|
# 저장본 2장 — 물음 [취소] = 요청 없음 · 모달 그대로
|
|
assert scale["cancel"] == {"calls": ["ask"], "open": True, "closed": 0}
|
|
save = scale["save"]
|
|
assert "저장본 2장" in save["ask"]
|
|
assert save["sent"] == {"longitudinal": {"x": 500, "y": None}} # Y = 기본 → null
|
|
assert save["open"] is False
|
|
assert save["closed"] == [[True, "longitudinal"]] # 화면 다시 세움 · 같은 도면
|
|
assert scale["reset"]["values"] == ["1000", "200"] # [기본값] = 칸만
|
|
assert scale["same"] == {"calls": 0, "toasts": ["바뀐 축척이 없습니다."]}
|
|
|
|
|
|
def test_485_2_토적_전개도_자동(out):
|
|
scale = out["scale"]
|
|
assert scale["massStart"] == {
|
|
"values": ["", "50000"],
|
|
"errors": [None, None],
|
|
"disabled": [True, False],
|
|
"autos": [True],
|
|
"save": True,
|
|
}
|
|
assert scale["massManual"]["errors"][0] and scale["massManual"]["save"] is False # 빈 칸
|
|
assert scale["massTyped"]["save"] is True
|
|
assert scale["massSent"] == {"ask": 0, "sent": {"mass_haul": {"x": 2000, "y": None}}}
|
|
assert scale["massBack"]["autos"] == [True] and scale["massBack"]["save"] is True
|
|
assert scale["dev"]["autos"] == [True, True] and scale["dev"]["disabled"] == [True, True]
|
|
assert scale["structure"] == 0 # 구조물 = 축척 칸 없음
|