Files
Aislo/resources/tester/test_est_pick.py
T
eomsangdonandClaude Opus 5.5 892285843d feat(est-test): 워드형 수량산출서 표 보존과 편집 도구
- 가져오기가 WPT 표를 버리던 결함 고침: 표 줄(칸·수량 표지·수식·세로병합) 보존, 문서 읽기 native-quantity-wpt.cjs 로 분리
- 표 셀 합·수량 지정·병합/분리·표 만들기·표 모양, 문단·그림·쪽 삽입/삭제·품명/규격 지정·공종명칭 입력·테두리선·산출근거 양식
- 수량 불러오기·수량목록보기·선택수량복사·내역서 수량붙여넣기(연결)
- ESTX 저장은 표 줄을 건너 문단만 맞춤, 표·서식 편집은 거부

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 18:35:50 +09:00

138 lines
5.5 KiB
Python

"""EST 테스트 앱 단가 고르기: 검색·넣기·바꾸기와 계산 연결."""
import json
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
FILES = [
"native-math.js",
"native-formula.js",
"native-machine.js",
"native-rows.js",
"native-text.js",
"native-quantity-wpt.cjs",
"native-quantity.cjs",
"native-wonga.js",
"native-engine.js",
"native-row-match.js",
"native-escalation.js",
"native-progress.js",
"native-business.js",
"native-review.js",
"native-resources.js",
"native-schedule.js",
"native-model.js",
"native-verify.js",
"native-pick.js",
]
def run_js(code: str):
runner = f"""
const fs = require('fs'), vm = require('vm');
const ctx = vm.createContext({{ structuredClone, console }});
for (const f of {json.dumps(FILES)})
vm.runInContext(fs.readFileSync('tools/est-test/' + f, 'utf8'), ctx, {{ filename: f }});
ctx.project = JSON.parse(fs.readFileSync('tools/est-test/sample.json', 'utf8'));
process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(code)}, ctx)));
"""
out = subprocess.check_output(["node", "-e", runner], cwd=ROOT)
return json.loads(out.decode("utf-8"))
def test_search_filters_by_kind_and_all_words():
result = run_js("""(() => {
const labor = project.tables.LaborcDat.find((r) => r.Code && r.Name);
const word = String(labor.Name).replace(/\\s+/g, "").slice(0, 2);
const one = EstPick.search(project, 6, word);
const all = EstPick.search(project, 0, labor.Code);
const none = EstPick.search(project, 6, word + " 없는말없는말");
return {
kinds: [...new Set(one.rows.map((r) => r.kind))],
hit: one.rows.some((r) => r.Code === labor.Code),
exact: all.rows.filter((r) => r.Code === labor.Code).length,
none: none.total,
};
})()""")
assert result == {"kinds": [6], "hit": True, "exact": 1, "none": 0}
def test_insert_links_rows_and_engine_prices_them():
result = run_js("""(() => {
const labor = project.tables.LaborcDat.find((r) => r.Code && Number(r.TC) > 0);
const pick = EstPick.items(project, 6).find((r) => r.Code === labor.Code);
const rows = project.tables.EstMainDat, before = rows.length;
const added = EstPick.insert(project, "EstMainDat", 0, "", [pick, pick]);
const done = EstNative.calculate(EstModel.validate(project));
const row = done.tables.EstMainDat[added[0]];
return {
added, grew: done.tables.EstMainDat.length - before,
link: row.LinkIndex, code: row.Code === labor.Code, name: row.Name === labor.Name,
priced: Number(row.NU) > 0 && Number(row.NU) === Number(pick.price),
};
})()""")
assert result == {
"added": [1, 2],
"grew": 2,
"link": 6,
"code": True,
"name": True,
"priced": True,
}
def test_replace_current_row_or_all_same_code():
result = run_js("""(() => {
const rows = project.tables.EstMainDat;
const idx = rows.findIndex((r) => r.Code && !(Number(r.Index) > 0) && [5, 6, 103, 104].includes(Number(r.LinkIndex)));
const target = rows[idx];
const twin = { ...target, PkID: 999999 };
rows.splice(idx + 1, 0, twin);
const pick = EstPick.items(project, 5).find((r) => r.Code !== target.Code);
const one = EstPick.replace(structuredClone(project), "EstMainDat", idx, "", pick, false);
const copy = structuredClone(project);
const all = EstPick.replace(copy, "EstMainDat", idx, "", pick, true);
return {
one: one.length, all: all.length >= 2,
moved: copy.tables.EstMainDat[idx + 1].Code === pick.Code,
kind: copy.tables.EstMainDat[idx].LinkIndex,
untouched: project.tables.EstMainDat[idx].Code === target.Code,
};
})()""")
assert result == {"one": 1, "all": True, "moved": True, "kind": 5, "untouched": True}
def test_guards_reject_wrong_view_self_reference_and_title_rows():
result = run_js("""(() => {
const out = [];
const tryIt = (fn) => { try { fn(); out.push("ok"); } catch (e) { out.push(e.message.slice(0, 12)); } };
const head = EstPick.items(project, 103)[0];
tryIt(() => EstPick.insert(project, "CostMainDat", 0, "", [head]));
tryIt(() => EstPick.insert(project, "HeadMainDat", 0, head.Code, [head]));
tryIt(() => EstPick.insert(project, "EstMainDat", 0, "", []));
const title = project.tables.HeadMainDat.findIndex((r) => Number(r.Index) > 0);
tryIt(() => EstPick.replace(project, "HeadMainDat", title, "", EstPick.items(project, 6)[0], false));
return out;
})()""")
expected = [
"내역서·일위대가 화면",
"일위대가 자신을 하위",
"넣을 자료를 하나 이상",
"일반 내역 행 하나를",
]
assert len(result) == len(expected)
assert all(message.startswith(head[:8]) for message, head in zip(result, expected))
def test_insert_into_head_group_prices_from_source():
result = run_js("""(() => {
const rows = project.tables.HeadMainDat;
const title = rows.findIndex((r) => Number(r.Index) > 0);
const stuff = EstPick.items(project, 5).find((r) => r.price > 0);
const added = EstPick.insert(project, "HeadMainDat", title, rows[title].Code, [stuff]);
const row = project.tables.HeadMainDat[added[0]];
return { at: added[0] === title + 1, link: row.LinkIndex, unit: Number(row.GU) === stuff.price };
})()""")
assert result == {"at": True, "link": 5, "unit": True}