"""203-1 B09 좌측 「데이터 점검」 묶음(사용자 60번). - 묶음 이름 = 데이터 점검 · 안 설명 글 없음(굳힌 시각 · 까닭 = [업데이트] 공용 호버) - [단가 갱신] → [업데이트] · 낡으면(서버 `book_stale` = 지금 M01 로 굳힌 책과 다름) 점 배지(is-pending) - 「연결 점검」 탭 내용 = 묶음 안 목록(누르면 그 탭 그 줄) · 위 탭 줄에서 뺌 · 첫 탭 = 공사원가계산서 """ import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402 from resources.tester.helper_b09_group_cards import TABS, run # noqa: E402 CHECK = "B09_Estimation/B09_Estimation_UI_Check.ts" SHELL = (ROOT / "B09_Estimation/B09_Estimation_UI_Shell.ts").read_text("utf-8") def test_묶음_이름_단추_글_설명_없음(): assert 'createPanelCard("데이터 점검")' in SHELL and "원가 단가" not in SHELL # 269 공용 카드 assert 'label: "업데이트",' in SHELL and "단가 갱신" not in SHELL # 옛 굳힌 시각 안내 글(made) 없음 · 묶음 = 제목 · [업데이트] · 연결 점검 assert "made.textContent" not in SHELL and "b09-estimation__hint" not in SHELL assert "section.append(refresh, check.list);" in SHELL # 제목 = 공용 카드(269) assert "check.draw(payload, sheets);" in SHELL def test_연결_점검_탭_없음_첫_탭_공사원가계산서(): out = run( [TABS], "\nconsole.log(JSON.stringify({ ids: TABS.map(([t]) => t), names: TABS.map(([, n]) => n)," " groups: TAB_GROUPS.map(([n]) => n), first: TABS[0][0] }));", ) assert "check" not in out["ids"] and "연결 점검" not in out["names"] + out["groups"] assert out["first"] == "cost" and out["groups"][0] == "공사원가계산서" assert "let current: TabId = TABS[0][0];" in SHELL assert "buildCheck" not in SHELL and "check:" not in SHELL def test_좌측_목록_이동_호버_배지(): out = run( [TABS, CHECK], """ const moves = []; const refresh = document.createElement("button"); const panel = createCheckPanel(refresh, (link) => moves.push(link)); const sheets = { gaps: [ { tab: "dg", key: "DG1", name: "찰쌓기", why: "대가 원천 없음" }, { tab: "material", key: "MT9", name: "모래", why: "단가 없음" }, ] }; const payload = { book: { made: "2026-10-01T09:00:00" }, stale: "M01 에서 바뀐 단가 2", notes: ["B08 자재 집계표 못 읽음"], inputs: { missing: ["EQ1", "LB2"] } }; panel.draw(payload, sheets); const view = () => ({ pending: refresh.classList.contains("is-pending"), texts: panel.list.children.map((c) => c.text), rows: (panel.list.children[1]?.children ?? []).map((r) => [r.dataset.i, r.className, r.text]), }); const a = view(); const rowsBox = panel.list.children[1]; rowsBox.children[1].click(); const tipRefresh = hoverCalls.find((h) => h.root === refresh).text(); const rowHover = hoverCalls.find((h) => h.root === panel.list); const tipRow = rowHover.text(rowsBox.children[0]); panel.draw({ book: null, stale: null }, { gaps: [] }); const b = view(); const tipEmpty = refreshTip({ book: null, stale: "단가 아직 안 굳힘" }); console.log(JSON.stringify({ a, b, moves, tipRefresh, tipRow, selector: rowHover.selector, tipEmpty })); """, ) a, b = out["a"], out["b"] assert a["pending"] and not b["pending"] assert a["texts"][0] == "연결 없음 2곳" assert a["rows"] == [ ["0", "ui-sidebar-row", "찰쌓기일위대가표"], ["1", "ui-sidebar-row", "모래재료비목록표"], ] assert a["texts"][2:] == ["B08 자재 집계표 못 읽음", "M01 에 없는 키 2 — EQ1 · LB2"] assert out["moves"] == [{"tab": "material", "key": "MT9"}] assert out["tipRow"] == "대가 원천 없음" and out["selector"] == "[data-i]" assert "단가 굳힘 2026-10-01 09:00:00" in out["tipRefresh"] assert "업데이트 필요 — M01 에서 바뀐 단가 2" in out["tipRefresh"] assert b["texts"] == ["연결 없음 0곳"] and b["rows"] == [] assert out["tipEmpty"].startswith("단가 아직 안 굳힘") and "업데이트 필요" in out["tipEmpty"] def test_배지_조건_서버_book_stale(): keys = ["EQ000004"] # 불도저 · 조종원 · 경유(test_144 와 같은 견본) book = json.loads(json.dumps(prices.refresh_book(keys), ensure_ascii=False)) assert prices.book_stale(book, keys) is None # 방금 굳힌 책 = 업데이트 필요 없음 assert prices.book_stale(None, keys) == "단가 아직 안 굳힘" first, *rest = list(book["rows"]) old = {**book, "rows": {k: book["rows"][k] for k in rest}, "fx": []} old["rows"][rest[0]] = {**old["rows"][rest[0]], "price": -1} old["driver_factor"] = {"num": 1, "den": 1, "text": "1/1"} assert prices.book_stale(old, keys) == ( "새 단가 줄 1 · M01 에서 바뀐 단가 1 · 운전사 계수 바뀜 · 환율 바뀜" ) router = (ROOT / "B09_Estimation/B09_Estimation_Router.py").read_text("utf-8") assert '"stale": await asyncio.to_thread(prices.book_stale, book, keys),' in router def test_배지_꼴_경고_색_토큰(): css = (ROOT / "B09_Estimation/B09_Estimation_UI_Page.css").read_text("utf-8") block = css[css.index(".b09-options .ui-btn.is-pending::after") :] block = block[: block.index("}")] assert "position: absolute" in block and "border-radius: 50%" in block assert "background: var(--color-warning)" in block