- 시험 615 통과 · 브라우저 화면 13개·메뉴 90개 오류 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
6.3 KiB
Python
146 lines
6.3 KiB
Python
"""STMATE 재현 앱 화면 편집(설계내역서 wBoq · 도구 대화상자 · 초기값 wM_Set_Sys · 단가목록표 wM_JabPum · 메뉴) 지킴 시험.
|
|
|
|
계산 = tools/stmate-test/ui_*.js(화면 없이 node) · 자료 = 실무 STC 불러오기 결과(server.api_load).
|
|
행 넣기·지우기·이동·계표시·붙여넣기 뒤 저장 원천(boq_rows)과 계산 원천(boqItems)이 함께 움직이는지 직공비 증감으로 대조.
|
|
"""
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "tools/stmate-test"))
|
|
import server # noqa: E402
|
|
|
|
HERE = Path(__file__).parent
|
|
DOCS = ROOT / "resources/knowledge/original/실무문서"
|
|
PROJECTS = {
|
|
"봉화2024": "04.산불 봉화/01. 내역서/13-2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.STC",
|
|
"영월2024": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC",
|
|
"대흥2024": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).STC",
|
|
"울진2025": "25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC",
|
|
"영덕변경": "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).STC",
|
|
}
|
|
# 공종번호 다시 매김 계획 — 원본 번호 꼴(봉화 T4 「1.」 · T2 「1.1」 / 영월 T3 「1」 · T2 「1.1」)
|
|
ITNUM_PLAN = {
|
|
"봉화2024": [
|
|
{"level": 4, "first": "", "last": ".", "prefix": "none"},
|
|
{"level": 2, "first": ".", "last": "", "prefix": "parent"},
|
|
],
|
|
"영월2024": [
|
|
{"level": 3, "first": "", "last": "", "prefix": "none"},
|
|
{"level": 2, "first": ".", "last": "", "prefix": "parent"},
|
|
],
|
|
}
|
|
_CACHE: dict = {}
|
|
|
|
|
|
def run(name):
|
|
if name not in _CACHE:
|
|
d = server.api_load(server.LoadReq(source_path=str(DOCS / PROJECTS[name])))
|
|
d["itnum_plan"] = ITNUM_PLAN.get(name, [])
|
|
out = subprocess.check_output(
|
|
["node", str(HERE / "helper_stmate_ui.cjs")],
|
|
input=json.dumps(d, ensure_ascii=False, default=str),
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
_CACHE[name] = (d, json.loads(out))
|
|
return _CACHE[name]
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(PROJECTS))
|
|
def test_boq_structure_untouched_and_items_in_step(name):
|
|
_d, r = run(name)
|
|
assert r["structure"] == {"same": True, "nest": True, "items": r["structure"]["items"]}
|
|
assert r["structure"]["items"][0] == r["structure"]["items"][1]
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(PROJECTS))
|
|
def test_insert_delete_rows_move_direct_cost(name):
|
|
_d, r = run(name)
|
|
ins, dl = r["insert"], r["deleteOrig"]
|
|
assert ins["ok"] and ins["rowT"] > 0 and ins["delta"] == ins["rowT"]
|
|
assert ins["back"] == 0 and ins["deleted"] == [] # 새 행은 지워도 원본 지움 목록에 안 듦
|
|
assert dl["ok"] and dl["deleted"] == [dl["recno"]]
|
|
assert dl["delta"] == -dl["leafT"] # 원본 행 지움 = 그 행 저장 금액만큼 직공비 감소
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(PROJECTS))
|
|
def test_move_copy_paste_group_level(name):
|
|
d, r = run(name)
|
|
assert r["move"] == {"up": True, "down": True, "back": True, "badRejected": True, "nest": True}
|
|
g = r["group"]
|
|
ini = d["start_ini"]
|
|
assert g["wrap"] and g["eTotal"] == g["sumLeaf"] > 0
|
|
assert g["eName"] == (ini.get("DISP_N1") or "소계").strip()
|
|
assert g["shift"] and g["shiftName"] == (ini.get("DISP_N3") or "합계").strip()
|
|
assert g["note"] and g["nest"]
|
|
p = r["paste"]
|
|
assert p["ok"] and p["added"] == p["clipRows"] and p["pairs"] >= 1 and p["nest"]
|
|
assert p["delta"] == p["srcT"] # 붙여넣은 묶음 금액만큼 직공비 증가
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(PROJECTS))
|
|
def test_expand_ilwi_components(name):
|
|
_d, r = run(name)
|
|
if "expand" not in r:
|
|
pytest.skip("구성표 있는 일위대가 내역 행 없음")
|
|
e = r["expand"]
|
|
assert e["ok"] and e["made"] == e["want"] and e["deleted"] and e["closing"] == "E1"
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(ITNUM_PLAN))
|
|
def test_itnum_reproduces_original_sequence(name):
|
|
_d, r = run(name)
|
|
for lv in r["itnum"]:
|
|
got, want = lv["got"], lv["want"]
|
|
# 원본은 지운 공종 자리를 비워 둔 번호가 있음(영월 1.9) — 첫 빈자리 앞까지는 같아야 함
|
|
k = next((i for i, (g, w) in enumerate(zip(got, want)) if g != w), len(got))
|
|
assert k >= 3, (lv["level"], got[: k + 1], want[: k + 1])
|
|
assert len(got) == len(want)
|
|
|
|
|
|
def test_qty_round_and_hal_rate():
|
|
_d, r = run("봉화2024")
|
|
assert r["qty"]["first"] == [1, 12.34, 12.34, 12.34]
|
|
assert r["qty"]["second"] == [1, 12]
|
|
# 할증율 = 기준단가에 곱할 백분율(TwBoqRate · 0% = 100%) — 103% → 계산 원천 가산 3%
|
|
assert r["qty"]["hal"] == [1, "103", 3]
|
|
assert r["qty"]["zero"] == [1, 0, 0, 0]
|
|
assert r["qty"]["w"] == [0, 1, 1, "110"] # 보호된 견적단가 수정안함(CBoxNotUC)
|
|
hb = r["halBase"]
|
|
assert hb["row"] == hb["want"] and hb["calc"] == hb["want"] # 저장 단가 = 화면 계산 단가
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(PROJECTS))
|
|
def test_initial_values_roundtrip(name):
|
|
d, r = run(name)
|
|
s = r["sys"]
|
|
assert s["changed"] == [] and s["errs"] == []
|
|
conld = s["conld"]
|
|
assert s["cond"] == ", ".join(conld[i : i + 6] for i in range(0, len(conld), 6))
|
|
assert s["xml"] == ["0", "2", True]
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(PROJECTS))
|
|
def test_unit_price_list(name):
|
|
_d, r = run(name)
|
|
a = r["dan"]
|
|
assert a["leafRecno"] in a["useBoq"]
|
|
assert a["counts"]["ALL"] == a["total"]
|
|
assert a["counts"]["BD"] == a["counts"]["B"] + a["counts"]["D"]
|
|
assert a["halFirst"] > 0 and a["halSecond"] == 0 # 기준단가 기준 — 두 번 적용해도 같은 값
|
|
|
|
|
|
def test_menu_shortcuts_follow_original():
|
|
_d, r = run("봉화2024")
|
|
m = r["menu"]
|
|
assert m["dec"] == ["F9", "Ctrl+A", "Ctrl+Shift+P", "Alt+Q", "Ctrl+Shift+Alt+Up"]
|
|
assert m["sc"]["F9"] == "Act_Calc_Grass" and m["sc"]["Ctrl+B"] == "Act_iLWi_San"
|
|
assert m["sc"]["Ctrl+N"] == "Act_PrjLst" and m["sc"]["Ctrl+F5"] == "Act_Refresh"
|
|
assert len(m["acts"]) == len(set(m["acts"]))
|