chore(시험정리): 2 차 69 파일 지움 — 목록 docs/raw/조사/2026-10-23_시험정리_2차_목록.md
This commit is contained in:
@@ -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()
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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 = 대상 밖
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 탭 줄 토글 걷음
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"<!doctype html><html><head><style>{TOKENS}{css}</style></head><body>"
|
||||
"<div class='b09-estimation'><div class='b09-estimation__tabs'><button>탭</button></div>"
|
||||
"<div class='b09-estimation__panel'></div></div></body></html>"
|
||||
)
|
||||
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 # 브라우저 배치는 위 시험이 잼
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 '<table class="ui-data-table__table" style="--dt-w:${x}px">' 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 "<button" not in src and 'createElement("button")' not in src
|
||||
assert 'createElement("input")' not in src and 'createElement("select")' not in src
|
||||
assert "createInputField(" in B06_PANEL and "createButton(" in B06_PANEL
|
||||
# 덧칠 없음 — 칸 글자 · 오류 칸 접기는 공용 양식 몫
|
||||
group = _block(B06_CSS, ".b06-profile__group")
|
||||
assert "font-size" not in group and not re.search(r"(?:^|\s)color\s*:", group)
|
||||
assert ".b06-profile__field-row .ui-field__error" not in B06_CSS
|
||||
for sel in (".ui-btn {", ".ui-input {", ".ui-select {"):
|
||||
for line in B06_CSS.splitlines():
|
||||
if line.endswith(sel) and line.startswith(".b06-profile"):
|
||||
rule = _block(B06_CSS, line[:-2])
|
||||
for prop in ("height", "padding", "font-size"):
|
||||
assert not re.search(r"(?:^|\s)" + prop + r"\s*:", rule), (line, prop)
|
||||
|
||||
|
||||
B08_OPTIONS = _read("B08_Quantity/B08_Quantity_UI_Options.ts")
|
||||
B09_SHELL = _read("B09_Estimation/B09_Estimation_UI_Shell.ts")
|
||||
# 기준(「구조물 배치」 폼 자신) · 부르는 곳 없는 옛 함수는 점검 밖
|
||||
PANEL_DIRS = (
|
||||
"A00_Common/design",
|
||||
"B05_Profile",
|
||||
"B06_Section",
|
||||
"B07_DesignDetail",
|
||||
"B08_Quantity",
|
||||
"B09_Estimation",
|
||||
)
|
||||
REFERENCE = {
|
||||
"B05_Profile/B05_Profile_UI_Structures_Form.ts",
|
||||
"B05_Profile/B05_Profile_UI_IrregularStations.ts",
|
||||
}
|
||||
|
||||
|
||||
def test_B05_칸_틀_두_보기_모두_공용_양식():
|
||||
frame = DESIGN_PANEL[DESIGN_PANEL.index("export function createSectionFrame(") :]
|
||||
frame = frame[: frame.index("\n}\n")]
|
||||
assert frame.count("ui-sidebar-section ui-panel-form") == 2
|
||||
|
||||
|
||||
def test_B08_B09_카드_공용_카드():
|
||||
assert "createPanelCard(L(title))" in B08_OPTIONS
|
||||
assert 'createPanelCard("데이터 점검", true)' in B09_SHELL
|
||||
for src in (B08_OPTIONS, B09_SHELL):
|
||||
assert "ui-sidebar-card" not in src
|
||||
|
||||
|
||||
def test_점검_B05_이상_좌측_컨테이너는_모두_공용_양식():
|
||||
"""손으로 짠 컨테이너(ui-sidebar-section)에 공용 양식이 빠진 곳 = 0."""
|
||||
misses, users = [], []
|
||||
for page in PANEL_DIRS:
|
||||
for path in (ROOT / page).glob("*.ts"):
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if "createIrregularStationsSection(" in text:
|
||||
users.append(path.name)
|
||||
if rel in REFERENCE:
|
||||
continue
|
||||
for n, line in enumerate(text.splitlines(), 1):
|
||||
code = line.split("//")[0]
|
||||
if "ui-sidebar-section" in code and "ui-panel-form" not in code:
|
||||
misses.append(f"{rel}:{n}")
|
||||
assert misses == []
|
||||
# 점검 밖 옛 함수 = 정말 부르는 곳 없음(살아나면 점검에 넣음)
|
||||
assert users == ["B05_Profile_UI_IrregularStations.ts"]
|
||||
@@ -1,120 +0,0 @@
|
||||
"""278 B09 단가산출근거 [박스보기] 머리 줄 = 운반 단가 창 ③ 칸 꼴(입력값 · 중간값 · 이름 · 값 · 식) ·
|
||||
줄 나눔 · 그리기 한 벌(`Haul_Steps.headSteps`) · 호표 줄 · 총계 · 다른 탭 카드 그대로 ·
|
||||
운반 단가 창 왼쪽 폭(278-1)."""
|
||||
|
||||
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_"
|
||||
STEPS = f"{B09}Haul_Steps.ts"
|
||||
|
||||
# 공용 el 짝(가짜 DOM) — data-* = dataset
|
||||
EL = r"""
|
||||
const el = (tag, o = {}) => {
|
||||
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
|
||||
@@ -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 # 위 탭은 그대로
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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<RecommendPart>(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
|
||||
@@ -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<HTMLElement>(".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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 막음
|
||||
@@ -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 ">측점</button>" in chrome and ">기울기</button>" 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
|
||||
@@ -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
|
||||
@@ -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'<div class="b05-routerec__row"><button[^>]*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)
|
||||
@@ -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
|
||||
@@ -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) 아래
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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('<div class="b05-crossmodal__head">') : modal.index(
|
||||
'<div class="b05-crossmodal__box">'
|
||||
)
|
||||
]
|
||||
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, );")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ">저장 전으로</button>" not in chrome and "노선 반영</button>" not in chrome
|
||||
assert ">취소</button>" not in chrome and ">확인</button>" not in chrome
|
||||
# 352-2 — 되돌리기 · 다시하기 = 글 없이 기호 + aria-label · title(단축키)
|
||||
# 381-1 — 곧은 꼬리 + 갈고리 SVG(회전 반 바퀴 그림과 헷갈리지 않게)
|
||||
assert 'aria-label="되돌리기" disabled>${HISTORY_ICON.undo}</button>' in chrome
|
||||
assert 'aria-label="다시하기" disabled>${HISTORY_ICON.redo}</button>' in chrome
|
||||
assert ">↶</button>" not in chrome and ">↷</button>" 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", "<h3>"):
|
||||
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("<aside", 1)[0]
|
||||
# 도구줄은 지도 칸 안 · [?] 는 도구줄 뒤
|
||||
wrap = chrome.index("b05-routeedit__canvas-wrap")
|
||||
assert wrap < chrome.index("b05-routeedit__tools") < chrome.index("b05-routeedit__hint")
|
||||
assert chrome.index("b05-routeedit__side-wrap") > 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
|
||||
@@ -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<DesignView, number> = { 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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<string, unknown>), ...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
|
||||
@@ -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")
|
||||
@@ -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<StructureDoc, "도면" | "더한장">),\n });'
|
||||
in card
|
||||
) # 지운 표의 연결표 줄은 뺌(114-2 · 도면 상세 저장과 같은 몸)
|
||||
assert "async function doSave(): Promise<void>" 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
|
||||
)
|
||||
@@ -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 한 곳을 거침
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user