diff --git a/resources/tester/bench_342_watershed.py b/resources/tester/bench_342_watershed.py deleted file mode 100644 index ef0653783..000000000 --- a/resources/tester/bench_342_watershed.py +++ /dev/null @@ -1,101 +0,0 @@ -"""342-3 · 347-1 배수유역 빠른 길 실측 — 검증_sub3 도엽 · 노선으로 preview_stages 를 돌려 -시간과 결과(적색 · 흐름 방향 · 도로 귀속 · 강도)를 tmp/342/<이름>.npz 에 남기고 기준과 대조한다. - - ./venv/Scripts/python.exe resources/tester/bench_342_watershed.py base # 기준 저장 - ./venv/Scripts/python.exe resources/tester/bench_342_watershed.py new base # 새로 돌려 대조 - ./venv/Scripts/python.exe resources/tester/bench_342_watershed.py s1 base store # 흐름 방향 저장본 - -pytest 대상 아님(DB · 프로젝트 파일이 있어야 돎). 분석 결과는 저장 안 함 — 도엽 좌표 저장본 -(drainage/00_sheet_metric.pkl)만 프로젝트에 남고 · 흐름 방향 저장본은 tmp/342/descent 에 둠. -""" - -import asyncio -import json -import logging -import sys -import time -from pathlib import Path -from uuid import UUID - -import numpy as np - -ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT)) -PID = UUID("1f93a515-df0a-4975-a8ba-daaa7672b123") # 검증_sub3 -OUT_DIR = ROOT / "tmp" / "342" - - -async def _prepare(): - from config.config_db import close_db_pool, init_db_pool - - await init_db_pool() - try: - from B04_PreProcess.B04_PreProcess_Router_Watershed import _prepare as prepare - - started = time.perf_counter() - prepared = await prepare(PID) - return prepared, time.perf_counter() - started - finally: - await close_db_pool() - - -def main() -> None: - name = sys.argv[1] - base = sys.argv[2] if len(sys.argv) > 2 and sys.argv[2] != "-" else None - store = len(sys.argv) > 3 and sys.argv[3] == "store" - OUT_DIR.mkdir(parents=True, exist_ok=True) - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s %(name)s:%(message)s", - handlers=[logging.FileHandler(OUT_DIR / f"{name}.log", "w", encoding="utf-8")], - ) - prepared, prepare_s = asyncio.run(_prepare()) - from B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze import preview_stages - - started = time.perf_counter() - pv = preview_stages( - prepared["vertices"], - prepared["contours"], - prepared["streams"], - (OUT_DIR / "descent", prepared["contour_key"]) if store else None, - ) - preview_s = time.perf_counter() - started - spec = pv.spec - red = pv.flow.reaches_road & pv.flow.analyzed - result = { - "spec": np.array([spec.x_min, spec.y_max, spec.cell_m, spec.n_rows, spec.n_cols]), - "domain": pv.domain, - "red": red, - "direction": pv.flow.direction, - "road_slot": pv.routing.road_slot, - "strength": np.array(pv.strength_profile), - "pipes": np.array([p.chainage_m for p in pv.pipes]), - } - np.savez_compressed(OUT_DIR / f"{name}.npz", **result) - report = { - "prepare_s": round(prepare_s, 2), - "preview_s": round(preview_s, 2), - "total_s": round(prepare_s + preview_s, 2), - "rounds": pv.expand_rounds, - "grid": [spec.n_rows, spec.n_cols], - "red_cells": int(red.sum()), - } - if base: - ref = np.load(OUT_DIR / f"{base}.npz") - same_spec = bool(np.array_equal(ref["spec"], result["spec"])) - report["same_spec"] = same_spec - if same_spec: - for key in ("domain", "red", "direction", "road_slot"): - report[f"diff_{key}"] = int((ref[key] != result[key]).sum()) - else: - report["base_spec"] = ref["spec"].tolist() - report["new_spec"] = result["spec"].tolist() - report["red_cells_base"] = int(ref["red"].sum()) - report["same_strength"] = bool(np.array_equal(ref["strength"], result["strength"])) - report["same_pipes"] = bool(np.array_equal(ref["pipes"], result["pipes"])) - print(json.dumps(report, ensure_ascii=False)) - (OUT_DIR / f"{name}.json").write_text(json.dumps(report, ensure_ascii=False, indent=1), "utf-8") - - -if __name__ == "__main__": - main() diff --git a/resources/tester/helper_236_1_fill_sizes.cjs b/resources/tester/helper_236_1_fill_sizes.cjs deleted file mode 100644 index 64c240b5d..000000000 --- a/resources/tester/helper_236_1_fill_sizes.cjs +++ /dev/null @@ -1,72 +0,0 @@ -/* 236-1 — 표 아래 안내 글이 한 줄일 때 창 끝까지 셈(`ui_template_data_table.ts` fillViewport)이 - * 창 크기마다 문서 맨 아래 = 창 끝(페이지 스크롤 0)이 되는지 단위검증 헬퍼. JSON 으로 냄. */ -const fs = require("fs"); -const path = require("path"); -const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); - -const file = path.join(__dirname, "..", "..", "ui_template", "ui_template_data_table.ts"); -const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { - compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, -}).outputText; - -const ONE_LINE = 18; // 안내 글 한 줄(caption 글꼴) 높이 -const win = { innerHeight: 0, scrollY: 0, addEventListener() {}, removeEventListener() {} }; -const observers = []; -class RO { - constructor(fn) { - observers.push(fn); - } - observe() {} - disconnect() {} -} -const el = (css = {}, height = 0) => ({ - css, - height, - top: 0, - parentElement: null, - nextElementSibling: null, - isConnected: true, - style: {}, - getBoundingClientRect() { - return { top: this.top, height: this.height }; - }, -}); -const sty = (n) => ({ - rowGap: n.css.rowGap ?? "normal", - paddingBottom: n.css.paddingBottom ?? "0px", - borderBottomWidth: n.css.borderBottomWidth ?? "0px", - marginBottom: n.css.marginBottom ?? "0px", -}); -const box = { exports: {} }; -new Function( - "exports", - "module", - "require", - "window", - "document", - "getComputedStyle", - "ResizeObserver", - js, -)(box.exports, box, (p) => (p.endsWith(".css") ? {} : require(p)), win, { body: null }, sty, RO); - -const out = []; -for (const h of [1080, 768, 600]) { - win.innerHeight = h; - observers.length = 0; - // 문서: body(padding-bottom 8) > card(padding-bottom 12 · gap 8) > [표 칸][안내 한 줄] - const body = el({ paddingBottom: "8px" }); - const card = el({ paddingBottom: "12px", rowGap: "8px" }); - const root = el(); - const hint = el({}, ONE_LINE); - card.parentElement = body; - root.parentElement = card; - hint.parentElement = card; - root.nextElementSibling = hint; - root.top = 200; - box.exports.fillViewport(root); - observers.forEach((fn) => fn()); - const grow = parseFloat(root.style.maxHeight); - const below = box.exports.spaceBelow(root); - out.push({ viewport: h, bottom: root.top + grow + below, grow, below }); -} -console.log(JSON.stringify(out)); diff --git a/resources/tester/helper_398_1_user_pick.cjs b/resources/tester/helper_398_1_user_pick.cjs deleted file mode 100644 index 51e3d13fb..000000000 --- a/resources/tester/helper_398_1_user_pick.cjs +++ /dev/null @@ -1,81 +0,0 @@ -/* PLAN 398-1 단위검증 헬퍼 — 진짜 `design_user_pick.ts` + 진짜 `design_page_data.ts`(묶음 차례 · 「계산 중」)로 - * 좌측 패널 펴기 한 곳(`design_shared` onReveal 과 같은 식)을 길마다 셈. 짝 = `test_398_1_reveal_user_pick.py`. - * 브라우저가 처리 중인 이벤트(`window.event`)는 `globalThis.event` 로 흉내. */ -const fs = require("fs"); -const path = require("path"); -const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); - -const ROOT = path.join(__dirname, "..", ".."); -const DESIGN = path.join(ROOT, "A00_Common", "design"); - -function load(file, stubs) { - const js = ts.transpileModule(fs.readFileSync(path.join(DESIGN, file), "utf8"), { - compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, - }).outputText; - const box = { exports: {} }; - new Function("exports", "module", "require", js)(box.exports, box, (name) => { - if (!stubs[name]) throw new Error(`stub 없음: ${name}`); - return stubs[name]; - }); - return box.exports; -} - -const data = load("design_page_data.ts", { - "./design_page_route": { setPageRouteId: () => undefined, pageRouteId: () => null }, - "../../B04_PreProcess/B04_PreProcess_Api_Fetch": { fetchDetailPipePoints: async () => ({}) }, - "../../B05_Profile/B05_Profile_Api_Structures": { - fetchStructures: async () => ({}), - dropStaleStructureTypes: async () => false, - }, -}); -const pick = load("design_user_pick.ts", { "./design_page_data": data }); - -const P = "p1"; -let reveals = 0; -/** 폼이 고른 것을 실을 때마다 부르는 펴기(`design_shared` onReveal 그대로). */ -const onReveal = () => pick.isUserPick(P) && (reveals += 1); -/** 브라우저가 그 이벤트를 처리하는 동안(그 마이크로태스크까지) — 끝나면 이벤트 없음. */ -async function during(event, body) { - globalThis.event = event; - try { - await body(); - } finally { - globalThis.event = undefined; - } -} -const trusted = (type) => ({ type, isTrusted: true }); -const count = async (event, body) => { - const before = reveals; - await during(event, body); - return reveals - before; -}; - -(async () => { - const out = {}; - // 사용자 고름 — 종단 알약 · 횡단 카드(click) · 3D 부재(pointerup) · 우클릭 추가(contextmenu) - out.click = await count(trusted("click"), () => onReveal()); - out.pointerup = await count(trusted("pointerup"), () => onReveal()); - out.contextmenu = await count(trusted("contextmenu"), () => onReveal()); - // 누름 밖 — 배수유역 다시 나눔 응답 · 횡단 다시 그림(서버 응답 · 타이머) - out.outside = await count(undefined, () => onReveal()); - // 방향키로 교점 옮김 · 끌기 도중(pointermove) - out.arrow = await count(trusted("keydown"), () => onReveal()); - out.drag = await count(trusted("pointermove"), () => onReveal()); - // 스크립트가 낸 가짜 누름 - out.synthetic = await count({ type: "click", isTrusted: false }, () => onReveal()); - // 교점 끌기 놓음 · 곡선 지우기 · 되돌리기 단추 — 묶음 차례가 떠 그 마이크로태스크에서 관 · 구조물 따라가며 다시 칠함 - let finish; - out.pipelineRepaint = await count(trusted("pointerup"), async () => { - data.trackPageBusy(P, new Promise((resolve) => (finish = resolve))); - await Promise.resolve(); - onReveal(); // follow → 폼 다시 실음 - }); - out.busyPick = await count(trusted("click"), () => onReveal()); // 차례가 도는 사이 새 고름도 폄(505-2) - finish(); - await new Promise((resolve) => setTimeout(resolve, 0)); - out.afterRun = await count(trusted("click"), () => onReveal()); // 차례가 끝나면 다시 폄 - process.stdout.write(JSON.stringify(out)); -})().catch((error) => { - console.error(error); - process.exit(1); -}); diff --git a/resources/tester/test_132_b08_hydro_verdict_color.py b/resources/tester/test_132_b08_hydro_verdict_color.py deleted file mode 100644 index 5ee50e3ce..000000000 --- a/resources/tester/test_132_b08_hydro_verdict_color.py +++ /dev/null @@ -1,43 +0,0 @@ -"""B08 수리 판정 글자색 (PLAN 132-1). - -O K = 초록 글(success) · N G = 붉은 글(danger) · 판정 없음 = 기본 글 · 배경 칠 없음. -""" - -from __future__ import annotations - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -CSS = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Hydro.css").read_text(encoding="utf-8") -TS = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Hydro_Sheet.ts").read_text(encoding="utf-8") - - -def rule(selector: str) -> str: - match = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", CSS) - assert match, selector - return match.group(1) - - -def test_OK_는_success_글() -> None: - assert "color: var(--color-success)" in rule(".b08-hydro__sheet.is-ok .is-key") - - -def test_NG_는_danger_글() -> None: - assert "color: var(--color-danger)" in rule(".b08-hydro__sheet.is-ng .is-key") - - -def test_판정_없음은_기본_글() -> None: - body = rule(".is-key.b08-hydro__out,\n.is-key.b08-hydro__verdict") - assert "color: var(--color-text-body)" in body - assert "--color-danger" not in body - - -def test_배경_칠_없음() -> None: - assert ".b08-hydro__verdict.is-ng" not in CSS - assert "background" not in rule(".b08-hydro__sheet.is-ng .is-key") - - -def test_판정이_장에_칠_갈래를_붙임() -> None: - assert 'sheet.classList.toggle("is-ok", judged && r.ok === true)' in TS - assert 'sheet.classList.toggle("is-ng", judged && !r.ok)' in TS diff --git a/resources/tester/test_143_3_b08_material_cards.py b/resources/tester/test_143_3_b08_material_cards.py deleted file mode 100644 index 926c7e346..000000000 --- a/resources/tester/test_143_3_b08_material_cards.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -"""B08 구조물 탭 — 자재 셋 값 없는 줄 숨김 · 표 넷 공용 카드 안(PLAN 143-3). - -숨김은 화면만 — 셈 · 열 · 단위수량은 그대로 · 고친 칸은 원래 줄 번호(`keep`)로 저장 자리를 찾음. -""" - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -MATERIAL = (ROOT / "B08_Quantity/B08_Quantity_UI_Material.ts").read_text(encoding="utf-8") + ( - ROOT / "B08_Quantity/B08_Quantity_Material_Tables.ts" -).read_text(encoding="utf-8") -SUMMARY = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8") + ( - ROOT / "B08_Quantity/B08_Quantity_Summary_Table.ts" -).read_text(encoding="utf-8") - - -def test_자재_셋_값_없는_줄_숨김(): - assert ( - MATERIAL.count("const keep = shown(rows, ") == 2 - ) # 총괄 · 자재수량(콘크리트 = 늘 · 163-5) - assert "row.qty != null || anyValue(row.units)" in MATERIAL # 손 단위수량 있으면 보임 - assert "row.qty != null || row.amount != null" in MATERIAL - # 고친 칸 → 원래 줄(숨긴 줄 건너뜀) - for target in ( - "rows[keep[i / 2]].id", - "rows[keep[i]].key, key.slice(4)", - "rows[keep[i]].panel", - ): - assert target in MATERIAL, target - - -def test_표_넷_공용_카드_안(): - assert ( - "createCard({ body: [toolbar, table.root, excel, notes] })" in MATERIAL - ) # + 엑셀 알림(160-4) · 안내 = 아래(202-1) - assert "createCard({ body: [table, ...parts] })" in SUMMARY diff --git a/resources/tester/test_160_2_b08_unassigned.py b/resources/tester/test_160_2_b08_unassigned.py deleted file mode 100644 index c9fe2b6b1..000000000 --- a/resources/tester/test_160_2_b08_unassigned.py +++ /dev/null @@ -1,71 +0,0 @@ -"""160-2 B08 구조물 집계표 빈 조합 칸 글(198-1 부터 미연결 램프 호버 글 · 「미지정」 열 · 칸 표기는 지움). - -`B08_Quantity_Summary_Data.ts` 를 `common_util_summary_item.ts` 와 이어 붙여 node 로 돌림(61-1 시험과 같은 꼴). -""" - -import json -from pathlib import Path - -from test_61_1_b08_summary_table import _item, _run - -ROOT = Path(__file__).resolve().parents[2] - - -def _slot(copy: str, group, item=None, linked=False) -> dict: - return {"copy": copy, "group": group, "item": item, "linked": linked} - - -FACILITIES = [ - { - "id": "pipe@100", - "type_id": "pipe", - "name": "배관", - "facility": "pipe", - "chainage_m": 100.0, - "slots": [ - _slot("pipe@100", None), - _slot("pipe@100__gpipe", "관", _item("SS2")), - _slot("pipe@100__gin", "관 보호공(유입)", _item("SS3"), linked=True), - ], - }, - { - "id": "s1", - "type_id": "ford_bridge", - "name": "세월교", - "facility": None, - "chainage_m": 100.004, - "slots": [_slot("s1__gfordout", "유출 구조", {"key": "깨짐"})], # 모양 틀림 = 조합 없음 - }, - { - "id": "s2", - "type_id": "box", - "name": "BOX", - "facility": None, - "chainage_m": 160.0, - "slots": [_slot("s2", None, _item("SS1"), linked=True)], # 빈 칸 없음 = 이름만 - }, -] - - -def test_빈_조합_칸_글(): - out = _run( - f"\nconst f = {json.dumps(FACILITIES, ensure_ascii=False)};" - "\nconst at = (s) => unlinkedAt(f, s);" - "\nconsole.log(JSON.stringify({" - " t100: unassignedText(at(100), '본체'), t160: unassignedText(at(160), '본체')," - " t0: unassignedText(at(0), '본체') }));" - ) - assert out["t100"] == "배관: 본체 · 관 / 세월교: 유출 구조" - assert out["t160"] == "BOX" and out["t0"] == "" - - -def test_화면_연결(): - summary = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8") + ( - ROOT / "B08_Quantity/B08_Quantity_Summary_Table.ts" - ).read_text(encoding="utf-8") - assert ( - 'unassignedText(unlinkedAt(unlinked, station), L("B08_Quantity_Link_Body"))' in summary - ) # 칸 이름 = 모달과 같은 규칙 - assert 'key: "unassigned"' not in summary and "unassignedCombos" not in summary # 198-1 - locale = (ROOT / "ui_template/ui_template_locale_b4.ts").read_text(encoding="utf-8") - assert "B08_Quantity_Summary_Unassigned" not in locale diff --git a/resources/tester/test_161_1_b08_labor_card.py b/resources/tester/test_161_1_b08_labor_card.py deleted file mode 100644 index d1708e07c..000000000 --- a/resources/tester/test_161_1_b08_labor_card.py +++ /dev/null @@ -1,34 +0,0 @@ -"""161-1 B08 구조물도 모달 — 일위대가 카드(보기 전용) · 사본 일위대가 읽기 · 카드 차례 도면 · 일위대가 · 면적산출.""" - -import re -from pathlib import Path - -DRAWING = Path("B08_Quantity/B08_Quantity_UI_Drawing.ts").read_text(encoding="utf-8") -CARDS = Path("M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts").read_text(encoding="utf-8") - - -def test_labor_handle_passed(): - # createItemCards 에 labor 손잡이 — 없으면 카드 자체를 안 만듦(ItemCards `opts.labor ?`) - call = DRAWING[DRAWING.index("createItemCards({") :] - call = call[: call.index("});")] - assert 'labor: () => openEdit(copy, name, "inputs", load)' in call # 163-3 넣을 값만 - assert "editable: () => true" in call - assert "const laborCard = opts.labor" in CARDS - - -def test_source_reads_copy_labor(): - # 142-1 고르기 모달과 같은 길 — 사본 일위대가 · 입력값 · 고른자재 → withCopyValues · 셈도 사본 - src = DRAWING[DRAWING.index("function sourceOf") : DRAWING.index("function comboSection")] - assert "labor: {" in src - for part in ('"일위대가"', '"입력값"', '"고른자재"'): - assert f"items.read(folder, {part})" in src, part - assert "withCopyValues(labor.줄, values, picks)" in src - assert "items.calcLabor(folder)" in src - assert re.search(r"if \(!labor\?\.줄\) throw", src) # 없으면 카드 「없음」 - assert "src.labor ? src.labor.read(at)" in CARDS - - -def test_card_order_and_heads(): - assert "const cards: Card[] = [info, ...(laborCard ? [laborCard] : []), ...sheets]" in CARDS - assert 'drawingInfo: "도면 정보"' in CARDS - assert 'labor: "일위대가"' in CARDS and 'area: "면적산출"' in CARDS diff --git a/resources/tester/test_163_b08_modal.py b/resources/tester/test_163_b08_modal.py deleted file mode 100644 index bc44600bd..000000000 --- a/resources/tester/test_163_b08_modal.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- -"""163 B08 구조물도 모달 — 연결 칩 · 접기(163-1) · 사본 고침(163-3) · [업데이트](163-4) · 콘크리트소요 줄(163-5). - -화면 글 판정 — 서버 · 짝 계산(Material_Calc)은 안 고침 · 고르기 모달 편집 화면은 재사용만. -""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -LINK = (ROOT / "B08_Quantity/B08_Quantity_UI_Link.ts").read_text(encoding="utf-8") -DRAWING = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.ts").read_text(encoding="utf-8") -CSS = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.css").read_text(encoding="utf-8") -MATERIAL = (ROOT / "B08_Quantity/B08_Quantity_UI_Material.ts").read_text(encoding="utf-8") + ( - ROOT / "B08_Quantity/B08_Quantity_Material_Tables.ts" -).read_text(encoding="utf-8") -LOCALE = (ROOT / "ui_template/ui_template_locale_b4.ts").read_text(encoding="utf-8") - - -def test_163_1_칩_한_줄_공용_접기(): - assert 'line.className = "b08-link__row"' in LINK # 시설마다 한 줄 - assert '"b08-link__chip is-linked"' in LINK and "chip.dataset.b08LinkSlot = slot.copy" in LINK - # 모두 연결이면 접힘 · 미연결 있으면 펼침 - assert 'ui-collapsible${missing ? "" : " is-collapsed"}' in LINK - assert "ui-collapsible__title" in LINK and "attachCollapsible(section)" in LINK - assert "state.title = state.textContent" in LINK # 긴 이름 = 툴팁 - assert re.search(r"\.b08-link__row \{[^}]*flex-wrap: wrap", CSS) - assert ".b08-link__slot" not in CSS + LINK # 옛 줄 꼴 없음 - - -def test_163_3_수정_사본_편집_화면(): - assert "editable: () => true" in DRAWING - assert "onEdit: (kind) => openEdit(copy, name, kind, load)" in DRAWING - assert 'labor: () => openEdit(copy, name, "inputs", load)' in DRAWING - assert 'import("@ui/ui_template_item_picker_edit")' in DRAWING - assert "createEditView(" in DRAWING and "items: projectItems(request.projectId)" in DRAWING - assert "info: { 폴더: copy.폴더, 원본: copy.항목 }" in DRAWING - # [뒤로] = 몸통 되돌림 · 저장했으면 카드 다시 읽음 · 닫을 때 집계표 다시 셈 - assert "body.replaceChildren(...kept)" in DRAWING and "if (saved) reload()" in DRAWING - assert "if (edited) request.onLinked()" in DRAWING - assert "if (editView && !(await editView.leave())) return false" in DRAWING - assert 'kind === "inputs" ? [note(L("B08_Quantity_Sheets_InputsOnly"))]' in DRAWING - assert "saveTemplate" not in DRAWING # 층 쓰기 없음 — 사본 PUT 만(편집 화면 `items.write`) - - -def test_163_4_배지_업데이트(): - assert ".list(folder)" in LINK and "structureFolder(slot.copy)" in LINK - for key in ("Edited", "Gone", "Changed"): - assert f'badge("B08_Quantity_Link_{key}"' in LINK, key - assert "button.disabled = !info.마스터바뀜 || !!info.원본없음" in LINK - assert "updateButton.disabled = true" in LINK # 대조 받기 전 꺼짐 - assert "projectItems(projectId).reapply(info)" in LINK - assert 'showConfirmDialog(ask, L("B08_Quantity_Link_Update"))' in LINK - assert 'info.고침 ? ` ${L("B08_Quantity_Link_UpdateEdited")}`' in LINK - # 저장 뒤 = [선택]과 같은 흐름(onLinked) - assert "saved\n ? onLinked()" in LINK - - -def test_163_5_콘크리트_5줄_늘_까닭(): - table = MATERIAL[ - MATERIAL.index("function concreteTable") : MATERIAL.index("function totalTable") - ] - assert "const keep = rows.map((_row, i) => i)" in table - assert 'editable: (_i, key) => key.startsWith("per:")' in table # 배합 늘 고침 - assert "function concreteWhy(" in MATERIAL - assert "matched(m.name, m.spec, m.unit)" in MATERIAL # 짝 계산 맞춤 그대로 - assert "source.snapshot?.units?.[row.id]" in MATERIAL - assert "concreteNote.textContent = concreteWhy(sheets, payload.source)" in MATERIAL - assert "Material_Calc" in MATERIAL and "concreteWhy" not in ( - ROOT / "B08_Quantity/B08_Quantity_Material_Calc.ts" - ).read_text(encoding="utf-8") - - -def test_locale_keys_exist(): - for text in (LINK, DRAWING): - for key in set(re.findall(r'L\("([A-Za-z0-9_]+)"\)', text)) | set( - re.findall(r'badge\("([A-Za-z0-9_]+)"', text) - ): - assert f"{key}:" in LOCALE or key.startswith(("B08_Quantity_Summary_", "Common_")), key diff --git a/resources/tester/test_170_b08_modal_head.py b/resources/tester/test_170_b08_modal_head.py deleted file mode 100644 index ddde235dc..000000000 --- a/resources/tester/test_170_b08_modal_head.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -"""170 B08 구조물도 모달 — 머리 단추(170-2) · 칩 테두리 상태 색(170-3) · 집계표 보조 탭 줄 자리(170-4). - -화면 글 판정 — 공용 모달(ui_template_modal)은 안 고침. -""" - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -PAGE = (ROOT / "B08_Quantity/B08_Quantity_UI_Page.ts").read_text(encoding="utf-8") -PAGE_CSS = (ROOT / "B08_Quantity/B08_Quantity_UI_Page.css").read_text(encoding="utf-8") - - -def test_170_4_집계표_보조_탭_줄_자리(): - branch = PAGE.split('if (tab === "summary" && projectId) {', 1)[1].split("\n }", 1)[0] - # 자재 탭 보조 탭 줄과 같은 클래스 · sm 단추 하나(보이지 않음 · 초점 안 감) - assert '"b08-quantity__tabs ui-subtabs b08-quantity__tabs--blank"' in branch # 279 공용 양식 - assert 'size: "sm"' in branch and "ghost.tabIndex = -1" in branch - assert 'setAttribute("aria-hidden", "true")' in branch - # 집계표는 줄 아래 몸통에 그림(replaceChildren 이 빈 줄을 지우지 않게) - assert ( - "view.append(blank, body)" in branch and "renderStructureSummary(body, projectId)" in branch - ) - assert ".b08-quantity__tabs--blank > .ui-btn {\n visibility: hidden;" in PAGE_CSS.replace( - "\r\n", "\n" - ) - - -LINK = (ROOT / "B08_Quantity/B08_Quantity_UI_Link.ts").read_text(encoding="utf-8") -DRAWING_CSS = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.css").read_text(encoding="utf-8") -THEME = (ROOT / "ui_template/ui_template_theme.css").read_text(encoding="utf-8") - - -def test_170_3_칩_테두리_상태_색(): - assert 'slot.linked ? "b08-link__chip is-linked" : "b08-link__chip is-missing"' in LINK - assert "paintCopyState(projectId, slot, chip, badges, updateButton)" in LINK - # 앞섬 = 없어짐 › 바뀜 › 고침 › 연결 - tone = LINK.split("const tone = info.원본없음", 1)[1].split(";", 1)[0] - assert tone.index('"is-gone"') < tone.index('"is-changed"') < tone.index('"is-edited"') - assert 'chip.classList.replace("is-linked", tone)' in LINK - for cls, token in ( - ("is-linked", "success"), - ("is-missing", "danger"), - ("is-changed", "warning"), - ("is-edited", "info"), - ("is-gone", "danger"), - ): - rule = DRAWING_CSS.split(f".b08-link__chip.{cls} {{", 1)[1].split("}", 1)[0] - assert f"border-color: var(--color-{token});" in rule - assert f"--color-{token}:" in THEME # 밝음 · 어두움 토큰 - assert ( - "border-style: dashed;" - in DRAWING_CSS.split(".b08-link__chip.is-gone {", 1)[1].split("}", 1)[0] - ) - - -DRAWING = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.ts").read_text(encoding="utf-8") -EDIT = (ROOT / "ui_template/ui_template_item_picker_edit.ts").read_text(encoding="utf-8") -PICKER = (ROOT / "ui_template/ui_template_item_picker.ts").read_text(encoding="utf-8") -MODAL = (ROOT / "ui_template/ui_template_modal.ts").read_text(encoding="utf-8") - - -def test_170_2_편집_화면_단추_모달_머리(): - # 편집 화면이 두 단추를 내보냄 · 고르기 모달(142-1)은 그대로 안 씀 - assert "back: HTMLButtonElement;" in EDIT and "save: HTMLButtonElement;" in EDIT - assert "return { root, back, save," in EDIT - assert "view.back" not in PICKER and "view.save" not in PICKER - # 빈 머리 칸을 열 때 미리 넘김(공용 모달은 mount 때 자식 있을 때만 머리에 붙임) - assert "if (actions.childElementCount) closeButton.before(actions)" in MODAL - assert "actions: [head]," in DRAWING and "head.hidden = true;" in DRAWING - # 들어갈 때 옮김 · 크기 = [닫기](md) · 차례 [← 뒤로] [저장] [닫기] - assert 'view.back.classList.remove("ui-btn--sm")' in DRAWING - assert 'view.save.classList.remove("ui-btn--sm")' in DRAWING - assert "head.replaceChildren(view.back, view.save);\n head.hidden = false;" in DRAWING - # 나올 때([뒤로] · 닫기) 거둠 - back = DRAWING.split("(saved) => {", 1)[1].split("},", 1)[0] - assert "clearHead();" in back - closing = DRAWING.split("beforeClose: async () => {", 1)[1].split("return true;", 1)[0] - assert "clearHead();" in closing - css = DRAWING_CSS - assert ".b08-sheets .ui-modal__actions[hidden] {\n display: none;" in css diff --git a/resources/tester/test_171_2_group_cards.py b/resources/tester/test_171_2_group_cards.py deleted file mode 100644 index a57b32cff..000000000 --- a/resources/tester/test_171_2_group_cards.py +++ /dev/null @@ -1,160 +0,0 @@ -"""171-2 묶음 카드 공용 양식(`ui_template_group_cards`) — Node(타입 지움) + 작은 가짜 DOM. - -- 머리 = 호표 · 명칭 · 규격 · 단위 · 합계 / 몸 = 이름 · 규격 · 수량 · 단위 · 합계 · 비고 / 합계 줄 = 노무 · 재료 · 경비 · 계 -- 줄 금액 = 합계만 · 비목 셋 = 풍선 · 숫자 오른쪽 · 글자 14px 한 벌 · 줄 간격 좁게(공용 카드 16px 끼움 없음) -- 가로 열 수 1~4(기본 2 · 고르면 알림) · 접기 없음(181-2) · 이동해 온 박스 = `is-pick`(붉은 외곽선) · 머리 누름 = 이동 -- 157-2 산근 카드 파일 = 이 양식으로 갈음(지움) -""" - -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 resources.tester.helper_b09_group_cards import COMMON, run # noqa: E402 - -GROUPS_JS = """ -const mk = (n) => ({ - key: `G${n}`, at: n * 10, no: `제 ${n} 호표`, name: `묶음${n}`, spec: "규격", specTip: "원문번호 1", - unit: "m3", sum: 1234.5, - lines: [ - { at: n * 10 + 1, name: "석공", spec: "", qty: 0.072, unit: "인", sum: 18643.3, - parts: [18643.3, null, null], note: "노무3", link: true }, - { at: n * 10 + 2, name: "인용", tag: "품셈" }, - ], - foot: { at: n * 10 + 3, parts: [100, 20, 3], sum: 123, note: "빈 줄 1", tip: "빈 줄 — 모래" }, -}); -const picks = []; -const cols = []; -const make = (count, extra = {}) => createGroupCards({ - groups: Array.from({ length: count }, (_, i) => mk(i + 1)), - onPick: (at) => picks.push(at), onColumns: (n) => cols.push(n), ...extra, -}); -""" - - -def _run(body): - return run([COMMON], GROUPS_JS + body) - - -def test_도우미_열수_요약_풍선(): - out = _run( - "\nconsole.log(JSON.stringify({" - " cols: [0, 1, 2, 3, 4, 5, '3', null, undefined].map(clampColumns)," - " tip: [partsTip([1000, 0, null]), partsTip([null, null, null]), partsTip(null)] }));" - ) - assert out["cols"] == [2, 1, 2, 3, 4, 2, 2, 2, 2] - assert out["tip"] == ["노무 1,000 · 재료 0 · 경비 -", None, None] - - -def test_카드_머리_몸_합계_줄(): - out = _run( - "\nconst root = make(1);" - "\nconst box = root.querySelector('.ui-group-card');" - "\nconst head = box.querySelector('.ui-group-card__head');" - "\nconst tds = box.querySelector('tbody').children[0].children;" - "\nconst ths = box.querySelector('thead').children[0].children;" - "\nconsole.log(JSON.stringify({" - " head: head.children.map((c) => [c.className, c.textContent, c.title])," - " ths: ths.map((c) => [c.textContent, c.className])," - " tds: tds.map((c) => [c.textContent, c.className, c.title])," - " tag: box.querySelector('.ui-group-card__tag').textContent," - " foot: box.querySelector('.ui-group-card__foot').children.map((c) => c.textContent)," - " link: box.querySelector('tbody').children.map((r) => r.classList.contains('is-link'))," - " cols: root.querySelector('.ui-group-cards__grid').style.props['--group-cols']," - " summary: root.all('.ui-btn').map((b) => b.textContent) }));" - ) - assert out["head"] == [ - ["ui-group-card__no", "제 1 호표", ""], - ["ui-group-card__title", "묶음1", ""], - ["ui-group-card__spec", "규격", "원문번호 1"], - ["ui-group-card__unit", "m3", ""], - ["ui-group-card__total", "1,234.5", ""], - ] - assert out["ths"] == [ - ["이름", ""], ["규격", ""], ["수량", "is-num"], ["단위", ""], ["합계", "is-num"], ["비고", ""], - ] # fmt: skip - # 줄 금액 = 합계만 · 비목 셋 = 풍선 · 숫자 칸 = 오른쪽(is-num) - assert out["tds"] == [ - ["석공", "ui-group-card__name", ""], - ["", "", ""], - ["0.1", "is-num", ""], - ["인", "", ""], - ["18,643.3", "is-num", "노무 18,643.3 · 재료 - · 경비 -"], - ["노무3", "ui-group-card__note", ""], - ] - assert out["tag"] == "품셈" - assert out["foot"] == ["노무 100", "재료 20", "경비 3", "계 123", "빈 줄 1"] - assert out["link"] == [True, False] - assert out["cols"] == "2" # 기본 2 - assert out["summary"] == ["1", "2", "3", "4"] # 가로 열 수 단추뿐(요약 · 접기 단추 없음) - - -def test_가로_열수_고름_알림_누름_이동(): - out = _run( - "\nconst root = make(2, { columns: 3 });" - "\nconst grid = root.querySelector('.ui-group-cards__grid');" - "\nconst before = grid.style.props['--group-cols'];" - "\nroot.querySelector('[data-group-cols=\"4\"]').click();" - "\nroot.querySelector('[data-at=\"21\"]').click();" - "\nroot.querySelector('[data-at=\"20\"]').click();" - "\nconsole.log(JSON.stringify({ before, after: grid.style.props['--group-cols'], cols," - " pressed: root.all('[data-group-cols]').map((b) => b.attrs['aria-pressed']), picks }));" - ) - assert out["before"] == "3" and out["after"] == "4" - assert out["cols"] == [4] - assert out["pressed"] == ["false", "false", "false", "true"] - assert out["picks"] == [21, 20] # 줄 · 머리 누름 = 표 줄 번호 그대로(갈 곳은 부르는 쪽 link) - - -def test_묶음_많아도_접힘_없음_고른_박스_붉은_외곽선(): - out = _run( - "\nconst root = make(13, { focus: 52 });" - "\nconst cards = root.all('.ui-group-card');" - "\nconst focused = root.querySelector('[data-at=\"52\"]');" - "\nconst first = cards.map((c) => c.classList.contains('is-pick'));" - "\ncards[0].querySelector('.ui-group-card__head').click();" - "\nconsole.log(JSON.stringify({ first, picks," - " closed: cards.map((c) => c.classList.contains('is-closed'))," - " buttons: root.all('.ui-btn').map((b) => b.textContent)," - " foot: cards.every((c) => c.querySelector('.ui-group-card__foot'))," - " focus: [focused.classList.contains('is-focus'), cards[4].scrolled === true] }));" - ) - # 이동해 온 줄(5번째 묶음 줄) = 그 박스만 붉은 외곽선 · 박스 스크롤(248-1 줄 강조 안 붙임) - assert out["first"] == [False] * 4 + [True] + [False] * 8 - assert out["focus"] == [False, True] - assert out["closed"] == [False] * 13 # 13 묶음도 접힘 없음 - assert out["picks"] == [10] # 머리 누름 = 이동(표 줄 번호 그대로) - assert out["buttons"] == ["1", "2", "3", "4"] # 모두 펼침 · 접기 단추 없음 - assert out["foot"] is True - - -def test_고른_박스_없으면_외곽선_없음(): - out = _run( - "\nconst root = make(3);" - "\nconsole.log(JSON.stringify(root.all('.ui-group-card').map(" - "(c) => c.classList.contains('is-pick'))));" - ) - assert out == [False, False, False] - - -def test_양식_글자_간격_숫자(): - css = (ROOT / "ui_template" / "ui_template_group_cards.css").read_text("utf-8") - theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8") - assert "--text-body-sm: 14px;" in theme - assert "font-size: var(--text-body-sm);" in css # 14px 한 벌 - assert [ln.strip() for ln in css.splitlines() if "font-size" in ln] == [ - "font-size: var(--text-body-sm);", - "font-size: inherit;", - ] - assert "padding: 1px var(--spacing-4);" in css # 줄 간격 좁게 - assert "is-closed" not in css and "is-closed" not in (ROOT / COMMON).read_text("utf-8") - assert "border-color: var(--color-danger);" in css # 고른 박스 = B06 고름 꼴(181-2) - assert "spacing-16" not in css and "card-padding" not in css # 공용 카드 16px 끼움 없음 - assert "grid-template-columns: repeat(var(--group-cols, 2), minmax(0, 1fr));" in css - assert "text-align: right;" in css - src = (ROOT / COMMON).read_text("utf-8") - assert "createCard" not in src - assert not (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Sangeun_Cards.ts").exists() diff --git a/resources/tester/test_171_3_b09_group_tabs.py b/resources/tester/test_171_3_b09_group_tabs.py deleted file mode 100644 index 36668ade1..000000000 --- a/resources/tester/test_171_3_b09_group_tabs.py +++ /dev/null @@ -1,182 +0,0 @@ -"""171-3 B09 묶음 탭 셋 = 묶음 카드 기본 · [표로 보기] = 표 그대로 · 보기 기억(탭마다). - -- 일위대가표 · 중기사용료 · 단가산출근거 — 표 줄을 호표마다 묶음(값 · 풍선 · 누름 이동 = 표와 같음) -- 기억 = `b_page_state` 두 줄(`b09-group-view` · `b09-group-cols`) · 탭마다 따로 · 모르는 값 = 카드 · 2 -- 집계표 넷 · 설계내역서 · B08 = 대상 밖 -""" - -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 resources.tester.helper_b09_group_cards import COMMON, GROUPS, RAW, TABS, run # noqa: E402 - -B09 = "B09_Estimation/B09_Estimation_UI_Tabs_" - -DG = { - "id": "DG1", "no": 1, "name": "찰쌓기", "spec": "기계=굴착기 0.6㎥", "spec_raw": "기계=0201-0070", - "unit": "m2", "kind": "품셈", "cite": "건설표준품셈 7-1-2", - "lines": [ - {"kind": "labor", "key": "L00005", "name": "석공", "qty": 0.072, "unit": "인", - "price": 258935, "amount": 18643.3, "ref_no": "노무3", - "prices": {"labor": 258935, "material": None, "expense": None}, - "amounts": {"labor": 18643.3, "material": None, "expense": None}}, - {"kind": "sangeun", "key": "SG1", "name": "뒷채움", "qty": 0.5, "unit": "m3", - "price": 8328, "amount": 4164, "ref_no": "산근 1호표", - "prices": {"labor": 6878, "material": 538, "expense": 912}, - "amounts": {"labor": 3439, "material": 269, "expense": 456}}, - ], - "sum": 22807, "labor": 22082, "material": 269, "expense": 456, - "blank": 1, "blank_names": ["모래"], -} # fmt: skip - -MB = { - "key": "EQ1", "name": "굴착기", "spec": "0.6㎥", "unit": "hr", - "lines": [ - {"kind": "expense", "name": "손료", "spec": "", "qty": 1, "unit": "hr", "price": 13041, - "amount": 13041, "ref": None}, - {"kind": "labor", "name": "운전사", "spec": "", "qty": 1, "unit": "인", "price": 55700, - "amount": 55700, "ref": {"key": "LB9"}}, - ], - "sum": 68741, "labor": 55700, "material": 0, "expense": 13041, -} # fmt: skip - -SG = { - "key": "SG:A", "no": 1, "name": "뒷채움", "unit": "m3", "spec": "규격=150", "spec_raw": "규격=150", - "head": ["뒷채움 / m3", "산림품셈 13-3-2 적용"], - "lines": [ - {"kind": "labor", "name": "보통인부", "ref": {"tab": "labor", "key": "LB2"}, "why": None, - "labor": 2979.8, "material": 0.0, "expense": 0.0, "text": ["보통인부 : 165,545 * 0.018 = 2,979.8"]}, - ], - "labor": 2979, "material": 0, "expense": 0, "sum": 2979, "why": None, "foot": "계 : 2,979", -} # fmt: skip - -# 카드 → 판정 거리(머리 · 몸 줄 · 합계 · 표 줄 번호 · 값이 표와 같은가 · 누름 이동) -DUMP = """ -const dump = (tab, t) => { - const cards = tabGroups(tab, t); - const first = t.columns[0].key; - return { - n: t.rows.length, - heads: cards.map((c) => [c.no, c.name, c.spec, c.specTip, c.unit, c.sum]), - lines: cards.flatMap((c) => c.lines.map((l) => [l.at, l.tag ?? null, l.name, l.qty ?? null, l.sum, l.parts, l.note])), - foot: cards.map((c) => [c.foot.at, c.foot.parts, c.foot.sum, c.foot.note, c.foot.tip]), - ats: cards.flatMap((c) => [c.at, ...c.lines.map((l) => l.at), c.foot.at]), - same: cards.every((c) => c.lines.every((l) => l.sum === (t.rows[l.at].sum ?? null))), - links: cards.flatMap((c) => c.lines.map((l) => (l.link ? t.link(l.at, first) : null))), - }; -}; -""" - - -def _tab(files, build, sheets, tab): - body = f"\nconsole.log(JSON.stringify(dump('{tab}', {build}({{ sheets: {json.dumps(sheets, ensure_ascii=False)} }}))));" - prelude = "" if any("Sangeun" in f for f in files) else RAW # raw 짝 = 산근 파일에 있음 - return run([COMMON, TABS, *files, GROUPS], DUMP + body, prelude) - - -def test_일위대가표_카드(): - out = _tab([f"{B09}Labor.ts"], "buildDaegaBlocks", {"daega_blocks": [DG]}, "dg") - assert out["heads"] == [ - ["제 1 호표", "찰쌓기", "기계=굴착기 0.6㎥", "원문번호 기계=0201-0070", "m2", 22807] - ] - assert out["lines"] == [ - [1, None, "건설표준품셈 7-1-2", None, None, [None, None, None], "품셈 인용"], - [2, None, "석공", 0.072, 18643.3, [18643.3, None, None], "노무3"], - [3, None, "뒷채움", 0.5, 4164, [3439, 269, 456], "산근 1호표"], - ] - assert out["foot"] == [[4, [22082, 269, 456], 22807, "빈 줄 1", "빈 줄 — 모래"]] - assert out["ats"] == list(range(out["n"])) and out["same"] is True - assert out["links"] == [None, {"tab": "labor", "key": "L00005"}, {"tab": "sg", "key": "SG1"}] - - -def test_중기사용료_카드(): - out = _tab([f"{B09}Base.ts"], "buildMachineBlocks", {"machine_blocks": [MB]}, "machine_blocks") - assert out["heads"] == [["제 1 호표", "굴착기", "0.6㎥", None, "hr", 68741]] - assert out["lines"] == [ - [1, None, "손료", 1, 13041, [None, None, 13041], ""], - [2, None, "운전사", 1, 55700, [55700, None, None], ""], - ] - assert out["foot"] == [[3, [55700, 0, 13041], 68741, "", None]] - assert out["ats"] == list(range(out["n"])) and out["same"] is True - assert out["links"] == [None, {"tab": "basis", "key": "LB9"}] - - -def test_단가산출근거_카드(): - out = _tab([f"{B09}Sangeun.ts"], "buildSg", {"sg": [SG]}, "sg") - assert out["heads"] == [["제 1 호표", "뒷채움", "규격=150", None, "m3", 2979]] - assert out["lines"] == [ - [1, "", "산림품셈 13-3-2 적용", None, None, [None, None, None], ""], - [2, "노무", "보통인부 : 165,545 * 0.018 = 2,979.8", None, 2979.8, [2979.8, 0, 0], ""], - ] - assert out["foot"][0][:3] == [3, [2979, 0, 0], 2979] - assert out["ats"] == list(range(out["n"])) and out["same"] is True - assert out["links"] == [None, {"tab": "labor", "key": "LB2"}] - - -def test_보기_한_값_열_수_탭마다(): - out = run( - [COMMON, TABS, GROUPS], - "\nconst a = [readGroupView(), readGroupCols('dg')];" - "\nkept.set('b09-group-view', JSON.stringify({ machine_blocks: 'table' }));" # 옛 탭별 값 - "\nrememberGroup('b09-group-cols', 'sg', 3);" - "\nconst b = [groupView(), readGroupView(), readGroupView()," - " readGroupCols('sg'), readGroupCols('dg'), readGroupCols('dg', { dg: 9 })];" - "\nconsole.log(JSON.stringify({ a, b, kept: Object.fromEntries(kept)," - " tabs: GROUP_TABS, other: ['bill', 'sum_material', 'sum_labor', 'sum_expense'," - " 'sum_machine', 'dg_list'].map(isGroupTab) }));", - ) - assert out["a"] == ["cards", 2] # 기본 = 카드 · 가로 2 - assert out["b"] == ["table", "table", "table", 3, 2, 2] # 304-1 셋 탭 한 값 - assert json.loads(out["kept"]["b09-group-view"]) == "table" # 첫 읽기에 옛 값 → 한 값 - assert json.loads(out["kept"]["b09-group-cols"]) == {"sg": 3} - assert out["tabs"] == ["dg", "machine_blocks", "sg"] - assert out["other"] == [False] * 6 # 집계표 넷 · 설계내역서 · 목록표 = 대상 밖 - - -def test_화면_보기_고름_누름_이동(): - out = run( - [COMMON, TABS, f"{B09}Labor.ts", GROUPS], - f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});" - "\nconst gone = []; let redraws = 0; const focused = []; const calls = [];" - "\nconst o = { tab: 'dg', spec, hint: null, focus: 'DG1|1', goto: (l) => gone.push(l)," - " redraw: () => redraws++, setTools: (t) => calls.push(t), makeTable: () => ({ root: document.createElement('table')," - " focusRow: (i) => focused.push(i) }) };" - "\nconst [view] = renderGroupPanel(o);" - "\nconst label = calls.flat().filter((e) => e.dataset.b09GroupView).length;" - "\nconst lit = view.querySelector('[data-at=\"3\"]').classList.contains('is-focus');" - "\nconst box = view.querySelector('.ui-group-card').classList.contains('is-pick');" - "\nview.querySelector('[data-at=\"2\"]').click();" - "\nview.querySelector('[data-at=\"0\"]').click();" - "\nconst before = calls.length; chooseGroupView('table');" - "\nconst [view2] = renderGroupPanel(o);" - "\nconsole.log(JSON.stringify({ label, table: view.dataset.b09Table, gone, lit, box, redraws," - " tools2: calls.length - before, tag2: view2.tagName, focused," - " kept: Object.fromEntries(kept) }));", - RAW, - ) - assert out["label"] == 0 and out["table"] == "dg" # 기본 = 카드 · 304-1 탭 줄 보기 단추 없음 - assert out["gone"] == [{"tab": "labor", "key": "L00005"}, None] # 줄 누름 = 표와 같은 이동 - assert out["lit"] is False # 248-1 이동해 온 줄 = 빨강 하나(줄 강조 안 붙임) - assert out["box"] is True # 그 호표 박스 = 붉은 외곽선(181-2) - assert out["redraws"] == 0 - assert out["tools2"] == 0 and out["tag2"] == "TABLE" and out["focused"] == [3] - assert json.loads(out["kept"]["b09-group-view"]) == "table" - - -def test_잇기(): - shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8") - assert ( - "if (isGroupTab(tab)) {" in shell - and "renderGroupPanel({" in shell - and "setTools: bars.setTools" in shell - ) - state = (ROOT / "A00_Common" / "b_page_state.ts").read_text("utf-8") - assert '"b09-group-view": { bucket: "pref", scope: "global" },' in state - assert '"b09-group-cols": { bucket: "pref", scope: "global" },' in state - b08 = "".join(p.read_text("utf-8") for p in (ROOT / "B08_Quantity").glob("*.ts")) - assert "createGroupCards" not in b08 # B08 = 대상 밖 diff --git a/resources/tester/test_181_1_b09_tab_groups.py b/resources/tester/test_181_1_b09_tab_groups.py deleted file mode 100644 index 09b83bd94..000000000 --- a/resources/tester/test_181_1_b09_tab_groups.py +++ /dev/null @@ -1,102 +0,0 @@ -"""181-1 B09 탭 두 층 — 위 탭 8(203-1 연결 점검 뺌 · 257-1 설계내역 한 묶음) · 아래 탭 묶음(사용자 30번) · 줄 그리기 · 마지막 본 아래 탭. - -- 위 탭 = `TAB_GROUPS`(`TABS` 18 키를 빠짐 · 겹침 없이 묶음) · `TABS`(엑셀 차례)는 그대로 -- 줄 = 위 md · 아래 sm · 아래 탭 없는 위 탭 = 빈 보조 줄(B08 꼴) -""" - -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 resources.tester.helper_b09_group_cards import TABS, run # noqa: E402 - -BAR = "B09_Estimation/B09_Estimation_UI_TabBar.ts" - -# 사용자 30번 — 위 탭 차례 = 지금 탭 차례(묶음 첫 자리) · 아래 = 글에 적힌 차례 -EXPECTED = [ - ["공사원가계산서", ["cost"]], - ["설계내역", ["total", "bill"]], - ["일위대가", ["dg_list", "dg"]], - ["단가산출", ["sg_list", "sg"]], - ["환율및기초자료", ["basis"]], - ["중기", ["machine", "machine_blocks", "sum_machine", "expense", "sum_expense"]], - ["재료", ["material", "compare", "sum_material"]], - ["노무", ["labor", "sum_labor"]], -] - - -def test_묶음_표_사용자_30번과_같고_18_탭이_한_번씩(): - out = run( - [TABS], - "\nconsole.log(JSON.stringify({ groups: TAB_GROUPS, ids: TABS.map(([t]) => t)," - " of: TABS.map(([t]) => groupOf(t)[0]) }));", - ) - assert out["groups"] == EXPECTED - flat = [t for _, tabs in out["groups"] for t in tabs] - assert sorted(flat) == sorted(out["ids"]) and len(flat) == len(set(flat)) == 18 - # 위 탭 차례 = 엑셀 차례에서 묶음이 처음 나오는 자리 - firsts = list(dict.fromkeys(out["of"])) - assert firsts == [g[0] for g in EXPECTED] - assert len(firsts) == 8 - - -def test_줄_그리기_두_층_빈_줄_마지막_본_아래_탭(): - out = run( - [TABS, BAR], - """ -const picks = []; -const bars = createTabBars((t) => picks.push(t)); -const view = () => ({ - top: bars.top.children.map((b) => [b.textContent, b.className, b.attrs['aria-selected'], - b.dataset.b09Group, b.dataset.b09Tab ?? null]), - sub: bars.sub.children.filter((b) => !b.classList.contains('b09-estimation__tools')).map((b) => [b.textContent, b.attrs['aria-selected'], b.dataset.b09Tab ?? null]), - blank: bars.sub.classList.contains('b09-estimation__tabs--blank'), - role: bars.sub.attrs.role, -}); -bars.draw('dg'); -const a = view(); -bars.top.children[2].click(); // 켜진 위 탭 다시 누름 = 아무 일 없음 -bars.sub.children[0].click(); // 일위대가목록표 -bars.draw('compare'); // 묶음을 넘는 이동 = 위 탭 따라 켜짐 -const b = view(); -bars.draw('sum_material'); -bars.draw('dg'); -bars.top.children[6].click(); // 재료 = 마지막 본 아래 탭(재료비수량금액집계표) -bars.top.children[5].click(); // 중기 = 본 적 없음 → 첫 아래 탭 -bars.draw('cost'); -const c = view(); -bars.top.children[1].click(); // 설계내역 = 첫 아래 탭(총괄) -bars.sub.children[0].click(); // 빈 줄 단추(숨김) = 누름 없음 -console.log(JSON.stringify({ a, b, c, picks })); -""", - ) - a, b, c = out["a"], out["b"], out["c"] - assert len(a["top"]) == 8 and [t[0] for t in a["top"]][:5] == [ - "공사원가계산서", "설계내역", "일위대가", "단가산출", "환율및기초자료" - ] # fmt: skip - assert [t[2] for t in a["top"]] == ["false"] * 2 + ["true"] + ["false"] * 5 - assert all("ui-btn--ghost" in t[1] for t in a["top"] if t[2] == "false") - assert a["sub"] == [["일위대가목록표", "false", "dg_list"], ["일위대가표", "true", "dg"]] - assert not a["blank"] and a["role"] == "tablist" - assert b["sub"] == [ - ["재료비목록표", "false", "material"], - ["자재단가대비표", "true", "compare"], - ["재료비수량금액집계표", "false", "sum_material"], - ] # fmt: skip - assert b["top"][6][2] == "true" and b["top"][2][2] == "false" - # 단독 위 탭 — 위 단추가 탭 키도 가짐 · 아래 줄 = 숨긴 단추 한 개 - assert c["blank"] and c["role"] == "presentation" and len(c["sub"]) == 1 - assert c["top"][0][3] == "cost" and c["top"][0][4] == "cost" and c["top"][3][4] is None - assert out["picks"] == ["dg_list", "sum_material", "machine", "total"] - - -def test_잇기_쉘이_두_줄을_쓰고_CSS_빈_줄_규칙(): - shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8") - assert "createTabBars((tab) => show(tab))" in shell and "bars.draw(tab);" in shell - assert "body.append(bars.top, bars.sub, panel);" in shell - css = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8") - assert ".b09-estimation__tabs--blank > .ui-btn" in css and "visibility: hidden" in css diff --git a/resources/tester/test_181_2_b09_box_pick.py b/resources/tester/test_181_2_b09_box_pick.py deleted file mode 100644 index 211dd7009..000000000 --- a/resources/tester/test_181_2_b09_box_pick.py +++ /dev/null @@ -1,78 +0,0 @@ -"""181-2 박스보기 — 이름 · 접기 없음 · 목록표에서 고른 호표 = 박스 붉은 외곽선 · 표 보기 = 줄 구간 전체. - -- 단추 글 「박스보기」 · 「호표보기」(304-1 좌측 스위치) · 요약(접기) 보기 지움 -- 표 보기 = 고른 호표 줄 구간(키 `|` 앞머리 같은 줄) 모두 `is-pick` · 첫 줄 `is-pick-top` · 끝 줄 `is-pick-end` -- 붉은 색 = `--color-danger`(B06 횡단 카드 고름 꼴) -""" - -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 resources.tester.helper_b09_group_cards import COMMON, GROUPS, TABS, run # noqa: E402 - -ROWS = [ # 호표 A(3줄) · 호표 B(4줄) · 호표 C(1줄) - {"key": "A"}, {"key": "A|1"}, {"key": "A|합계"}, - {"key": "B"}, {"key": "B|1"}, {"key": "B|2"}, {"key": "B|총계"}, - {"key": "C"}, -] # fmt: skip - - -def test_구간_첫_끝_줄(): - out = run( - [COMMON, TABS, GROUPS], - f"\nconst rows = {json.dumps(ROWS)};" - "\nconst show = (focus) => { const r = pickRange(rows, focus);" - " return [r, rows.map((_, i) => pickClass(r, i))]; };" - "\nconsole.log(JSON.stringify({ a: show('A|1'), b: show('B'), c: show('C')," - " none: show(undefined), unknown: show('Z|1') }));", - ) - assert out["a"][0] == [0, 2] - assert out["a"][1] == [ - "is-pick is-pick-top", "is-pick", "is-pick is-pick-end", None, None, None, None, None, - ] # fmt: skip - assert out["b"][0] == [3, 6] - assert out["b"][1][3:7] == [ - "is-pick is-pick-top", "is-pick", "is-pick", "is-pick is-pick-end", - ] # fmt: skip - assert out["b"][1][:3] == [None] * 3 # 다른 호표 없음 - assert out["c"][0] == [7, 7] and out["c"][1][7] == "is-pick is-pick-top is-pick-end" - assert out["none"] == [None, [None] * 8] and out["unknown"] == [None, [None] * 8] - - -def test_표_보기_줄_class_넘김(): - out = run( - [COMMON, TABS, GROUPS], - f"\nconst spec = {{ columns: [{{ key: 'key' }}], rows: {json.dumps(ROWS)} }};" - "\nlet extra = null;" - "\nconst o = { tab: 'dg', spec, hint: null, focus: 'B|1', goto: () => {}, redraw: () => {}, setTools: () => {}," - " makeTable: (e) => { extra = e; return { root: document.createElement('table')," - " focusRow: () => {} }; } };" - "\nchooseGroupView('table');" - "\nrenderGroupPanel(o);" - "\nconsole.log(JSON.stringify(spec.rows.map((_, i) => extra(i))));", - ) - assert out == ( - [None] * 3 + ["is-pick is-pick-top", "is-pick", "is-pick", "is-pick is-pick-end"] + [None] - ) - - -def test_글_접기_없음_CSS_잇기(): - groups = (ROOT / GROUPS).read_text("utf-8") - assert '"박스보기" : "호표보기"' in groups and "호표별 보기" not in groups # 304-1 글 - common = (ROOT / COMMON).read_text("utf-8") - for gone in ("isSummary", "GROUP_SUMMARY_OVER", "is-closed", "모두 펼침", "모두 접기"): - assert gone not in common - assert 'box?.classList.add("is-pick")' in common - page = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8") - for sel in ("tr.is-pick td", "tr.is-pick.is-pick-top td", "tr.is-pick.is-pick-end td"): - assert sel in page - assert ( - page.count("var(--color-danger)") == 5 - ) # 위 · 아래 · 왼 · 오른 2px + 204-1 끌기 못 놓음 점선 - shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8") - assert "extra?: (index: number) => string | null" in shell diff --git a/resources/tester/test_183_2_b09_compare_head.py b/resources/tester/test_183_2_b09_compare_head.py deleted file mode 100644 index 97ff1246e..000000000 --- a/resources/tester/test_183_2_b09_compare_head.py +++ /dev/null @@ -1,30 +0,0 @@ -"""183-2 자재단가대비표 머리 — 견적 「면」 뺌 · 견적서 = 견적 아래 · 다른 출처 「출처」 열은 185-4 에서 호버로. - -소스 시험(열 정의 · 고칠 칸) — 셈 · 저장(`quote_page` 남음)은 안 바뀜(test_166_4). -""" - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -SRC = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Base.ts").read_text(encoding="utf-8") - - -def _compare() -> str: - start = SRC.index("export const buildCompare") - return SRC[start : SRC.index("export const buildMaterial")] - - -def test_견적은_단가_견적서_그_밖은_단가_하나(): - # 185-4 — 출처 열 없앰(출처 뜻 = 단가 칸 호버 · test_185_4) - body = _compare() - assert '[money(`p:${s}`, "단가", [s]), text("files", "견적서", 72, [s])]' in body - assert ': [money(`p:${s}`, "단가", [s])],' in body - assert "g:${s}" not in body and '"출처", 64' not in body - assert '"면"' not in body # 면 머리 없음 - assert body.count('text("files", "견적서"') == 1 # 맨 끝 견적서 열 지움 - - -def test_견적_면_수기_칸_없앰(): - body = _compare() - assert 'key === "g:견적"' not in body and "quote_page" not in body - assert "견적 단가는 수기 입력" in body and "견적 단가 · 면" not in body diff --git a/resources/tester/test_185_2_b09_view_bar.py b/resources/tester/test_185_2_b09_view_bar.py deleted file mode 100644 index e18aa71c4..000000000 --- a/resources/tester/test_185_2_b09_view_bar.py +++ /dev/null @@ -1,84 +0,0 @@ -"""185-2 B09 묶음 탭 가로 열 수 단추 한 줄 · 보기 고름 = 좌측 하단(304-1 · 탭 줄에 없음). - -- 가로 열 수 = 도구 묶음(`setTools` · 202-1 로 페이지 보조 탭 줄 오른쪽) · 카드 판 머리 띠 = 묶음 수만 -- 단추 · 라벨 = `white-space: nowrap` · 도구 묶음 = 줄바꿈 없음(`flex-wrap: nowrap`) -""" - -import json -import re -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 resources.tester.helper_b09_group_cards import COMMON, GROUPS, RAW, TABS, run # noqa: E402 - -B09 = "B09_Estimation/B09_Estimation_UI_Tabs_" -DG = {"id": "DG1", "no": 1, "name": "찰쌓기", "unit": "m2", "lines": [], "sum": 0} -HINT = "(() => { const h = document.createElement('p'); h.className = 'hint'; return h; })()" - - -def _block(css: str, selector: str) -> str: - m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css) - assert m, selector - return m.group(1) - - -def _panel(view: str, hint: str): - """고른 보기 · 안내 글로 그린 뒤 도구 묶음(보기 바꿈 단추 · 열 수 단추) · 판 안 머리 띠.""" - return run( - [COMMON, TABS, f"{B09}Labor.ts", GROUPS], - f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});" - f"\nchooseGroupView('{view}');" - "\nconst tools = [];" - f"\nconst o = {{ tab: 'dg', spec, hint: {hint}, goto: () => {{}}, redraw: () => {{}}," - " setTools: (t) => tools.push(...t)," - " makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) };" - "\nconst out = renderGroupPanel(o);" - "\nconst btn = tools.find((e) => e.dataset.b09GroupView);" - "\nconst bar = out[0].all('.ui-group-cards__bar')[0] ?? null;" - "\nconsole.log(JSON.stringify({ n: out.length, first: out[0].className, label: btn ? btn.textContent : null, nTools: tools.length," - " cols: tools.flatMap((e) => e.all('[data-group-cols]')).map((b) => b.dataset.groupCols)," - " barBtn: bar ? bar.all('[data-b09-group-view]').length : null," - " barCols: bar ? bar.all('[data-group-cols]').length : null }));", - RAW, - ) - - -def test_카드_보기_단추가_열수_단추와_같은_도구_묶음에(): - out = _panel("cards", "null") - assert out["label"] is None # 304-1 보기 단추 = 좌측 하단(탭 줄에 없음) - assert out["cols"] == ["1", "2", "3", "4"] # 같은 묶음에 열 수 단추 넷 - assert out["barBtn"] == 0 and out["barCols"] == 0 # 카드 판 머리 띠에는 없음 - assert out["n"] == 1 # 안내 글 없으면 카드 판 하나뿐 - - -def test_카드_보기_안내_글은_카드_판_아래에(): - # 202-1 — 안내 글 = 카드 판 아래(상세 = test_202_1) - out = _panel("cards", HINT) - assert out["n"] == 2 and out["first"] == "ui-group-cards" - - -def test_표_보기_단추는_도구_묶음에(): - out = _panel("table", "null") - assert out["label"] is None and out["n"] == 1 - assert out["cols"] == [] and out["nTools"] == 0 # 표 보기 = 도구 없음 - - -def test_CSS_한_줄_nowrap(): - cards = (ROOT / "ui_template" / "ui_template_group_cards.css").read_text("utf-8") - assert "flex-wrap: nowrap" in _block(cards, ".ui-group-cards__bar") - item = _block( - cards, - ".ui-group-cards__bar .ui-btn,\n.ui-group-cards__label,\n.ui-group-cards__count", - ) - assert "white-space: nowrap" in item and "flex: none" in item - page = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8") - assert "flex-wrap: nowrap" in _block(page, ".b09-estimation__tools") - tools = _block( - page, ".b09-estimation__tools .ui-btn,\n.b09-estimation__tools .ui-group-cards__label" - ) - assert "white-space: nowrap" in tools - assert "toggle" not in (ROOT / GROUPS).read_text("utf-8") # 304-1 탭 줄 토글 걷음 diff --git a/resources/tester/test_187_1_b05_head_button_size.py b/resources/tester/test_187_1_b05_head_button_size.py deleted file mode 100644 index 53f0f67fa..000000000 --- a/resources/tester/test_187_1_b05_head_button_size.py +++ /dev/null @@ -1,89 +0,0 @@ -"""187-1 B05 종단 그래프 머리 줄 — [초기화]~▼ 단추 크기 = [새 창] 크기([배수유역도] 단추는 282-1 로 지움). - -- 기준값 = [새 창] 단추의 계산 크기(`.ui-btn` 바탕 + 줄 단추 토큰) — 높이 16 · 글자 12 · 좌우 여백 8 -- 왼쪽 단추(`b05-route-profile__tool`) = 같은 토큰 · 고정 높이 · 최소 너비 없음 -""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def _read(rel: str) -> str: - return (ROOT / rel).read_text(encoding="utf-8") - - -def _block(css: str, selector: str) -> str: - m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css) - assert m, selector - return m.group(1) - - -def _decl(block: str, prop: str) -> str: - m = re.search(r"(?:^|;|\s)" + re.escape(prop) + r"\s*:\s*([^;]+);", block) - assert m, prop - return m.group(1).strip() - - -THEME = _read("ui_template/ui_template_theme.css") -BASE = _read("ui_template/ui_template_elements_styles.ts") -LEFT = _block(_read("B05_Profile/B05_Profile_UI_Style.css"), ".b05-route-profile__tool") -POPOUT = _block( - _read("A00_Common/design/design_page.css"), - ".b05-route-profile__tools > .b05-route-profile__popout", -) - - -def _px(value: str) -> int: - return int(re.fullmatch(r"(\d+)px", value).group(1)) - - -def _token(name: str) -> str: - return _decl(THEME, name) - - -def _size(block: str) -> dict: - """계산 크기 — padding · font-size 를 토큰으로 풀고 line-height 1 · 테두리 1px 로 높이를 셈.""" - pad = _decl(block, "padding") - font = _decl(block, "font-size") - if pad == "var(--btn-bar-padding)": - pad = _token("--btn-bar-padding") - if font == "var(--btn-bar-font-size)": - font = _token("--btn-bar-font-size") - v = re.fullmatch(r"(\d+)px var\(--spacing-8\)", pad).group(1) - spacing8 = _px(_token("--spacing-8")) - caption = _px(_token("--text-caption")) if font == "var(--text-caption)" else _px(font) - return {"padV": int(v), "padH": spacing8, "font": caption, "height": caption + 2 * int(v) + 2} - - -def test_기준값_오른쪽_단추_16_12_8(): - for block in (POPOUT,): - assert _size(block) == {"padV": 1, "padH": 8, "font": 12, "height": 16} - # 높이 계산의 바탕 = .ui-btn 줄 높이 1 · 테두리 1px - assert re.search(r"\.ui-btn \{[^}]*line-height: 1;[^}]*border: 1px solid", BASE) - - -def test_왼쪽_단추_높이_글자_여백이_오른쪽_기준값과_같음(): - assert _size(LEFT) == _size(POPOUT) - assert _decl(LEFT, "line-height") == "1" - assert re.search(r"border:\s*1px solid", LEFT) - - -def test_왼쪽_단추에_옛_고정_크기_없음(): - assert "height:" not in LEFT.replace("line-height", "") - assert "min-width" not in LEFT - assert "9px" not in LEFT - - -def test_두_단추_모두_같은_토큰(): - for block in (LEFT, POPOUT): - assert _decl(block, "padding") == "var(--btn-bar-padding)" - assert _decl(block, "font-size") == "var(--btn-bar-font-size)" - assert _decl(LEFT, "font-family") == "var(--font-body)" # .ui-btn 바탕과 같은 글꼴 - - -def test_토큰은_theme_한_곳(): - assert _token("--btn-bar-padding") == "1px var(--spacing-8)" - assert _token("--btn-bar-font-size") == "var(--text-caption)" - assert _px(_token("--spacing-8")) == 8 and _px(_token("--text-caption")) == 12 diff --git a/resources/tester/test_200_1_b09_box_tool_line.py b/resources/tester/test_200_1_b09_box_tool_line.py deleted file mode 100644 index b0c95cb85..000000000 --- a/resources/tester/test_200_1_b09_box_tool_line.py +++ /dev/null @@ -1,187 +0,0 @@ -"""200-1 B09 박스보기 안내 글 줄임표 + 호버 · 카드 같은 가로 줄 높이 같게. - -- 안내 글 = 203 줄 수 변수만큼(넘치면 스크롤) · 가로로 잘렸을 때만 공용 호버(`attachHoverInfo`)로 전문 · 자리 = 표 · 카드 판 아래(202-1 로 옮김) -- 카드 격자 = 행 늘림(`align-items: stretch`) · 카드 = 세로 flex · 몸통이 늘어 합계 줄이 아래 -- 카드 제목 줄 · 합계 줄 바탕색(200-3) -""" - -import json -import re -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 resources.tester.helper_b09_group_cards import COMMON, GROUPS, RAW, TABS, run # noqa: E402 - -B09 = "B09_Estimation/B09_Estimation_UI_Tabs_" -DG = {"id": "DG1", "no": 1, "name": "찰쌓기", "unit": "m2", "lines": [], "sum": 0} -LONG = "손료 = 기준가(천원) × 손료계수 ÷ 10,000 · 운전사 = 인 × 시간 노임 · 연료 = ℓ/hr × 적용 단가" -HINT = ( - "(() => { const h = document.createElement('p');" - f" h.className = 'ui-general-block__empty'; h.textContent = '{LONG}'; return h; }})()" -) - - -def _block(css: str, selector: str) -> str: - m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css) - assert m, selector - return m.group(1) - - -def _panel(view: str, hint: str): - """고른 보기 · 안내 글로 그린 뒤 안내 글 자리 · 호버 기록.""" - return run( - [COMMON, TABS, f"{B09}Labor.ts", GROUPS], - f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});" - f"\nchooseGroupView('{view}');" - f"\nconst hint = {hint};" - "\nconst o = { tab: 'dg', spec, hint, goto: () => {}, redraw: () => {}, setTools: () => {}," - " makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) };" - "\nconst out = renderGroupPanel(o);" - "\nconst last = out[out.length - 1];" - "\nconst inBar = hint ? out.some((e) => e.all('.ui-group-cards__bar').some((b) => b.contains(hint))) : false;" - "\nconst call = hoverCalls[0];" - "\nconst wide = call ? call.text({ scrollWidth: 900, clientWidth: 300, textContent: 'T' }) : null;" - "\nconst fit = call ? call.text({ scrollWidth: 300, clientWidth: 300, textContent: 'T' }) : null;" - "\nconsole.log(JSON.stringify({ n: out.length, hintLast: !!hint && last === hint, inBar," - " hintCls: hint ? hint.className : '', hover: hoverCalls.length," - " hoverIsHint: call ? call.root === hint : false, wide, fit, first: out[0].tagName }));", - RAW, - ) - - -def test_카드_보기_안내_글은_카드_판_아래(): - out = _panel("cards", HINT) - assert out["n"] == 2 and out["hintLast"] and not out["inBar"] # 카드 판 + 안내 글(맨 아래) - assert "b09-estimation__line-hint" in out["hintCls"] - - -def test_표_보기_안내_글은_표_아래(): - out = _panel("table", HINT) - assert out["n"] == 2 and out["first"] == "TABLE" and out["hintLast"] # 표 + 안내 글 - - -def test_안내_글_잘렸을_때만_공용_호버로_전문(): - for view in ("cards", "table"): - out = _panel(view, HINT) - assert out["hover"] == 1 and out["hoverIsHint"] - assert out["wide"] == "T" # 잘림(scrollWidth > clientWidth) = 전문 - assert out["fit"] is None # 다 보이면 안 띄움 - - -def test_안내_글_없으면_호버도_없음(): - for view in ("cards", "table"): - out = _panel(view, "null") - assert out["n"] == 1 and out["hintLast"] is False and out["hover"] == 0 - - -def test_CSS_안내_글_줄_수_변수(): - page = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8") - hint = _block(page, ".b09-estimation__line-hint") - for need in ( # 203 한 줄 줄임표 → 줄 수 변수만큼 · 넘치면 스크롤 - "max-height: calc(var(--b09-hint-lines) * 1lh)", - "overflow-y: auto", - "min-width: 0", - ): - assert need in hint, need - - -def test_CSS_카드_같은_줄_높이_같게(): - css = (ROOT / "ui_template" / "ui_template_group_cards.css").read_text("utf-8") - grid = _block(css, ".ui-group-cards__grid") - assert "align-items: stretch" in grid and "align-items: start" not in grid - card = _block(css, ".ui-group-card") - assert "display: flex" in card and "flex-direction: column" in card - assert "flex: 1 1 auto" in _block(css, ".ui-group-card__body") # 합계 줄 = 카드 아래 - - -def test_잇기(): - groups = (ROOT / GROUPS).read_text("utf-8") - assert "attachHoverInfo(hint," in groups and "placeTools: o.setTools" in groups - cards = (ROOT / COMMON).read_text("utf-8") - assert "opts.lead" not in cards and "if (opts.placeTools) {" in cards - - -# ── 200-3 카드 제목 줄 · 합계 줄 바탕색(밝음 · 어두움) ───────────────────────── - - -def _theme_vars(dark: bool) -> dict: - """테마 CSS 의 `:root`(밝음) · 어두움 덮어쓰기 토큰 → 한 벌.""" - css = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8") - light = dict(re.findall(r"(--[\w-]+):\s*([^;]+);", _block(css, ":root"))) - if dark: - light.update(re.findall(r"(--[\w-]+):\s*([^;]+);", _block(css, ':root[data-theme="dark"]'))) - return light - - -def _rgb(token: str, theme: dict) -> tuple: - """토큰 → (r, g, b) · var() 별칭 따라감 · 16진수만.""" - val = theme[token].split("/*")[0].strip() - while m := re.fullmatch(r"var\((--[\w-]+)\)", val): - val = theme[m.group(1)].split("/*")[0].strip() - assert re.fullmatch(r"#[0-9a-fA-F]{6}", val), (token, val) - return tuple(int(val[i : i + 2], 16) for i in (1, 3, 5)) - - -def _mix(spec: str, theme: dict) -> tuple: - """`color-mix(in srgb, var(A) N%, var(B))` → 섞은 (r, g, b).""" - m = re.fullmatch( - r"color-mix\(in srgb,\s*var\((--[\w-]+)\)\s*(\d+)%,\s*var\((--[\w-]+)\)\)", spec - ) - assert m, spec - a, n, b = _rgb(m.group(1), theme), int(m.group(2)) / 100, _rgb(m.group(3), theme) - return tuple(a[i] * n + b[i] * (1 - n) for i in range(3)) - - -def _lum(c: tuple) -> float: - ch = [ - (v / 255) / 12.92 if v / 255 <= 0.03928 else (((v / 255) + 0.055) / 1.055) ** 2.4 for v in c - ] - return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2] - - -def _contrast(a: tuple, b: tuple) -> float: - hi, lo = sorted((_lum(a), _lum(b)), reverse=True) - return (hi + 0.05) / (lo + 0.05) - - -def _tokens() -> tuple: - css = (ROOT / "ui_template" / "ui_template_group_cards.css").read_text("utf-8") - root = _block(css, ".ui-group-cards") - head = re.search(r"--group-head-bg:\s*([^;]+);", root) - foot = re.search(r"--group-foot-bg:\s*([^;]+);", root) - assert head and foot - return css, head.group(1), foot.group(1) - - -def test_카드_제목_줄과_합계_줄_바탕색_토큰(): - css, head, foot = _tokens() - assert head != foot - assert "background-color: var(--group-head-bg)" in _block(css, ".ui-group-card__head") - assert "background-color: var(--group-foot-bg)" in _block(css, ".ui-group-card__foot") - for spec in (head, foot): # 고른 값 없이 테마 변수만(색 고정값 없음) - assert "#" not in spec and "rgb(" not in spec - - -def test_밝음_어두움_두_색이_다르고_글_대비_유지(): - _, head, foot = _tokens() - for dark in (False, True): - theme = _theme_vars(dark) - h, f = _mix(head, theme), _mix(foot, theme) - assert sum(abs(h[i] - f[i]) for i in range(3)) >= 40, (dark, h, f) # 서로 다른 색 - card = _rgb("--color-surface-raised", theme) - assert sum(abs(f[i] - card[i]) for i in range(3)) >= 40 # 카드 바탕과도 구분 - for color in ("--color-text", "--color-text-secondary", "--color-accent"): - assert _contrast(_rgb(color, theme), h) >= 4.5, (dark, "head", color) - for color in ("--color-text", "--color-text-body", "--color-text-secondary"): - assert _contrast(_rgb(color, theme), f) >= 4.5, (dark, "foot", color) - - -def test_어두움_테마_값(): - # 어두움 테마에서 쓰는 토큰이 실제로 어두움 값으로 덮임(밝음 값 그대로가 아님) - light, dark = _theme_vars(False), _theme_vars(True) - for token in ("--color-surface", "--color-surface-raised", "--color-accent"): - assert _rgb(token, light) != _rgb(token, dark), token diff --git a/resources/tester/test_203_b09_hint_lines.py b/resources/tester/test_203_b09_hint_lines.py deleted file mode 100644 index 7d58bdf37..000000000 --- a/resources/tester/test_203_b09_hint_lines.py +++ /dev/null @@ -1,185 +0,0 @@ -"""203 B09 탭 상세 안내 글 영역 = 줄 수 변수(`--b09-hint-lines` 기본 3)만큼 · 넘치면 영역 안 스크롤 · 표 칸 = 창 끝까지. - -- CSS: 줄 수 변수 하나 · 안내 글 최대 높이 = 변수 × 줄 높이(1lh) · 세로 스크롤 · 한 줄 줄임표 걷음 -- Shell: 탭 표 = 공용 fillViewport(팝업 · 인쇄 표는 그대로) · 설계내역서 레벨 안내 + 탭 안내 = 한 영역 -- 공용 spaceBelow: 아래 형제 바깥 여백(B09 안내 글 margin-top)도 셈 — 빼면 페이지 스크롤이 생김 -- 실제 브라우저(정적 쪽 · 서버 불필요): 변수 3 · 2 × 줄 많음 · 적음 — 안내 글 높이 · 합계 줄 · 안내 글 자리 · 페이지 스크롤 0 -""" - -import re -import subprocess -from pathlib import Path - -import pytest - -ROOT = Path(__file__).resolve().parents[2] -B09 = ROOT / "B09_Estimation" -UI = ROOT / "ui_template" - - -def _read(path: Path) -> str: - return path.read_text("utf-8") - - -def _block(css: str, selector: str) -> str: - m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css) - assert m, selector - return m.group(1) - - -def test_CSS_줄_수_변수_하나(): - css = _read(B09 / "B09_Estimation_UI_Page.css") - assert re.search(r"--b09-hint-lines:\s*3;", _block(css, ".b09-estimation")) - assert css.count("--b09-hint-lines:") == 1 # 고칠 자리 하나 - hint = _block(css, ".b09-estimation__line-hint") - assert "max-height: calc(var(--b09-hint-lines) * 1lh)" in hint - assert "overflow-y: auto" in hint - for gone in ("white-space: nowrap", "text-overflow: ellipsis"): - assert gone not in hint, gone - - -def test_Shell_표_창_끝_안내_한_영역(): - shell = _read(B09 / "B09_Estimation_UI_Shell.ts") - assert "fillViewport: true" in shell - assert "panel.replaceChildren(table.root, ...(foot ? [foot] : []))" in shell - assert "hint.textContent = `${level.guide.textContent} · ${hint.textContent}`" in shell - # 팝업 · 인쇄 표는 창 끝 셈 안 씀(팝업 = 창 안 max-height · 인쇄 = 펼침) - assert "fillViewport" not in _read(B09 / "B09_Estimation_UI_Builders.ts") - assert "fillViewport" not in _read(B09 / "B09_Estimation_UI_Popup.ts") - - -# ── 실제 브라우저 ───────────────────────────────────────────────────────────── - -TOKENS = ( - ":root { --spacing-4: 4px; --spacing-8: 8px; --spacing-16: 16px; --spacing-24: 24px;" - " --text-caption: 12px; --radius-lg: 8px; }" - " html, body { margin: 0; line-height: 1.5; font-family: sans-serif; }" -) -LONG = " · ".join(["긴 안내 글 시험 문장"] * 40) -SHORT = "줄을 누르면 단가를 낸 탭 · 줄로 이동" - - -def _module_js() -> str: - """공용 표 모듈(TS) → 브라우저 ES 모듈(CSS import 뺌).""" - src = UI / "ui_template_data_table.ts" - code = ( - "const ts = require(process.argv[1]);" - "const fs = require('fs');" - "const out = ts.transpileModule(fs.readFileSync(process.argv[2], 'utf8')," - " { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2020 } });" - "process.stdout.write(out.outputText);" - ) - ts_path = ROOT / "config" / "node_modules" / "typescript" - proc = subprocess.run( - ["node", "-e", code, str(ts_path), str(src)], - capture_output=True, - text=True, - encoding="utf-8", - timeout=60, - ) - assert proc.returncode == 0, proc.stderr - return re.sub(r'^import\s+"[^"]+\.css";\s*$', "", proc.stdout, flags=re.M) - - -MEASURE = """async ([rows, lines, text]) => { - const { createDataTable } = window.dt; - const body = document.querySelector('.b09-estimation'); - body.style.setProperty('--b09-hint-lines', String(lines)); - const panel = body.querySelector('.b09-estimation__panel'); - const table = createDataTable({ - columns: [{ key: 'name', label: '이름' }, { key: 'sum', label: '금액', kind: 'number' }], - rows: Array.from({ length: rows }, (_, i) => ({ name: '줄 ' + i, sum: i })), - total: { name: '계', sum: 1 }, - fillViewport: true, - }); - const hint = document.createElement('p'); - hint.className = 'ui-general-block__empty b09-estimation__line-hint'; - hint.textContent = text; - panel.replaceChildren(table.root, hint); - await new Promise((r) => setTimeout(r, 300)); - const t = table.root.getBoundingClientRect(); - const h = hint.getBoundingClientRect(); - const foot = table.root.querySelector('tfoot').getBoundingClientRect(); - return { - vh: innerHeight, - page: document.scrollingElement.scrollHeight, - lh: parseFloat(getComputedStyle(hint).lineHeight), - hintH: h.height, - hintScrolls: hint.scrollHeight > hint.clientHeight, - tableTop: t.top, - tableBottom: t.bottom, - tableScrolls: table.root.scrollHeight > table.root.clientHeight, - maxH: parseFloat(table.root.style.maxHeight), - hintTop: h.top, - hintBottom: h.bottom, - padB: parseFloat(getComputedStyle(body).paddingBottom), - footBottom: foot.bottom, - }; -}""" - - -@pytest.fixture(scope="module") -def page(): - from playwright.sync_api import sync_playwright - - css = _read(UI / "ui_template_data_table.css") + _read(B09 / "B09_Estimation_UI_Page.css") - html = ( - f"" - "
" - "
" - ) - js = _module_js() - with sync_playwright() as p: - browser = p.chromium.launch(channel="msedge", headless=True) - pg = browser.new_page(viewport={"width": 1280, "height": 900}) - pg.set_content(html) - pg.add_script_tag( - type="module", - content=f"{js}\nwindow.dt = {{ createDataTable }};", - ) - pg.wait_for_function("!!window.dt") - yield pg - browser.close() - - -@pytest.mark.parametrize("lines", [3, 2]) -def test_줄_많음_표_창_끝_합계_줄_고정_안내_글_바닥(page, lines): - out = page.evaluate(MEASURE, [300, lines, LONG]) - assert out["hintH"] == pytest.approx(lines * out["lh"], abs=0.5), out # 높이 = 줄 수 × 줄 높이 - assert out["hintScrolls"], out # 넘친 글 = 영역 안 스크롤 - assert out["tableScrolls"], out - assert out["page"] == out["vh"], out # 페이지 스크롤 0 - assert out["hintBottom"] + out["padB"] == pytest.approx(out["vh"], abs=1.5), out # 창 끝까지 씀 - assert out["hintTop"] == pytest.approx(out["tableBottom"] + 8, abs=0.5), out # 표 바로 아래 - assert out["tableBottom"] - 30 < out["footBottom"] <= out["tableBottom"], ( - out - ) # 합계 줄 = 표 칸 바닥 - - -def test_줄_수_2_이면_표가_한_줄만큼_더_김(page): - three = page.evaluate(MEASURE, [300, 3, LONG]) - two = page.evaluate(MEASURE, [300, 2, LONG]) - assert two["maxH"] - three["maxH"] == pytest.approx(three["lh"], abs=1), (two, three) - - -@pytest.mark.parametrize("lines", [3, 2]) -def test_줄_적음_합계_줄_안내_글_끌려_올라옴(page, lines): - out = page.evaluate(MEASURE, [3, lines, LONG]) - assert not out["tableScrolls"], out - assert out["tableBottom"] - out["tableTop"] < out["maxH"] - 100, out # 내용만큼(창 끝 아님) - assert out["hintTop"] == pytest.approx(out["tableBottom"] + 8, abs=0.5), out # 표 끝에 붙음 - assert out["hintBottom"] < out["vh"] - 300, out # 화면 바닥에 떨어져 있지 않음 - assert out["hintH"] == pytest.approx(lines * out["lh"], abs=0.5), out - assert out["footBottom"] <= out["tableBottom"], out - - -def test_짧은_글은_한_줄만(page): - out = page.evaluate(MEASURE, [300, 3, SHORT]) - assert out["hintH"] == pytest.approx(out["lh"], abs=0.5), out - assert not out["hintScrolls"], out - assert out["page"] == out["vh"], out - - -def test_spaceBelow_형제_바깥_여백_셈(): - src = _read(UI / "ui_template_data_table.ts") - assert "px(m.marginTop) + px(m.marginBottom)" in src # 브라우저 배치는 위 시험이 잼 diff --git a/resources/tester/test_205_1_structure_actions_height.py b/resources/tester/test_205_1_structure_actions_height.py deleted file mode 100644 index 9d3ae79b4..000000000 --- a/resources/tester/test_205_1_structure_actions_height.py +++ /dev/null @@ -1,50 +0,0 @@ -"""205-1 「구조물 배치」 맨 아래 [추가] · [삭제] · [리셋] 줄 — 단추 높이 = 상자 안 작은 단추 · 늘 보임. - -- 높이 = `--b05-control-h`(상자 안 단추 · 입력 칸과 같은 값) · 상자 이름표 [삭제]와 같은 높이 식 -- 줄 = 스크롤 영역 아래 끝에 붙음(sticky · bottom 0) · 단추 · 줄에 hidden · display none · visibility · - 호버 조건이 없음 -""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -CSS = (ROOT / "B05_Profile/B05_Profile_UI_Style_Structures.css").read_text(encoding="utf-8") -FORM = (ROOT / "B05_Profile/B05_Profile_UI_Structures_Form.ts").read_text(encoding="utf-8") -SECTION = ".b05-structure-section .b05-route__irregular-actions" - - -def _block(selector: str) -> str: - m = re.search(r"(?m)^" + re.escape(selector) + r"\s*\{([^}]*)\}", CSS) - assert m, selector - return m.group(1) - - -def _decl(block: str, prop: str) -> str: - m = re.search(r"(?:^|;|\s)" + re.escape(prop) + r"\s*:\s*([^;]+);", block) - assert m, prop - return m.group(1).strip() - - -def test_buttons_same_height_as_inner_buttons(): - outer = _block(SECTION + " > button") - inner = re.search(r"\.b05-structure__group > legend > button\s*\{([^}]*)\}", CSS).group(1) - assert _decl(outer, "height") == "var(--b05-control-h)" - for prop in ("height", "box-sizing", "padding-block", "font-size", "line-height"): - assert _decl(outer, prop) == _decl(inner, prop), prop - assert re.search(r"--b05-control-h:\s*26px;", CSS) - - -def test_row_always_visible(): - row = _block(SECTION) - assert _decl(row, "position") == "sticky" - assert _decl(row, "bottom") == "0" - assert "background" in row - for text in (_block(SECTION), _block(SECTION + " > button")): - assert not re.search(r"display\s*:\s*none|visibility|opacity|overflow", text) - assert not re.search(re.escape(SECTION) + r"[^{]*(:hover|:focus-within|\[hidden\])", CSS) - - -def test_form_never_hides_buttons(): - assert "removeButton.hidden" not in FORM and "resetButton.hidden" not in FORM - assert "actions.hidden" not in FORM diff --git a/resources/tester/test_217_1_b06_notice_spacing.py b/resources/tester/test_217_1_b06_notice_spacing.py deleted file mode 100644 index e15d8ef74..000000000 --- a/resources/tester/test_217_1_b06_notice_spacing.py +++ /dev/null @@ -1,30 +0,0 @@ -"""217-1 B06 알림 칩 줄 여백 줄임 — 줄 위 여백 16→8 · 칩 줄 ↔ 첫 카드 28→16 (CSS 값 고정).""" - -from test_196_1_b06_notice_chips import B06 - -CSS = (B06 / "B06_Section_UI_Style_Cross.css").read_text(encoding="utf-8") - - -def _rule(selector: str) -> str: - body = CSS[CSS.index(selector + " {") :] - return body[: body.index("}")] - - -def test_칩_줄_위_여백은_8_아래_여백은_0(): - bar = _rule(".b06-section__notice-bar") - assert "padding-block: var(--spacing-8) 0;" in bar - assert "spacing-16) var(--spacing-4)" not in bar - - -def test_첫_카드와의_간격은_카드_칸_gap_24에서_8을_당겨_16(): - bar = _rule(".b06-section__notice-bar") - # 위 = 카드 칸 padding(16) 상쇄 · 아래 = gap(24) 중 8 당김 - assert "margin-block: calc(-1 * var(--spacing-16)) calc(-1 * var(--spacing-8));" in bar - assert "gap: var(--spacing-24)" in _rule(".b06-section__cards") - - -def test_sticky_top_과_scroll_margin_그대로(): - bar = _rule(".b06-section__notice-bar") - assert "position: sticky" in bar - assert "top: calc(-1 * var(--spacing-16))" in bar - assert "scroll-margin-top: calc(var(--b06-notice-h, 0px) + var(--spacing-8))" in CSS diff --git a/resources/tester/test_229_1_b09_level_buttons_subbar.py b/resources/tester/test_229_1_b09_level_buttons_subbar.py deleted file mode 100644 index 6d3d1a40a..000000000 --- a/resources/tester/test_229_1_b09_level_buttons_subbar.py +++ /dev/null @@ -1,96 +0,0 @@ -"""229-1 B09 설계내역서 레벨 단추 = 보조 탭 줄 오른쪽 — 단추 넷 · 다른 탭엔 없음 · 표 위 도구 줄 없음. - -- 단추 = 보조 탭 줄 도구 자리(`setTools`) · 안내 글 = 표 아래 · 못 하는 동작 = 흐림(200-2 그대로) -- 쉘 배선 = 소스 글 대조(test_200_2 · test_202_1 과 같은 꼴) -""" - -import re -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 resources.tester.helper_b09_group_cards import TABS, run # noqa: E402 - -B09 = "B09_Estimation/B09_Estimation_UI_" -FILES = [TABS, f"{B09}TabBar.ts", f"{B09}Tabs_Bill_Level.ts"] -# 가짜 DOM 에 줄 찾기(closest · querySelector(All))만 더함 · 끌기 = 빈 함수 -PRELUDE = r""" -document.addEventListener = () => {}; -const attachDrag = () => {}; -E.prototype.closest = function () { for (let e = this; e; e = e.parent) if (e.tagName === "TR") return e; return null; }; -E.prototype.querySelectorAll = function () { return this.all("TR").filter((t) => t.cls.has("is-focus")); }; -E.prototype.querySelector = function (s) { - const i = s.match(/data-i="(-?\d+)"/)?.[1]; - return this.children.find((c) => c.dataset.i === i) ?? null; -}; -""" - - -def test_설계내역서_보조_줄에_단추_넷_다른_탭엔_없음(): - out = run( - FILES, - """ -const bars = createTabBars(() => {}); -const items = ["들여쓰기 →", "내어쓰기 ←", "위로 ↑", "아래로 ↓"].map((t) => [t, () => {}]); -const table = { rows: [{ key: "r1" }], menu: (i) => (i === 0 ? items.slice(1, 3) : []) }; -const root = document.createElement("div"); -const tr = document.createElement("tr"); -tr.dataset.i = "0"; -root.append(tr); -const toolsOf = () => bars.sub.children[bars.sub.children.length - 1]; -const snap = (lv) => ({ - labels: toolsOf().children.map((b) => b.textContent), - levels: toolsOf().children.map((b) => b.dataset.b09Level ?? null), - guideIn: lv ? lv.guide.textContent.length > 0 : null, -}); -const show = (tab) => { - bars.draw(tab); - bars.setTools([]); - if (tab !== "bill") return null; - const lv = levelBar(root, table); - bars.setTools(lv.tools); - return lv; -}; -const bill = show("bill"); -const a = snap(bill); -const disabledBefore = bill.tools.map((b) => b.disabled); -tr.click(); -const disabledAfter = bill.tools.map((b) => b.disabled); -show("dg"); -const b = snap(null); -show("bill"); -const c = snap(null); -console.log(JSON.stringify({ - a, disabledBefore, disabledAfter, b: b.labels, c: c.labels, - guideTag: bill.guide.tagName, guideCls: bill.guide.className, subBlank: bars.sub.classList.contains("b09-estimation__tabs--blank"), -})); -""", - PRELUDE, - ) - names = ["→ 들여쓰기", "← 내어쓰기", "↑ 위로", "↓ 아래로"] - assert out["a"]["labels"] == names # 설계내역서 = 단추 넷(보조 줄 오른쪽 도구 자리) - assert out["a"]["levels"] == ["들여쓰기 →", "내어쓰기 ←", "위로 ↑", "아래로 ↓"] - assert out["a"]["guideIn"] is True # 안내 글 = 단추 줄이 아니라 따로(표 아래로) - assert out["guideTag"] == "P" and "ui-general-block__empty" in out["guideCls"] - assert not out["subBlank"] # 257-1 설계내역 = 아래 탭 둘(총괄 · 설계내역서) · 빈 보조 줄 아님 - assert out["disabledBefore"] == [True] * 4 # 고른 줄 없음 = 흐림 - assert out["disabledAfter"] == [True, False, False, True] # 줄 누름 = 메뉴 항목대로 - assert out["b"] == [] # 다른 탭(일위대가표) = 단추 없음 - assert out["c"] == names # 설계내역서로 돌아오면 다시 있음 - - -def test_표_위_도구_줄_없음_쉘_배선(): - shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8") - assert "if (level) bars.setTools(level.tools);" in shell - assert ( - "panel.replaceChildren(table.root, ...(foot ? [foot] : []))" in shell - ) # 203 레벨 안내 + 탭 안내 = 한 영역 - assert "...(level ? [level] : [])" not in shell # 표 위 자리 없음 - css = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8") - lvl = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Level.ts").read_text("utf-8") - assert "b09-estimation__level" not in css and "b09-estimation__level" not in lvl - # 키 · 단추 = 우클릭 메뉴 같은 항목 · 키 안 바뀜(Tab · Shift+Tab · Alt+↑↓ · 방향키) - assert re.search(r"ACTS\[e\.shiftKey \? 1 : 0\]\[1\]", lvl) diff --git a/resources/tester/test_236_1_hint_one_line.py b/resources/tester/test_236_1_hint_one_line.py deleted file mode 100644 index df3f087c5..000000000 --- a/resources/tester/test_236_1_hint_one_line.py +++ /dev/null @@ -1,163 +0,0 @@ -"""236-1 B08 · B09 표 아래 안내 글 — 제작 · 셈 설명 지움 · 늘 한 줄 · 페이지 스크롤 0. - -- 안내 글 낱말에 제작 · 셈 설명(ROUND · 「 = 」 · 장 번호 · M01 · 곱셈 나눗셈 기호)이 없음 -- 안내 글 한 조각 70 자 이하(긴 글은 줄임표 + 공용 호버) -- B08 안내 줄(footNote) = 한 줄(nowrap · 줄임표) · B09 안내 글 = 203 줄 수 변수만큼(test_203) -- 창 크기(1080 · 768 · 600)마다 한 줄 안내 + 표 칸 = 창 끝(199-1 fillViewport 셈) -""" - -import json -import re -import subprocess -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -B08 = ROOT / "B08_Quantity" -B09 = ROOT / "B09_Estimation" - -STRING = re.compile(r'"([^"\\\n]*)"|`([^`\n]*)`') -FORBID = ["ROUND", " = ", "÷", "×", "Σ", "M01", "역전개", "원문"] -CHAPTER = re.compile(r"\(\d{2,3}-\d\)|\b\d{1,3}-\d-\d\b|\b\d{2,3}-\d\b") -MAX_PIECE = 70 - - -def _read(path: Path) -> str: - return path.read_text("utf-8") - - -def _pieces(expr: str) -> list[str]: - out = [] - for m in STRING.finditer(expr): - text = m.group(1) if m.group(1) is not None else m.group(2) - out.append(re.sub(r"\$\{[^}]*\}", "", text).strip()) - return [p for p in out if p] - - -def _b09_hints() -> list[tuple[str, str]]: - found: list[tuple[str, str]] = [] - for path in sorted(B09.glob("B09_Estimation_UI_Tabs_*.ts")): - src = _read(path) - for m in re.finditer(r"\bhint:\s*(.*?),\n\s+\w+[:(]", src, re.S): - found += [(path.name, p) for p in _pieces(m.group(1))] - for m in re.finditer( - r"^const (?:LINK_HINT|BLANK|HINT|EXPENSE_NONE|GUIDE)\s*=\s*(.*?);\n", src, re.S | re.M - ): - found += [(path.name, p) for p in _pieces(m.group(1))] - return found - - -def _b08_hints() -> list[tuple[str, str]]: - found: list[tuple[str, str]] = [] - for name in ( - "B08_Quantity_UI_Earthwork.ts", - "B08_Quantity_UI_Earthwork_Sheets.ts", - "B08_Quantity_UI_Material.ts", - ): - src = _read(B08 / name) - for m in re.finditer(r"footNote\(((?:\s|\+|\"[^\"\n]*\"|`[^`\n]*`)+)\)", src): - found += [(name, p) for p in _pieces(m.group(1))] - for m in re.finditer(r"^const HINTS[^=]*=\s*\{(.*?)\n\};", src, re.S | re.M): - found += [(name, p) for p in _pieces(m.group(1))] - for m in re.finditer(r"^const HIDDEN\s*=\s*(.*?);\n", src, re.S | re.M): - found += [(name, p) for p in _pieces(m.group(1))] - return found - - -def _server_notes() -> list[tuple[str, str]]: - found: list[tuple[str, str]] = [] - for name in ("Router_Summary", "Router_Material"): - src = _read(B08 / f"B08_Quantity_{name}.py") - for m in re.finditer(r"notes\.append\(\s*(.*?)\)\n", src, re.S): - found += [(name, p) for p in _pieces(m.group(1).replace('f"', '"'))] - sangeun = _read(B09 / "B09_Estimation_Sangeun.py") - for m in re.finditer(r'^(?:NO_SOURCE|EMPTY) = "([^"]*)"', sangeun, re.M): - found.append(("Sangeun", m.group(1))) - return found - - -def _all() -> list[tuple[str, str]]: - return _b09_hints() + _b08_hints() + _server_notes() - - -def test_안내_글_모음이_비지_않음(): - assert len(_b09_hints()) >= 20 - assert len(_b08_hints()) >= 4 - assert len(_server_notes()) >= 7 - - -def test_제작_셈_설명_낱말_없음(): - bad = [] - for where, text in _all(): - bad += [(where, token, text) for token in FORBID if token in text] - if CHAPTER.search(text): - bad.append((where, "장 번호", text)) - assert not bad, bad - - -def test_안내_글_조각_짧음(): - long = [(w, t) for w, t in _all() if len(t) > MAX_PIECE] - assert not long, long - - -# ── 늘 한 줄 ─────────────────────────────────────────────────────────────── - - -def _block(css: str, selector: str) -> str: - m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css) - assert m, selector - return m.group(1) - - -def test_B09_안내_글_줄_수_변수(): - css = _read(B09 / "B09_Estimation_UI_Page.css") - block = _block(css, ".b09-estimation__line-hint") - # 203 한 줄 줄임표 → 줄 수 변수만큼 · 넘치면 스크롤(test_203_b09_hint_lines) - assert "max-height: calc(var(--b09-hint-lines) * 1lh)" in block and "overflow-y: auto" in block - shell = _read(B09 / "B09_Estimation_UI_Shell.ts") - assert "lineHint(note(spec.hint))" in shell # 표 탭도 묶음 탭과 같은 꼴 - assert "export function lineHint" in _read(B09 / "B09_Estimation_UI_Tabs_Groups.ts") - level = _read(B09 / "B09_Estimation_UI_Tabs_Bill_Level.ts") - assert 'guide.className = "ui-general-block__empty b09-estimation__line-hint"' in level - - -def test_B08_안내_줄_한_줄(): - css = _read(B08 / "B08_Quantity_UI_Page.css") - block = _block(css, ".b08-quantity__foot") - assert "white-space: nowrap" in block and "text-overflow: ellipsis" in block - assert "display" not in block # hidden 속성(콘크리트 빈 까닭)을 덮지 않음 - foot = _read(B08 / "B08_Quantity_UI_Foot.ts") - assert "b08-quantity__foot" in foot and "attachHoverInfo" in foot - for name in ("Earthwork", "Earthwork_Sheets", "Material", "Summary"): - assert "footNote" in _read(B08 / f"B08_Quantity_UI_{name}.ts"), name - - -def test_B08_표_아래_안내는_footNote만(): - material = _read(B08 / "B08_Quantity_UI_Material.ts") - assert "note(BASIS)" not in material # 「구조물 수량 기준 …」 제작 설명 줄 없음 - assert "const excel = footNote(" in material - assert "const concreteNote = footNote(" in material - summary = _read(B08 / "B08_Quantity_UI_Summary.ts") - assert 'footNote(summary.notes.join(" · "))' in summary # 서버 알림 = 한 줄로 이어 붙임 - sheets = _read(B08 / "B08_Quantity_UI_Earthwork_Sheets.ts") - assert ( - "tableCard(haulNote(table), grid.root, averages.map(note))" in sheets - ) # 제작 설명 줄 없음 - - -# ── 창 끝까지 셈 ──────────────────────────────────────────────────────────── - - -def test_창_크기마다_한_줄_안내_뒤_창_끝(): - proc = subprocess.run( - ["node", str(ROOT / "resources" / "tester" / "helper_236_1_fill_sizes.cjs")], - capture_output=True, - text=True, - encoding="utf-8", - timeout=60, - ) - assert proc.returncode == 0, proc.stderr - rows = json.loads(proc.stdout) - assert [r["viewport"] for r in rows] == [1080, 768, 600] - for r in rows: - assert r["below"] == 18 + 8 + 12 + 8 # 안내 한 줄 + 줄 사이 + 카드 + 본문 여백 - assert r["bottom"] == r["viewport"], r # 문서 맨 아래 = 창 끝 → 페이지 스크롤 0 diff --git a/resources/tester/test_240_1_b09_bill_folder_font_drag.py b/resources/tester/test_240_1_b09_bill_folder_font_drag.py deleted file mode 100644 index ec2b4564f..000000000 --- a/resources/tester/test_240_1_b09_bill_folder_font_drag.py +++ /dev/null @@ -1,60 +0,0 @@ -"""PLAN 240 · 121 · 122 — B09 설계내역서 폴더 줄 글꼴 한 벌 · 들여쓰기 폭 · 끌기 미리 보기 · 데이터 점검 틈.""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -B09 = ROOT / "B09_Estimation" - - -def _read(p: Path) -> str: - return p.read_text(encoding="utf-8") - - -def test_폴더_줄은_처음이든_레벨_고쳐_생겼든_같은_줄_꼴(): - src = _read(B09 / "B09_Estimation_UI_Tabs_Bill.ts") - assert 'const title = r.kind === "item" ? "" : "is-title";' in src - assert '"is-group"' not in src - css = _read(B09 / "B09_Estimation_UI_Page.css") - assert "tr.is-group" not in css # 옛 묶음 줄 꼴(색 다름) 없음 - - -def test_들여쓰기_한_단_넓힘_끌기_한_단과_같음(): - css = _read(ROOT / "ui_template" / "ui_template_data_table.css") - assert re.search(r"--dt-indent: 22px;", css) - drag = _read(B09 / "B09_Estimation_UI_Tabs_Bill_Drag.ts") - assert 'getPropertyValue("--dt-indent")' in drag - - -def test_끌기_미리_보기_어디_안_몇_레벨_폴더_테두리_끄는_줄_흐림(): - drag = _read(B09 / "B09_Estimation_UI_Tabs_Bill_Drag.ts") - for part in ( - "b09-estimation__drop-tag", - "is-drop-into", - "is-drag-src", - "못 놓음", - "안", - "맨 위", - ): - assert part in drag, part - css = _read(B09 / "B09_Estimation_UI_Page.css") - for sel in (".b09-estimation__drop-tag", "tr.is-drop-into td", "tr.is-drag-src td"): - assert sel in css, sel - # 끝나면 표시를 모두 걷음 - assert 'into?.classList.remove("is-drop-into")' in drag - assert 'el.classList.remove("is-drag-src")' in drag - - -def test_데이터_점검_업데이트_위_틈_카드_왼쪽_여유와_같음(): - css = _read(B09 / "B09_Estimation_UI_Page.css") - m = re.search(r"\.b09-options \[data-b09-refresh\] \{\s*margin-top: ([^;]+);", css) - # 269 — 공용 카드 틈(8) + 4 = 카드 왼쪽 여유(12) - assert m and m.group(1) == "var(--spacing-4)" - overlay = _read(ROOT / "ui_template" / "ui_template_overlay.css") - assert re.search( - r":where\(\.ui-sidebar-card\.ui-panel-form\) \{[^}]*gap: var\(--spacing-8\);", overlay - ) - card = _read(ROOT / "ui_template" / "ui_template_overlay.css") - assert re.search( - r"\.ui-sidebar-card \{\s*padding: calc\(var\(--spacing-8\) \+ var\(--spacing-4\)\);", card - ) diff --git a/resources/tester/test_241_1_input_cell_border.py b/resources/tester/test_241_1_input_cell_border.py deleted file mode 100644 index 4d1b2a9d6..000000000 --- a/resources/tester/test_241_1_input_cell_border.py +++ /dev/null @@ -1,37 +0,0 @@ -"""PLAN 241-1 · 123 → 246-1 · 128 — B08 · B09 표 입력 칸 = 테두리 되돌림(241 앞 채움 색).""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -CSS = ROOT / "ui_template" / "ui_template_data_table.css" - - -def _rule(css: str, selector: str) -> str: - m = re.search(re.escape(selector) + r" \{([^}]*)\}", css) - assert m, selector - return m.group(1) - - -def test_입력_칸_채움_색_테두리_없음(): - body = _rule(CSS.read_text(encoding="utf-8"), ".ui-data-table__input") - # 246-1 — 241 앞 모양(accent 5% 채움) · 안쪽 테두리 없음 - assert "background: color-mix(in srgb, var(--color-accent) 5%, var(--color-canvas));" in body - assert "box-shadow" not in body - assert "#" not in body # 하드코딩 색 없음 — 테마 변수만 - assert "border: 0;" in body - assert "height: var(--dt-row-h);" in body - - -def test_초점_테두리_그대로(): - body = _rule(CSS.read_text(encoding="utf-8"), ".ui-data-table__input:focus") - assert "outline: 2px solid var(--color-focus-ring);" in body - assert "outline-offset: -2px;" in body - - -def test_고친_값_색_칸_그대로(): - css = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text(encoding="utf-8") - body = _rule(css, ".b09-estimation .ui-data-table__table tbody td.is-edited") - assert "color-mix(in srgb, var(--color-warning) 22%, var(--color-canvas))" in body - ts = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill.ts").read_text(encoding="utf-8") - assert '"is-edited"' in ts diff --git a/resources/tester/test_245_1_merged_hover.py b/resources/tester/test_245_1_merged_hover.py deleted file mode 100644 index bf19478f8..000000000 --- a/resources/tester/test_245_1_merged_hover.py +++ /dev/null @@ -1,43 +0,0 @@ -"""245-1 병합 표 호버 정리 — 병합 칸(is-merged)은 줄 강조(호버 · 이동해 온 줄 · 줄무늬) 밖. - -규칙 (가) = 병합 칸은 칠하지 않고 그 줄 나머지 칸만. 같은 세기 선택자라 병합 칸 규칙이 뒤에 와야 이김. -""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -CSS = (ROOT / "ui_template" / "ui_template_data_table.css").read_text("utf-8") -MERGED = ".ui-data-table__table tbody tr td.is-merged {" - - -def _at(sel: str) -> int: - i = CSS.find(sel) - assert i >= 0, sel - return i - - -def test_병합_칸_줄_강조_밖(): - body = CSS[_at(MERGED) :].split("}", 1)[0] - assert "background: var(--color-canvas);" in body - assert "box-shadow: none;" in body - - -def test_호버_고른줄_줄무늬_뒤에_옴(): - at = _at(MERGED) - for sel in ( - ".ui-data-table__table tbody tr:hover td {", - ".ui-data-table__table tbody tr.is-focus td {", - ".ui-data-table__table tbody tr.is-alt td {", - ): - assert _at(sel) < at, sel - - -def test_병합_칸_글_위_정렬_그대로(): - assert re.search(r"td\.is-merged \{\s*vertical-align: top;", CSS) - - -def test_B09_빨강_범위는_병합_칸에도_남음(): - page = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8") - # 빨강(is-pick)은 .b09-estimation 이 붙어 더 셈 — 병합 칸 box-shadow: none 에 안 덮임 - assert ".b09-estimation .ui-data-table__table tbody tr.is-pick td {" in page diff --git a/resources/tester/test_246_1_b09_bill_menu_simple.py b/resources/tester/test_246_1_b09_bill_menu_simple.py deleted file mode 100644 index 4df6d88a6..000000000 --- a/resources/tester/test_246_1_b09_bill_menu_simple.py +++ /dev/null @@ -1,84 +0,0 @@ -"""246-1 · 128 — B09 설계내역서 우클릭 메뉴 줄임 · 입력 칸 테두리 되돌림. - -- 입력 칸 = 241 앞 모양(글 상자 accent 5% 채움 · td 표시 없음) -- 더함 = 「항목 추가」 하나 — 항목 = 바로 아래 같은 부모 · 폴더 = 그 폴더 첫 하위 -- 맨 위 1단(새 공종) = ← 내어쓰기 — 직접비 · 손 줄은 묶음 · 관급자재대 앞 · 자재 줄은 못 냄 -- 연결 이름 = 「구조물 조합」 · 「일위대가 사전조합」 · 「단가산출」 · 지움 = 「줄 삭제」 -""" - -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent)) -from test_184_3_b09_bill_menu import DG, ROWS, _menu, _order, _row # noqa: E402 - -ROOT = Path(__file__).resolve().parents[2] -OLD_ADDS = ["위에 줄 추가", "아래에 줄 추가", "아래에 하위 줄 추가", "새 공종 추가(1단)"] - - -def test_입력_칸_채움_색_td_테두리_옅게(): - css = (ROOT / "ui_template" / "ui_template_data_table.css").read_text("utf-8") - body = css.split(".ui-data-table__input {")[1].split("}")[0] - assert "background: color-mix(in srgb, var(--color-accent) 5%, var(--color-canvas));" in body - assert "box-shadow" not in body - assert "td.is-can-edit {" in css # 258-2 — 고칠 칸 옅은 outline 되살림 - - -def test_더함_하나_줄_삭제_연결_이름_셋(): - keys = ["sec:1", "earth:4", "hand:g", "hand:a", "fee", "sec:G"] - out = _menu({}, labels=keys)["labels"] - for key in keys: - assert [x for x in out[key] if "추가" in x] == ["항목 추가"], key - assert not set(OLD_ADDS) & set(out[key]) and "줄 지우기" not in out[key] - assert "줄 삭제" in out["earth:4"] and "줄 삭제" in out["hand:a"] - link = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Link.ts").read_text("utf-8") - # 251-6 고르기 다섯 = 단가연결 모달 하나 · 종류 이름 = 단가산출 · 사전조합 · 구조물 조합 · 호표 둘 - kinds = link.split("kindFilter: [")[1].split("],\n pick")[0] - for name in ["일위대가 호표", "단가산출 호표", "구조물 조합", "일위대가 사전조합", "단가산출"]: - assert f'text: "{name}"' in kinds - for word in ["서고", "마스터", "회사", "개인", "M01", "새 산근", "로직"]: - assert word not in kinds - toasts = [part.split(",")[0] for part in link.split("showToast(")[1:]] - assert toasts and not any("서고" in t for t in toasts) # 알림 글 쉬운 말 - bill = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill.ts").read_text("utf-8") - assert "항목 추가 · 줄 삭제" in bill # 표 아래 안내 글 - - -#: 관급자재대 안 손 묶음(hand:s · 자재 없음) · 손 항목(hand:t) -SUPPLY = [*ROWS[:-1], _row("hand:s", "sec:G", 2, "group", cost="supply"), - _row("hand:t", "hand:s", 3, cost="supply", qty=1, ref=DG), ROWS[-1]] # fmt: skip - - -def test_자리_규칙(): - runs = [("hand:g", "항목 추가"), ("hand:a", "항목 추가"), ("hand:b", "항목 추가"), - ("hand:a", "내어쓰기 ←"), ("hand:g", "내어쓰기 ←"), ("hand:s", "내어쓰기 ←"), - ("hand:t", "내어쓰기 ←"), ("mat:M1", "내어쓰기 ←")] # fmt: skip - out = _menu({}, runs=runs, rows=SUPPLY)["runs"] - new = lambda b: [(e.get("parent"), e.get("name", e["key"])) for e in b["rows"]] # noqa: E731 - # 폴더 = 그 폴더 첫 하위 · 항목 = 바로 아래 같은 부모 - got = new(out["hand:g|항목 추가"]) - assert got[got.index(("sec:1.2", "hand:g")) + 1] == ("hand:g", "새 줄") - got = [e["key"] for e in out["hand:a|항목 추가"]["rows"]] - row = out["hand:a|항목 추가"]["rows"][got.index("hand:a") + 1] - assert (row["name"], row["parent"]) == ("새 줄", "hand:g") - got = out["hand:b|항목 추가"]["rows"] - row = got[[e["key"] for e in got].index("hand:b") + 1] - assert (row["name"], row["parent"], "kind" in row) == ("새 줄", "sec:1.2", False) - # ← = 한 단씩 — 레벨 4 → 3(sec:1.2 아래) · 레벨 3 → 2(sec:1 아래) - assert ("hand:a", "sec:1.2") in _order(out["hand:a|내어쓰기 ←"]) - assert ("hand:g", "sec:1") in _order(out["hand:g|내어쓰기 ←"]) - # 관급 안 손 묶음 ← = 새 공종(직접비) · 관급자재대 앞 - b = out["hand:s|내어쓰기 ←"] - tops = [k for k, p in _order(b) if p is None] - assert tops == ["sec:1", "sec:4", "hand:s", "sec:G"] - s = {e["key"]: e for e in b["rows"]}["hand:s"] - assert (s["kind"], s["cost"]) == ("group", "direct") - # 손 항목 ← 관급 묶음 밖(sec:G 아래) · 자재 줄 = 1단 안 됨 - assert ("hand:t", "sec:G") in _order(out["hand:t|내어쓰기 ←"]) - assert out["mat:M1|내어쓰기 ←"] is None - # 1단 손 항목 → 묶음(수량 · 연결 지움) — 2단 손 항목을 두 번 냄 - one = _menu({"bill": out["hand:t|내어쓰기 ←"]}, runs=[("hand:t", "내어쓰기 ←")], - rows=SUPPLY)["runs"]["hand:t|내어쓰기 ←"] # fmt: skip - t = {e["key"]: e for e in one["rows"]}["hand:t"] - assert ("parent" in t, t["kind"], t["qty"], t["ref"], t["cost"]) == ( - False, "group", None, None, "direct") # fmt: skip diff --git a/resources/tester/test_248_1_b09_group_jump_unpick.py b/resources/tester/test_248_1_b09_group_jump_unpick.py deleted file mode 100644 index 5c555a3b9..000000000 --- a/resources/tester/test_248_1_b09_group_jump_unpick.py +++ /dev/null @@ -1,90 +0,0 @@ -"""248-1 B09 묶음 탭(일위대가표 · 중기사용료 · 단가산출근거) 다른 탭 이동 빨강 = 설계내역서(242-1) 규칙. - -- 이동해 온 호표 = 빨강 하나 — 카드 = 박스 is-pick 만(줄 is-focus 안 붙음) · 표 = 구간 is-pick 만 · - 공용 focusRow(at, false) = 스크롤만 -- 줄 · 카드 누름 · 방향키 · Esc = 빨강 풂 · 누른 줄만 고름(is-focus) · Esc = 고름도 풂 · 다시 그려도 그대로 -- 글 칸 안 키 · 다른 키 = 그대로 -""" - -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 resources.tester.helper_b09_group_cards import COMMON, GROUPS, RAW, TABS, run # noqa: E402 -from resources.tester.test_171_3_b09_group_tabs import DG # noqa: E402 -from resources.tester.test_181_2_b09_box_pick import ROWS # noqa: E402 - -LABOR = "B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts" -INPUT = "{ closest: (s) => (s.includes('input') ? {} : null) }" # 글 칸 안 키 - - -def test_카드_빨강_하나_누름_키_풂(): - out = run( - [COMMON, TABS, LABOR, GROUPS], - f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});" - "\nconst o = { tab: 'dg', spec, hint: null, focus: 'DG1|1', goto: () => {}, redraw: () => {}," - " setTools: () => {}, makeTable: () => null };" - "\nconst marks = (v) => [v.all('.is-pick').length, v.all('.is-focus').map((e) => e.dataset.at ?? 'box')];" - "\nconst out = {};" - "\nlet [v] = renderGroupPanel(o); out.came = marks(v);" - "\nv.querySelector('[data-at=\"2\"]').click(); out.line = marks(v);" - "\n[v] = renderGroupPanel(o); v.querySelector('.ui-group-card').click(); out.box = marks(v);" - "\n[v] = renderGroupPanel(o); key('a'); key('ArrowDown', " - + INPUT - + "); out.kept = marks(v);" - "\nkey('ArrowDown'); out.arrow = marks(v);" - "\n[v] = renderGroupPanel(o); v.querySelector('[data-at=\"2\"]').click(); key('Escape'); out.esc = marks(v);" - "\nconsole.log(JSON.stringify(out));", - RAW, - ) - assert out["came"] == [1, []] # 박스 빨강만 · 줄 강조 없음 - assert out["line"] == [0, ["2"]] # 누른 줄만 고름 - assert out["box"] == [0, []] # 카드 빈 곳 누름 = 빨강만 풂 - assert out["kept"] == [1, []] # 다른 키 · 글 칸 안 = 그대로 - assert out["arrow"] == [0, []] - assert out["esc"] == [0, []] # Esc = 고름도 풂 - - -def test_표_빨강_하나_누름_키_풂(): - out = run( - [COMMON, TABS, GROUPS], - f"\nconst spec = {{ columns: [{{ key: 'key' }}], rows: {json.dumps(ROWS)} }};" - "\nlet extra = null; let root = null; const scrolled = [];" - "\nconst makeTable = (e) => { extra = e; root = document.createElement('table');" - " spec.rows.forEach((_, i) => { const tr = document.createElement('tr'); tr.dataset.i = String(i);" - " tr.className = e(i) ?? ''; tr.append(document.createElement('td')); root.append(tr); });" - " return { root, focusRow: (i, mark) => scrolled.push([i, mark]) }; };" - "\nconst o = { tab: 'dg', spec, hint: null, focus: 'B|1', goto: () => {}, redraw: () => {}," - " setTools: () => {}, makeTable };" - "\nchooseGroupView('table');" - "\nconst marks = () => [root.children.map((tr) => tr.className || null)," - " spec.rows.map((_, i) => extra(i))];" - "\nconst out = {};" - "\nrenderGroupPanel(o); out.came = marks(); out.scrolled = [...scrolled];" - "\nroot.children[1].children[0].click(); out.click = marks();" - "\nrenderGroupPanel(o); key('Enter'); key('ArrowUp', " + INPUT + "); out.kept = marks()[0];" - "\nkey('ArrowUp'); out.arrow = marks();" - "\nrenderGroupPanel(o); root.children[5].click(); key('Escape'); out.esc = marks();" - "\nconsole.log(JSON.stringify(out));", - ) - red = [None] * 3 + ["is-pick is-pick-top", "is-pick", "is-pick", "is-pick is-pick-end", None] - none = [None] * 8 - assert out["came"] == [red, red] # 구간 빨강만 - assert out["scrolled"] == [[4, False]] # 스크롤만(공용 강조 안 붙임) - focus1 = [None, "is-focus"] + [None] * 6 - assert out["click"] == [focus1, focus1] # 누른 줄만 · 다시 그려도 - assert out["kept"] == red - assert out["arrow"] == [none, none] - assert out["esc"] == [none, none] - - -def test_배선(): - groups = (ROOT / GROUPS).read_text("utf-8") - assert "table.focusRow(at, false)" in groups - common = (ROOT / COMMON).read_text("utf-8") - assert 'el?.classList.add("is-focus")' not in common - assert "unpickCards(grid, true);" in common diff --git a/resources/tester/test_255_3_b08_milestone_place_modal.py b/resources/tester/test_255_3_b08_milestone_place_modal.py deleted file mode 100644 index 244906939..000000000 --- a/resources/tester/test_255_3_b08_milestone_place_modal.py +++ /dev/null @@ -1,42 +0,0 @@ -"""255-3 거리이정표 거래처 · 기준점 모달 — 칸 이름 ≥ 내용 글 · 「소재지 — 주소 찾기 · 지도에서 고르기」 -줄 지움 · 기준점 이름 한 줄(빈 칸 자리 비움) · 주소 · 지도 위 가로선 · -256-1 장소 그림 = 등록 · 좌표 바뀜 · 경로 탭 끌기 뒤 capturePlace.""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def _read(rel): - return (ROOT / rel).read_text(encoding="utf-8") - - -def _rule(css, selector): - m = re.search(re.escape(selector) + r" \{([^}]*)\}", css) - assert m, selector - return m.group(1) - - -def test_모달_꼴(): - modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts") - assert "주소 찾기 · 지도에서 고르기" not in modal and 'el("h4"' not in modal - assert ( - 'item.root.style.display = dest.root.style.display = k === "base" ? "none" : "";' in modal - ) - css = _read("B08_Quantity/B08_Quantity_UI_Milestone.css") - label = _rule(css, ".b08-milestone__modal .ui-field__label") - assert "font-size: var(--text-body-sm);" in label # 입력 글(text-body-sm)과 같음 - assert "white-space: nowrap;" in label - assert "border-top: 1px solid var(--color-border);" in _rule(css, ".b08-milestone__place-field") - place = _read("B08_Quantity/B08_Quantity_UI_Milestone_Place.ts") - assert 'className: "b08-milestone__place-field"' in place - - -def test_장소_그림_캡처_부름(): - modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts") - assert modal.count("segment = placed = ") == 2 # 기준점 · 거래처 좌표 바뀜 - assert "placed = addRoute?.({" in modal and "id: placed," in modal # 새 거래처 · 새 기준점 - assert "void capturePlace(projectId, placed, {" in modal - route = _read("B08_Quantity/B08_Quantity_UI_Milestone_Route.ts") - assert "void capturePlace(projectId, key, { name: target.name || label, lon, lat });" in route diff --git a/resources/tester/test_257_2_b09_pick_blank_grand.py b/resources/tester/test_257_2_b09_pick_blank_grand.py deleted file mode 100644 index ef5fdd736..000000000 --- a/resources/tester/test_257_2_b09_pick_blank_grand.py +++ /dev/null @@ -1,64 +0,0 @@ -"""257-2 B09 이동해 온 줄 = 모든 탭 빨강 상자 · 비고 「빈 줄 N」 없음 · 총공사비 = 표 아래 고정 줄. - -- 쉘 = 설계내역서 밖 · 묶음 아닌 탭에도 줄 범위 [at, at] 빨강(pickClass) · 줄 누름 · Esc · 방향키로 풂(unpickOnRelease) -- 비고 = 제목 줄 「(제외금액)」만 · 묶음 줄 빈칸(Py · TS 짝 · blank 수는 줄 값 그대로) -- 총괄 · 원가계산서 총공사비(grand) = `TabTable.total`(몸 줄에서 빠짐 · 줄 이동 누름 없음 — 185-1 시험이 값 확인) -""" - -import re -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 resources.tester.helper_b09_group_cards import COMMON, GROUPS, RAW, TABS, run # noqa: E402 - -B09 = ROOT / "B09_Estimation" - - -def test_쉘_모든_탭_빨강_배선(): - shell = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8") - assert "!bill && at >= 0 ? [at, at] : null" in shell - assert "pickClass(range, i)" in shell and "unpickOnRelease(table.root)" in shell - assert "table.focusRow(at, false)" in shell - - -def test_풂_줄_누름_Esc_빨강만_지움(): - out = run( - [COMMON, TABS, GROUPS], - """ -const root = document.createElement("div"); -const rows = [0, 1, 2].map((i) => { - const tr = document.createElement("tr"); - tr.dataset.i = String(i); - if (i === 1) tr.classList.add("is-pick", "is-pick-top", "is-pick-end"); - root.append(tr); - return tr; -}); -unpickOnRelease(root); -const before = rows.map((t) => t.className); -key("Escape"); -const afterEsc = rows.map((t) => t.className); -rows[1].classList.add("is-pick"); -root.click(); -const afterClick = rows.map((t) => t.className); -console.log(JSON.stringify({ before, afterEsc, afterClick })); -""", - RAW - + """ -E.prototype.querySelectorAll = function () { return this.all("TR").filter((t) => t.cls.has("is-pick")); }; -""", - ) - assert out["before"] == ["", "is-pick is-pick-top is-pick-end", ""] - assert out["afterEsc"] == ["", "", ""] - assert out["afterClick"] == ["", "", ""] - - -def test_비고_빈_줄_없음_Py_TS_짝(): - py = (B09 / "B09_Estimation_Calc_Bill.py").read_text("utf-8") - ts = (B09 / "B09_Estimation_Calc_Bill.ts").read_text("utf-8") - assert "빈 줄 {" not in py and not re.search(r"빈 줄 \$\{", ts) - assert 'row["note"] = "(제외금액)" if row["key"] == "sec:G" else ""' in py - assert 'row.note = row.key === "sec:G" ? "(제외금액)" : ""' in ts diff --git a/resources/tester/test_257_3_b09_compare_applied_menu.py b/resources/tester/test_257_3_b09_compare_applied_menu.py deleted file mode 100644 index c631ea294..000000000 --- a/resources/tester/test_257_3_b09_compare_applied_menu.py +++ /dev/null @@ -1,67 +0,0 @@ -"""257-3 B09 자재단가대비표 적용 단가 — 초록 테두리 · 우클릭 「적용 단가 바꾸기」(출처 목록). - -- 고른 출처 단가 칸 = 초록 테두리(inset box-shadow · 바탕 색 아님) -- 우클릭 = 적용 단가 칸 · 고른 출처 단가 칸 → 「적용 단가 바꾸기」 하위 목록(왼쪽 누름 메뉴와 같은 항목 · 최저가여도 바꿈) -- 왼쪽 누름 메뉴(choices)는 그대로 -""" - -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 resources.tester.helper_b09_group_cards import TABS, run # noqa: E402 - -BASE = "B09_Estimation/B09_Estimation_UI_Tabs_Base.ts" -PRELUDE = r""" -const SOURCES = ["관급", "물가정보", "물가자료", "유통물가", "견적"]; -const openQuotes = () => {}; -""" - -JS = """ -let user = {}; -const sheets = { compare: [ - { key: "m1", no: 1, name: "철근", spec: "D13", unit: "kg", lowest: 1000, lowest_from: "유통물가", - applied: 1000, applied_from: "유통물가", pick: null, - prices: { 관급: null, 물가정보: 1200, 물가자료: 0, 유통물가: 1000, 견적: null } }, -] }; -const t = buildCompare({ sheets, get user() { return user; }, setUser: (u) => (user = u) }); -const menu = t.cellMenu(0, "applied"); -const labels = menu[0].children.map((c) => c[0]); -menu[0].children[0][1](); // 물가정보 = 최저가 아닌 출처로 바꿈 -const afterPick = JSON.stringify(user.materials); -menu[0].children[1][1](); // 유통물가 = 최저가여도 고른 출처로 박음 -const afterLowest = JSON.stringify(user.materials); -console.log(JSON.stringify({ - label: menu[0].label, labels, afterPick, afterLowest, - onPicked: t.cellMenu(0, "p:유통물가")?.[0].label ?? null, - other: [t.cellMenu(0, "name"), t.cellMenu(0, "p:물가정보"), t.cellMenu(5, "applied")], - left: t.choices(0, "applied").length, leftOther: t.choices(0, "name"), - cls: [t.cellClass(0, "p:유통물가"), t.cellClass(0, "p:물가정보")], -})); -""" - - -def test_우클릭_적용_단가_바꾸기_출처_목록(): - out = run([TABS, BASE], JS, PRELUDE) - assert out["label"] == "적용 단가 바꾸기" - assert out["labels"] == ["물가정보 1,200", "✓ 유통물가 1,000 · 최저", "✓ 최저(자동)"] - assert out["afterPick"] == '{"m1":{"pick":"물가정보"}}' - assert out["afterLowest"] == '{"m1":{"pick":"유통물가"}}' # 최저가여도 바꿈 - assert out["onPicked"] == "적용 단가 바꾸기" # 초록 테두리 칸 우클릭도 같은 메뉴 - assert out["other"] == [None, None, None] - assert ( - out["left"] == 4 and out["leftOther"] is None - ) # 왼쪽 누름 메뉴 그대로(출처 둘 · 구분선 · 최저) - assert out["cls"] == ["is-picked", None] - - -def test_배선_쉘_우클릭_CSS_초록_테두리(): - shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8") - assert 'made.root.addEventListener("contextmenu"' in shell and "spec.cellMenu" in shell - assert "openMenuAt(event.clientX, event.clientY, items)" in shell - css = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8") - box = css.split("td.is-picked {", 1)[1].split("}", 1)[0] - assert "box-shadow: inset 0 0 0 2px var(--color-success)" in box and "background" not in box diff --git a/resources/tester/test_258_1_table_width_var.py b/resources/tester/test_258_1_table_width_var.py deleted file mode 100644 index 7831b90d7..000000000 --- a/resources/tester/test_258_1_table_width_var.py +++ /dev/null @@ -1,52 +0,0 @@ -"""258-1 · 258-2 · 258-3 — 표 폭 CSS 변수 · 입력 칸 테두리 옅게 · 구조물 조합 미지정 회색(소스 시험). - -- 템플릿 표 인라인 = `--dt-w`(열 합) · css `width: var(--dt-w)` → 페이지 css 가 width 로 덮음 -- 거리이정표 표 = 틀 폭 100% -- 고칠 칸 td.is-can-edit = accent 28% outline · box-shadow 아님 -- B05 미지정 = 회색(text-muted) -""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -DT_CSS = ROOT / "ui_template" / "ui_template_data_table.css" -DT_TS = ROOT / "ui_template" / "ui_template_data_table.ts" - - -def _rule(css: str, selector: str) -> str: - m = re.search(re.escape(selector) + r" \{([^}]*)\}", css) - assert m, selector - return m.group(1) - - -def test_표_폭_인라인_변수_css_width_변수(): - ts = DT_TS.read_text("utf-8") - assert '' in ts - assert 'style="width:${x}px"' not in ts # 인라인 width 가 페이지 css 를 이기지 않음 - body = _rule(DT_CSS.read_text("utf-8"), ".ui-data-table__table") - assert "width: var(--dt-w);" in body - assert "table-layout: fixed;" in body - - -def test_거리이정표_표_틀_폭_전체(): - css = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Milestone.css").read_text("utf-8") - body = _rule(css, ".b08-milestone > .ui-data-table .ui-data-table__table") - assert "width: 100%;" in body # 변수 규칙(0,1,0)보다 선택자가 구체적 - - -def test_입력_칸_테두리_옅게_outline(): - css = DT_CSS.read_text("utf-8") - body = _rule(css, ".ui-data-table__table td.is-can-edit") - m = re.search(r"var\(--color-accent\) (\d+)%", body) - assert m and 25 <= int(m.group(1)) <= 30 # 예전 60% → 옅게 - assert "outline: 1px solid" in body and "box-shadow" not in body - assert '"is-can-edit"' in DT_TS.read_text("utf-8") - - -def test_구조물_조합_미지정_회색(): - css = (ROOT / "B05_Profile" / "B05_Profile_UI_Style_Structures.css").read_text("utf-8") - body = _rule(css, ".b05-structure__summary-name:not(.is-assigned)") - assert "border-color: var(--color-text-muted);" in body - assert "--color-text-muted" in body.split("background:")[1] - assert "--color-warning" not in body diff --git a/resources/tester/test_258_b06_plan_peek.py b/resources/tester/test_258_b06_plan_peek.py deleted file mode 100644 index d5e12010a..000000000 --- a/resources/tester/test_258_b06_plan_peek.py +++ /dev/null @@ -1,93 +0,0 @@ -"""258 · 425 · 436 — B06 위 평면 겹침 패널. - -종단 보기 평면 노드를 옮겨 붙임(둘째 캔버스 없음) · 닫을 때 · 횡단 숨김 때 제자리 · 손잡이 = -공용 bottom 위로 접힘 · z 4 · 처음 높이 32 % · 아래 끝 끌어 높이(화면 기억) · 단추 줄 · 안내 등 감춤 · -조작(끌기 · 끼우기 · 지우기 · 방향키 · 휠 · 팬)은 B05 평면 그대로 · 처음 보일 때 B05 칸 크기로 셈. -배선은 글로 봄. -./venv/Scripts/python.exe -m pytest resources/tester/test_258_b06_plan_peek.py -q -""" - -from __future__ import annotations - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -DESIGN = ROOT / "A00_Common" / "design" -PEEK = (DESIGN / "design_plan_peek.ts").read_text(encoding="utf-8") -CROSS = (DESIGN / "design_view_cross.ts").read_text(encoding="utf-8") -CSS = (DESIGN / "design_page.css").read_text(encoding="utf-8") -B05 = ROOT / "B05_Profile" - - -def _rule(selector: str) -> str: - body = CSS[CSS.index(selector + " {") :] - return body[: body.index("}")] - - -def test_횡단_보기가_패널을_세움() -> None: - assert 'import { mountPlanPeek } from "./design_plan_peek";' in CROSS - assert CROSS.index("center.append(mainArea);") < CROSS.index( - "mountPlanPeek(center, host, shared);" - ) - - -def test_평면_노드를_옮기고_제자리로() -> None: - assert 'createWorkflowPanelHandle("bottom", "up", "평면")' in PEEK - assert 'await shared.ensureView("profile");' in PEEK # B06 바로 열기 = 종단 보기 숨긴 채 - assert '".b05-route__main > .b05-routeedit"' in PEEK - assert "plan.replaceWith(home);" in PEEK and "body.append(plan);" in PEEK - assert "home.replaceWith(plan);" in PEEK # 닫기 = 자리표 자리로 - assert "host.onHide(close);" in PEEK # 횡단 숨김(B05 로 감) = 제자리 - assert 'createElement("canvas")' not in PEEK - assert len(PEEK.splitlines()) <= 700 - - -def test_조작은_B05_평면_그대로() -> None: - # 패널이 누름 · 휠을 가로채지 않음 — 캔버스에 붙은 B05 처리기가 그대로 받음 - for event in ('"pointerdown"', '"dblclick"', '"contextmenu"', '"wheel"', '"keydown"'): - assert event not in PEEK - assert "stopPropagation" not in PEEK - - -def test_높이_끌기와_기억() -> None: - assert "createPanelResizer({" in PEEK - assert 'cssVar: "--design-plan-peek-height",' in PEEK and "direction: 1," in PEEK - assert 'storageKey: stateKey("plan-peek-height")' in PEEK - assert "max: () => (root.offsetParent?.clientHeight ?? 0) - 48," in PEEK - assert "resizer.restore();" in PEEK[PEEK.index("const open = async") :] - state = (ROOT / "A00_Common" / "b_page_state.ts").read_text(encoding="utf-8") - assert '"plan-peek-height": { bucket: "pref", scope: "global" },' in state - handle = _rule(".design-plan-peek > .ui-resizer") - assert "bottom: -3px;" in handle and "display: none;" in handle - assert "display: block;" in _rule(".design-plan-peek.is-open > .ui-resizer") - - -def test_겹침_자리와_감춤() -> None: - root = _rule(".design-plan-peek") - assert "position: absolute;" in root and "z-index: 4;" in root - assert "pointer-events: none;" in root # 닫힌 동안 카드 누름 통과 - assert "height: var(--design-plan-peek-height, max(32%, 160px));" in _rule( - ".design-plan-peek.is-open" - ) - hidden = CSS[CSS.index(".design-plan-peek .b05-routeedit__tools,") :] - hidden = hidden[: hidden.index("}")] - for part in ("__tools", "__info", "__measure", "__side-wrap", "b05-contourlock"): - assert part in hidden - assert "display: none;" in hidden - - -def test_평면_방향키는_마지막_누름이_평면_안() -> None: - for name in ("B05_Profile_UI_RouteEdit_Keys.ts", "B05_Profile_UI_RouteEdit_StructureMarks.ts"): - text = (B05 / name).read_text(encoding="utf-8") - assert 'lastPointerWithin(".b05-routeedit")' in text # 겹침 평면도(436) - assert ".b05-route__main .b05-routeedit" not in text - - -def test_처음_보일_때_B05_칸_크기로_셈() -> None: - editor = (B05 / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8") - body = editor[editor.index("function openView(): void {") :] - body = body[: body.index("\n }")] - assert 'const atHome = overlay.closest(".b05-route__main") !== null;' in body - assert 'atHome ? null : overlay.closest(".design-page__center")' in body - assert "openingView(projectId, meta, home," in body - assert "restoreSavedView(meta, view, captureView(meta, opening.view, 0))" in body diff --git a/resources/tester/test_262_1_b08_milestone_title_start_pin.py b/resources/tester/test_262_1_b08_milestone_title_start_pin.py deleted file mode 100644 index c002f4175..000000000 --- a/resources/tester/test_262_1_b08_milestone_title_start_pin.py +++ /dev/null @@ -1,40 +0,0 @@ -"""262 B08 거리이정표 상자 창 제목 · 소재지 지도 시점 표지(사용자 164-1). - -262-1 거래처 · 기준점 창 제목 = 이름만(빈 이름 = 「거래처」 · 「기준점」) · 시점 창 = 「시점 · 중점」 · 요소 추가 창 그대로 -262-2 소재지 지도 = 시점 표지 고정(끌기 없음 · 맞춤에서 빠짐) · 시점 없으면 표지 없음 · 소재지 없으면 시점 중심 -""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -B08 = "B08_Quantity/B08_Quantity_UI_Milestone" - - -def _read(name): - return (ROOT / f"{B08}{name}").read_text(encoding="utf-8") - - -def test_제목_이름만(): - modal = _read("_Modal.ts") - title = modal[modal.index("const title = target.add") : modal.index("openModal({")] - assert '? base.name || "기준점"' in title and ': route!.name || "거래처"' in title - assert '"요소 추가 — 기준점 · 거래처"' in title - assert 'title: "시점 · 중점",' in modal and "공사지 —" not in modal - assert re.search(r"title: `?[^\n]*(거래처|기준점|공사지) —", modal) is None - - -def test_시점_표지_고정(): - place = _read("_Place.ts") - assert "startPoint(liveMilestone(projectId))" in place # 거리이정표가 쓰는 시점 - assert "center: start" in place # 소재지 없음 = 시점 중심 - assert "movable: false, fixed: true" in place and 'name: "시점"' in place - assert "...(s" in place and " : [])," in place # 시점 없음 = 표지 없음 - kakao = _read("_KakaoMap.ts") - assert "fixed?: boolean;" in kakao - assert "const moving = pins.filter((p) => !p.fixed);" in kakao - assert "...moving.map(" in kakao # 처음 보기 = 소재지 중심(시점은 맞춤에서 빠짐) - assert "moving.find(" in kakao # 보기 이동에서도 빠짐 - assert "if (p.fixed)" in kakao and "b08-milestone__map-start" in kakao - css = _read(".css") - assert ".b08-milestone__map-start {" in css diff --git a/resources/tester/test_267_1_pipe_info_font.py b/resources/tester/test_267_1_pipe_info_font.py deleted file mode 100644 index d819bb537..000000000 --- a/resources/tester/test_267_1_pipe_info_font.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -"""PLAN 267-1 — B06 횡단 카드 「관매설 …」 글 크기 = 「노폭 …」 글 크기 · 줄 겹침 없음. - -./venv/Scripts/python.exe -m pytest resources/tester/test_267_1_pipe_info_font.py -q -p no:cacheprovider --basetemp=tmp/pt -""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -CSS = (ROOT / "B06_Section" / "B06_Section_UI_Style_Cross_Areas.css").read_text(encoding="utf-8") -TYPES = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Types.ts").read_text(encoding="utf-8") - - -def _font_px(selector: str) -> float: - """셀렉터 블록의 마지막 font-size(px) — 뒤에 오는 규칙이 이긴다.""" - blocks = re.findall(re.escape(selector) + r"\s*\{([^}]*)\}", CSS) - sizes = [float(m) for b in blocks for m in re.findall(r"font-size:\s*([\d.]+)px", b)] - assert sizes, selector - return sizes[-1] - - -def test_관_정보_글_크기가_노폭_라벨과_같다() -> None: - road = _font_px(".b06-chart__carriageway-label") - info = _font_px(".b06-chart__culvert-label.b06-chart__pipe-info") - assert road == info == 11.0 - # 다른 culvert-label(집수정 · 박스 이름)은 그대로 9px - assert _font_px(".b06-chart__culvert-label") == 9.0 - - -def test_관_정보_글_줄이_노폭_라벨과_겹치지_않는다() -> None: - step = float(re.search(r"- (\d+) \* \(stacked \+ 1\)", TYPES).group(1)) - font = _font_px(".b06-chart__culvert-label.b06-chart__pipe-info") - road_font = _font_px(".b06-chart__carriageway-label") - asc, desc = 0.8, 0.2 # 글자 상자 근사 — 기준선 위 80% · 아래 20% - road_top = 0 - asc * road_font # 노폭 라벨 기준선 y=0 - for n in (1, 2, 3): # 관 정보 글이 쌓인 줄 수 - info_y = -step * n - info_bottom = info_y + desc * font - assert info_bottom < road_top, (n, info_bottom, road_top) - if n > 1: # 윗줄 바닥 < 아랫줄 윗면 - upper_bottom = -step * n + desc * font - lower_top = -step * (n - 1) - asc * font - assert upper_bottom < lower_top, n diff --git a/resources/tester/test_269_panel_form.py b/resources/tester/test_269_panel_form.py deleted file mode 100644 index 4926a7c3a..000000000 --- a/resources/tester/test_269_panel_form.py +++ /dev/null @@ -1,176 +0,0 @@ -# -*- coding: utf-8 -*- -"""269 좌측 패널 컨테이너 공용 양식 — 기준 = B05/B06 「구조물 배치」. - -못박는 것 - - 공용 양식 = ui_template 한 곳(`.ui-panel-form` · `.ui-panel-btn` · 카드 `createPanelCard`) · - 칸 높이 · 라벨 틈 토큰 = theme 한 곳 · 「구조물 배치」도 같은 토큰. - - B07 컨테이너 = 공용 카드 · 도면 단추 = `.ui-panel-btn` · 페이지 덧칠(크기 · 글자 · 바탕) 없음. - - B06 컨테이너(횡단 보기 설정 · 표준 횡단면 설정) = 칸 틀 `createSectionFrame` 이 공용 양식을 붙임 · - 단추 · 입력칸 = 공용 함수(createButton · createInputField · createSelectField) · B06 덧칠 없음. -""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def _read(rel: str) -> str: - return (ROOT / rel).read_text(encoding="utf-8") - - -THEME = _read("ui_template/ui_template_theme.css") -OVERLAY = _read("ui_template/ui_template_overlay.css") -COLLAPSIBLE = _read("ui_template/ui_template_collapsible.ts") -B05_STRUCT_CSS = _read("B05_Profile/B05_Profile_UI_Style_Structures.css") -B07_CSS = _read("B07_DesignDetail/B07_DesignDetail_UI_Style.css") -B07_PANELS = _read("B07_DesignDetail/B07_DesignDetail_UI_Panels.ts") -B07_PLAN = _read("B07_DesignDetail/B07_DesignDetail_UI_PlanKinds.ts") -DESIGN_PANEL = _read("A00_Common/design/design_panel.ts") -B06_CSS = _read("B06_Section/B06_Section_UI_Style.css") -B06_PANEL = _read("B06_Section/B06_Section_UI_Page_Panel.ts") -B06_STD = _read("B06_Section/B06_Section_UI_Standard_Panel.ts") - - -def _block(css: str, selector: str) -> str: - m = re.search(r"(?:^|\n)" + re.escape(selector) + r"\s*\{([^}]*)\}", css) - assert m, selector - return m.group(1) - - -def test_공용_양식_토큰은_theme_한_곳_구조물_배치도_같은_토큰(): - assert "--panel-control-h: 26px;" in THEME - assert "--panel-label-gap: 2px;" in THEME - assert "--b05-control-h: var(--panel-control-h);" in B05_STRUCT_CSS - assert "--b05-label-gap: var(--panel-label-gap);" in B05_STRUCT_CSS - - -def test_공용_양식_칸_라벨_단추_규칙(): - field = _block( - OVERLAY, - '.ui-panel-form input:not([type="checkbox"], [type="radio"], [type="range"]),\n' - ".ui-panel-form select", - ) - assert "height: var(--panel-control-h);" in field - assert "padding: 0 6px;" in field - assert "font-size: var(--text-caption);" in field - label = _block(OVERLAY, ".ui-panel-form .ui-field,\n.ui-panel-form .b05-route__field") - assert "gap: var(--panel-label-gap);" in label - assert "display: none;" in _block(OVERLAY, ".ui-panel-form .ui-field__error:not(.is-visible)") - button = _block(OVERLAY, ".ui-panel-form .ui-btn:not(.ui-help-btn, .ui-step-nav-btn)") - # 「구조물 배치」 단추(b05-route__irregular-btn)와 같은 크기 - assert "padding: var(--spacing-8);" in button - assert "font-size: var(--text-caption);" in button - assert "border-radius: var(--radius-inputs);" in button - raw = _block(OVERLAY, ":where(.ui-panel-btn)") - for decl in ( - "padding: var(--spacing-8);", - "border-radius: var(--radius-inputs);", - "background: var(--color-surface);", - "font-size: var(--text-caption);", - ): - assert decl in raw, decl - # 아래 단추 묶음은 컨테이너 밖 — 공용 양식이 `.ui-sidebar-actions` 를 겨누지 않음 - form_rules = OVERLAY[OVERLAY.index("좌측 패널 컨테이너 공용 양식(269)") :] - form_rules = form_rules[: form_rules.index("/* 카드 안에 다시 카드")] - assert "ui-sidebar-actions {" not in form_rules and "ui-sidebar-actions ." not in form_rules - - -def test_공용_카드_함수(): - assert "export function createPanelCard(" in COLLAPSIBLE - assert "ui-collapsible ui-sidebar-section ui-sidebar-card ui-panel-form" in COLLAPSIBLE - - -def test_B07_컨테이너_공용_카드_도면_단추_공용_꼴(): - assert "createPanelCard(\n `${group.label} ${items.length}`,\n isFolded(" in ( - B07_PANELS.replace("\r\n", "\n") - ) - assert "createPanelCard(`평면도 ${items.length}`, collapsed, onManage)" in B07_PLAN - for src in (B07_PANELS, B07_PLAN): - assert "ui-collapsible ui-sidebar-section" not in src # 손으로 짠 카드 없음 - assert B07_PANELS.count('"b07-drawing-button ui-panel-btn"') == 2 - assert "b07-plan-manage-open" not in B07_PLAN # 280-5 [평면도 관리] 단추 → 제목 ⚙ - - -def test_B07_페이지_덧칠_없음(): - assert not re.search(r"(?:^|\n)\.b07-drawing-group\s*\{", B07_CSS) - assert ".b07-drawing-group h3 {" not in B07_CSS - assert ".b07-plan-manage-open {" not in B07_CSS - button = _block(B07_CSS, ".b07-drawing-button") - for prop in ("min-height", "padding", "border-radius", "background", "font-size"): - assert not re.search(r"(?:^|\s)" + prop + r"\s*:", button), prop - assert "font-size" not in _block(B07_CSS, ".b07-drawing-button__name") - assert ".b07-cross-layout .ui-btn" not in B07_CSS - assert ".b07-cross-layout .ui-field__error" not in B07_CSS - - -def test_B06_컨테이너_공용_양식(): - frame = DESIGN_PANEL[DESIGN_PANEL.index("export function createSectionFrame(") :] - frame = frame[: frame.index("\n}\n")] - cross = frame[frame.index("b06-profile__group ui-collapsible") :] - assert "ui-panel-form" in cross.splitlines()[0] - for src in (B06_PANEL, B06_STD): - assert "createSectionFrame(" in src or "createButton(" in src - assert " { - const n = document.createElement(tag); - if (o.className) n.className = o.className; - if (o.text !== undefined) n.textContent = o.text; - for (const [k, v] of Object.entries(o.attrs ?? {})) { - n.setAttribute(k, v); - if (k.startsWith("data-")) n.dataset[k.slice(5)] = v; - } - for (const c of o.children ?? []) n.append(c); - return n; -}; -""" - -SG = { - "key": "SG:A", "no": 1, "name": "골재운반", "unit": "㎥", "spec": "Cm=100", "spec_raw": "Cm=100", - "source": "산림품셈 10-3-1", - "head": ["골재운반 / ㎥", "산림품셈 10-3-1 적용", "qt : 15 ㎥", "Cm : 운반 사이클 = 100 분", - "작업량 = 골재운반 덤프트럭 시간당 작업량 = 8.1", "└ 결과 = 반올림(60 * 15 / 100, 3) = 8.1", - "덤프트럭 15ton 수량 = 1 / 작업량 = 1 / 8.1 = 0.1235 시간"], - "lines": [ - {"kind": "labor", "name": "보통인부", "ref": {"tab": "labor", "key": "LB2"}, "why": None, - "labor": 2979.8, "material": 0.0, "expense": 0.0, "text": ["보통인부 : 165,545 * 0.018 = 2,979.8"]}, - ], - "labor": 2979, "material": 0, "expense": 0, "sum": 2979, "why": None, "foot": "계 : 2,979", -} # fmt: skip - -PANEL = """ -const gone = []; -const o = { tab: TAB, spec, hint: null, goto: (l) => gone.push(l), redraw: () => {}, - setTools: () => {}, makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) }; -const [view] = renderGroupPanel(o); -const top = view.querySelector('.b09-sgsteps'); -const rows = top ? top.querySelectorAll('.b09-haulstep__row').map((r) => - [r.dataset.at ?? null, ...r.children.map((c) => [c.className, c.textContent])]) : null; -const groups = top ? top.querySelectorAll('.b09-haulstep__group').map((g) => g.textContent) : null; -const body = view.querySelectorAll('tr').filter((tr) => tr.dataset.at !== undefined).map((tr) => [tr.dataset.at, tr.children[0].text]); -const foot = view.querySelector('.ui-group-card__foot')?.dataset.at ?? null; -if (top) top.querySelectorAll('.b09-haulstep__row')[2].click(); -const lit = view.querySelectorAll('.is-focus').map((e) => e.dataset.at); -console.log(JSON.stringify({ rows, groups, body, foot, gone, lit })); -""" - - -def _panel(tab, files, build, sheets, prelude=""): - js = ( - f"\nconst TAB = '{tab}';" - f"\nconst spec = {build}({{ sheets: {json.dumps(sheets, ensure_ascii=False)} }});" + PANEL - ) - return run([COMMON, TABS, *files, STEPS, GROUPS], js, EL + prelude) - - -def test_박스보기_머리_줄_나눔(): - out = _panel("sg", [f"{B09}Tabs_Sangeun.ts"], "buildSg", {"sg": [SG]}) - assert out["groups"] == ["산림품셈 10-3-1", "입력값", "중간값"] # 출처 = 칸 맨 위 - v, f = "b09-haulstep__value", "b09-haulstep__formula" - assert out["rows"] == [ - ["2", ["", "qt"], [v, "15 ㎥"]], # 표 줄 번호 그대로(머리 첫 줄 = 카드 머리 · 출처 줄 = 위) - ["3", ["", "Cm"], [v, "100 분"], [f, "운반 사이클"]], - ["4", ["", "작업량"], [v, "8.1"], - [f, "골재운반 덤프트럭 시간당 작업량\n└ 결과 = 반올림(60 * 15 / 100, 3) = 8.1"]], - ["6", ["", "덤프트럭 15ton 수량"], [v, "0.1235 시간"], [f, "1 / 작업량 = 1 / 8.1"]], - ] # fmt: skip - # 호표 줄 · 총계 그대로(머리 줄은 표 몸에서 빠짐) - assert out["body"] == [ - ["7", "보통인부 : 165,545 * 0.018 = 2,979.8노무"] - ] # 가짜 DOM 글 = 이름 + 꼬리표 - assert out["foot"] == "8" - # 머리 줄 누름 = 고름(is-focus) · 이동 없음(표와 같음) - assert out["lit"] == ["4"] and out["gone"] == [None] - - -def test_다른_탭_카드_그대로(): - dg = { - "id": "DG1", "no": 1, "name": "찰쌓기", "spec": "", "spec_raw": "", "unit": "m2", - "kind": "", "cite": "", - "lines": [{"kind": "labor", "key": "L1", "name": "석공", "qty": 0.072, "unit": "인", - "price": 1, "amount": 1, "ref_no": "", - "prices": {"labor": 1, "material": None, "expense": None}, - "amounts": {"labor": 1, "material": None, "expense": None}}], - "sum": 1, "labor": 1, "material": 0, "expense": 0, "blank": 0, "blank_names": [], - } # fmt: skip - out = _panel("dg", [f"{B09}Tabs_Labor.ts"], "buildDaegaBlocks", {"daega_blocks": [dg]}, RAW) - assert out["rows"] is None and out["body"] == [["1", "석공"]] - - -def test_한_벌_잇기(): - groups = (ROOT / GROUPS).read_text("utf-8") - assert 'import { headSteps } from "./B09_Estimation_UI_Haul_Steps";' in groups - assert 'indexOf(" : ")' not in groups # 줄 나눔 복사 없음 - assert "sgHeadCards(tabGroups(o.tab, o.spec), o.spec.rows)" in groups - steps = (ROOT / STEPS).read_text("utf-8") - assert "export function headSteps" in steps and "[...headSteps(b), ...group(" in steps - common = (ROOT / COMMON).read_text("utf-8") - assert "top?: HTMLElement | null;" in common and "if (g.top) body.append(g.top);" in common - sangeun = (ROOT / f"{B09}Tabs_Sangeun.ts").read_text("utf-8") - assert 'source: b.source ?? ""' in sangeun - - -def test_운반_창_왼쪽_폭(): - modal = (ROOT / f"{B09}Haul_Modal.ts").read_text("utf-8") - assert "grid-template-columns:minmax(260px,0.7fr) minmax(340px,1.4fr)" in modal diff --git a/resources/tester/test_279_1_subtab_height.py b/resources/tester/test_279_1_subtab_height.py deleted file mode 100644 index b336ee1b9..000000000 --- a/resources/tester/test_279_1_subtab_height.py +++ /dev/null @@ -1,62 +0,0 @@ -"""279 B08 · B09 보조 탭 단추 높이 한 꼴. - -- 테마 토큰 `--subtab-height` = 24px(B09 단가산출근거 박스보기 때 잰 값) -- 공용 양식 `.ui-subtabs`(가운데 맞춤 · 단추 높이 = 토큰) · `.ui-subtabs__tools`(높이 = 토큰 · 줄 높이 안 바꿈) -- B09 아래 줄 · 도구 · B08 보조 탭 줄 모두(빈 줄 자리 포함)가 씀 · 위 탭 줄은 안 씀 -- `ui-btn--sm` 전체는 그대로 -""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def _read(rel: str) -> str: - return (ROOT / rel).read_text(encoding="utf-8") - - -def _block(css: str, selector: str) -> str: - m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css) - assert m, selector - return m.group(1) - - -def test_token_is_box_view_height(): - theme = _read("ui_template/ui_template_theme.css") - assert re.search(r"--subtab-height:\s*24px;", theme) - - -def test_shared_class_centers_and_fixes_height(): - css = _read("ui_template/ui_template_general_layout.css") - assert "align-items: center" in _block(css, ".ui-subtabs") - assert "height: var(--subtab-height)" in _block(css, ".ui-subtabs > .ui-btn") - tools = _block(css, ".ui-subtabs__tools") - assert "align-items: center" in tools - assert "height: var(--subtab-height)" in tools - - -def test_sm_button_untouched(): - styles = _read("ui_template/ui_template_elements_styles.ts") - assert "height" not in _block(styles, ".ui-btn--sm") - - -def test_b09_sub_row_and_tools_use_shared_class(): - bar = _read("B09_Estimation/B09_Estimation_UI_TabBar.ts") - assert 'sub.className = "b09-estimation__tabs ui-subtabs"' in bar - assert 'tools.className = "b09-estimation__tools ui-subtabs__tools"' in bar - assert 'top.className = "b09-estimation__tabs"' in bar # 위 탭은 그대로 - - -def test_b08_sub_rows_use_shared_class(): - sub_rows = { - "B08_Quantity/B08_Quantity_UI_Page.ts": 2, # 자재 보조 탭 · 구조물 집계표 빈 줄 자리 - "B08_Quantity/B08_Quantity_UI_Earthwork.ts": 1, - "B08_Quantity/B08_Quantity_UI_Hydro.ts": 1, - "B08_Quantity/B08_Quantity_UI_Milestone.ts": 1, - } - for rel, n in sub_rows.items(): - src = _read(rel) - assert src.count('"b08-quantity__tabs ui-subtabs') == n, rel - page = _read("B08_Quantity/B08_Quantity_UI_Page.ts") - assert 'tabBar.className = "b08-quantity__tabs";' in page # 위 탭은 그대로 diff --git a/resources/tester/test_289_1_mass_badge_size.py b/resources/tester/test_289_1_mass_badge_size.py deleted file mode 100644 index b79eb6a7f..000000000 --- a/resources/tester/test_289_1_mass_badge_size.py +++ /dev/null @@ -1,98 +0,0 @@ -"""289 종단도 누가토량 알약 크게 · 측점 ▲ 줄 아래로. - -- 테마 토큰 `--pill-font-size-lg` = 글자 한 단 위(14px · 공용 알약 12px) -- 배지(B05 · B06 한 벌 `common_util_mass_haul.css`) = 토큰 글자 · 줄 간격 · 여백 4/12 -- 자리 = 위에서 52px(▲ 줄 바닥 47px 아래 · 잰 값) · 종단 오버레이 한 곳(`design_profile_overlays.ts`) -""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def _read(rel: str) -> str: - return (ROOT / rel).read_text(encoding="utf-8") - - -def _block(css: str, selector: str) -> str: - m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css) - assert m, selector - return m.group(1) - - -def test_token_one_step_up(): - theme = _read("ui_template/ui_template_theme.css") - assert "--pill-font-size-lg: var(--text-body-sm);" in theme - - -def test_badge_uses_large_pill(): - badge = _block(_read("common_util/common_util_mass_haul.css"), ".mass-haul-badge") - assert "font-size: var(--pill-font-size-lg)" in badge - assert "line-height: var(--leading-body-sm)" in badge - assert "padding: var(--spacing-4, 4px) var(--spacing-12, 12px)" in badge - - -def test_badge_below_up_buttons(): - overlays = _read("A00_Common/design/design_profile_overlays.ts") - m = re.search(r'"--mass-haul-badge-top", "(\d+)px"', overlays) - assert m and int(m.group(1)) > 47 # 측점 ▲ 줄 바닥(top 30 + 17) - - -# 289-2 붉은 계통 — 테마 위험색 섞음 · 밝음 · 어두움 대비(글자 4.5 : 1 이상) -THEMES = { - "light": {"danger": "#c0392b", "surface": "#ffffff", "body": "#333333", "secondary": "#615e6e"}, - "dark": {"danger": "#e8695a", "surface": "#251f38", "body": "#e4e0f0", "secondary": "#b3aec6"}, -} - - -def _rgb(hex_: str) -> tuple[float, ...]: - return tuple(int(hex_[i : i + 2], 16) / 255 for i in (1, 3, 5)) - - -def _mix(a: str, pct: int, b: str) -> tuple[float, ...]: - """CSS color-mix(in srgb, a pct%, b) — 불투명 근사.""" - return tuple(x * pct / 100 + y * (100 - pct) / 100 for x, y in zip(_rgb(a), _rgb(b))) - - -def _lum(c: tuple[float, ...]) -> float: - lin = [v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4 for v in c] - return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2] - - -def _contrast(a: tuple[float, ...], b: tuple[float, ...]) -> float: - hi, lo = sorted((_lum(a), _lum(b)), reverse=True) - return (hi + 0.05) / (lo + 0.05) - - -def test_badge_red_tone_uses_theme_danger(): - css = _read("common_util/common_util_mass_haul.css") - badge = _block(css, ".mass-haul-badge") - assert "border: 1px solid color-mix(in srgb, var(--color-danger) 55%, transparent)" in badge - assert "color: color-mix(in srgb, var(--color-danger) 70%, var(--color-text-body))" in badge - # 바탕 불투명(309-1) — 그래프 선 · 채움이 비치지 않게 - assert ( - "background: color-mix(in srgb, var(--color-danger) 8%, var(--color-surface-raised))" - in badge - ) - em = _block(css, ".mass-haul-badge em") - assert "color-mix(in srgb, var(--color-danger) 55%, var(--color-text-body))" in em - over = _block(css, ".mass-haul-badge.is-over") - assert "border-color: var(--color-danger)" in over - assert "var(--color-danger) 12%" in over - over_value = _block(css, ".mass-haul-badge.is-over strong") - assert "color-mix(in srgb, var(--color-danger) 85%, var(--color-text-body))" in over_value - assert not re.search(r"#[0-9a-fA-F]{3,6}\b", badge + em + over + over_value) # 하드코딩 색 없음 - - -def test_badge_red_tone_contrast_both_themes(): - for name, t in THEMES.items(): - bg = _mix(t["danger"], 8, t["surface"]) - text = _mix(t["danger"], 70, t["body"]) - caption = _mix(t["danger"], 55, t["body"]) - over_bg = _mix(t["danger"], 12, t["surface"]) - over_value = _mix(t["danger"], 85, t["body"]) # 음수 값 = 위험색 더 많이 - assert _contrast(text, bg) >= 4.5, name - assert _contrast(caption, bg) >= 4.5, name - assert _contrast(over_value, over_bg) >= 4.5, name - assert _contrast(caption, over_bg) >= 4.5, name diff --git a/resources/tester/test_303_b09_row_popup.py b/resources/tester/test_303_b09_row_popup.py deleted file mode 100644 index a6f1d1eb8..000000000 --- a/resources/tester/test_303_b09_row_popup.py +++ /dev/null @@ -1,365 +0,0 @@ -"""303 B09 행 고름 = 팝업 · 하이퍼링크 스위치 · 좌측 하단 줄. - -303-1 좌측 하단 고정 줄 — [박스보기|호표보기] · [하이퍼링크|팝업] · `DRAFT.actions`(비면 줄 없음) · - [?][저장][내역 초기화](도움말 줄) · 스위치 = pref global · 기본 = 팝업 -303-2 표 짜기 = `…_UI_Builders`(화면 · 팝업 · 인쇄 같은 옵션) -""" - -import json -import re -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 resources.tester.helper_b09_group_cards import COMMON, GROUPS, TABS, run # noqa: E402 - -B09 = ROOT / "B09_Estimation" -DOCK = "B09_Estimation/B09_Estimation_UI_Dock.ts" -BUILDERS = "B09_Estimation/B09_Estimation_UI_Builders.ts" - -DOCK_JS = ( - "\nconst save = document.createElement('button'); save.textContent = '저장';" - "\nconst reset = document.createElement('button'); reset.textContent = '내역 초기화';" - "\nconst shape = (dock) => dock.children.map((r) => ({ sw: r.dataset.b09Switch ?? null," - " draft: 'b09DraftActions' in r.dataset, help: 'helpRow' in r.dataset," - " text: r.children.map((b) => b.textContent) }));" -) - - -def test_하단_줄_차례_초안_단추_없으면_줄_없음(): - out = run( - [COMMON, TABS, GROUPS, DOCK], - DOCK_JS + "\nconst a = createDock([save, reset], () => {});" - "\nDRAFT.actions.push(() => { const b = document.createElement('button');" - " b.textContent = '인쇄 미리보기'; return b; });" - "\nconst b = createDock([save, reset], () => {});" - "\nconsole.log(JSON.stringify({ cls: a.className, a: shape(a), b: shape(b) }));", - ) - assert out["cls"] == "ui-sidebar-actions b09-dock" # 바닥 고정 = 공용 액션 줄 한 벌 - view = {"sw": "view", "draft": False, "help": False, "text": ["박스보기", "호표보기"]} - link = {"sw": "link", "draft": False, "help": False, "text": ["하이퍼링크", "팝업"]} - main = {"sw": None, "draft": False, "help": True, "text": ["저장", "내역 초기화"]} - assert out["a"] == [view, link, main] # 초안 단추 없음 = 줄 없음 - draft = {"sw": None, "draft": True, "help": False, "text": ["인쇄 미리보기"]} - assert out["b"] == [view, link, draft, main] # 초안 단추 = [저장] 줄 위 - - -def test_스위치_기본_팝업_누름_pref(): - out = run( - [COMMON, TABS, GROUPS, DOCK], - DOCK_JS + "\nconst dock = createDock([save, reset], () => {});" - "\nconst row = dock.children[1];" - "\nconst pressed = () => row.children.map((b) => b.attrs['aria-pressed']);" - "\nconst a = [linkMode(), pressed()];" - "\nrow.children[0].click();" - "\nconst b = [linkMode(), pressed(), kept.get('b09-link-mode')];" - "\nrow.children[1].click();" - "\nconst c = [linkMode(), pressed(), kept.get('b09-link-mode')];" - "\nkept.set('b09-link-mode', '\"x\"');" - "\nconsole.log(JSON.stringify({ a, b, c, odd: linkMode() }));", - ) - assert out["a"] == ["popup", ["false", "true"]] # 기본 = 팝업 - assert out["b"] == ["link", ["true", "false"], '"link"'] - assert out["c"] == ["popup", ["false", "true"], '"popup"'] - assert out["odd"] == "popup" # 모르는 값 = 팝업 - - -def test_DRAFT_actions_자리(): - tabs = (B09 / "B09_Estimation_UI_Tabs.ts").read_text("utf-8") - assert "actions: Array<() => HTMLElement>;" in tabs - assert "{ builders: {}, extend: [], panels: [], actions: [] }" in tabs - state = (ROOT / "A00_Common/b_page_state.ts").read_text("utf-8") - assert '"b09-link-mode": { bucket: "pref", scope: "global" },' in state - css = (B09 / "B09_Estimation_UI_Page.css").read_text("utf-8") - m = re.search(r"\.b09-dock\.ui-sidebar-actions\s*\{([^}]*)\}", css) - assert m and "flex-direction: column" in m.group(1) # 줄 여럿 세로 - - -# 표 짜기 — 탭 짜기 18 개 대신 가짜(줄 하나) · 옵션 = 표 그대로 넘김 -FAKE_BUILDS = "".join( - f"\nconst {name} = () => ({{ columns: [], rows: [{{ key: '{name}' }}] }});" - for name in re.findall( - r":\s*(build\w+),", - (ROOT / BUILDERS).read_text("utf-8"), - ) -) - - -def test_Builders_화면_같은_옵션(): - out = run( - [TABS, BUILDERS], - "\nDRAFT.builders.draft_x = () => '안내';" - "\nconst spec = { columns: [{ key: 'a', label: 'A' }], rows: [{ key: 'r' }], frozen: 2," - " editable: () => true, onEdit: () => {}, rowClass: () => 'x', cellClass: () => 'y'," - " level: { key: 'a', of: () => 1 }, mergeSame: ['a'], link: () => null, menu: () => [] };" - "\nconst o = tableOptionsOf(spec, 'bill');" - "\nconst plain = tableOptionsOf({ columns: [], rows: [], editOnDouble: false });" - "\nconsole.log(JSON.stringify({ keys: Object.keys(o).sort(), caption: o.caption," - " same: ['columns', 'rows', 'editable', 'onEdit', 'rowClass', 'cellClass', 'level', 'mergeSame']" - ".every((k) => o[k] === spec[k]), frozen: o.frozen, total: o.total, empty: o.empty," - " dbl: [o.editOnDouble, plain.editOnDouble], key: [o.keyEdit, plain.keyEdit]," - " plainCaption: plain.caption ?? null, plainFrozen: plain.frozen," - " bill: tabTableOf('bill', {}).rows[0].key, draft: tabTableOf('draft_x', {})," - " none: tabTableOf('nope', {}) }));", - FAKE_BUILDS, - ) - assert out["keys"] == sorted( - [ - "columns", - "rows", - "total", - "frozen", - "empty", - "caption", - "editable", - "editOnDouble", - "keyEdit", - "onEdit", - "rowClass", - "cellClass", - "level", - "mergeSame", - ] - ) # 누름 · 메뉴 · 풍선 없음(그리는 쪽 몫) - assert out["caption"] == "설계내역서" and out["same"] is True - assert out["frozen"] == 2 and out["total"] is None and out["empty"] == "" - assert out["dbl"] == [True, False] # 243-1 기본 = 더블클릭 - assert out["key"] == [False, True] # 설계내역서(메뉴) Enter · F2 = 레벨 키 몫 - assert out["plainCaption"] is None and out["plainFrozen"] == 0 - assert out["bill"] == "buildBill" and out["draft"] == "안내" and out["none"] is None - - -def test_화면이_Builders_옵션을_씀(): - shell = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8") - assert "...tableOptionsOf(spec, tab)," in shell - assert "const spec = tabTableOf(tab, ctx);" in shell - assert "BUILDERS" not in shell - - -# ── 303-2 팝업 창 ────────────────────────────────────────────────────────── -POPUP = "B09_Estimation/B09_Estimation_UI_Popup.ts" - - -def _fn(path: str, name: str) -> str: - """파일에서 함수 하나 꺼냄(맨 앞 `export` 뗌) — 이어 붙이면 이름이 부딪히는 파일 대신.""" - src = (ROOT / path).read_text("utf-8") - m = re.search(rf"export function {name}\(.*?\n}}\n", src, re.S) - assert m, name - return m.group(0).replace("export ", "", 1) - - -POPUP_PRELUDE = ( - FAKE_BUILDS - + "\nconst GAP = 6;\n" - + _fn("ui_template/ui_template_hover_info.ts", "placeHoverInfo") - + _fn("B09_Estimation/B09_Estimation_UI_Tabs_Bill_Level.ts", "subtreeEnd") - + r""" -E.prototype.remove = function () { - if (this.parent) this.parent.children = this.parent.children.filter((c) => c !== this); - this.parent = null; -}; -E.prototype.getBoundingClientRect = function () { - return this.rect ?? { left: 0, top: 0, right: 0, bottom: 0, width: 400, height: 300 }; -}; -document.body = new E("body"); -const winFns = { keydown: [], hashchange: [], pointerdown: [], click: [], contextmenu: [] }; -const winKeys = winFns.keydown; -const hashFns = winFns.hashchange; -globalThis.window = { innerWidth: 1200, innerHeight: 800, - addEventListener: (t, f) => winFns[t].push(f) }; -const esc = () => { - const ev = { key: "Escape", stopped: false, preventDefault() {}, stopPropagation() { ev.stopped = true; } }; - winKeys.forEach((f) => f(ev)); - return ev.stopped; -}; -// 308 창 밖 누름 — pointerdown → click(또는 우클릭 메뉴) · 먹힘 = stopped -const press = (target, type = "click") => { - const ev = (t) => ({ type: t, target, stopped: false, preventDefault() {}, stopPropagation() { this.stopped = true; } }); - const down = ev("pointerdown"); - winFns.pointerdown.forEach((f) => f(down)); - const up = ev(type); - winFns[type].forEach((f) => f(up)); - return [down.stopped, up.stopped]; -}; -const tables = []; -const createDataTable = (o) => { - const root = document.createElement("div"); - o.rows.forEach((r, i) => { - const tr = document.createElement("tr"); - tr.dataset.i = String(i); - tr.rect = { left: 40, top: 300 + i * 28, right: 900, bottom: 328 + i * 28 }; - root.append(tr); - }); - tables.push(o); - return { root }; -}; -""" -) - -SCENE = r""" -const col = [{ key: "name", label: "명칭", group: ["머리"] }]; -const specs = { - bill: { columns: col, menu: () => [], level: { key: "name", of: (i) => [1, 2, 2, 1][i] }, - rows: [{ key: "b1", level: 1 }, { key: "b2", level: 2 }, { key: "b3", level: 2 }, { key: "b4", level: 1 }], - rowClass: (i) => (i === 0 ? "is-title" : null), - editable: (i, k) => k === "qty", - link: (i) => ({ tab: "labor", key: "L1" }) }, - labor: { columns: col, rows: [{ key: "L0" }, { key: "L1" }, { key: "L2" }], - link: (i) => (i === 1 ? { tab: "dg", key: "D1|1" } : null) }, - dg: { columns: col, rows: [{ key: "D1" }, { key: "D1|1" }, { key: "D1|합계" }, { key: "D2" }] }, -}; -const gone = []; -const base = { specOf: (t) => specs[t] ?? null, go: (l) => gone.push(l) }; -const anchor = document.createElement("tr"); -anchor.rect = { left: 40, top: 72, right: 900, bottom: 100 }; -const boxes = () => document.body.children.map((b) => b.dataset.b09Popup); -const rows = (k) => tables[k].rows.map((r) => r.key); -const cls = (k) => tables[k].rows.map((_, i) => tables[k].rowClass(i)); -""" - - -def _popup(body: str): - return run([TABS, GROUPS, BUILDERS, POPUP], SCENE + body, POPUP_PRELUDE) - - -def test_강조_범위_줄_수(): - out = _popup( - "\nconsole.log(JSON.stringify([" - "popupRange('bill', specs.bill, 'b1'), popupRange('bill', specs.bill, 'b4')," - "popupRange('labor', specs.labor, 'L1'), popupRange('dg', specs.dg, 'D1|1')," - "popupRange('labor', specs.labor, 'X')]));" - ) - # 설계내역서 = 그 줄 + 자손 · 그 밖 = 한 줄 · 묶음 = 호표 구간 · 없음 = null - assert out == [[0, 2], [3, 3], [1, 1], [0, 2], None] - - -def test_창_열_제목_강조_줄_자리(): - out = _popup( - "\nopenRowPopup({ ...base, link: { tab: 'bill', key: 'b1' }, anchor });" - "\nconst box = document.body.children[0];" - "\nconst o = tables[0];" - "\nconsole.log(JSON.stringify({ boxes: boxes(), n: popupCount(), cls: box.className," - " head: box.children[0].children.map((e) => e.textContent), rows: rows(0), rowCls: cls(0)," - " columns: o.columns === specs.bill.columns, caption: o.caption, total: o.total," - " editable: o.editable ?? null, lv: [0, 1, 2].map((i) => o.level.of(i))," - " table: box.children[1].dataset.b09Table, left: box.style.left, top: box.style.top }));" - ) - assert out["boxes"] == ["bill"] and out["n"] == 1 - assert out["cls"] == "b09-estimation b09-popup ui-popup" # 창 안 = b09 표 꼴(빨강 테두리) - assert out["head"] == ["설계내역서", "탭 이동", "×"] - assert out["rows"] == ["b1", "b2", "b3"] # 그 줄 + 자손 - assert out["rowCls"] == ["is-title is-pick is-pick-top", "is-pick", "is-pick is-pick-end"] - assert out["columns"] is True and out["caption"] == "설계내역서" # 열 제목(머리 여러 줄) 그대로 - assert out["total"] is None and out["editable"] is None # 합계 · 고칠 칸 없음 - assert out["lv"] == [1, 2, 2] and out["table"] == "bill" - assert out["left"] == "40px" and out["top"] == "106px" # 누른 줄 아래(간격 6) - - -def test_이어진_창_겹침_Esc_차례(): - out = _popup( - "\nopenRowPopup({ ...base, link: { tab: 'bill', key: 'b1' }, anchor });" - "\ntables[0].onCellClick(1, 'qty');" # 고칠 칸 = 안 엶 - "\nconst a = boxes();" - "\ntables[0].onCellClick(1, 'name');" # 창 안 링크 줄 = 이어진 창 - "\nconst second = document.body.children[1];" - "\ntables[1].onCellClick(0, 'name');" # 창 안 줄 0 = 원래 줄 1(L1) → dg - "\nconst b = boxes();" - "\nconst dg = rows(2);" - "\nconst e1 = esc(); const c = boxes();" - "\nconst e2 = esc();" - "\nconsole.log(JSON.stringify({ a, b, dg, top2: second.style.top, c, e: [e1, e2]," - " d: boxes(), gone }));" - ) - assert out["a"] == ["bill"] - assert out["b"] == ["bill", "labor", "dg"] # 겹쳐 위에(뒤 DOM = 위) - assert out["dg"] == ["D1", "D1|1", "D1|합계"] # 호표 구간 - assert out["top2"] == "362px" # 그 줄(창 안 2번째 줄) 곁 - assert out["c"] == [] # 308 Esc = 모두 - assert out["e"] == [True, False] # 창 없으면 Esc 안 가로챔 - assert out["d"] == [] and out["gone"] == [] - - -def test_닫기_탭으로_새_누름_주소(): - out = _popup( - "\nconst open = (key = 'b1') => openRowPopup({ ...base, link: { tab: 'bill', key }, anchor });" - "\nopen(); tables[0].onCellClick(0, 'name');" - "\nconst head = (k) => document.body.children[k].children[0].children;" - "\nhead(0)[2].click();" # 아래 창 × = 그 창 + 위 창 - "\nconst a = boxes();" - "\nopen(); tables[2].onCellClick(0, 'name'); open('b4');" # 화면 새 누름 = 있던 창 모두 닫고 새로 - "\nconst b = [boxes(), rows(4)];" - "\nhead(0)[1].click();" # [탭 이동] = 옮기고 모두 닫음 - "\nconst c = [boxes(), [...gone]];" - "\nopen(); hashFns.forEach((f) => f());" - "\nconst d = boxes();" - "\nopenRowPopup({ ...base, link: { tab: 'labor', key: 'X' }, anchor });" # 보일 줄 없음 = 옮김 - "\nconsole.log(JSON.stringify({ a, b, c, d, e: [boxes(), gone.length] }));" - ) - assert out["a"] == [] - assert out["b"] == [["bill"], ["b4"]] - assert out["c"] == [[], [{"tab": "bill", "key": "b4"}]] - assert out["d"] == [] # 주소 바뀜 = 모두 닫음 - assert out["e"] == [[], 2] - - -def test_308_맨_위_창_밖_누름_모두_닫힘_누름_먹음(): - out = _popup( - "\nconst outside = document.createElement('td'); document.body.append(outside);" - "\nconst open3 = () => { openRowPopup({ ...base, link: { tab: 'bill', key: 'b1' }, anchor });" - " tables[tables.length - 1].onCellClick(1, 'name');" - " tables[tables.length - 1].onCellClick(0, 'name'); };" - "\nconst pops = () => boxes().filter(Boolean);" - "\nconst top = () => document.body.children[document.body.children.length - 1];" - "\nconst under = () => document.body.children.filter((b) => b.dataset.b09Popup)[1];" - "\nopen3(); const inTop = press(top().children[1]); const a = pops();" # 맨 위 창 안 = 그대로 - "\nconst right0 = press(top().children[0], 'contextmenu'); const a2 = pops();" - "\nconst low = press(under().children[1]); const b = pops();" # 아래 창 누름 - "\nopen3(); const blank = press(outside); const c = pops();" # 쪽 빈 곳 · 톡 - "\nopen3(); const right = press(outside, 'contextmenu'); const d = pops();" # 우클릭 - "\nconst none = press(outside);" # 창 없음 = 안 가로챔 - "\nconsole.log(JSON.stringify({ inTop, a, right0, a2, low, b, blank, c, right, d, none, gone }));" - ) - full = ["bill", "labor", "dg"] - assert out["inTop"] == [False, False] and out["a"] == full # 맨 위 창 안 = 그대로 - assert out["right0"] == [False, False] and out["a2"] == full - assert out["low"] == [True, True] and out["b"] == [] # 모두 닫힘 · 그 누름 먹음 - assert out["blank"] == [True, True] and out["c"] == [] - assert out["right"] == [True, True] and out["d"] == [] - assert out["none"] == [False, False] and out["gone"] == [] - - -def test_창_자리_화면_안(): - out = _popup( - "\nconst v = { width: 1200, height: 800 };" - "\nconsole.log(JSON.stringify([" - "popupPlace({ left: 40, top: 700, right: 900, bottom: 728 }, { width: 400, height: 300 }, v)," - "popupPlace({ left: 1000, top: 100, right: 1190, bottom: 128 }, { width: 400, height: 300 }, v)," - "popupPlace({ left: 40, top: 400, right: 900, bottom: 428 }, { width: 400, height: 700 }, v)]));" - ) - assert out[0] == {"x": 40, "y": 394} # 아래 모자람 = 위 - assert out[1] == {"x": 794, "y": 134} # 오른쪽 화면 안 - assert out[2] == {"x": 40, "y": 92} # 위아래 다 모자람 = 화면 안으로 밀어 넣음 - - -def test_화면_잇기_모드별_누름_길(): - shell = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8") - assert ( - 'if (linkMode() === "link") return show(link.tab, link.key);' in shell - ) # 하이퍼링크 그대로 - assert "openRowPopup({" in shell and "go: (to) => show(to.tab, to.key)," in shell - assert "closePopups(); // 303-2 탭 바꿈" in shell # 탭 바꿈 = 모두 닫음 - assert 'goto(spec.link(i, key), made.root.querySelector(`tr[data-i="${i}"]`));' in shell - assert "if (spec.link && !spec.editable?.(i, key))" in shell # 고칠 칸 = 안 옮김 - assert ( - "createCheckPanel(refresh, (link) => show(link.tab, link.key));" in shell - ) # 연결 없음 = 옮김 - groups = (B09 / "B09_Estimation_UI_Tabs_Groups.ts").read_text("utf-8") - assert 'cards.querySelector(`[data-at="${i}"]`)' in groups # 박스 카드 누름도 같은 길 - popup = (ROOT / POPUP).read_text("utf-8") - assert "...tableOptionsOf(spec, tab)," in popup # 화면과 같은 표 짜기 - css = (B09 / "B09_Estimation_UI_Page.css").read_text("utf-8") - m = re.search(r"\.b09-estimation\.b09-popup\s*\{([^}]*)\}", css) - assert m and "max-width: 90vw" in m.group(1) and "position: fixed" in m.group(1) diff --git a/resources/tester/test_312_empty_pipes.py b/resources/tester/test_312_empty_pipes.py deleted file mode 100644 index 83ceb81ce..000000000 --- a/resources/tester/test_312_empty_pipes.py +++ /dev/null @@ -1,187 +0,0 @@ -"""312 — 관 목록 「빈 목록 = 관 0개」 · 「None = 자동 배치」. - -마지막 관을 지우면 화면이 자동 배치로 다시 차고 새로고침 · [저장] 뒤 저장분 하나가 되살아나던 것. -서버는 검증_sub5 storage **사본** + 읽기 전용 DB 로 부름(원본 · DB 안 바뀜). -""" - -from __future__ import annotations - -import json -import re -import shutil -from contextlib import asynccontextmanager -from pathlib import Path - -import pytest - -ROOT = Path(__file__).resolve().parents[2] -PROJECT_ID = "bec7fe41-83e7-4df7-86a3-fdddaa1f8fec" # OWNERS.md — laptop-sub5 `검증_sub5` -SOURCE = ROOT / "storage" / "1" / "3" / PROJECT_ID -PIPES_FILE = "B04_PreProcess/drainage/edits/pipe_points.json" -BASE = f"/api/projects/{PROJECT_ID}" - - -def _read(rel: str) -> str: - return (ROOT / rel).read_text(encoding="utf-8") - - -def _copy_project(dest: Path) -> None: - for rel in ("B04_PreProcess/drainage", "B05_Profile"): - shutil.copytree(SOURCE / rel, dest / rel) - for rel in ("B04_PreProcess/models", "B04_PreProcess/processed"): - (dest / rel).mkdir(parents=True, exist_ok=True) - for item in (SOURCE / rel).iterdir(): - if item.is_file() and item.stat().st_size <= 20 * 1024 * 1024: - shutil.copy2(item, dest / rel / item.name) - for item in SOURCE.iterdir(): - if item.is_file(): - shutil.copy2(item, dest / item.name) - - -@pytest.fixture() -def api(tmp_path, monkeypatch): - if not (SOURCE / PIPES_FILE).is_file(): - pytest.skip("배정 storage 프로젝트가 없음") - aiomysql = pytest.importorskip("aiomysql") - from fastapi import FastAPI - from fastapi.testclient import TestClient - - import B04_PreProcess.B04_PreProcess_Router_Basins as basins - import common_util.common_util_storage as storage - import config.config_db as config_db - from config import config_system as cfg - - storage_root = tmp_path / "storage" - root = storage_root / "1" / "3" / PROJECT_ID - _copy_project(root) - monkeypatch.setattr(storage, "STORAGE_BASE_DIR", str(storage_root)) - - @asynccontextmanager - async def lifespan(_app): - try: - pool = await aiomysql.create_pool( - host=cfg.DB_HOST, - port=cfg.DB_PORT, - db=cfg.DB_NAME, - user=cfg.DB_USER, - password=cfg.DB_PASSWORD, - minsize=1, - maxsize=4, - autocommit=True, - charset="utf8mb4", - init_command="SET time_zone='+00:00', SESSION tx_read_only=1", - ) - except Exception as exc: # DB 가 안 닿는 곳 - pytest.skip(f"DB 에 안 닿음: {exc}") - config_db.db_pool = pool - try: - yield - finally: - config_db.db_pool = None - pool.close() - await pool.wait_closed() - - app = FastAPI(lifespan=lifespan) - app.include_router(basins.router) - with TestClient(app) as client: - client.root = root # type: ignore[attr-defined] - yield client - - -def _file_points(api) -> list[dict]: - return json.loads((api.root / PIPES_FILE).read_text(encoding="utf-8"))["points"] - - -def _stored(api) -> list[dict]: - got = api.get(f"{BASE}/drainage/pipe-points").json()["pipe_points"] - return [{k: v for k, v in p.items() if k != "lonlat"} for p in got] - - -def test_PUT_빈_목록은_관_0개로_저장(api) -> None: - keep = _stored(api) - assert len(keep) >= 2 - one = api.put(f"{BASE}/drainage/pipe-points", json={"points": keep[:1]}).json() - assert one["saved_count"] == 1 - res = api.put(f"{BASE}/drainage/pipe-points", json={"points": []}).json() - assert res["saved_count"] == 0 and res["pipe_points"] == [] and res["basins"] == [] - assert _file_points(api) == [] - got = api.get(f"{BASE}/drainage/pipe-points").json() - assert got["pipe_points"] == [] and got["saved"] is True # 새로고침 = 0 · 되살아나지 않음 - - -def test_POST_빈_목록은_관_0_유역_0(api) -> None: - res = api.post(f"{BASE}/drainage/detail-basins", json={"points": []}).json() - assert res["pipe_points"] == [] and res["basins"] == [] - assert len(_file_points(api)) > 0 # 계산만 · 저장 안 함 - - -def _fresh_analysis(api) -> None: - """사본의 배수유역 분석에 지금 노선 지문을 찍음 — 옛 분석(지문 없음)은 낡음(342-3).""" - signature = api.get(f"{BASE}/drainage/pipe-points").json()["route_signature"] - path = api.root / "B04_PreProcess/drainage/00_watershed_response.json" - saved = json.loads(path.read_text(encoding="utf-8")) - path.write_text(json.dumps({**saved, "route_signature": signature}), encoding="utf-8") - - -def test_칸_없음_파일_없음_DELETE_노선_바뀜은_자동_배치(api) -> None: - # 옛 분석(노선 지문 없음) = 낡음 — 관 목록이 없으면 자동 배치 대신 409(342-3 지문) - stale = api.post(f"{BASE}/drainage/detail-basins", json={}) - assert stale.status_code == 409 and stale.json()["status"] == "stale" - _fresh_analysis(api) - auto = api.post(f"{BASE}/drainage/detail-basins", json={}).json()["pipe_points"] - assert len(auto) > 0 # 요청에 칸 없음 = 자동 배치 - api.put(f"{BASE}/drainage/pipe-points", json={"points": []}) - reset = api.delete(f"{BASE}/drainage/pipe-points").json() - assert len(reset["pipe_points"]) == len(auto) and reset["saved"] is False # [초기화] - assert not (api.root / PIPES_FILE).exists() - got = api.get(f"{BASE}/drainage/pipe-points").json() # 파일 없음 - assert len(got["pipe_points"]) == len(auto) and got["saved"] is False - # 노선 바뀜(지문 다름 · 좌표 없는 옛 저장분) = None → 자동 배치 - (api.root / PIPES_FILE).parent.mkdir(parents=True, exist_ok=True) - (api.root / PIPES_FILE).write_text( - json.dumps({"route_signature": "old", "points": [{"chainage_m": 50.0}]}), - encoding="utf-8", - ) - got = api.get(f"{BASE}/drainage/pipe-points").json() - assert len(got["pipe_points"]) == len(auto) and got["saved"] is False - - -def test_빈_목록과_None_을_가르는_자리() -> None: - src = _read("common_util/common_util_drainage_detail.py") - assert "if confirmed_chainages is not None:" in src - router = _read("B04_PreProcess/B04_PreProcess_Router_Basins.py") - assert "points = requested if requested is not None else stored" in router - assert "requested or stored" not in router - - -def _body(rel: str, start: str, end: str) -> str: - text = _read(rel) - return text[text.index(start) : text.index(end, text.index(start))] - - -def test_화면_초안_빈_목록은_저장분으로_안_넘어감() -> None: - load = _body( - "B05_Profile/B05_Profile_UI_Drainage_Panel.ts", "const loadSaved = ", "const analyze = " - ) - assert "pending !== null" in load and "pending?.length" not in load - # B06 목록도 같은 규칙(`??`) - b06 = _read("B06_Section/B06_Section_UI_Page_Structures_Panel.ts") - assert "(pendingPipes ?? pipeResponse.pipe_points)" in b06 - - -def test_횡단_벽_초안_빈_목록은_다_지움_그리고_옛_결함_막이() -> None: - # 빈 목록 = 다 지움 — 현황 = 초안 `??` 저장분(빈 목록도 초안) · 형상 얹기가 모든 열쇠를 걷고 다시(323-4) - drafts = _read("B06_Section/B06_Section_Section_Drafts.ts") - assert "structuresDraft ?? savedOf(projectId)" in drafts - assert "const drafts = !!(pipesDraft || structuresDraft);" in drafts - assert "!drafts.length" not in drafts - # 옛 「놓아도 안 보임」(09-09) — 옛 배열 참조가 `[]` 를 알리던 것. 정본 주입은 배열 안을 - # 채우고(갈아끼우지 않음) onChange 를 울리지 않아야 함. - inject = _body( - "B05_Profile/B05_Profile_UI_Structures_Panel.ts", " setStructures(next) {", " },\n" - ) - assert "structures.splice(0, structures.length" in inject - assert not re.search(r"\bemit\(|onChange\(", inject) - # 알림은 지금 목록 그대로를 실어 보냄(빈 배열을 따로 만들지 않음) - commit = _read("B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts") - assert "ctx.callbacks.onChange([...ctx.structures]);" in commit diff --git a/resources/tester/test_345_4_recommend_panel.py b/resources/tester/test_345_4_recommend_panel.py deleted file mode 100644 index 60d6915e6..000000000 --- a/resources/tester/test_345_4_recommend_panel.py +++ /dev/null @@ -1,63 +0,0 @@ -"""345-4 노선 추천 → 좌측 새 칸 「노선 추천」(지운 「계획노선」 자리) — [노선 추천] · 현재 추천 · [초기화]. - -· 칸 요소는 `_Panel_Recommend` 가 만들고 편집기(`_RouteEdit_Initial`)가 밖에서 받아 건다 -· 추천 창은 공용 모달 하나 · [적용] = 본문 평면에 되돌리기 한 칸(`restoreSnapshot(…, true)` → 이력 한 걸음) -· 추천 창의 「지금 노선」은 본문 편집기 지금 노드 -칸 생김새 · 차례는 `test_design_view_parity` · `test_311_panel_folded` 가 봄(화면 확인은 브레인). -""" - -from __future__ import annotations - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def _read(rel: str) -> str: - return (ROOT / rel).read_text(encoding="utf-8") - - -def test_칸_자리와_재료(): - sections = _read("B05_Profile/B05_Profile_UI_Panel_Sections.ts") - at = sections.index('id: "route-recommend"') - block = sections[at : at + 300] - assert 'title: "노선 추천"' in block and "order: 30," in block # 지운 계획노선 자리 - assert 'part(ctx, "recommend").nodes' in block - panel = _read("B05_Profile/B05_Profile_UI_Panel.ts") - assert "const recommend = createRecommendPart();" in panel - assert "Object.assign(shared.parts, { settings, recommend });" in panel - view = _read("A00_Common/design/design_view_profile.ts") - assert "recommend: panel.recommend," in view - - -def test_편집기가_칸_요소를_밖에서_받음(): - initial = _read("B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts") - assert "overlay" not in initial - assert ( - "const { recommendButton, nowLine } = params.controls;" in initial - ) # 칸 [초기화] 지움(418) - assert "current: params.planned().map(([x, y]) => [x, y])," in initial # 지금 노선 - main = _read("B05_Profile/B05_Profile_UI_RouteEdit.ts") - assert "controls: options.recommend," in main - assert "planned: () => planned," in main - assert len(main.splitlines()) <= 700 - - -def test_적용은_본문_평면에_되돌리기_한_칸(): - main = _read("B05_Profile/B05_Profile_UI_RouteEdit.ts") - assert "apply: (snapshot, message) => restoreSnapshot(snapshot, message, true)," in main - restore = main[main.index("function restoreSnapshot(") :] - restore = restore[: restore.index("\n }\n")] - assert "applyEdit(message, record, false);" in restore # 366-3 — 추천 적용은 측점 맞춤 안 함 - apply_edit = main[main.index("function applyEdit(") :] - assert "if (record) history?.commit(snapshotNow());" in apply_edit[:900] - initial = _read("B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts") - on_apply = initial[initial.index("onApply: (picked) => {") :] - assert on_apply[:500].count("params.apply(") == 1 - - -def test_추천_창은_공용_모달_하나(): - rec = _read("B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts") - assert rec.count("openModal(") == 1 - # 뒤 판(평면 · 종단도) 단축키가 창 안 Ctrl+Z 를 받지 않게 — 창 막에서 끊음 - assert '?.closest(".ui-modal__backdrop")' in rec and "key.stopPropagation();" in rec diff --git a/resources/tester/test_345_5_plan_full_overlay.py b/resources/tester/test_345_5_plan_full_overlay.py deleted file mode 100644 index 83966b029..000000000 --- a/resources/tester/test_345_5_plan_full_overlay.py +++ /dev/null @@ -1,70 +0,0 @@ -"""345-5 사용자 의견(요구 233) — 종단 보기 평면 = 가운데 전체 영역 · 아래 종단 판 = 뜬 판 · -노선 편집 안 「원지반선 · 계획 종단선」 띠 없음 · 아래 종단 판 종단 그래프는 그대로. - -꼴은 글로 봄(화면 확인은 브레인). -./venv/Scripts/python.exe -m pytest resources/tester/test_345_5_plan_full_overlay.py -q -""" - -from __future__ import annotations - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def _read(rel: str) -> str: - return (ROOT / rel).read_text(encoding="utf-8") - - -def _block(css: str, selector: str) -> str: - found = re.search(rf"^{re.escape(selector)} \{{([^}}]*)\}}", css, re.M) - assert found, selector - return found.group(1) - - -def test_평면은_가운데_전체_영역(): - page = _read("A00_Common/design/design_page.css") - main = _block(page, ".design-page__main") - assert "position: relative;" in main and "height: 100%;" in main - assert "flex: 1 1 auto;" in _block(page, ".design-page__center") - # 종단 보기에서 판을 흐름에 세우는 규칙 없음 — 가운데가 위아래 끝까지 - flow = re.findall(r"^(\.design-page__main[^{\n]*\.b05-route-profile) \{", page, re.M) - assert flow == [ - '.design-page__main[data-view="cross"] > .design-page__profile > .b05-route-profile' - ] - route = _read("B05_Profile/B05_Profile_UI_Style_RouteEdit.css") - assert "flex: 1 1 auto;" in _block(route, ".b05-routeedit") - assert "flex: 1 1 auto;" in _block(route, ".b05-routeedit__canvas-wrap") - # 캔버스 크기 = 칸 크기(ResizeObserver) · 평면은 가운데 칸에 붙음 - editor = _read("B05_Profile/B05_Profile_UI_RouteEdit.ts") - assert "new ResizeObserver(() => resize())" in editor - assert ( - 'sizeWatch.observe(overlay.querySelector(".b05-routeedit__canvas-wrap")!)' - in editor - ) - view = _read("A00_Common/design/design_view_profile.ts") - assert "center.append(mainContent);" in view - assert "mountRouteEditor(mainContent, host, activeProjectId, rebuildAfterReplan," in view - - -def test_종단_판은_평면_위_뜬_판(): - style = _read("B05_Profile/B05_Profile_UI_Style.css") - panel = _block(style, ".b05-route-profile") - for line in ("position: absolute;", "z-index: 3;", "bottom: 0;", "left: 0;", "right: 0;"): - assert line in panel - # 끌어 높이 · 접기 · 새 창 떼기 그대로 - assert "height: var(--b05-profile-height, 60dvh);" in panel - assert ".b05-route-profile.is-collapsed > .ui-resizer" in style - page = _read("A00_Common/design/design_page.css") - assert "position: relative;" in _block(page, ".b05-route-profile.is-popout") - - -def test_띠_없음_종단_그래프_있음(): - chrome = _read("B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts") - assert "data-strip" not in chrome - assert not (ROOT / "B05_Profile/B05_Profile_UI_RouteEdit_ProfileStrip.ts").exists() - # 아래 종단 판(종단 그래프 · 표 · 유토곡선)은 그대로 세움 - view = _read("A00_Common/design/design_view_profile.ts") - assert "const profilePanel = createRouteProfilePanel(" in view - assert (ROOT / "B05_Profile/B05_Profile_UI_Profile_Panel.ts").exists() diff --git a/resources/tester/test_349_routeedit_theme_redraw.py b/resources/tester/test_349_routeedit_theme_redraw.py deleted file mode 100644 index 18369d666..000000000 --- a/resources/tester/test_349_routeedit_theme_redraw.py +++ /dev/null @@ -1,51 +0,0 @@ -"""349 — 밝음 · 어두움을 바꾸면 노선 편집 평면 캔버스를 다시 그림(바탕 · 등고선 · 노선 · 발자국 색). - -그리기는 그릴 때마다 테마 토큰을 다시 읽음 — 테마 바꿈 알림에 다시 그리기만 붙었는지 글로 봄. -./venv/Scripts/python.exe -m pytest resources/tester/test_349_routeedit_theme_redraw.py -q -""" - -from __future__ import annotations - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -B05 = ROOT / "B05_Profile" -RENDER = (B05 / "B05_Profile_UI_RouteEdit_Render.ts").read_text(encoding="utf-8") -EDITOR = (B05 / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8") - - -def _body(text: str, head: str) -> str: - at = text.index(head) - return text[at : text.index("\n}", at)] - - -def test_테마_알림_받아_다음_프레임에_다시_그림() -> None: - watch = _body(RENDER, "export function watchTheme(") - assert 'attributeFilter: ["data-theme"]' in watch # 단추로 바꿈 - assert 'matchMedia("(prefers-color-scheme: dark)")' in watch # 시스템 바꿈 - # 공용 색 캐시가 먼저 비워지게 다음 프레임에 · 뗄 때 모두 뗌 - assert "requestAnimationFrame(onChange)" in watch - assert "observer.disconnect();" in watch and 'removeEventListener("change", later)' in watch - - -def test_편집기가_붙이고_떠날_때_뗌() -> None: - assert "const themeWatch = watchTheme(() => draw());" in EDITOR - dispose = _body(EDITOR, "const dispose = (): void => {") - assert "themeWatch();" in dispose - assert len(EDITOR.splitlines()) <= 700 - - -def test_그릴_때마다_토큰을_다시_읽음() -> None: - scene = _body(RENDER, "export function drawRouteEditScene(") - assert "const style = getComputedStyle(document.documentElement);" in scene - assert 'style.getPropertyValue("--color-surface")' in scene # 바탕 - assert 'style.getPropertyValue("--map-contour-major")' in scene # 등고선 - assert 'style.getPropertyValue("--map-route")' in scene # 노선 - footprint = (B05 / "B05_Profile_UI_RouteEdit_Footprint.ts").read_text(encoding="utf-8") - assert "context.strokeStyle = style.getPropertyValue(token).trim() || fallback;" in footprint - - -def test_교각점_녹색_토큰_밝음_어두움_따로() -> None: - css = (ROOT / "ui_template" / "ui_template_theme.css").read_text(encoding="utf-8") - assert css.count("--map-route-node:") == 3 # 밝음 1 + 어두움(단추 · 시스템) 2 - assert 'getPropertyValue("--map-route-node")' in RENDER diff --git a/resources/tester/test_353_3_contour3d_style.py b/resources/tester/test_353_3_contour3d_style.py deleted file mode 100644 index 6164ac994..000000000 --- a/resources/tester/test_353_3_contour3d_style.py +++ /dev/null @@ -1,30 +0,0 @@ -"""353-3 3D 등고선 — 주곡선 5 m 고정 · 1 m 옅게 · 평면 색 토큰(B05 · B04 3D 같은 재질). - -./venv/Scripts/python.exe -m pytest resources/tester/test_353_3_contour3d_style.py -q -""" - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -STYLE = ROOT / "ui_template" / "ui_template_contour3d.ts" -VIEWERS = [ - ROOT / "B05_Profile" / "B05_Profile_UI_Viewer.ts", - ROOT / "B04_PreProcess" / "B04_PreProcess_UI_TerrainViewer_Contours.ts", -] - - -def test_재질은_평면_색_토큰과_5m_고정() -> None: - src = STYLE.read_text("utf-8") - assert '"--map-contour-major"' in src and '"--map-contour-minor"' in src - assert "CONTOUR_MAJOR_STEP_M = 5;" in src - # 1 m 선 불투명 = 토큰 알파(0.45) · 주곡선은 진하게 - assert "opacity: minor.alpha" in src and "opacity: 0.95 * major.alpha" in src - - -def test_두_3D_뷰어가_같은_재질을_씀() -> None: - for path in VIEWERS: - src = path.read_text("utf-8") - assert 'from "@ui/ui_template_contour3d"' in src, path.name - assert "isMajorContour(" in src and "contourMaterials()" in src, path.name - # 옛 갈래(간격 × 5 · 주황 두 색) 없음 - assert "interval * 5" not in src and "0xf59e0b" not in src, path.name diff --git a/resources/tester/test_364_6b_std_note_same_line.py b/resources/tester/test_364_6b_std_note_same_line.py deleted file mode 100644 index 5adcfe662..000000000 --- a/resources/tester/test_364_6b_std_note_same_line.py +++ /dev/null @@ -1,23 +0,0 @@ -"""364-6 덧 2 — 암 글이 라벨과 같은 줄 끝(줄 수 안 늘림). 화면 실측은 브레인.""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -TS = (ROOT / "B06_Section/B06_Section_UI_Standard_Panel.ts").read_text(encoding="utf-8") -CSS = (ROOT / "B06_Section/B06_Section_UI_Style.css").read_text(encoding="utf-8") - - -def rule(sel: str) -> str: - m = re.search(re.escape(sel) + r"\s*\{([^}]*)\}", CSS) - assert m, sel - return m.group(1) - - -def test_label_row_one_line(): - assert "replaceChildren(text, note)" in TS - assert "display: flex" in rule(".b06-std__label-row") - text = rule(".b06-std__label-text") - assert "text-overflow: ellipsis" in text and "white-space: nowrap" in text - note = rule(".b06-std__group-note") - assert "margin-left: auto" in note and "white-space: nowrap" in note diff --git a/resources/tester/test_368_3_cross_modal.py b/resources/tester/test_368_3_cross_modal.py deleted file mode 100644 index 8e4f1128c..000000000 --- a/resources/tester/test_368_3_cross_modal.py +++ /dev/null @@ -1,36 +0,0 @@ -"""368-3 원지반 점 임시 횡단 작은 창 · 추가 측점 점선 · 우클릭 「측점 삭제」 — 연결 자리를 소스로 봄. - -화면 동작(누름 → 창 → [측점 추가] → B06 카드 → 삭제)은 ORCA 로 확인. -""" - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def _read(relative: str) -> str: - return (ROOT / relative).read_text(encoding="utf-8") - - -def test_추가_측점_선_점선_끌기_없음() -> None: - graph = _read("B06_Section/B06_Section_UI_Longitudinal.ts") - assert 'station.kind === "irregular" && !station.plus' in graph - assert 'station.plus ? "plus" : station.kind' in graph - css = _read("B06_Section/B06_Section_UI_Style_Cross.css") + _read( - "B06_Section/B06_Section_UI_Style_Cross_Chart.css" - ) - rule = css[css.index(".b06-chart__station-line--plus") :].split("}")[0] - assert "var(--color-warning)" in rule and "stroke-dasharray: 5 3" in rule - # 주인 앞머리는 공용 `common_util_structure_station.ts` 로 옮겨짐(Layout.ts 에는 없음) - assert 'PLUS_OWNER_PREFIX = "plus-"' in _read("common_util/common_util_structure_station.ts") - - -def test_누름_작은_창_우클릭_삭제() -> None: - render = _read("B05_Profile/B05_Profile_UI_Profile_Render.ts") - assert "crossPickFromEvent(chartWrap, event" in render and "openCrossModal(" in render - assert "onRemovePlus: removePlusStation" in render - menu = _read("B05_Profile/B05_Profile_UI_Profile_Structures.ts") - assert '"측점 삭제", () => options.onRemovePlus?.(near.chainage_m)' in menu - modal = _read("B05_Profile/B05_Profile_UI_Profile_CrossModal.ts") - assert "previewStructureSections(projectId(), [m])" in modal - assert "add.disabled = taken" in modal # 같은 m 막음 diff --git a/resources/tester/test_381_2_routeedit_marks.py b/resources/tester/test_381_2_routeedit_marks.py deleted file mode 100644 index e3b8b67c3..000000000 --- a/resources/tester/test_381_2_routeedit_marks.py +++ /dev/null @@ -1,58 +0,0 @@ -"""381-2 B05 평면 [측점] [기울기] 켬끔(요구 241-11). - -- 단추 = 절/성토선 왼쪽 · 켬끔 모듈(기본 [측점] 켬 · [기울기] 끔(390-1 요구 242-17) · 페이지 동안) -- [측점] 끔 = 눈금 · 번호 안 그림 · 눈금 집기 안 함(라벨 0개 = 전부 집힘 함정) · 시점 · 종점 이름표 남김 -- [기울기] 끔 = 기울기 라벨(높이 묻기) 안 부름 -""" - -from pathlib import Path - -B05 = Path(__file__).resolve().parents[2] / "B05_Profile" - - -def _read(name: str) -> str: - return (B05 / name).read_text(encoding="utf-8") - - -def test_켬끔_모듈_기본_측점_켬_기울기_끔(): - marks = _read("B05_Profile_UI_RouteEdit_Marks.ts") - assert "{ stations: true, slopes: false }" in marks - assert "scheduleRedraw();" in marks - assert "export function scheduleRedraw" in _read("B05_Profile_UI_RouteEdit_Footprint.ts") - - -def test_단추는_절성토선_왼쪽_배선(): - chrome = _read("B05_Profile_UI_RouteEdit_Chrome.ts") - stations = chrome.index('data-act="stations"') - slopes = chrome.index('data-act="slopes"') - assert stations < slopes < chrome.index('data-act="footprint"') - assert ">측점" in chrome and ">기울기" in chrome - assert '[data-act="stations"]\')!, "stations")' in chrome - assert '[data-act="slopes"]\')!, "slopes")' in chrome - - -def test_측점_끄면_눈금만_빠지고_이름표_남음(): - render = _read("B05_Profile_UI_RouteEdit_Render.ts") - body = render[render.index("function drawStationMarks(") : render.index("function endLabel(")] - # 라벨 상자를 먼저 비우고(누름 판정) · 눈금은 켬일 때만 · 이름표는 밖 - assert body.index("setStationLabelBoxes(") < body.index('if (markShown("stations"))') - guard = body.index('if (markShown("stations"))') - assert guard < body.index("drawStationTicks(") < body.index("`시점 ") - assert body.count("endLabel(") == 2 - - -def test_기울기_끄면_라벨도_높이_요청도_없음(): - render = _read("B05_Profile_UI_RouteEdit_Render.ts") - assert 'if (markShown("slopes")) scene.overStations?.(context);' in render - assert render.count("scene.overStations?.(context)") == 1 - - -def test_측점_끄면_눈금_집기_없음(): - pointer = _read("B05_Profile_UI_RouteEdit_Pointer.ts") - # 집기에 쓰는 눈금 찾기는 켬일 때만 — 라벨 0개면 전부 집히는 함정 - at = 0 - calls = 0 - while (at := pointer.find("stationAtScreen(", at + 1)) >= 0: - calls += 1 - assert 'markShown("stations")' in pointer[at - 300 : at], at - assert calls >= 1 diff --git a/resources/tester/test_381_3_routeedit_node_pick.py b/resources/tester/test_381_3_routeedit_node_pick.py deleted file mode 100644 index edc6a58f0..000000000 --- a/resources/tester/test_381_3_routeedit_node_pick.py +++ /dev/null @@ -1,66 +0,0 @@ -"""381-3 B05 평면 교각점(요구 241-14) — 크기 · 그리는 차례 · 맴돔 강조 · 집기 한 길. - -겹침 셈(`pickAtScreen`)은 test_b05_routeedit_pick 이 돌려 봄 — 여기는 배선만. -""" - -from pathlib import Path - -B05 = Path(__file__).resolve().parents[2] / "B05_Profile" - - -def _read(name: str) -> str: - return (B05 / name).read_text(encoding="utf-8") - - -def test_노드_크기_문턱(): - assert "const NODE_R = 6;" in _read("B05_Profile_UI_RouteEdit_Render.ts") - pointer = _read("B05_Profile_UI_RouteEdit_Pointer.ts") - assert "const NODE_HIT_PX = 12;" in pointer and "const NODE_CORE_PX = 6;" in pointer - - -def test_노드는_라벨_뒤_맨_위_회전_안(): - render = _read("B05_Profile_UI_RouteEdit_Render.ts") - scene = render[render.index("export function drawRouteEditScene(") :] - scene = scene[: scene.index("context.restore(); // 회전 끝")] - labels = scene.index("drawContourLabels(context, scene, band, style);") - for later in ("scene.planned.forEach(", "drawPickedCurve(", "drawPickHover("): - assert labels < scene.index(later), later - for earlier in ("drawStationMarks(", "scene.overStations?.(", "drawMeasureMarks("): - assert scene.index(earlier) < labels, earlier - - -def test_곡선_네모는_예상노선_뒤_노선_앞(): - """414-2 — 모든 곡선 BC · EC 네모 = 예상노선 뒤 · 노선 앞(노선이 네모 위를 지나감).""" - render = _read("B05_Profile_UI_RouteEdit_Render.ts") - scene = render[render.index("export function drawRouteEditScene(") :] - expected = scene.index("scene.expected,") - squares = scene.index("scene.curveInfo.forEach(") - route = scene.index("strokePolyline(", squares) - assert expected < squares < route - assert scene[route:].split(",")[2].strip() == "line" - - -def test_집기_맴돔_한_함수(): - pointer = _read("B05_Profile_UI_RouteEdit_Pointer.ts") - assert "pickAtScreen(" in pointer - down = pointer[pointer.index('addEventListener("pointerdown"') :] - down = down[: down.index('addEventListener("pointermove"')] - assert "hover(null);" in down and "const pick = choice.pick;" in down # chooseAt 안 pickAt(433) - assert "nodeAt(px, py)" not in down and "stationAtScreen(" not in down - move = pointer[pointer.index('addEventListener("pointermove"') :] - move = move[: move.index("const endDrag")] - # 끄는 동안은 앞에서 돌아감 — 맴돔은 끌기 갈래 뒤 - assert move.index("if (dragNode >= 0)") < move.index("chooseAt(px, py)") - assert "hover(over);" in move - assert 'addEventListener("pointerleave"' in pointer - - -def test_맴돔_그림(): - hover = _read("B05_Profile_UI_RouteEdit_Hover.ts") - assert "pickHover(context.canvas)" in hover - assert ( - '"node" in hover' in hover - and "drawStationNormal(context, scene, line, hover.chainage)" in hover - ) - # 391-1 맴돔 = 고름 한 함수 · 좌우 20 m 고정 - assert "const STATION_NORMAL_HALF_M = 20;" in hover diff --git a/resources/tester/test_383_3_rerun_row.py b/resources/tester/test_383_3_rerun_row.py deleted file mode 100644 index 859aef478..000000000 --- a/resources/tester/test_383_3_rerun_row.py +++ /dev/null @@ -1,20 +0,0 @@ -"""383-3 노선 추천 모달 [재계산] — 종단 반영 오른쪽 같은 줄 · 늘 켜짐(421) · 보임 = 저장본 없음 · 칸 바꿈 때만(508-2). - -./venv/Scripts/python.exe -m pytest resources/tester/test_383_3_rerun_row.py -q -""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -B05 = ROOT / "B05_Profile" - - -def test_재계산_한_줄_늘_켜짐() -> None: - ts = (B05 / "B05_Profile_UI_RouteEdit_Recommend.ts").read_text(encoding="utf-8") - assert re.search(r'
]*data-act="rerun">재계산', ts) - assert "rerun.hidden = !(missing || dirty);" in ts # 508-2 — 저장본 없음(409) · 칸 바꿈 때만 - assert "rerun.before(profileToggle)" in ts # 종단 반영 토글이 같은 줄 왼쪽(421) - assert "rerun.disabled = busy || invalid.size > 0;" in ts - css = (B05 / "B05_Profile_UI_Style_RouteRecommend.css").read_text(encoding="utf-8") - assert re.search(r"\.b05-routerec__row \{\s*display: flex;", css) diff --git a/resources/tester/test_384_6_save_one_line.py b/resources/tester/test_384_6_save_one_line.py deleted file mode 100644 index bf51dba45..000000000 --- a/resources/tester/test_384_6_save_one_line.py +++ /dev/null @@ -1,47 +0,0 @@ -"""PLAN 386-6 sub7 — [저장] 본문 보기 무관(B05 만 연 길 지움). - -못박는 것 - - [저장] · [확정] 둘 다 횡단 보기를 (숨긴 채라도) 세움 → 횡단 본문 = 횡단 보기 몫 한 벌(`crossSaveParts`). - - 종단 보기 몫엔 횡단 본문이 없음 — 옛 「횡단 보기를 세운 적 없이 [저장]」 캐시 패치 길(saveCachedCrossPatches) 지움. - - 횡단 보기를 못 세우면 반쪽 저장 대신 실패 알림. -화면(B05 만 연 [저장] 본문 = B06 다녀온 본문)은 PLAN 386-6 검증 줄. -""" - -from __future__ import annotations - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def _read(rel: str) -> str: - return (ROOT / rel).read_text(encoding="utf-8") - - -def _save_body() -> str: - shared = _read("A00_Common/design/design_shared.ts") - start = shared.index("export async function saveSharedDesign") - return shared[start : shared.index("\n}\n", shared.index("async function saveOnce(", start))] - - -def test_저장도_횡단_보기를_세움(): - body = _save_body() - assert 'await shared.ensureView("cross");' in body - assert 'if (confirm) await shared.ensureView("cross")' not in body - # 472-6 — 횡단 본문 = 횡단 보기 조각(`crossPart`) · 한 본문 창구에 실림 - assert " crossPart," in body - assert "profileSave?.cross" not in body - - -def test_못_세우면_실패_알림(): - body = _save_body() - assert "if (!crossPart) {" in body - assert '"B06_Profile_Save_Failed"' in body and '"B06_Profile_Confirm_Failed"' in body - - -def test_종단_보기_몫엔_횡단_본문_없음(): - actions = _read("B05_Profile/B05_Profile_UI_Page_Actions.ts") - start = actions.index("export function profileSaveParts(") - parts = actions[start:] - assert "saveCachedCrossPatches" not in actions - assert "\n cross: " not in parts and "crossPart" not in parts diff --git a/resources/tester/test_398_1_reveal_user_pick.py b/resources/tester/test_398_1_reveal_user_pick.py deleted file mode 100644 index 02af90667..000000000 --- a/resources/tester/test_398_1_reveal_user_pick.py +++ /dev/null @@ -1,51 +0,0 @@ -"""PLAN 398-1 sub7 — 교점 옮기면 좌측 패널 열림(요구 248) · 513-2 로 뒤집음. - -못박는 것 - - 513-2 — 고름은 내용만 · 좌측 패널 · 「구조물 배치」 카드 열림 · 닫힘은 사용자가 둔 그대로 - (옛 onReveal · `isUserPick` 펴기 걷음 · 우클릭 추가도 카드 안 폄). - - 499-1 그대로 — 좌측 패널은 안 엶(`revealLeft` · `setOptionsOpen(` 없음) · - 패널을 열 때 고른 줄 · 강조 상자로 한 번 밂 · B06 평면 겹침 기준 = main 전체. -""" - -from __future__ import annotations - -from pathlib import Path - -HERE = Path(__file__).resolve().parent -ROOT = HERE.parents[1] - - -def _read(rel: str) -> str: - return (ROOT / rel).read_text(encoding="utf-8") - - -def test_513_2_고름은_카드_열림_안_건드림(): - shared = _read("A00_Common/design/design_shared.ts") - assert "onReveal" not in shared and "isUserPick" not in shared - for rel in ("A00_Common/design/design_shared.ts", "A00_Common/design/design_page.ts"): - assert "revealLeft" not in _read(rel) and "setOptionsOpen(" not in _read(rel) # 499-1 - panel = _read("B05_Profile/B05_Profile_UI_Structures_Panel.ts") - assert "onReveal" not in panel - assert 'root.classList.remove("is-collapsed")' not in panel - assert not (ROOT / "A00_Common/design/design_user_pick.ts").exists() - - -def test_열_때_고른_줄로_밂(): - page = _read("A00_Common/design/design_page.ts") - assert "onOptionsOpenChange: (isOpen) => isOpen && scrollToPick(shared.structures)" in page - shared = _read("A00_Common/design/design_shared.ts") - body = shared[shared.index("export function scrollToPick(") :] - body = body[: body.index("\n}\n")] - assert 'listRoot.querySelector(".is-selected")' in body - assert 'root.querySelector("fieldset.is-active")' in body - layout = _read("ui_template/ui_template_workflow_layout.ts") - assert "options.onOptionsOpenChange?.(isOpen);" in layout - - -def test_B06_평면_겹침_기준_main_전체(): - css = _read("A00_Common/design/design_page.css") - rule = css[css.index('.design-page__main[data-view="cross"] > .design-page__center {') :] - assert "position: static;" in rule[: rule.index("}")] - peek = css[css.index(".design-plan-peek {") :] - z = int(peek[peek.index("z-index:") + 8 : peek.index(";", peek.index("z-index:"))]) - assert 3 < z < 199 # 종단 판(3) 위 · 서랍 덮개(199) 아래 diff --git a/resources/tester/test_401_3_plan_label_under_tools.py b/resources/tester/test_401_3_plan_label_under_tools.py deleted file mode 100644 index b5900ae63..000000000 --- a/resources/tester/test_401_3_plan_label_under_tools.py +++ /dev/null @@ -1,25 +0,0 @@ -"""401-3 평면 곡선 R 창 · 등고선 자물쇠가 도구줄 [되돌리기] 를 덮어 누름이 안 가던 것. - -지도 위 표지 층(1) < 도구줄 · 횡단 판 층(2) — 표지가 도구줄 · 판 손잡이 누름을 안 가로챔. -./venv/Scripts/python.exe -m pytest resources/tester/test_401_3_plan_label_under_tools.py -q -""" - -import re -from pathlib import Path - -B05 = Path(__file__).resolve().parents[2] / "B05_Profile" - - -def _z(css_name: str, selector: str) -> int: - css = (B05 / css_name).read_text(encoding="utf-8") - block = css.split(selector + " {", 1)[1].split("}", 1)[0] - return int(re.search(r"z-index:\s*(\d+)", block).group(1)) - - -def test_표지는_도구줄_판_밑(): - tools = _z("B05_Profile_UI_Style.css", ".b05-route__view-controls") - side = _z("B05_Profile_UI_Style_RouteEdit.css", ".b05-routeedit__side-wrap") - label = _z("B05_Profile_UI_Style_RouteEdit.css", ".b05-routeedit__label") - lock = _z("B05_Profile_UI_Style_RouteEdit_ContourLock.css", ".b05-contourlock") - assert label < tools and lock < tools - assert label < side and lock < side diff --git a/resources/tester/test_408_3_measure_side_lift.py b/resources/tester/test_408_3_measure_side_lift.py deleted file mode 100644 index f2c1f4b44..000000000 --- a/resources/tester/test_408_3_measure_side_lift.py +++ /dev/null @@ -1,28 +0,0 @@ -"""408-3 잰 값 창 · 오른쪽 횡단 판(손잡이)도 뜬 종단 판 위로 — 408-2 올림 값(`--b05-profile-cover`) 재사용. - -판 열림 때 잰 값 창은 판 밑에 깔리고, 횡단 판 손잡이(판 가운데 높이)도 판 밑이라 횡단을 못 열었음. - - ./venv/Scripts/python.exe -m pytest resources/tester/test_408_3_measure_side_lift.py -q -""" - -from pathlib import Path - -import pytest - -CSS = Path(__file__).resolve().parents[2] / "B05_Profile" / "B05_Profile_UI_Style_RouteEdit.css" -LIFT = "bottom: calc(var(--spacing-12) + var(--b05-profile-cover, 0px));" - - -def _rules(selector: str) -> list[str]: - """그 선택자의 규칙 덩이 모두(같은 선택자가 여러 덩이에 나뉨).""" - css = CSS.read_text(encoding="utf-8") - parts = css.split(f"{selector} {{")[1:] - return [part[: part.index("}")] for part in parts] - - -@pytest.mark.parametrize( - "selector", - [".b05-routeedit__info", ".b05-routeedit__measure", ".b05-routeedit__side-wrap"], -) -def test_지도_아래_글판_판은_종단_판_위로(selector) -> None: - assert any(LIFT in rule for rule in _rules(selector)) diff --git a/resources/tester/test_500_1_curve_keeps_pick.py b/resources/tester/test_500_1_curve_keeps_pick.py deleted file mode 100644 index 231a51d22..000000000 --- a/resources/tester/test_500_1_curve_keeps_pick.py +++ /dev/null @@ -1,95 +0,0 @@ -"""500-1 B05 평면 — 곡선(교각점 · 손잡이 · 호) 누름 = 측점 고름 유지 · 풀림 = 오른쪽 판도 비움 · 임시 횡단 창 단추 = 제목 줄 · 제목 줄 끌기. - -./venv/Scripts/python.exe -m pytest resources/tester/test_500_1_curve_keeps_pick.py -q -""" - -from __future__ import annotations - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def _read(*parts: str) -> str: - return ROOT.joinpath(*parts).read_text(encoding="utf-8") - - -def _down() -> str: - """평면 누름 — 마커 갈래 뒤 ~ 교각점 갈래 앞.""" - pointer = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Pointer.ts") - start = pointer.index("if (choice.mark && marks.press(px, py)) {") - return pointer[start : pointer.index("if (hitNode >= 0) {", start)] - - -# 누름별 — 고름 유지(풂 건너뜀) 조건 한 줄씩 -KEEP = { - "교각점": '(!!pick && "node" in pick)', - "손잡이": "choice.handle !== null", - "호(눈금 아님)": '(!(pick && "chainage" in pick) && arcAtScreen(state.curveInfo, toScreen, px, py) >= 0)', -} - - -def test_곡선_누름은_고름_유지() -> None: - down = _down() - for name, cond in KEEP.items(): - assert cond in down, name - assert "if (!onCurve) marks.deselect();" in down # 그 밖(빈 땅 · 등고선 · 눈금) = 풂 - assert down.count("marks.deselect()") == 1 # 무조건 풂 걷힘 - - -def test_차례_마커_다음_교각점_앞() -> None: - down = _down() - assert down.index("canvas.setPointerCapture(event.pointerId);") < down.index("const onCurve") - assert down.index("const onCurve") < down.index("if (!onCurve) marks.deselect();") - - -def test_풀림은_오른쪽_판도_비움() -> None: - cross = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Cross.ts") - line = "if (from !== PLAN_PICK_FROM || selection.chainageM === null) follow(selection);" - assert line in cross - follow = cross[cross.index("function follow(") : cross.index(line)] - assert "watching = null;" in follow and "current.show(null, interval());" in follow - # 풂(값 없음 · 평면 이름)을 내는 곳 = 마커 풂 하나 · Esc 도 같은 길 - marks = _read("B05_Profile", "B05_Profile_UI_RouteEdit_StructureMarks.ts") - assert marks.count("selectDesign(projectId, NO_SELECTION, { from: PLAN_PICK_FROM });") == 1 - assert "!activeMoving(canvas) && deselect()" in marks - - -def test_임시_횡단_단추_제목_줄_오른쪽() -> None: - modal = _read("B05_Profile", "B05_Profile_UI_Profile_CrossModal.ts") - head = modal[ - modal.index('
') : modal.index( - '
' - ) - ] - for act in ("add", "remove", "close"): - assert f'data-act="{act}"' in head - assert head.index("임시 횡단") < head.index("b05-crossmodal__actions") - css = _read("B05_Profile", "B05_Profile_UI_Profile_CrossModal.css") - actions = css[css.index(".b05-crossmodal__actions {") :] - assert "margin-left: auto;" in actions[: actions.index("}")] - - -def test_제목_줄_끌기_화면_안() -> None: - modal = _read("B05_Profile", "B05_Profile_UI_Profile_CrossModal.ts") - assert 'bindHeadDrag(root, $(".b05-crossmodal__head"))' in modal - drag = modal[modal.index("function bindHeadDrag(") : modal.index("interface CrossModal {")] - assert '.closest("button")' in drag # 단추 누름은 단추 몫 - assert "innerWidth" in drag and "innerHeight" in drag # 화면 밖 안 나감 - assert "root.parentElement!.getBoundingClientRect()" in drag # 종단 칸 안 - assert "refit();" in modal # 다시 열 때 가둠 - - -def test_500_2_측점선_띠_안_임시_횡단_먼저() -> None: - modal = _read("B05_Profile", "B05_Profile_UI_Profile_CrossModal.ts") - # 단추 = other 먼저 · 추가 측점선 = plusM · 그 밖 측점선 = station(띠 안이면 임시 횡단) - flat = " ".join(modal.split()) - assert 'target.closest("button") ? "other" : marker ? "station" : null' in flat - logic = _read("B05_Profile", "logic", "B05_Profile_Profile_CrossPick.ts") - assert '"station"' in logic and 'input.hit === "other"' in logic - # 누름 = 잡는 단계에서 띠 안 측점선의 자기 고름을 막고 임시 횡단을 엶 - render = _read("B05_Profile", "B05_Profile_UI_Profile_Render.ts") - start = render.index("// 잡는 단계(capture)") - click = render[start : render.index("const derived = deriveProfile", start)] - assert "event.stopPropagation()" in click and "openCrossModal(" in click - assert " ".join(click.split()).endswith("}, true, );") diff --git a/resources/tester/test_505_3_cross_card_text.py b/resources/tester/test_505_3_cross_card_text.py deleted file mode 100644 index a734832fb..000000000 --- a/resources/tester/test_505_3_cross_card_text.py +++ /dev/null @@ -1,86 +0,0 @@ -"""505-3 B05 그림 · 글 — 임시 횡단 창 = B06 카드(형상 얹은 단면) · 빈칸 칩 그래프 오른쪽 위 · 곡선 판 가둠 · -지반선 · 계획선 누름 통과 · STALE 글 · 창 단추 공용 꼴. - -./venv/Scripts/python.exe -m pytest resources/tester/test_505_3_cross_card_text.py -q -""" - -from __future__ import annotations - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def _read(*parts: str) -> str: - return ROOT.joinpath(*parts).read_text(encoding="utf-8") - - -def test_측점_뜨기는_형상_얹은_단면도_돌려줌() -> None: - probe = _read("A00_Common", "design", "design_pipeline_probe.ts") - assert "Promise<{ preview: CrossPreviewResponse; section: CrossSection } | null>" in probe - assert "const section = stationSection(station, cross, {}, intervalM);" in probe - assert "attachStatus([section], status);" in probe - detail = _read("B05_Profile", "logic", "B05_Profile_Pipeline_Detail.ts") - assert "export function attachStatus(sections: CrossSection[], status: ShapeStatus)" in detail - - -def test_임시_횡단_창은_B06_카드() -> None: - modal = _read("B05_Profile", "B05_Profile_UI_Profile_CrossModal.ts") - assert "crossSourceOf(" in modal and "routeCrossCard(source," in modal - assert "now ? [now.preview] : await previewStructureSections(projectId(), [m])" in modal - # 서버 미리 뜬 것(분석 지형 다름)만 선 그림 - assert "buildCrossSvg(preview, width, height)" in modal - assert 'class="ui-button"' not in modal - assert modal.count('class="ui-btn ui-btn--ghost ui-btn--sm" data-act=') == 3 - css = _read("B05_Profile", "B05_Profile_UI_Profile_CrossModal.css") - assert ".b05-crossmodal__box > .b06-cross-card {" in css - - -def test_판_재료_한_벌() -> None: - card = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Cross_Card.ts") - assert ( - "return crossSourceOf(projectId, found, sections, detail.longitudinal.design_profiles, pane);" - in card - ) - assert card.count("export function crossSourceOf(") == 1 - - -def test_빈칸_칩은_머리_걷기_전에_꺼냄() -> None: - card = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Cross_Card.ts") - chips = card.index('":scope > header .b06-cross-card__warning"') - remove = card.index(':scope > header, :scope > footer, .b06-cross-card__zoom"') - assert chips < remove - assert 'box.className = "b05-cross-chips";' in card - css = _read("B05_Profile", "B05_Profile_UI_Style_RouteEdit.css") - rule = css[css.index(".b05-cross-chips {") :] - assert "top: 4px;" in rule[:200] and "right: 4px;" in rule[:200] - assert ".b06-cross-card:has(> .b05-cross-chips) {\n position: relative;" in css - - -def test_곡선_판_가둠_도구줄_횡단판_종단판() -> None: - bar = _read("B05_Profile", "B05_Profile_UI_RouteEdit_CurveBar.ts") - for selector in ( - ".b05-routeedit__tools", - ".b05-routeedit__side-wrap", - ".b05-route-profile", - ): - assert f'box("{selector}")' in bar - assert "top: Math.max(rect.top, tools?.bottom ?? rect.top) + 8," in bar - assert "right: Math.min(rect.right, side?.left ?? rect.right) - 8," in bar - assert "bottom: Math.min(rect.bottom, profile?.top ?? rect.bottom) - 8," in bar - - -def test_지반선_계획선_누름_통과() -> None: - css = _read("B06_Section", "B06_Section_UI_Style_Cross.css") + _read( - "B06_Section", "B06_Section_UI_Style_Cross_Chart.css" - ) - assert ".b06-chart__profile,\n.b06-chart__design-profile {\n pointer-events: none;" in css - - -def test_STALE_글_앞_단계() -> None: - locale = _read("ui_template", "ui_template_locale_common.ts") - assert ( - 'WF_State_Stale: ["다시 확인 필요 (앞 단계 변경)", "Stale (an earlier step changed)"],' - in locale - ) - assert "하위 단계" not in locale diff --git a/resources/tester/test_61_2_b08_structure_sheets.py b/resources/tester/test_61_2_b08_structure_sheets.py deleted file mode 100644 index d390057d8..000000000 --- a/resources/tester/test_61_2_b08_structure_sheets.py +++ /dev/null @@ -1,47 +0,0 @@ -"""61-2 · 95-3 B08 구조물도 모달 — 집계표 행에서 열림 · 읽기만 · 집계표와 같은 조합 목록 · 안내 글 키.""" - -import re -from pathlib import Path - -DRAWING = Path("B08_Quantity/B08_Quantity_UI_Drawing.ts").read_text(encoding="utf-8") -SUMMARY = Path("B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8") -PAGE = Path("B08_Quantity/B08_Quantity_UI_Page.ts").read_text(encoding="utf-8") -LOCALE = Path("ui_template/ui_template_locale_b4.ts").read_text(encoding="utf-8") -LOCALE_B2 = Path("ui_template/ui_template_locale_b2.ts").read_text(encoding="utf-8") - - -def test_sheet_tab_gone_modal_wired(): - # 구조물도 탭 없음 · 집계표 행 누름이 모달을 늦게 불러 엶 - assert '"sheet"' not in PAGE and "renderStructureSheets" not in PAGE - assert "B08_Quantity_Tab_StructureSheet" not in PAGE + LOCALE + LOCALE_B2 - assert "onRowClick" in SUMMARY and 'import("./B08_Quantity_UI_Drawing")' in SUMMARY - assert "combosAt(things, station)" in SUMMARY - - -def test_modal_own_class_and_cleanup(): - css = Path("B08_Quantity/B08_Quantity_UI_Drawing.css").read_text(encoding="utf-8") - assert "wide: true" in DRAWING and "min(1320px" not in css # 공용 큰 창(156-4) - assert "beforeClose" in DRAWING and "cards.destroy()" in DRAWING - assert 'dialogClass: "b08-sheets"' in DRAWING - - -def test_no_layer_write(): - # 층 쓰기 · 카드 안 저장 없음 — 고침은 [수정] → 사본 편집 화면만(163-3 · 사본 PUT) - for word in ("saveTemplate", "write:", "method:", "sheetEdit", "onImport", "onClear"): - assert word not in DRAWING, word - assert "editable: () => true" in DRAWING - - -def test_copy_only(): - # 143-1 — 연결된 사본만(그룹은 `{id}__{그룹}` 사본) · 없으면 「사본 없음」 · M02 층 폴백 없음 - assert ( - "request.linked[combo.copyId]" in DRAWING - and "sourceOf(request.projectId, copy.폴더)" in DRAWING - ) - assert "readTemplate" not in DRAWING and "FromMaster" not in DRAWING - assert "사본 없음" in LOCALE - - -def test_locale_keys_exist(): - for key in set(re.findall(r'L\("([A-Za-z0-9_]+)"\)', DRAWING)): - assert f"{key}:" in LOCALE or key.startswith(("B08_Quantity_Summary_", "Common_")), key diff --git a/resources/tester/test_b05_corridor_structures_order.py b/resources/tester/test_b05_corridor_structures_order.py deleted file mode 100644 index d0f3a1314..000000000 --- a/resources/tester/test_b05_corridor_structures_order.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -"""3D 코리도 구조물 — 선언 차례(PLAN 24-6a). - -`ringsAt` 를 `const` 로 `pushSwept` 뒤에 선언했더니, 그 사이 기슭막이 단 반복이 `pushSwept` 를 곧바로 불러 -ReferenceError(TDZ)가 나 기슭막이 있는 구간의 3D 코리도가 통째로 안 만들어졌다(2026-08-28 e371c275 ~). -쓰는 `const` 는 그것을 부르는 첫 호출보다 앞에 선언돼야 한다. -""" - -from pathlib import Path - -SOURCE = ( - Path(__file__).resolve().parents[2] / "B05_Profile" / "B05_Profile_UI_Corridor_Structures.ts" -) - - -def test_ringsAt_는_pushSwept_와_첫_호출보다_앞에_선언(): - source = SOURCE.read_text(encoding="utf-8") - rings = source.index("const ringsAt = ") - swept = source.index("const pushSwept = (") - first_call = ( - source.index("pushSwept(\n") if "pushSwept(\n" in source else source.index("pushSwept(") - ) - assert rings < swept, "pushSwept 가 ringsAt 를 쓴다 — 뒤에 선언하면 TDZ" - # 선언(`const pushSwept = (`) 뒤에 나오는 첫 호출 자리 - first_call = source.index("pushSwept(", swept + len("const pushSwept = (")) - assert rings < first_call - - -def test_스윕_쓰는_const_는_호출보다_앞(): - """pushSwept · pushPierced 가 쓰는 도우미 const 가 첫 호출(기슭막이 단 반복)보다 앞에 있다.""" - source = SOURCE.read_text(encoding="utf-8") - first_revet_loop = source.index("for (const tier of revetLayout.tiers)") - for name in ("const ringChainagesOf = ", "const ringsAt = ", "const pushSwept = ("): - assert source.index(name) < first_revet_loop, name diff --git a/resources/tester/test_b05_map_interval_spans.py b/resources/tester/test_b05_map_interval_spans.py deleted file mode 100644 index d8cc48e40..000000000 --- a/resources/tester/test_b05_map_interval_spans.py +++ /dev/null @@ -1,69 +0,0 @@ -"""평면(지도)에 **구간형 구조물 띠**가 서는지 — 계획서 3-6 마지막 항목. - -왜 (2026-09-07 사용자 지시) — 산마루측구·도수로·옹벽처럼 **구간**으로 놓이는 시설이 종단에는 -띠로 보이는데 **평면에는 아무 표시가 없었다**. 계획서에는 「노선 위에 임의 구간을 얹는 부품이 -없어 새로 짜야 함」으로 남아 있었다. - -새로 짠 것은 작다 — 계획선 표본(`strengthSamples`)이 **1m 간격이라 배열 인덱스가 곧 -누가거리**여서, 구간 → 화면 선은 그 토막을 잘라 굵게 긋기만 하면 된다. - -이 시험이 지키는 것 — ① 띠를 그리는 부품이 있고 ② 배수유역도가 그것을 계획선 **위**, -강도 색칠 **아래**에 그리며 ③ 띠 자료는 **종단과 같은 자료**(구조물 정본 + 타입 레지스트리)에서 -나오고 ④ 구간이 없는 점형 시설은 빠지는 것. -""" - -from pathlib import Path - -PROJECT_ROOT = Path(__file__).resolve().parents[2] -_B05 = PROJECT_ROOT / "B05_Profile" -_SPANS = _B05 / "B05_Profile_UI_Drainage_Spans.ts" -_RENDER = _B05 / "B05_Profile_UI_Drainage_Render.ts" -_PANEL = _B05 / "B05_Profile_UI_Drainage_Panel.ts" -# 종단 패널 본체는 종단도 한 벌로 옮겼다(계획서 24-2c) — 배수유역도는 오른쪽 판(side). -_PROFILE = _B05.parent / "A00_Common" / "design" / "design_profile.ts" - - -def test_띠를_그리는_부품이_있다(): - source = _SPANS.read_text(encoding="utf-8") - assert "export function drawRouteSpans" in source - # 인덱스 = 누가거리(m) 라는 전제가 코드에 남아 있어야 한다 — 표본 간격이 바뀌면 깨진다. - assert "Math.floor(Math.min(span.startM, span.endM))" in source - - -def test_범위_밖_구간도_안전하게_잘린다(): - """저장분에 노선보다 긴 구간이 남아 있어도 그리다 죽지 않아야 한다.""" - source = _SPANS.read_text(encoding="utf-8") - assert "Math.max(0, Math.min(last," in source - assert "Math.max(from + 1," in source, "시작=끝인 구간이 사라지지 않게 한 칸을 준다" - - -def test_계획선_위_강도색칠_아래에_그린다(): - """쌓는 순서가 뒤집히면 띠가 계획선을 덮거나 마커에 가린다.""" - render = _RENDER.read_text(encoding="utf-8") - assert "drawRouteSpans(context, scene.strengthSamples" in render - # 호출 자리끼리 견준다 — 파일 머리의 import 는 순서 판정에 쓰면 안 된다. - order_route = render.index("drawPreparedLayer(context, scene.routeLayer") - order_span = render.index("drawRouteSpans(context") - order_strength = render.index("drawStrengthLine(context") - assert order_route < order_span < order_strength - - -def test_종단과_같은_자료에서_뽑는다(): - """색은 레지스트리 표시색, 구간은 구조물 정본 — 새 정본을 만들지 않는다.""" - spans = _SPANS.read_text(encoding="utf-8") - assert "export function routeSpansFromStructures" in spans - assert 'item.placement !== "interval"' in spans, "구간형만 띠로 그린다" - assert "type.style?.color" in spans, "색을 새로 정하지 않고 레지스트리 값을 쓴다" - profile = _PROFILE.read_text(encoding="utf-8") - assert "drainagePanel?.setIntervalSpans(routeSpansFromStructures(" in profile - - -def test_목록이_바뀌면_다시_그린다(): - """구조물을 넣거나 빼면 띠도 따라와야 한다 — 안 그러면 옛 띠가 남는다.""" - profile = _PROFILE.read_text(encoding="utf-8") - body = profile[profile.index("setStructures(next: StructureInstance[])") :][:400] - assert "syncDrainageSpans()" in body - types_body = profile[profile.index("setStructureTypes(types: StructureType[])") :][:300] - assert "syncDrainageSpans()" in types_body - panel = _PANEL.read_text(encoding="utf-8") - assert "setIntervalSpans(spans) {" in panel and "scheduleDraw();" in panel diff --git a/resources/tester/test_b05_routeedit_chrome_modal.py b/resources/tester/test_b05_routeedit_chrome_modal.py deleted file mode 100644 index f6d2dff87..000000000 --- a/resources/tester/test_b05_routeedit_chrome_modal.py +++ /dev/null @@ -1,141 +0,0 @@ -"""계획노선 편집 겉틀 — 345-3 부터 모달이 아니라 종단 보기 가운데 위 본문(PLAN 127-1 의 공용 모달 조각은 걷음). - -· 덮개(`ui-modal__backdrop`) · 전체 화면 · 닫기 단추 없음 · 부모를 받아 붙음(`document.body` 아님) -· [저장 전으로] 없음(352-2) · ↶ ↷ 는 기호 단추 · 머리 끝 `tools` 자리([3D]) -· 오른쪽 횡단 미리보기 = 접는 칸 · 재계산 대기 막은 화면 전체(fixed) · R 라벨은 칸 안 절대 위치 -· 바깥 누름 · Esc 로 안 닫힘(모달 아님) -""" - -from __future__ import annotations - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -PAGE = ROOT / "B05_Profile" - - -def _block(css: str, selector: str) -> str: - found = re.search(rf"^{re.escape(selector)} \{{([^}}]*)\}}", css, re.M) - assert found, selector - return found.group(1) - - -def _read(name: str) -> str: - return (PAGE / name).read_text(encoding="utf-8") - - -def test_겉틀은_본문_칸(): - chrome = _read("B05_Profile_UI_RouteEdit_Chrome.ts") - assert "ui-modal" not in chrome - assert "document.body.append" not in chrome - assert "parent.append(overlay);" in chrome - assert 'overlay.className = "b05-routeedit";' in chrome - assert "b05-routeedit__close" not in chrome and 'data-slot="close"' not in chrome - assert '"tools"' in chrome and "replaceWith(...tools)" in chrome - # 346-3 — [노선 반영] 없앰(노선은 페이지 [저장]이 맨 앞에서 보냄) · 352-2 — [저장 전으로] 없앰 - assert 'data-act="apply"' not in chrome and 'data-act="cancel"' not in chrome - assert ">저장 전으로" not in chrome and "노선 반영" not in chrome - assert ">취소" not in chrome and ">확인" not in chrome - # 352-2 — 되돌리기 · 다시하기 = 글 없이 기호 + aria-label · title(단축키) - # 381-1 — 곧은 꼬리 + 갈고리 SVG(회전 반 바퀴 그림과 헷갈리지 않게) - assert 'aria-label="되돌리기" disabled>${HISTORY_ICON.undo}' in chrome - assert 'aria-label="다시하기" disabled>${HISTORY_ICON.redo}' in chrome - assert ">↶" not in chrome and ">↷" not in chrome - tools = _read("B05_Profile_UI_Profile_Tools.ts") - assert "undo.innerHTML = HISTORY_ICON.undo;" in tools - assert "redo.innerHTML = HISTORY_ICON.redo;" in tools - assert 'title="되돌리기 (Ctrl+Z)"' in chrome and 'title="다시하기 (Ctrl+Y)"' in chrome - # 시험 · 배선이 집는 자리는 남김 - assert "b05-routeedit__measure-close" in chrome - # 382-1 — 오른쪽 횡단 판 = 패널 양식(손잡이 · 머리 · 폭 끌기 · 열림 · 폭 기억) · 칸 높이를 채움 - assert "createSidePanels({" in chrome and 'prefKey: "b05-routeedit-cross-v2"' in chrome - assert "follow: false" in chrome and 'data-act="side-toggle"' not in chrome - - -def test_393_1_횡단_판_기본_접힘_측점_고름은_판을_열지_않음(): - # 513-2 — 측점 고름 = 내용만 갱신 · 열림 · 닫힘은 손잡이(누름)만 · 자동 열림(393-1) 걷음 - chrome = _read("B05_Profile_UI_RouteEdit_Chrome.ts") - assert "defaultOpen: null," in chrome and "innerWidth >= 1500" not in chrome - cross = _read("B05_Profile_UI_RouteEdit_Cross.ts") - assert "panels.open(" not in cross and "sidePanelsOf" not in cross - side = (ROOT / "ui_template" / "ui_template_side_panels.ts").read_text(encoding="utf-8") - assert "function open(id: string | null, remember = true): void {" in side - assert "JSON.stringify({ open: kept, width })" in side - assert "if (remember) {\n kept = next;\n save();" in side - - -def test_CSS_덮개_걷음_대기막은_전체(): - css = _read("B05_Profile_UI_Style_RouteEdit.css") - root = _block(css, ".b05-routeedit") - assert "position: relative;" in root and "flex: 1 1 auto;" in root - assert "z-index" not in root and "background" not in root - busy = _block(css, ".b05-routeedit__busy") - assert "position: fixed;" in busy and "z-index: var(--z-modal, 1000);" in busy - label = _block(css, ".b05-routeedit__label") - assert "position: absolute;" in label and "position: fixed" not in label - # 382-1 — 자체 [횡단] 단추 · 접기 걷음(접기는 양식 손잡이) - assert "b05-routeedit__side-toggle" not in css and "is-side-open" not in css - assert "@media (width < 1500px)" not in css # 접기는 단추로(처음만 폭으로) - - -def test_352_1_머리줄_상자_없음_도구는_뜬_단추_횡단은_우측_덮개(): - chrome = _read("B05_Profile_UI_RouteEdit_Chrome.ts") - for gone in ("b05-routeedit__box", "b05-routeedit__head", "b05-routeedit__actions", "

"): - assert gone not in chrome - # 도구줄 = 3D 와 같은 떠 있는 유리 단추 · 구분선도 3D 것 - assert 'class="b05-route__view-controls b05-routeedit__tools"' in chrome - assert "ui-btn--ghost" not in chrome.split(" chrome.index("b05-routeedit__busy") - css = _read("B05_Profile_UI_Style_RouteEdit.css") - for gone in (".b05-routeedit__box", ".b05-routeedit__head", ".b05-routeedit__actions"): - assert gone not in css - root = _block(css, ".b05-routeedit") - assert "padding" not in root and "gap" not in root - side = _block(css, ".b05-routeedit__side-wrap") - assert "position: absolute;" in side and "right: var(--spacing-12);" in side - z = int(re.search(r"z-index: (\d+);", side).group(1)) - assert z < 3 # 아래 종단 판(z 3)이 위 - style = _read("B05_Profile_UI_Style.css") - assert "z-index: 3;" in _block(style, ".b05-route-profile") - # 회전 단추 · 잰 값 창은 펴진 횡단 판을 비킴 - assert "right: var(--spacing-12);" in _block(css, ".b05-routeedit__spin") # 도구줄 줄 오른쪽 - assert "34px" in side # 횡단 판은 도구줄 줄 아래에서 시작 - assert "var(--b05-side-open)" in _block(css, ".b05-routeedit__measure") - assert '"--b05-side-open"' in chrome # 판 폭(손잡이 포함)을 적음 - glass = _block(css, ".b05-routeedit__terrain") - assert "backdrop-filter: blur(6px);" in glass and "background:" in glass - - -def test_페이지_수명_닫기_없음(): - main = _read("B05_Profile_UI_RouteEdit.ts") - assert "openModal" not in main and "Escape" not in main - assert "openRouteEditModal" not in main - assert "export async function mountRouteEditor(" in main - assert 'window.addEventListener("resize"' not in main # 칸 크기는 ResizeObserver - assert "new ResizeObserver(" in main - assert "host.onDispose(dispose);" in main - assert "historyControls.listen(host.visible());" in main - assert "history?.reset()" not in main # 352-2 — [저장 전으로] 지움 - hist = _read("B05_Profile_UI_RouteEdit_History.ts") - assert "reset" not in hist.split("export interface RouteEditHistory", 1)[1].split("}", 1)[0] - assert len(main.splitlines()) <= 700 - label = _read("B05_Profile_UI_RouteEdit_Label.ts") - assert "document.body.append" not in label and "container.append(root);" in label - assert '"곡선 삭제" : "곡선 추가"' in label # 352-5 — 버튼 이름은 명사형 - assert "container: overlay," in main - apply = _read("B05_Profile_UI_RouteEdit_Apply.ts") - assert "close" not in apply.split("export function bindRouteApply", 1)[1] - - -def test_393_5_접힌_손잡이는_지도_오른쪽_끝에_붙음(): - css = _read("B05_Profile_UI_Style_RouteEdit.css") - flush = _block(css, ".b05-routeedit__side-wrap:has(.ui-side-panels.is-collapsed)") - assert "right: 0;" in flush - # 펼친 판은 그대로 띄움 - assert "right: var(--spacing-12);" in _block(css, ".b05-routeedit__side-wrap") - side = (ROOT / "ui_template" / "ui_template_side_panels.ts").read_text(encoding="utf-8") - assert 'root.classList.toggle("is-collapsed", now === null);' in side diff --git a/resources/tester/test_design_page_single.py b/resources/tester/test_design_page_single.py deleted file mode 100644 index 5b4f0e082..000000000 --- a/resources/tester/test_design_page_single.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- coding: utf-8 -*- -"""한 페이지 뼈대(계획서 24-3a) · 겹친 알약(24-6b) 회귀검증. - -주소 둘(`#/b05-profile` · `#/b06-section`)이 같은 페이지(`A00_Common/design/design_page.ts`)를 -열고, 같은 페이지 안 이동은 다시 만들지 않고 보기만 바꾸는지, 떠날 때 3D(WebGL 문맥) · -종단도 · 횡단을 정리하는지, 숨긴 3D 는 그리기 루프를 멈추는지, 창에 건 키 듣기를 떼는지 본다. -화면 수치(문맥 수 · 루프 · 같은 노드 · 전환 시간)는 ORCA 로 잰다 — 여기서는 길이 끊기지 않게 잠근다. -""" - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent.parent - - -def _read(relative: str) -> str: - return (ROOT / relative).read_text(encoding="utf-8") - - -def test_주소_둘이_같은_페이지(): - router = _read("A00_Common/router.ts") - assert router.count('(await import("./design/design_page")).renderDesignPage') == 2 - assert "export function showWithinPage(" in router - # 다른 페이지로 떠나면 정리 · 늦게 끝난 옛 렌더는 버림 - assert "mounted?.dispose();" in router - assert "if (page && seq !== renderSeq) page.dispose();" in router - - -def test_같은_페이지면_셸도_다시_안_그림(): - main = _read("A00_Common/main.ts") - within = main.index("showWithinPage(currentRoute())") - assert within < main.index("renderShell(appRoot") - assert "if (within) return;" in main - - -def test_보기_약속_가운데만(): - """끝 모습(24-3b) — 보기는 가운데 위 칸만 채우고 좌측 패널 · 단추 · 종단도는 한 벌을 쓴다.""" - page = _read("A00_Common/design/design_page.ts") - assert "export interface DesignPageView" in page - assert "mount?: (root" not in page # 보기가 틀을 통째로 그리던 중간 길은 걷음 - assert "mountCenter: (" in page and "shared: DesignPageShared" in page - assert 'import("./design_view_profile")).mountProfileView' in page - assert 'import("./design_view_cross")).mountCrossView' in page - for hook in ("onShow:", "onHide:", "onDispose:"): - assert hook in page - # 보기는 처음 보일 때 한 번만 만든다 — 있으면 보이기만. - assert "if (slot) {\n slot.show();" in page - - -def test_페이지가_숨김_떠남을_건다(): - life = _read("B05_Profile/B05_Profile_UI_Page_Life.ts") - assert "deps.viewer.setActive(false)" in life and "deps.viewer.dispose()" in life - assert "bindProfileViewLife(host," in _read("A00_Common/design/design_view_profile.ts") - cross = _read("A00_Common/design/design_view_cross.ts") - assert "sectionView.dispose();" in cross[cross.index("host.onDispose(") :] - # 종단도는 한 벌 — 보기가 숨어도 그대로 두고, 페이지를 떠날 때 한 번 정리한다. - assert "shared.profile?.dispose();" in _read("A00_Common/design/design_page.ts") - - -def test_3D_멈춤과_문맥_놓기(): - viewer = _read("B05_Profile/B05_Profile_UI_Viewer.ts") - assert "setActive(active)" in viewer - assert "renderer.forceContextLoss();" in viewer - # 보관 지형은 다음 뷰어가 쓴다 — 떠날 때 버리지 않는다. - assert "if (terrain !== cachedTerrain.object) disposeObject(terrain);" in viewer - assert len(viewer.splitlines()) <= 700 - - -def test_창_키_듣기를_뗌(): - tools = _read("B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts") - assert 'window.removeEventListener("keydown", onKeyDown);' in tools - assert 'window.addEventListener("keydown", (event)' not in tools - profile = _read("A00_Common/design/design_profile.ts") - assert "setListening(false);" in profile # dispose - pick = _read("B05_Profile/B05_Profile_UI_Viewer_Structure_Pick.ts") - assert "setListening(on)" in pick - - -def test_겹친_알약은_누른_것을_그대로(): - profile = _read("A00_Common/design/design_profile.ts") - assert re.search(r"structureIdKeeping\(\s*stationId,\s*selectedStructureId", profile) - # 판정은 `_Profile_Structures` 로 옮김 — 비정규 측점 짝 · 규칙 측점(B06 카드) 기준점(53-2) - rule = _read("B05_Profile/B05_Profile_UI_Profile_Structures.ts") - assert "stationIdAtStructure(selectedId, stations, structures, types) === stationId" in rule - assert "Math.abs(structureAnchorM(selected) - at) < SAME_CHAINAGE_M" in rule - - -def test_진행_단계_두_칸이_보기를_따라감(): - """종단설계 · 횡단설계 두 칸은 같은 페이지 — 누르면 주소만 바꿔 보기만 바뀐다(계획서 24-3b).""" - page = _read("A00_Common/design/design_page.ts") - assert "const VIEW_STEP: Record = { profile: 2, cross: 3 };" in page - assert "layout?.setActiveStep(VIEW_STEP[view]);" in page - assert "layout?.setTitle(" in page - click = page[page.index("onStepClick: (stepIndex) => {") :][:260] - assert "if (designViewOf(route)) navigateTo(route);" in click - layout = _read("ui_template/ui_template_workflow_layout.ts") - assert "setActiveStep: (stepIndex) =>" in layout and "setTitle: (title) =>" in layout - - -def test_초기화_계획노선_편집은_다시_세움(): - """[초기화] · [노선 반영](옛 계획노선 편집 [확인]) = 새 초기값 — 옛 캐시를 다 버리고 페이지를 통째로 다시 읽는다(PLAN 77).""" - reset = _read("A00_Common/design/design_reset.ts") - assert ( - 'namesInBucket("draft")' in reset and "clearResults(projectId);" in reset - ) # B08 · B09 초안 남김(418) - assert "location.reload()" in reset - view = _read("A00_Common/design/design_view_profile.ts") - assert "location.reload()" not in view # [노선 반영]은 새로고침 대신 다시 세움(345-3) - rebuild = view[view.index("const rebuildAfterReplan") :] - rebuild = rebuild[: rebuild.index("};")] - for step in ( - "clearRouteLatestCache(activeProjectId);", - "invalidateSectionDetail(activeProjectId);", - "remountRoute(currentRoute());", - ): - assert step in rebuild - assert rebuild.index("invalidateSectionDetail") < rebuild.index("remountRoute(") - assert "mountRouteEditor(mainContent, host, activeProjectId, rebuildAfterReplan," in view - # 346-3 — [저장]이 노선을 보낸 뒤 결과 캐시를 비우고 편집기가 건 다시 세우기(rebuildAfterReplan) - apply = _read("B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts") - assert "clearResults(projectId);" in apply and "rebuild: params.onApplied," in apply - - -def test_종단_보기_확정은_횡단_보기를_숨긴_채_세움(): - page = _read("A00_Common/design/design_page.ts") - assert "ensureView: (view) => (slots.get(view) ?? mountView(view, false)).ready," in page - cross = _read("A00_Common/design/design_view_cross.ts") - # 숨긴 채 섰으면 한 벌 폼은 종단 보기 몫 — 보일 때 싣는다. - assert "if (host.visible()) void structuresPanel.load()" in cross diff --git a/resources/tester/test_design_panel_dock_fixed.py b/resources/tester/test_design_panel_dock_fixed.py deleted file mode 100644 index d507560c9..000000000 --- a/resources/tester/test_design_panel_dock_fixed.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -"""B05 · B06 좌측 패널 아래 고정(계획서 44-3) — 단추 묶음이 패널 바닥에 붙고 위 칸만 스크롤. - -공용 쪼개기(`splitSidebarActions`)가 [스크롤 칸][아래 고정 단추] 로 나누는데, 한 페이지(design_page)는 -좌측 자리를 빈 채 틀에 넘겨 쪼개기가 지나가고, 보기 전환이 패널 클래스를 통째 덮어 `ui-sidebar-fill` 을 -지웠다. 세 자리가 다시 어긋나면 빨강. 화면 수치는 `ui_checks/44-3_좌측패널_하단고정.js`. -""" - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -OVERLAY = ROOT / "ui_template" / "ui_template_overlay.ts" -PANEL = ROOT / "A00_Common" / "design" / "design_panel.ts" -SHARED = ROOT / "A00_Common" / "design" / "design_shared.ts" - - -def read(path: Path) -> str: - return path.read_text(encoding="utf-8") - - -def test_split_is_exported_and_idempotent(): - src = read(OVERLAY) - assert "export function splitSidebarActions(" in src - assert 'if (body.classList.contains("ui-sidebar-split")) return;' in src - - -def test_shared_panel_splits_after_attach(): - src = read(SHARED) - attach = src.index("shared.left.append(panel.root);") - assert "splitSidebarActions(body)" in src[attach : attach + 400] - - -def test_panel_keeps_fill_class_and_scroll_host(): - src = read(PANEL) - # 보기 전환이 클래스를 통째 덮으면 ui-sidebar-fill 이 지워져 단추 묶음이 바닥에서 뜬다. - assert "root.className = next" not in src - # 늦게 서는 칸은 스크롤 칸 안으로 — 패널 루트에 바로 끼우면 스크롤 밖에 남는다. - assert ":scope > .ui-sidebar-scroll" in src diff --git a/resources/tester/test_m01_side_fuel.py b/resources/tester/test_m01_side_fuel.py deleted file mode 100644 index 15a8e8b7f..000000000 --- a/resources/tester/test_m01_side_fuel.py +++ /dev/null @@ -1,44 +0,0 @@ -"""38-2 왼쪽 패널 「유가 · 전력」 컨테이너 — 재료 다음 따로 · 재료 컨테이너에서는 뺌 · 서버 재료 그룹 그대로.""" - -import json -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -SIDE = (ROOT / "M01_MasterData/M01_MasterData_UI_Side.ts").read_text(encoding="utf-8") - - -def _array(name: str) -> list[str]: - body = re.search(rf"const {name}(?:: \w+\[\])? = \[(.*?)\]", SIDE, re.S).group(1) - return [x.strip().strip('"') for x in body.split(",") if x.strip()] - - -def test_fuel_container_right_after_material(): - groups = _array("GROUPS") - assert groups[groups.index("재료") + 1] == "FUEL_GROUP" - assert 'const FUEL_GROUP = "유가 · 전력";' in SIDE - - -def test_material_container_has_no_fuel(): - assert _array("MATERIAL_ORDER") == ["자재품목", "품셈재료"] - assert "list.filter((f) => !isFuel(f))" in SIDE - - -def test_fuel_container_reads_server_material_group(): - # 서버로는 재료 그룹을 받아 유가전력 파일만 · 첫 받기에서 따로 부르지 않음 - assert 'if (group === FUEL_GROUP) return (await refresh("재료")).filter(isFuel);' in SIDE - assert "GROUPS.filter((g) => g !== FUEL_GROUP).map(refresh)" in SIDE - # 거름 저장 열쇠 그대로 — 전에 고른 구분이 이어짐 - assert "store: `m01.filter.재료.${FUEL}`" in SIDE - - -def test_fuel_file_still_in_material_group(): - data = json.loads( - (ROOT / "resources/master_data/재료_유가전력.json").read_text(encoding="utf-8") - ) - assert data - # 흐름도 넣을 값 판 · 단가산출 로직 요소 고르기는 서버 재료 그룹을 그대로 씀 - panel = (ROOT / "M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Build.ts").read_text( - encoding="utf-8" - ) - assert '유가전력: { group: "재료", part: "유가전력" }' in panel diff --git a/resources/tester/test_m02_area_panel.py b/resources/tester/test_m02_area_panel.py deleted file mode 100644 index cccd2e3f7..000000000 --- a/resources/tester/test_m02_area_panel.py +++ /dev/null @@ -1,112 +0,0 @@ -"""면적산출 상세 오른쪽 일위대가 패널 · 칸 지정 모달(PLAN 39-4 D2 · D5 · D6) — 화면 조작은 ORCA. - -· 패널 = 그 열 일위대가 보기 갈래(`Labor_View`) · 조합 목록 · 찾기 · [이 열에 쓰기] · [조합 고치기] 없음 -· 줄 누름 = 칸 지정 모달 → 스프레드시트 `setLink`(면적 문서 `연결` 한 곳 · 알약과 같은 길) -· 시트 칸 값 · 연결이 바뀌면 `setQty` 로 바로 곱함(서버 안 부름) -""" - -from __future__ import annotations - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -M02 = ROOT / "M02_MasterTemplete" - - -def _text(name: str) -> str: - return (M02 / name).read_text(encoding="utf-8") - - -def test_면적_길은_일위대가_패널(): - main = _text("M02_MasterTemplete_UI_Main.ts") - body = main.split("async function mountSheet")[1].split("async function remount")[0] - area = body.split('if (kind === "area") {')[1].split("\n }\n")[0] - assert "apanel = buildAreaPanel({" in area and "host.append(apanel.root);" in area - assert "picked: () => picked," in area and "pills: () => links.항목," in area - assert ( - "apanel?.refresh();" in body - and "onSelect: (s) => ((picked = s.칸), toDrawing.select(s))," in body - ) - assert "buildComboBar" not in main # 조합 줄 · [조합 고치기] 지움 - assert body.index('if (at.layer === "project") return;') < body.index("buildAreaPanel") - assert "apanel?.destroy();\n apanel = null;" in main # drop - - -def test_패널_몸은_보기_갈래_줄_누름은_칸_지정(): - panel = _text("M02_MasterTemplete_AreaPanel.ts") - assert "createLaborView(openLink)" in panel - assert "void view.show(opts.layer, opts.column, quantities(opts.sheet()));" in panel - assert "view.setQty(quantities(opts.sheet()), cellsOf(opts.sheet()))" in panel - assert panel.index("void view.show(") < panel.index("refresh(); // 칸 주소") - assert "openAreaLink({" in panel and "if (!row.pill) return showToast(TEXT.noPill" in panel - for gone in ("fetchUsableCombos", "setColumnCombo", "branchCombo"): - assert gone not in panel - - -def test_칸_지정_모달은_setLink_만(): - link = _text("M02_MasterTemplete_AreaLink.ts") - assert 'opts.sheet.setLink(opts.key, a1, opts.info, choice === "바꾸기")' in link - assert ( - "linksAt(book, sheetId, a1, opts.key)" in link and "askShare(" in link - ) # 칸 하나 알약 여럿 - assert "done(here)" in link and "done(address.input.value)" in link and "done(null)" in link - assert "address.setError(TEXT.bad)" in link # 틀린 · 빈 주소는 모달 그대로 - assert "pick.disabled = link.disabled = address.input.disabled = opts.readOnly;" in link - assert "saveTemplate" not in link # 저장은 [저장] - - -def test_빈_칸은_수량_없음_서버와_같음(): - """39-8 ② — 빈 칸에 이은 알약 = 수량 없음(반영 「—」 · 「수량 빈 칸(A1)」) · 0 칸 = 0 · 값 칸 = 값.""" - from M02_MasterTemplete import M02_Labor_Doc as labor - - doc = { - "시트": [ - { - "id": "s1", - "칸": {"A2": {"값": 0}, "A3": {"값": "1,250"}, "A4": {"값": "글"}}, - "연결": [ - {"키": k, "칸": c} - for k, c in (("빈", "A1"), ("영", "A2"), ("값", "A3"), ("글", "A4")) - ], - } - ] - } - assert {k: float(v) for k, v in labor.area_values(doc).items()} == {"영": 0, "값": 1250} - panel = _text("M02_MasterTemplete_AreaPanel.ts") - body = panel.split("function quantities")[1].split("\n}\n")[0] - assert 'toNumber(cell?.식 !== undefined ? (v && "수" in v ? v.수 : null) : cell?.값)' in body - assert 'String(raw).replace(/,/g, "").trim()' in panel and 'typeof raw === "boolean"' in panel - sheet = (ROOT / "A00_Common" / "spreadsheet" / "spreadsheet.ts").read_text(encoding="utf-8") - assert 'const empty = !cell || (cell.식 === undefined && (cell.값 ?? "") === "");' in sheet - rows = _text("M02_MasterTemplete_Labor_Rows.ts") - assert '${tc("Labor_Qty")} ${tc("In_QtyEmpty")}(${at})' in rows - assert "if (!value) return `${link.칸} = ${TEXT.empty}`;" in _text( - "M02_MasterTemplete_AreaLink.ts" - ) - - -def test_카드_알약_취향은_계정_받은_뒤_올림(): - """39-8 ③ — 화면이 뜰 때 브라우저 옛 값이 계정 값을 덮지 않게 · 카드는 열 때마다 취향을 다시 읽음.""" - state = (ROOT / "A00_Common" / "b_page_state.ts").read_text(encoding="utf-8") - push = state.split("function pushUiPrefs")[1].split("export function syncUiPrefs")[0] - assert "void (prefsPull ?? Promise.resolve())" in push and "prefs: localPrefs()" in push - assert "prefsPull = pullUiPrefs();" in state - cards = _text("M02_MasterTemplete_ItemCards.ts") - load = cards.split("async function load(")[1] - assert 'paintPills(readStateRaw("m02-card-pills") === "true");' in load - - -def test_엑셀_칸_고르기_모달_부품(): - """40-3 ① · 43-1 — 읽기만 시트(수량산출 모달 안에 바로 · `mountAreaSheet`) · 칸 누름 = 주소 · - 풀기 · 손 · 확인 · 닫기는 일위대가 수량산출 모달(`Labor_QtyModal`) · 저장 안 함.""" - pick = _text("M02_MasterTemplete_AreaCellPick.ts") - assert 'export { linkAreaCell } from "../A00_Common/spreadsheet/spreadsheet_link_set";' in pick - assert "export function mountAreaSheet(" in pick and "readOnly: true," in pick - assert 'opts.onPick(choice === "바꾸기" ? { 칸: at, 바꾸기: true } : { 칸: at });' in pick - qty = _text("M02_MasterTemplete_Labor_QtyModal.ts") - assert "sheet = mountAreaSheet(host, {" in qty and 'pending = { kind: "unlink" };' in qty - assert "엑셀에서 고르기" not in qty # 두 단계 모달 없앰(43-1) - assert "linksAt(book, sheetId, at, opts.pill)" in pick and "askShare(" in pick - assert "if ((e as MouseEvent).button === 0 && moved) void choose();" in pick - assert "handle?.destroy();" in pick - assert "saveTemplate" not in pick diff --git a/resources/tester/test_m02_card_sheet_edit.py b/resources/tester/test_m02_card_sheet_edit.py deleted file mode 100644 index 6a4abdaac..000000000 --- a/resources/tester/test_m02_card_sheet_edit.py +++ /dev/null @@ -1,109 +0,0 @@ -"""면적산출 · 재료산출 카드 미리보기 수정(PLAN 31-3) — 화면 조작은 ORCA. - -· 스프레드시트 `preview` = 도구 모음 · 오른쪽 클릭 메뉴 없음 · 수식 입력 줄 · 칸 고침 - (다른 화면 그대로) -· 카드 = `sheetEdit` 가 있을 때만 고침(M02 구조물 조합) · 고르기 모달은 보기만 그대로 -· 알약 = 전체 편집기와 같은 부품 · 카드 [저장] = 같은 저장 길 · 저장 안 한 고침 물음 -""" - -from __future__ import annotations - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -M02 = ROOT / "M02_MasterTemplete" - - -def _text(path: Path) -> str: - return path.read_text(encoding="utf-8") - - -def test_스프레드시트_미리보기_수정_옵션(): - src = _text(ROOT / "A00_Common" / "spreadsheet" / "spreadsheet.ts") - assert "preview?: boolean;" in src - assert "const bare = readOnly || !!opts.preview;" in src - assert "bare ? null : mountToolbar(ctx)" in src - # 미리보기 우클릭 = 「복사」 한 줄 — 메뉴 쪽이 ss--preview 로 가름 - assert "attachMenu(ctx)" in src and '" ss--preview"' in src - menu = _text(ROOT / "A00_Common" / "spreadsheet" / "spreadsheet_menu.ts") - assert 'classList.contains("ss--preview")' in menu and "copyOnlyItems(ctx)" in menu - # 편집 · 수식 입력 줄은 readOnly 만 막음 — preview 는 고칠 수 있음 - assert "const readOnly = !!opts.readOnly;" in src - assert re.search(r"for \(const node of \[toolbar\?\.root, ed\.bar, grid\.root", src) - - -def test_카드는_sheetEdit_있을_때만_고침(): - cards = _text(M02 / "M02_MasterTemplete_ItemCards.ts") - assert "sheetEdit?: SheetEditOptions;" in cards - assert ( - "if (opts.sheetEdit?.can()) return await paintEditable(c, mine, createSpreadsheet);" - in cards - ) - body = cards.split("const paintEditable")[1].split("const paintSheet")[0] - assert "preview: true," in body and "links," in body - assert "c.host.append(extra.below)" in body # 재료산출 = 자재 조합 판 - # 보기만 길 그대로(고르기 모달) - assert "createSpreadsheet(c.host, c.doc, { readOnly: true, tabs: false })" in cards - detail = _text(ROOT / "ui_template" / "ui_template_item_picker_detail.ts") - assert "sheetEdit" not in detail - - -def test_카드_저장은_읽은_문서에_시트를_덮음(): - cards = _text(M02 / "M02_MasterTemplete_ItemCards.ts") - save = cards.split("async function saveSheet")[1].split("\n }\n")[0] - assert "...(c.doc as unknown as Record), ...c.handle.getDoc()" in save - assert "c.version = await opts.sheetEdit.save(c.kind, at, next, c.version);" in save - assert 'c.version = String((got.value as { 판?: string | null }).판 ?? "");' in cards - struct = _text(M02 / "M02_MasterTemplete_Structure.ts") - assert "saveTemplate(getActiveLayer(), kind, at, null, version, doc)" in struct - - -def test_알약은_전체_편집기와_같은_부품(): - struct = _text(M02 / "M02_MasterTemplete_Structure.ts") - links = struct.split("async function cardLinks")[1].split("export interface")[0] - assert "pillsFor(kind, ctl.column)" in links - assert "buildMaterialPanel" not in struct # 자재 조합 판은 없앰 - assert "can: () => opts.isAdmin" in struct - - -def test_저장_안_한_카드_고침_물음(): - struct = _text(M02 / "M02_MasterTemplete_Structure.ts") - show = struct.split("const show = ")[1].split("const pickPrice")[0] - assert "cards.dirty()" in show and "leaveOrStay(TEXT.discardCards," in show - assert "opts.keepColumn?.(from)" in show - main = _text(M02 / "M02_MasterTemplete_UI_Main.ts") - assert "const busy = (): boolean => dirty || !!panel?.dirty() || toDrawing.waiting();" in main - assert main.count("busy()") >= 2 # 떠나기 · 새로고침 막기 - assert ( - 'askUnsaved(L("M02_TreeUnsaved"))' in main - ) # 나무 고침(settle)도 같은 물음 · 카드 저장 실패까지 봄 - assert main.index("await panel.saveCards();") < main.index("await panel.reload();") - assert "keepColumn: (colId) => sheet?.selectColumn(colId)" in main - - -def test_면적산출_카드_알약_줄_켬끔_기본_숨김(): - cards = _text(M02 / "M02_MasterTemplete_ItemCards.ts") - assert 'paintPills(readStateRaw("m02-card-pills") === "true");' in cards # 없으면 숨김 - assert 'writeStateRaw("m02-card-pills", String(shown));' in cards - assert 'sheets[0].host.classList.toggle("is-pills-off", !shown);' in cards - assert "pills.hidden = !sheetEdit;" in cards # 고칠 수 있는 카드만(보기만은 알약 줄 없음) - css = _text(M02 / "M02_MasterTemplete_Structure.css") - assert re.search(r"\.m02-struct__sheet\.is-pills-off \.ss-links \{\s*display: none;", css) - state = _text(ROOT / "A00_Common" / "b_page_state.ts") - assert '"m02-card-pills": { bucket: "pref", scope: "global" },' in state - # 상세(전체 편집기)는 켬끔 없음 — 늘 보임 - assert "is-pills-off" not in _text(M02 / "M02_MasterTemplete_UI_Main.ts") - - -def test_면적산출_알약은_일위대가_pills_길(): - """39-4 D7 — 로직 물량 「이름|단위」 + 자재 「자재|MT키」 · 다 고정 · 선택 입력 알약 빠짐.""" - struct = _text(M02 / "M02_MasterTemplete_Structure.ts") - pills = struct.split("export async function pillsFor")[1].split("async function cardLinks")[0] - area = pills # 재료산출 갈래는 없앰 — 몸통이 면적산출 알약 - assert 'kind === "material"' not in pills - assert "await fetchLaborPills(column)" in area - assert "고정: true" in area and "...(p.자재 ? { 자재: p.자재 } : {})" in area - assert "inputPills" not in area and "fetchComboInputs" not in struct - api = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts") - assert "/layers/${activeLayer}/labor/${enc(column)}/pills" in api diff --git a/resources/tester/test_m02_card_shell.py b/resources/tester/test_m02_card_shell.py deleted file mode 100644 index 49976ec1e..000000000 --- a/resources/tester/test_m02_card_shell.py +++ /dev/null @@ -1,192 +0,0 @@ -"""M02 항목 카드 공용 틀(PLAN 31-7 · `M02_MasterTemplete_CardShell.ts`) — 네 카드가 틀 하나를 씀. - -정함: 제목 줄(제목 · 점 ● · 오른쪽 단추) + 한 줄 부제 · 「〇〇 수정」 = 빈 단추 한 모양 · -[저장] = 늘 보임 · 고쳐야 켜짐 · 카드 안 Ctrl+S · 알림 글 한 벌 · ? = 도면 · 엑셀 · -불러오는 중 = 네 카드 · 실패 = 카드 안 글 + [다시 불러오기] · 물음 한 번. 화면 조작 판정은 ORCA. -""" - -from __future__ import annotations - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -M02 = ROOT / "M02_MasterTemplete" - - -def _text(name: str) -> str: - return (M02 / name).read_text(encoding="utf-8") - - -def test_틀_제목_줄_부제_상태(): - shell = _text("M02_MasterTemplete_CardShell.ts") - assert "children: [title, dot, tools]," in shell - assert 'const title = el("span", { text: opt.title });' in shell - assert 'tools.addEventListener("click", (event) => event.stopPropagation());' in shell - assert 'el("div", { className: "m02-struct__sub", children: opt.sub })' in shell - assert "loading: () => showLoading(host)," in shell - assert "fail: (reason, retry) => host.replaceChildren(loadingError(reason, retry))," in shell - assert "setDirty: (dirty) => (dot.hidden = !dirty)," in shell - assert 'isCtrlKey(event, "s")' in shell # 카드 안 Ctrl+S - - -def test_저장_단추_알림_한_벌(): - shell = _text("M02_MasterTemplete_CardShell.ts") - assert 'saved: "저장했음"' in shell and 'failed: "저장 못 함 — {value}"' in shell - assert 'stale: "그 사이 바뀜 — 다시 읽음"' in shell - assert "button.disabled = !dirty || saving;" in shell - assert "error instanceof StaleError ? CARD_TEXT.stale" in shell - assert "shell.onSave(() => void (dirty && run()));" in shell - - -def test_네_카드가_틀을_씀_수정_단추_한_모양(): - cards = _text("M02_MasterTemplete_ItemCards.ts") - assert "const shell = cardShell({ title, body, sub: [no, name] });" in cards - assert ( - "shell.tools.append(saver.button, take, edit, del);" in cards - ) # 40-2 · ? 는 제목 옆(47-1) - assert 'variant: "filled"' not in cards # [수정] 은 모두 빈 단추(ghost) - assert "c.saver.button.hidden = !can;" in cards # [저장] 늘 보임 · ? 는 늘 - assert "helpButton(shell, title, CARD_HELP[kind])" in cards - inputs = _text("M02_Labor_Values.ts") - assert inputs.count("cardShell({") == 1 # 넣을 값 카드(선택 입력 카드는 없앰 · 39-7b) - assert "shell.fail(" in inputs and "shell.loading()" in inputs - assert "m02-struct__card ui-collapsible" not in inputs # 손으로 만든 겉 틀 없음 - drawing = _text("M02_MasterTemplete_DrawingCard.ts") - assert "createButton" not in drawing and "showToast" not in drawing # 단추 · 알림은 틀 - - -def test_실패는_카드_안_글과_다시_불러오기(): - cards = _text("M02_MasterTemplete_ItemCards.ts") - assert "showToast" not in cards # 토스트 안 씀(저장 알림은 틀) - assert "c.shell.fail(" in cards and "reload" in cards - picker = (ROOT / "ui_template" / "ui_template_item_picker_detail.ts").read_text("utf-8") - # 모달 일위대가 카드도 M02 카드 틀 안 실패 글(142-1 · 옛 요약 · 값 카드 retry 없음) - assert "paintLabor" not in picker and 'labor: () => void edit("inputs"),' in picker - - -def test_일위대가_카드는_상세_카드_갈래(): - """43-4 — 일위대가 카드 몸 = Labor_Detail 카드 갈래(단가산출 로직 2행 표 + 자재목록 · 순서 · - 로직 추가 · 다이어그램 없음 · 빼기 그대로) · 카드 [저장] = 면적 먼저 → 일위대가 · - 한 줄 표 지움.""" - cards = _text("M02_MasterTemplete_ItemCards.ts") - assert "mod.mountLaborDetail(laborHost, got.문서, {" in cards and "card: true," in cards - assert 'makeCard("labor", PART_WORD.labor, laborHost, () => saveLabor())' in cards - save = cards[cards.index("async function saveLabor()") :] - assert save.index("laborEd.saveArea()") < save.index('saveTemplate(layer, "labor"') - # 고칠 수 있음 = editable + 쓰는 길(142-1 · 고르기 모달은 보기만) - assert "onChanged: (dirty) => laborCard.saver.mark(dirty && laborWritable())," in cards - assert "Labor_View" not in cards # 한 줄 표(40-2) 안 씀 - detail = _text("M02_MasterTemplete_Labor_Detail.ts") - assert "move: reorder," in detail # 85-3 — 카드도 순서(▲▼ · 끌기) · 빼기는 그대로 - # 서고 모드(50-3 · lib)도 더하기 · 다이어그램 없음 - # 51-11 사전조합 = 일위대가 상세와 같은 [로직 추가] · 같은 목록(걸러냄 없음) - assert "opt.card ? null : addLogic" in detail - assert "only" not in _text("M02_MasterTemplete_Labor_Pick.ts") - assert "...(opt.card || lib ? [] : [dgBox])," in detail - assert "opt.card ? null : addMat" in detail # 49-6 — 카드엔 [자재 추가] 없음 · 상세는 그대로 - rows = _text("M02_MasterTemplete_Labor_Rows.ts") - assert ( - "const tools = !!(move || remove || option || ref);" in rows and "compactTable" not in rows - ) - assert "compact" not in _text("M02_MasterTemplete_Labor_View.ts").split("*/", 1)[1] - - -def test_일위대가_상세_단위는_표준_단위_고르기(): - """44-7 — 상세 머리 「단위」 = 표준 단위 목록(UnitPick) · 지금 값이 목록에 없어도 선택지 · - 고르면 캐시만(저장은 상세 [저장]) · 「-」 = 없음.""" - detail = _text("M02_MasterTemplete_Labor_Detail.ts") - assert 'import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick";' in detail - assert "extra: [unit0, cur.단위].filter((u): u is string => !!u)," in detail - assert "cur.단위 = u === NO_UNIT ? null : u;" in detail - assert "readOnly: !edit," in detail and "maxLength = 20" not in detail - - -def test_도움말_제목_옆_네_절(): - """47-1 — ? = 카드 제목 바로 옆 · 도움말 = 설명 · 단축키 · 기능 · 사용법 · 카드 셋 모두.""" - shell = _text("M02_MasterTemplete_CardShell.ts") - assert "shell.title.after(button);" in shell - for part in ("about", "keys", "features", "steps"): - assert f'"{part}"' in shell - assert "event.stopPropagation(); // 누름 ≠ 접기" in shell - help_ = _text("M02_MasterTemplete_CardHelp.ts") - for kind in ("structure:", "labor:", "area:"): - assert kind in help_ - assert "keys: DRAWING_KEYS," in help_ and "keys: SHEET_KEYS," in help_ - for label in ("가져오기", "수정", "삭제", "저장", "알약"): - assert f'"{label}"' in help_ - - -def test_미리보기_파일_불러오기_없음(): - """47-2 · 495-2 — M02 [파일 업로드] 없음(카드 · 상세 모두) · DWG = CAD [삽입 → DWG 불러오기] 한 길.""" - drawing = _text("M02_MasterTemplete_Drawing.ts") - for gone in ("noImport", "canImport", "importDrawingFile", "/m02/drawing-import"): - assert gone not in drawing - for name in ("M02_MasterTemplete_DrawingCard.ts", "M02_MasterTemplete_AreaDrawing.ts"): - assert "noImport" not in _text(name) - - -def test_일위대가_줄_고름과_옵션_단추(): - """47-5 — 줄 누름 = 고름(하이라이트 · 키로 둬서 다시 그려도 유지) · 넣을 값 모달 = [옵션] · - 단추 위로 · 아래로 · 옵션 · 삭제(빨강) · 묶음 간격 = 공용 `ui-btn-group`.""" - detail = _text("M02_MasterTemplete_Labor_Detail.ts") - assert "open: pick," in detail and "option: openRow," in detail - assert ( - "{ open: pick, option: openRow, ref: openRef }" in detail - ) # 읽기만도 고름 · [옵션] = 보기 모달 - assert "selected: picked" in detail - rows = _text("M02_MasterTemplete_Labor_Rows.ts") - # 85-3 — ▲▼ 작은 단추(글은 title · aria) + 손잡이 끌기 - up, down = rows.index('arrow("▲", tc("Up")'), rows.index('arrow("▼", tc("Down")') - assert rows.index("grip(),") < up and 'b.setAttribute("aria-label", label)' in rows - assert "move: reorder," in detail and "opt.card ? {} : { move }" not in detail # 카드도 순서 - opt, rm = ( - rows.index('small(tc("Labor_Option")'), - rows.index('small(tc("Remove"), () => remove(i), "danger")'), - ) - assert up < down < opt < rm and 'className: "ui-btn-group"' in rows - text = _text("M02_MasterTemplete_Labor_Text.ts") - for t in ('Up: ["위로"', 'Down: ["아래로"', 'Remove: ["삭제"', 'Labor_Option: ["옵션"'): - assert t in text - css = (ROOT / "ui_template" / "ui_template_general_layout.css").read_text(encoding="utf-8") - assert ".ui-btn-group {" in css and "gap: var(--btn-group-gap);" in css - - -def test_순서칸_2행_참조줄_옵션_수정(): - """51-14 — 순서 칸 2행(위 [위로] [아래로] / 아래 [옵션]·[수정] [삭제]) · 일위조합 [수정] = - 서고 원본 모달(저장 = 서고 저장 길 · 자기 층만 고침) · 닫으면 다시 셈 · 옛 단가승격 참조 줄 = 단추 없음(134-2).""" - rows = _text("M02_MasterTemplete_Labor_Rows.ts") - assert 'className: "m02-labrow__tools"' in rows - assert 'ref && !row.참조.키.startsWith("SP-")' in rows - assert '[small(tc("Labor_Edit"), () => ref(i))]' in rows - detail = _text("M02_MasterTemplete_Labor_Detail.ts") - assert "readOnly: !edit || ref.층 !== opt.layer," in detail - assert "closed: () => alive && recalc()," in detail - ref = _text("M02_MasterTemplete_Labor_RefEdit.ts") - assert "saveTemplate(layer, kind, opt.ref.키, null, 판, body, opt.ref.주인)" in ref - assert 'mode: "precombo",' in ref and "showConfirmDialog" in ref - assert 'Labor_Edit: ["수정"' in _text("M02_MasterTemplete_Labor_Text.ts") - - -def test_옵션_줄_구분_이름_별칭_병합(): - """113-1 — 옵션 글 = 구분 · 이름 · 별칭 병합 칸(colspan 3) · 나머지 칸은 위 줄에서 rowspan 2 · - 옵션 없는 로직은 한 줄 · 아래선 · hover · 고름 띠 · 끌기 아래선 맞춤.""" - rows = _text("M02_MasterTemplete_Labor_Rows.ts") - assert "const SPAN = 3;" in rows - assert "if (!note) return [tr, ...subs];" in rows - assert '".m02-labrow__kind, .m02-labrow__name, .m02-labrow__alias"' in rows - assert 'td.setAttribute("rowspan", "2");' in rows - css = _text("M02_MasterTemplete_Labor.css") - assert ".m02-labrow__row:has(+ .m02-labrow__optrow) td:not([rowspan])" in css - assert ".m02-labrow__row.is-open:has(+ .m02-labrow__optrow:hover)" in css - assert "tr:has(+ tr.m02-labrow__optrow.is-dragunder) > td[rowspan]" in css - assert ".m02-labrow__optrow.is-selected > td:first-child" not in css - - -def test_도면_상세_머리_파일_업로드_단추(): - """362-2 → 495-2 — 도면 상세 머리 [파일 업로드] 지움 · 도움말 = CAD [삽입] DWG 불러오기.""" - drawing = _text("M02_MasterTemplete_Drawing.ts") - assert "파일 업로드" not in drawing and "importButton" not in drawing - assert "📂" not in drawing and "파일 불러오기 (DXF" not in drawing - help_ = _text("M02_MasterTemplete_CardHelp.ts") - assert "파일 업로드" not in help_ and '["DWG 불러오기",' in help_ - assert "📂" not in _text("M02_MasterTemplete_CardHelp.ts") diff --git a/resources/tester/test_m02_drawing_card.py b/resources/tester/test_m02_drawing_card.py deleted file mode 100644 index a24d0d6cf..000000000 --- a/resources/tester/test_m02_drawing_card.py +++ /dev/null @@ -1,81 +0,0 @@ -"""도면 카드 미리보기 수정(PLAN 31-4) — 메뉴 없이 단축키로 고치기 · 카드 [저장] · 떠날 때 물음. - -코드 글 모양으로 봄(CAD 는 iframe · 화면 검증은 ORCA). - - 고치기 = 층 고칠 권한 + 자료 자리의 저장 길(`write`) 둘 다 · 아니면 예전처럼 보기만. - - 고칠 때 CAD 메뉴(제목줄 · 리본 · 패널 · 상태줄)는 숨김 · CAD 앱은 안 고침(같은 출처 스타일). - - 저장 = 전체 편집기와 같은 길(`saveTemplate`) · 읽은 문서에 `도면` · `더한장`(모든 장)을 갈아 끼움 · - Ctrl+S 도 같음 — 지금 탭 장을 안 받음(2장 보다 저장해도 1장 그대로 · 105-3). - - 여러 장 = 공용 탭 줄 고르기만(`mountStructureSheets` · `pickTabs`) · 「장 n」 배지 없음. - - 저장 안 한 카드 고침 = 다른 항목 열기 · 페이지 떠나기 때 물음. -""" - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -M02 = ROOT / "M02_MasterTemplete" - - -def _text(name: str) -> str: - return (M02 / name).read_text(encoding="utf-8") - - -def test_고치기는_권한과_저장_길_둘_다(): - card = _text("M02_MasterTemplete_DrawingCard.ts") - assert "const edit = drawingEditable(opt.editable, opt.source);" in card - assert "editable && !!source.write;" in card - assert "readOnly: !edit," in card and "preview: !edit," in card - assert ( - "onSave: () => void opt.saver.run(() => doSave())," in card - ) # CAD Ctrl+S = 카드 저장(문서 전체) - source = _text("M02_MasterTemplete_ItemSource.ts") - assert ( - "write: (part, column, 판, 문서) => saveTemplate(layer, part, column, null, 판, 문서)" - in source - ) - - -def test_메뉴는_숨기고_저장은_문서_전체(): - card = _text("M02_MasterTemplete_DrawingCard.ts") - for part in (".cad-titlebar", ".cad-ribbon", ".cad-inspector", ".cad-statusbar"): - assert part in card.split("const HIDE_EDIT")[1].split(";\n")[0] - # 도번 · 열 · 판 은 읽은 그대로 · 모든 장(지금 탭 하나가 1장을 덮던 흠 · 105-3) - assert ( - 'const next = fitLinks({\n ...doc,\n ...((await handle.getDoc()) as Pick),\n });' - in card - ) # 지운 표의 연결표 줄은 뺌(114-2 · 도면 상세 저장과 같은 몸) - assert "async function doSave(): Promise" in card - assert 'write("structure", opt.column, version, next)' in card - for key in ("L · PL", "E · Delete", "Ctrl+Z · Ctrl+Y", "Ctrl+S"): - assert f'["{key}"' in card # ? 도움말 — 전체 편집기 명령 별칭과 같은 것 - - -def test_저장_안_한_카드_고침은_물음(): - cards = _text("M02_MasterTemplete_ItemCards.ts") - # 물음 한 번(31-7) — 어느 카드든(도면 · 엑셀) 고침이 있으면 · 틀의 저장 표시(saver)로 봄 - assert ( - '(await leaveOrStay(TEXT.discard, { save: saveAll, stillDirty: anyDirty })) === "stay"' - in cards - ) - assert "const anyDirty = (): boolean => cards.some((c) => c.saver.dirty());" in cards - assert "dirty: anyDirty," in cards - assert "dirty: () => cards.dirty()," in _text("M02_MasterTemplete_Structure.ts") - main = _text("M02_MasterTemplete_UI_Main.ts") - assert "const busy = (): boolean => dirty || !!panel?.dirty() || toDrawing.waiting();" in main - assert main.count("!busy()") >= 2 # 떠날 때 물음 · 창 닫기 막음 - - -def test_카드_미리보기는_탭_고르기만(): - card = _text("M02_MasterTemplete_DrawingCard.ts") - assert "const handle = mountStructureSheets(host, sheets, {" in card - assert "pickTabs: true," in card - assert "m02-drawing__sheets" not in card # 「장 n」 배지 지움 - panel = _text("M02_MasterTemplete_AreaDrawing.ts") # 면적 도면 패널도 탭(고르기만) - assert "view = mountStructureSheets(stage, sheets, {" in panel and "pickTabs: true," in panel - drawing = _text("M02_MasterTemplete_Drawing.ts") - assert ( - "readOnly || options.pickTabs ? {} : { add: true, rename: true, remove: true, reorder: true }," - in drawing - ) - assert ( - "if ((readOnly || options.pickTabs) && !more.length) tabs.element.hidden = true;" in drawing - ) diff --git a/resources/tester/test_m02_labor_tree.py b/resources/tester/test_m02_labor_tree.py deleted file mode 100644 index c8b7aa5ce..000000000 --- a/resources/tester/test_m02_labor_tree.py +++ /dev/null @@ -1,73 +0,0 @@ -"""좌측 패널 일위대가(PLAN 39-2) — 맨 위 「일위대가 조합」 컨테이너 · 조합 나무 · 둘째 층 단추 벌 지움 · -일위대가는 구조물 조합 하위(구조물 도면 → 일위대가 → 면적산출). 글 검사(화면은 ORCA). - -옛 일위대가 라이브러리 나무 시험(PLAN 27-2b · 30-3 · 30-5)은 나무를 지우며 이 검사로 갈음. -""" - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -M02 = ROOT / "M02_MasterTemplete" - - -def _text(name: str) -> str: - return (M02 / name).read_text("utf-8") - - -def test_일위대가_나무_파일과_이음이_없음(): - assert not (M02 / "M02_MasterTemplete_Labor.ts").exists() - assert not (M02 / "M02_MasterTemplete_Labor_Tree.ts").exists() - page = _text("M02_MasterTemplete_UI_Page.ts") - assert "buildLaborSection" not in page and 'layerBar("일위대가"' not in page - side = _text("M02_MasterTemplete_UI_Side.ts") - assert "LaborSection" not in side and "labor?." not in side - - -def test_나무_부품에_일위대가_전용_칸_없음(): - panel = _text("M02_MasterTemplete_TreePanel.ts") - for gone in ("maxDepth", "opt.item", "opt.clipKind", "clipKind?:"): - assert gone not in panel, gone - - -def test_좌측_하위_컨테이너와_전체_항목_길_없음(): - """45-2 — 구조물 조합 안 하위(구조물 도면 · 일위대가 · 면적산출 · 각 「전체 항목」) 없앰 · - 상세는 항목 카드 [수정]으로.""" - side = _text("M02_MasterTemplete_UI_Side.ts") - for gone in ( - "COLUMN_KINDS", - "columnSections", - "onAll", - "allPicked", - "selectColumn", - "m02-side__rule", - ): - assert gone not in side, gone - page = _text("M02_MasterTemplete_UI_Page.ts") - assert "buildAllItems" not in page and "onAll" not in page and "ALL_TITLE" not in page - assert "buildAllItems" not in _text("M02_MasterTemplete_ItemList.ts") - api = _text("M02_MasterTemplete_Api_Fetch.ts") - assert '| "labor" |' in api and "일위대가번: string;" in api - locale = (ROOT / "ui_template" / "ui_template_locale_m2.ts").read_text("utf-8") - for gone in ("M02_ItemsAll", "M02_KindLabor", "M02_KindStructure", "M02_KindArea"): - assert gone not in locale, gone - assert "B01_Dashboard_CardUnitCombo" not in locale # 대시보드 일위대가 카드 없앰(44-7) - css = _text("M02_MasterTemplete_UI_Style.css") - assert ".m02-side__rule" not in css and ".m02-side__summary" not in css - - -def test_층_단추는_구조물_조합_한_벌(): - page = _text("M02_MasterTemplete_UI_Page.ts") - # 구조물 조합 줄 아래 하나 · 일위대가 사전조합은 층 따로(134-3 · `preBar`) - assert page.count('layerBar("') == 1 - assert "const preBar = (): HTMLElement =>" in page - assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page - - -def test_나무가_바뀌면_집계표를_다시_엶(): - """39-8 H1 — 새 항목 · 복사 생성 · 붙여넣기 · 지우기 · 이름 · 옮기기 = 나무 `apply` → - `onChanged` 한 길 · 좌측 하위 열 줄은 없앰(45-2).""" - page = _text("M02_MasterTemplete_UI_Page.ts") - changed = page.split("onChanged: async (colId) => {")[1].split("},")[0] - assert "main.openTable(layer, colId, true)" in changed and "side.refresh()" not in changed - panel = _text("M02_MasterTemplete_TreePanel.ts") - assert panel.count("await opt.onChanged(") == 1 # 나무 고침은 모두 apply 한 곳을 거침 diff --git a/resources/tester/test_modal_head_actions.py b/resources/tester/test_modal_head_actions.py deleted file mode 100644 index a4fef5604..000000000 --- a/resources/tester/test_modal_head_actions.py +++ /dev/null @@ -1,110 +0,0 @@ -"""공용 모달 머리 단추 · 큰 창 · M02 수량산출 창(PLAN 147) — 소스 검사 · 화면 판정은 사용자. - -· 147-1 곁값 `actions`(머리 [닫기] 바로 왼쪽 · 넘긴 창만) · `wide`(96vw × 94vh · 몸만 스크롤) -· 147-2 연결창 단추 넷 → 머리 · 큰 창 · 글 묻는 창 [확인] 머리 · [취소] 없음 -· 147-3 M02 입력 · 수량산출 · 참조 창 — 아래 단추 줄 없음 · [확인] 머리 · 읽기만이면 없음 -· 147-4 「손 입력」 → 「수기 입력」 · 짧은 글 「손」 → 「수기」 · [면적산출에서 잇기] · goArea 없음 -· 147-5 수량산출 시트 위 「고른 칸 … [확인]」 · 「면적산출 시트 — …」 없음 · [연결 풀기] 누르면 꺼짐 -""" - -from __future__ import annotations - -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def _text(rel: str) -> str: - return (ROOT / rel).read_text(encoding="utf-8") - - -def _block(css: str, selector: str) -> str: - found = re.search(rf"^{re.escape(selector)} \{{([^}}]*)\}}", css, re.M) - assert found, selector - return found.group(1) - - -def test_147_1_곁값_actions_wide(): - ts = _text("ui_template/ui_template_modal.ts") - assert "actions?: HTMLElement[];" in ts - assert "wide?: boolean;" in ts - # 몸에서 만든 단추도 넣게 mount 셋째 인자 = 머리 자리 - assert "opts.mount(body, close, actions);" in ts - # 단추가 있을 때만 [닫기] 바로 앞에 — 없으면 지금 꼴(h3 · [닫기]) - assert "if (actions.childElementCount) closeButton.before(actions);" in ts - assert 'children: [el("h3", { text: opts.title }), closeButton]' in ts - assert '"ui-modal--wide "' in ts - - -def test_147_1_머리_자리_큰_창_css(): - css = _text("ui_template/ui_template_modal.css") - assert "margin-right: auto;" in _block(css, ".ui-modal__top > h3") - assert "justify-content: space-between;" in _block(css, ".ui-modal__top") - wide = _block(css, ".ui-modal.ui-modal--wide") - for rule in ("width: 96vw;", "height: 94vh;", "--modal-max: 94vh;", "overflow: hidden;"): - assert rule in wide, rule - body = _block(css, ".ui-modal--wide > .ui-modal__body") - assert "overflow: auto;" in body and "min-height: 0;" in body - - -def test_147_2_연결창_머리_단추_큰_창(): - ts = _text("ui_template/ui_template_item_picker.ts") - assert "children: [copyButton, manageButton, clearButton, applyButton]," in ts - assert "actions: [headButtons]," in ts and "wide: true," in ts - # 편집 화면 = 머리 단추도 숨김(편집 중 [적용] 막음) · 돌아오면 다시 - assert "headButtons.hidden = true;" in ts and "headButtons.hidden = false;" in ts - # 안내 글(구조물 [저장]) 은 아래 남김 - assert "ui-item-picker__saveline" in ts - css = _text("ui_template/ui_template_item_picker.css") - assert ".ui-item-picker .ui-modal__actions[hidden]" in css - assert "width: min(1320px, 96vw);" not in css - - -def test_147_2_글_묻는_창_확인만_머리(): - ask = _text("ui_template/ui_template_item_picker_ask.ts") - assert "head.append(createButton({ label: opts.okLabel" in ask - assert "cancelLabel" not in ask - for rel in ( - "ui_template/ui_template_item_picker.ts", - "ui_template/ui_template_item_picker_detail.ts", - ): - assert "cancelLabel" not in _text(rel), rel - - -def test_147_3_m02_창_셋_머리_확인(): - m02 = "M02_MasterTemplete/M02_MasterTemplete_" - for name in ("Labor_Input", "Labor_QtyModal", "Labor_RefEdit"): - ts = _text(f"{m02}{name}.ts") - assert "m02-labin__foot" not in ts, name - assert "mount: (body, " in ts and ", head) => {" in ts, name - # 읽기만 = 머리 [닫기]만 - assert "if (!opt.readOnly)" in ts and "head.append(" in ts, name - assert "In_Cancel" not in _text(f"{m02}Labor_Text.ts") - assert ".m02-labin__foot" not in _text(f"{m02}Labor.css") - - -def test_147_4_수기_입력_잇기_없음(): - m02 = ROOT / "M02_MasterTemplete" - text = _text("M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts") - assert 'In_QtyHand: ["수기 입력", "Manual"],' in text - assert 'Labor_Hand: ["수기", "Manual"],' in text - assert "In_QtyGoArea" not in text - assert "손 입력" not in _text("M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts") - for path in m02.glob("*.ts"): - src = path.read_text(encoding="utf-8") - assert "goArea" not in src and "GoArea" not in src, path.name - - -def test_147_5_시트_위_글_없음_풀기_꺼짐(): - qty = _text("M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts") - text = _text("M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts") - for key in ("Labor_SheetHint", "Labor_CellPending", "Labor_UnlinkPending"): - assert key not in qty and key not in text, key - assert "data-labor-qty-state" not in qty and "m02-cellpick__hint" not in qty - assert ".m02-cellpick__hint" not in _text("M02_MasterTemplete/M02_MasterTemplete_AreaPanel.css") - # [연결 풀기] = 누르면 꺼짐 · 칸을 고르면 다시 켜짐 · 고름 상태는 [확인] 때 씀 - assert 'pending = { kind: "unlink" };' in qty - assert "if (unlink) unlink.disabled = true;" in qty - assert "if (unlink) unlink.disabled = false;" in qty - assert "...(opt.handOnly ? [] : [host])," in qty diff --git a/resources/tester/test_sheet_f2_caret.py b/resources/tester/test_sheet_f2_caret.py deleted file mode 100644 index ade04c092..000000000 --- a/resources/tester/test_sheet_f2_caret.py +++ /dev/null @@ -1,24 +0,0 @@ -"""표 부품(`ui_template_sheet.ts`) 칸 고치기 — 엑셀처럼 F2 는 커서를 글 끝에(이어 씀). - -화면 조작은 ORCA 로 봄 — 여기서는 키마다 고치기 방식이 안 섞이는지만 봄. -""" - -from __future__ import annotations - -import re -from pathlib import Path - -SHEET = Path(__file__).resolve().parents[2] / "ui_template" / "sheet" / "ui_template_sheet.ts" - - -def test_F2_는_글_끝_바로_치기는_바꿈_두_번_누르기는_그대로(): - body = SHEET.read_text(encoding="utf-8") - # Enter · F2 = 글 끝 커서 - assert re.search( - r'event\.key === "F2"\) \{\s*event\.preventDefault\(\);\s*edit\(undefined, true\);', body - ) - # 바로 치기 = 누른 글자로 바꿈 - assert "edit(event.key);" in body - # 두 번 누르기 = 예전대로(글 전부 고름) - assert 'else if (target.closest("td[data-r]")) edit();' in body - assert "box.setSelectionRange(box.value.length, box.value.length)" in body