- 원본 검색 창(TFRMSERCHPROJECT)처럼 7종 자료를 검색어로 찾아 여러 개 골라 넣기 - 원본 데이타 변경 확인(TFRMDBLCLICKRTN)처럼 선택 행 연결 바꾸기, 같은 코드 전체 변경 선택 - 넣거나 바꾼 행만 원천 단가 재조회, 실패 시 되돌림 - 시험 test_est_pick.py 추가 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
5.5 KiB
Python
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}
|