Files
Aislo/resources/tester/test_est_pick.py
T
eomsangdonandClaude Opus 5.5 d25183e89d feat(est-test): 내역·일위대가 단가 고르기 창
- 원본 검색 창(TFRMSERCHPROJECT)처럼 7종 자료를 검색어로 찾아 여러 개 골라 넣기
- 원본 데이타 변경 확인(TFRMDBLCLICKRTN)처럼 선택 행 연결 바꾸기, 같은 코드 전체 변경 선택
- 넣거나 바꾼 행만 원천 단가 재조회, 실패 시 되돌림
- 시험 test_est_pick.py 추가

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 14:36:34 +09:00

137 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.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}