537 lines
22 KiB
Python
537 lines
22 KiB
Python
from test_est_rea_parity import run
|
||
|
||
|
||
def test_data_issues_detect_duplicate_missing_and_mismatched_links_without_modifying_data():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p=EstNative.empty();p.tables.StuffcDat=[{Code:'A',Name:'sand',Size:'large'},
|
||
{Code:'B',Name:'first'},{Code:'B',Name:'second'}];
|
||
p.tables.HeadMainDat=[{Index:1,Code:'H',Name:'group'},{Index:0,Code:'A',LinkIndex:5,Name:'sand',Size:'large'}];
|
||
p.tables.EstMainDat=[{Code:'A',LinkIndex:5,Name:'stone',Size:'small'},
|
||
{Code:'B',LinkIndex:5,Name:'ambiguous'},{Code:'missing',LinkIndex:5,Name:'missing'},
|
||
{Code:'H',LinkIndex:103,Name:'group'}];
|
||
const before=JSON.stringify(p),issues=EstIssues.scan(p);
|
||
return {issues,same:before===JSON.stringify(p)};
|
||
})()""",
|
||
)
|
||
assert result["same"]
|
||
items = {(row["table"], row["index"], row["message"]) for row in result["issues"]}
|
||
assert ("StuffcDat", 1, "코드 중복") in items
|
||
assert ("StuffcDat", 2, "코드 중복") in items
|
||
assert ("EstMainDat", 0, "연결 품명 차이") in items
|
||
assert ("EstMainDat", 0, "연결 규격 차이") in items
|
||
assert ("EstMainDat", 1, "연결 코드 중복") in items
|
||
assert ("EstMainDat", 2, "연결 자료 없음") in items
|
||
assert len(items) == 6
|
||
|
||
|
||
def test_data_issues_detect_cost_main_dat_missing_and_duplicate_links():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p = EstNative.empty();
|
||
p.tables.StuffcDat = [{Code:'M_GOOD', Name:'자재1'}, {Code:'M_DUP', Name:'자재2'}, {Code:'M_DUP', Name:'자재2-복사'}];
|
||
p.tables.CostMainDat = [
|
||
{Index:1, Code:'D001', Name:'단가산출호표1'},
|
||
{Index:0, Code:'M_GOOD', LinkIndex:5, LineData:'정상 자재행'},
|
||
{Index:0, Code:'M_MISSING', LinkIndex:5, LineData:'미연결 자재행'},
|
||
{Index:0, Code:'M_DUP', LinkIndex:5, LineData:'중복 자재행'}
|
||
];
|
||
const issues = EstIssues.scan(p);
|
||
return {
|
||
costIssues: issues.filter(x => x.table === 'CostMainDat')
|
||
};
|
||
})()""",
|
||
)
|
||
items = {(row["index"], row["code"], row["message"]) for row in result["costIssues"]}
|
||
assert (2, "M_MISSING", "연결 자료 없음") in items
|
||
assert (3, "M_DUP", "연결 코드 중복") in items
|
||
assert len(items) == 2
|
||
|
||
|
||
def test_data_issues_filtering_by_table_and_type():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p = EstNative.empty();
|
||
p.tables.StuffcDat = [{Code:'M_DUP', Name:'A'}, {Code:'M_DUP', Name:'B'}];
|
||
p.tables.EstMainDat = [{Code:'M_MISS', LinkIndex:5, Name:'없음'}];
|
||
const all = EstIssues.scan(p);
|
||
const stuffOnly = all.filter(i => (i.table || '') === 'StuffcDat');
|
||
const dupOnly = all.filter(i => i.message === '코드 중복');
|
||
return {
|
||
total: all.length,
|
||
stuffCount: stuffOnly.length,
|
||
dupCount: dupOnly.length
|
||
};
|
||
})()""",
|
||
)
|
||
assert result["total"] == 3
|
||
assert result["stuffCount"] == 2
|
||
assert result["dupCount"] == 2
|
||
|
||
|
||
def test_cost_error_location_requires_unique_group_and_valid_row():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p=EstNative.empty();
|
||
p.tables.CostMainDat=[{Index:1,Code:'A'}, {Index:0}, {Index:0},
|
||
{Index:1,Code:'B'}, {Index:0}, {Index:1,Code:'B'}, {Index:0}];
|
||
p.calculation={errors:['A:2 bad formula','A:8 invalid row','B:2 duplicate','other error']};
|
||
return EstIssues.scan(p).filter(x=>x.message.includes('formula') ||
|
||
x.message.includes('row') || x.message.includes('duplicate') || x.message==='other error');
|
||
})()""",
|
||
)
|
||
assert result[0]["table"] == "CostMainDat"
|
||
assert result[0]["index"] == 1
|
||
assert all(row["table"] == "" for row in result[1:])
|
||
|
||
|
||
def test_data_issues_detect_quantity_target_missing_and_duplicate():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p = EstNative.empty();
|
||
p.tables.EstMainDat = [
|
||
{ PkID: 101, Code: 'E1', Name: '단일 대상' },
|
||
{ PkID: 201, Code: 'DUP', Name: '중복 대상 1' },
|
||
{ PkID: 202, Code: 'DUP', Name: '중복 대상 2' }
|
||
];
|
||
p.quantitySheets = [
|
||
{
|
||
Code: 'Q1',
|
||
items: [
|
||
{ Code: 'r1', TargetCode: 'E1' },
|
||
{ Code: 'r2', TargetCode: 'MISSING' },
|
||
{ Code: 'r3', TargetCode: 'DUP' },
|
||
// 두 행이 코드 중복('DUP')이지만 TargetKey('id:201')로 고유하게 연결된 정상 사례
|
||
{ Code: 'r4', TargetCode: 'DUP', TargetKey: 'id:201' },
|
||
// LinkCode fallback으로 정상 연결된 사례
|
||
{ Code: 'r5', LinkCode: 'E1' },
|
||
// 잘못된 TargetKey 사례
|
||
{ Code: 'r6', TargetKey: 'id:999' }
|
||
]
|
||
}
|
||
];
|
||
p.quantities = [
|
||
{ Code: 'm1', TargetKey: 'id:202' },
|
||
{ Code: 'm2', TargetCode: 'MISSING_M' }
|
||
];
|
||
return EstIssues.scan(p).filter(x => x.table === 'quantity');
|
||
})()""",
|
||
)
|
||
items = {(row["code"], row["message"], row["sheetCode"]) for row in result}
|
||
# r1: 정상 (E1 단일 매칭) -> 오류 없음
|
||
# r4: 정상 (DUP이지만 TargetKey id:201 지정) -> 오류 없음
|
||
# r5: 정상 (LinkCode E1 지정) -> 오류 없음
|
||
# m1: 정상 (수식표 id:202 지정) -> 오류 없음
|
||
assert ("r2", "수량 대상 내역 없음: MISSING", "Q1") in items
|
||
assert ("r3", "수량 대상 내역 중복: DUP", "Q1") in items
|
||
assert ("r6", "수량 대상 내역 없음: id:999", "Q1") in items
|
||
assert ("m2", "수량 대상 내역 없음: MISSING_M", "manual") in items
|
||
assert len(items) == 4
|
||
|
||
|
||
def test_edge_issues_click_quantity_navigates_to_sheet():
|
||
from playwright.sync_api import sync_playwright
|
||
import pytest
|
||
|
||
with sync_playwright() as p:
|
||
browser = p.chromium.launch(channel="msedge", headless=True)
|
||
page = browser.new_page()
|
||
try:
|
||
page.goto("http://100.65.141.15:5175", timeout=5000)
|
||
except Exception:
|
||
pytest.skip("EST 5175 server not reachable")
|
||
|
||
page.wait_for_function("() => Boolean(EstApp.state.project)")
|
||
|
||
# 오류 검색 화면으로 전환 및 인위적 오류 수량산출서 주입
|
||
page.evaluate("""() => {
|
||
EstApp.change(p => {
|
||
p.quantitySheets ||= [];
|
||
p.quantitySheets.push({
|
||
Code: 'TEST_ERR_Q',
|
||
Name: '오류 테스트 산출서',
|
||
rows: [{ LineData: '1 = \\x120\\x12', markers: ['e_r1'] }],
|
||
items: [{ Code: 'e_r1', TargetCode: 'NON_EXISTENT_CODE_XYZ', GN: 10 }]
|
||
});
|
||
}, false);
|
||
EstApp.navigate('issues');
|
||
}""")
|
||
|
||
# 오류 검색 화면에서 '산출서 열기' 버튼 탐색 및 클릭
|
||
btn = page.locator("button[data-issue-quantity='TEST_ERR_Q']")
|
||
assert btn.count() >= 1
|
||
btn.first.click()
|
||
|
||
# 수량산출 화면으로 전환되었고 해당 산출서가 활성화되었는지 확인
|
||
page.wait_for_function("() => EstApp.state.view === 'quantity'")
|
||
current_view = page.evaluate("EstApp.state.view")
|
||
current_sheet = page.evaluate("EstApp.state.quantitySheet")
|
||
|
||
assert current_view == "quantity"
|
||
assert current_sheet == "TEST_ERR_Q"
|
||
|
||
# 다시 오류 검색 화면으로 돌아가서 바로 내역 연결 드롭다운 선택 시험
|
||
page.evaluate("""() => {
|
||
EstApp.navigate('issues');
|
||
}""")
|
||
page.wait_for_function("() => EstApp.state.view === 'issues'")
|
||
|
||
# select[data-issue-target-select='TEST_ERR_Q'] 요소 확인 및 옵션 선택
|
||
select = page.locator("select[data-issue-target-select='TEST_ERR_Q']")
|
||
assert select.count() >= 1
|
||
|
||
# 첫 번째 유효 내역 행의 value를 선택
|
||
options = select.locator("option")
|
||
assert options.count() > 1
|
||
target_val = options.nth(1).get_attribute("value")
|
||
select.select_option(target_val)
|
||
|
||
# 상태 변경이 작업본(project)에 즉시 반영되었는지 확인
|
||
linked_key = page.evaluate("""() => {
|
||
const sheet = EstApp.state.project.quantitySheets.find(s => s.Code === 'TEST_ERR_Q');
|
||
return sheet ? sheet.items[0].TargetKey : null;
|
||
}""")
|
||
assert linked_key == target_val
|
||
|
||
browser.close()
|
||
|
||
|
||
def test_data_issues_detect_wonga_formula_syntax_and_missing_reference_errors():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p = structuredClone(project);
|
||
const normalIssues = EstIssues.scan(p).filter(x => x.table === 'Wonga');
|
||
|
||
// 원가 계산서 항목에 잘못된 참조 및 잘못된 범위 산식 주입
|
||
const badP = structuredClone(project);
|
||
badP.tables.Wonga[5].VSusic = '4 * 0.165 + UNKNOWN_VAR';
|
||
badP.tables.Wonga[21].VSusic = '(6:INVALID_RANGE_END)';
|
||
const badIssues = EstIssues.scan(badP).filter(x => x.table === 'Wonga');
|
||
|
||
return {
|
||
normalCount: normalIssues.length,
|
||
badIssues
|
||
};
|
||
})()""",
|
||
)
|
||
assert result["normalCount"] == 0
|
||
items = {(row["index"], row["code"], row["message"]) for row in result["badIssues"]}
|
||
assert (5, "5", "참조 항목 없음: UNKNOWN_VAR") in items
|
||
assert (21, "C", "범위 참조 없음: 6:INVALID_RANGE_END") in items
|
||
assert len(items) == 2
|
||
|
||
|
||
def test_wonga_scan_handles_division_zero_values_cycles_ranges_and_basefield():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
// 1) 실제 값이 다른 A, B의 유효한 1/(A-B) 나눗셈 (scan에서 1로 단순 치환하지 않고 실제 값으로 평가해 0으로 나눔 오탐 없음)
|
||
const w1 = [{Gubun:'A',Cost:10},{Gubun:'B',Cost:5},{Gubun:'C',VSusic:'1/(A-B)'}];
|
||
const sc1 = EstWonga.scan(w1);
|
||
|
||
// 2) 실제 0값 참조 계산 (A가 0이고 A + 10 정상 평가)
|
||
const w2 = [{Gubun:'A',Cost:0},{Gubun:'B',VSusic:'A + 10'}];
|
||
const sc2 = EstWonga.scan(w2);
|
||
|
||
// 3) 상호 순환 참조 감지 (A -> B -> A)
|
||
const w3 = [{Gubun:'A',VSusic:'B'},{Gubun:'B',VSusic:'A'}];
|
||
const sc3 = EstWonga.scan(w3);
|
||
|
||
// 4) BaseField 우선: VSusic에 유효하지 않은 식이어도 BaseField가 우선 적용되어 오류 없음
|
||
const w4 = [{Gubun:'A',BaseField:'NC',VSusic:'INVALID_IF_EVALUATED',Cost:0}];
|
||
const sc4 = EstWonga.scan(w4, {}, {NC: 500});
|
||
|
||
// 5) 정상 범위 참조 (A:B 역순/정순 포함)
|
||
const w5 = [{Gubun:'B',Cost:10},{Gubun:'A',Cost:20},{Gubun:'C',VSusic:'(A:B)'}];
|
||
const sc5 = EstWonga.scan(w5);
|
||
|
||
// 6) 유효하지 않은 범위 참조
|
||
const w6 = [{Gubun:'A',Cost:10},{Gubun:'B',VSusic:'(A:INVALID)'}];
|
||
const sc6 = EstWonga.scan(w6);
|
||
|
||
return {
|
||
sc1, sc2, sc3, sc4, sc5, sc6
|
||
};
|
||
})()""",
|
||
)
|
||
# 1) A!=B 나눗셈: 오탐 0건
|
||
assert result["sc1"] == []
|
||
# 2) 0값 참조: 오탐 0건
|
||
assert result["sc2"] == []
|
||
# 3) 순환 참조: calculate와 동일하게 순환 탐지
|
||
assert any("원가 수식 순환 참조" in item["message"] for item in result["sc3"])
|
||
assert len(result["sc3"]) >= 1
|
||
# 4) BaseField 우선: 수식 무시되고 기본 비목 매핑되어 오탐 0건
|
||
assert result["sc4"] == []
|
||
# 5) 정상 범위 (A:B): 정상 합산되어 오탐 0건
|
||
assert result["sc5"] == []
|
||
# 6) 비정상 범위: 오류 정확 탐지
|
||
assert any("범위 참조 없음: A:INVALID" in item["message"] for item in result["sc6"])
|
||
assert len(result["sc6"]) == 1
|
||
|
||
|
||
def test_wonga_scan_parity_with_calculate_forward_refs_old_cost_bases_ranges_and_cycles():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
// 1) 전방 참조 및 오래된 Cost:
|
||
// B의 수식이 'A*2'로 실제 값은 20이지만 저장된 Cost=0 상태.
|
||
// C가 '100/B'를 계산할 때, 오래된 Cost(0)를 쓰면 0으로 나눔 오류이지만,
|
||
// calculate와 동일하게 재귀 평가 시 B=20이 되어 C=5 정상 계산되고 오탐 없어야 함.
|
||
const w1 = [
|
||
{Gubun:'B',Cost:0,VSusic:'A*2'},
|
||
{Gubun:'A',Cost:10},
|
||
{Gubun:'C',VSusic:'100/B'}
|
||
];
|
||
const calc1 = EstWonga.calculate(w1);
|
||
const scan1 = EstWonga.scan(w1);
|
||
|
||
// 2) 기초 비목 0 및 양수 전달:
|
||
// NC=1000이 전달되면 BaseField: 'NC' 또는 식 내 'NC' 참조가 정상 작동.
|
||
// scan에서 bases가 전달되지 않아 0으로 판정되는 문제가 없어야 함.
|
||
const w2 = [
|
||
{Gubun:'A',BaseField:'NC',Cost:0},
|
||
{Gubun:'B',VSusic:'NC * 0.1'},
|
||
{Gubun:'C',VSusic:'1000 / B'}
|
||
];
|
||
const bases = { NC: 1000, 직접공사비: 1000 };
|
||
const calc2 = EstWonga.calculate(w2, {}, bases);
|
||
const scan2 = EstWonga.scan(w2, {}, bases);
|
||
|
||
// 3) 구간 제외 행(CalcIndex: 2 또는 3):
|
||
// 범위 (A:B)에서 A가 CalcIndex: 2(제외)인 경우 합산에서 제외되나,
|
||
// 순환 참조 및 범위 유효성 규칙은 calculate와 일치.
|
||
const w3 = [
|
||
{Gubun:'A',Cost:10,CalcIndex:2},
|
||
{Gubun:'B',Cost:20,CalcIndex:0},
|
||
{Gubun:'C',VSusic:'(A:B)'}
|
||
];
|
||
const calc3 = EstWonga.calculate(w3);
|
||
const scan3 = EstWonga.scan(w3);
|
||
|
||
// 4) 순환 참조 일치성:
|
||
// calculate에서 발생하는 순환 오류와 scan의 오류 메시지가 동일
|
||
const w4 = [
|
||
{Gubun:'X',VSusic:'Y'},
|
||
{Gubun:'Y',VSusic:'X'}
|
||
];
|
||
const calc4 = EstWonga.calculate(w4);
|
||
const scan4 = EstWonga.scan(w4);
|
||
|
||
// 5) issues.scan(project)에서 project.calculation.direct 기초금액 전달 검증
|
||
const project = EstNative.empty();
|
||
project.tables.Wonga = [
|
||
{Gubun:'M',BaseField:'NC',Cost:0},
|
||
{Gubun:'N',VSusic:'NC * 0.5'},
|
||
{Gubun:'O',VSusic:'500 / N'}
|
||
];
|
||
project.calculation = {
|
||
direct: { NC: 1000, GC: 0, JC: 0, SC: 0, TC: 1000 }
|
||
};
|
||
const projectIssues = EstIssues.scan(project).filter(i => i.table === 'Wonga');
|
||
|
||
return {
|
||
calc1Errors: calc1.errors,
|
||
scan1,
|
||
calc1CostB: calc1.rows[0].Cost,
|
||
calc1CostC: calc1.rows[2].Cost,
|
||
calc2Errors: calc2.errors,
|
||
scan2,
|
||
calc3Errors: calc3.errors,
|
||
scan3,
|
||
calc3CostC: calc3.rows[2].Cost,
|
||
calc4Errors: calc4.errors,
|
||
scan4,
|
||
projectIssues
|
||
};
|
||
})()""",
|
||
)
|
||
# 1) 전방 참조 & 오래된 Cost(0): calculate와 scan 모두 오류 0건, 정확한 값 평가
|
||
assert result["calc1Errors"] == []
|
||
assert result["scan1"] == []
|
||
assert result["calc1CostB"] == 20
|
||
assert result["calc1CostC"] == 5
|
||
|
||
# 2) 기초 비목 양수 전달: bases 전달로 0 나눔 없이 정상 평가
|
||
assert result["calc2Errors"] == []
|
||
assert result["scan2"] == []
|
||
|
||
# 3) 구간 제외 행(CalcIndex: 2): A=10 제외, B=20만 합산되어 C=20, scan 오류 0건
|
||
assert result["calc3Errors"] == []
|
||
assert result["scan3"] == []
|
||
assert result["calc3CostC"] == 20
|
||
|
||
# 4) 순환 참조: calculate와 scan 메시지 일치
|
||
assert "원가 수식 순환 참조: X" in result["calc4Errors"][0]
|
||
assert any("원가 수식 순환 참조" in item["message"] for item in result["scan4"])
|
||
|
||
# 5) project.calculation.direct를 통한 issues.scan 연동: 오탐 0건
|
||
assert result["projectIssues"] == []
|
||
|
||
|
||
def test_data_issues_detect_unite_mismatch_and_sync_source_names():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p = EstNative.empty();
|
||
p.tables.LaborcDat = [
|
||
{ Code: "L001", Name: "보통인부", Size: "공통", Unite: "인" },
|
||
{ Code: "L002", Name: "특별인부", Size: "숙련(원천)", Unite: "인" },
|
||
{ Code: "L003", Name: "용접공", Size: "1급", Unite: "인" }
|
||
];
|
||
p.tables.EstMainDat = [
|
||
// 1) 품명 불일치 (원천 규격, 단위와 동일)
|
||
{ Code: "L001", LinkIndex: 6, Name: "보통 인부(오타)", Size: "공통", Unite: "인" },
|
||
// 2) 규격 불일치
|
||
{ Code: "L002", LinkIndex: 6, Name: "특별인부", Size: "숙련(내역)", Unite: "인" },
|
||
// 3) 단위 불일치 (일괄 반영 대상에서 제외되어야 함)
|
||
{ Code: "L003", LinkIndex: 6, Name: "용접공", Size: "1급", Unite: "시간" }
|
||
];
|
||
|
||
const beforeScan = EstIssues.scan(p);
|
||
const nameIssue = beforeScan.find(i => i.message === "연결 품명 차이");
|
||
const sizeIssue = beforeScan.find(i => i.code === "L002" && i.message === "연결 규격 차이");
|
||
const uniteIssue = beforeScan.find(i => i.message === "연결 단위 차이");
|
||
|
||
return {
|
||
hasNameIssue: !!nameIssue,
|
||
hasSizeIssue: !!sizeIssue,
|
||
hasUniteIssue: !!uniteIssue,
|
||
nameSource: nameIssue?.sourceName,
|
||
sizeSource: sizeIssue?.sourceSize,
|
||
uniteSource: uniteIssue?.sourceUnite
|
||
};
|
||
})()""",
|
||
)
|
||
assert result["hasNameIssue"] is True
|
||
assert result["hasSizeIssue"] is True
|
||
assert result["hasUniteIssue"] is True
|
||
assert result["nameSource"] == "보통인부"
|
||
assert result["sizeSource"] == "숙련(원천)"
|
||
assert result["uniteSource"] == "인"
|
||
|
||
|
||
def test_data_issues_selective_sync_preserves_unit_and_non_empty_size():
|
||
from playwright.sync_api import sync_playwright
|
||
import pytest
|
||
|
||
with sync_playwright() as p:
|
||
browser = p.chromium.launch(channel="msedge", headless=True)
|
||
page = browser.new_page()
|
||
try:
|
||
page.goto("http://100.65.141.15:5175", timeout=5000)
|
||
except Exception:
|
||
pytest.skip("EST 5175 server not reachable")
|
||
page.wait_for_selector("body")
|
||
page.wait_for_function("() => EstApp && EstApp.state && EstApp.state.project !== null")
|
||
|
||
# 테스트용 데이터 주입
|
||
page.evaluate("""() => {
|
||
EstApp.change(p => {
|
||
p.tables.LaborcDat ||= [];
|
||
p.tables.LaborcDat.push(
|
||
{ Code: "TEST_L1", Name: "원천 보통인부", Size: "원천 규격A", Unite: "인" },
|
||
{ Code: "TEST_L2", Name: "원천 특별인부", Size: "원천 규격B(신규)", Unite: "인" },
|
||
{ Code: "TEST_L3", Name: "원천 용접공", Size: "원천 규격C", Unite: "인" }
|
||
);
|
||
p.tables.EstMainDat ||= [];
|
||
p.tables.EstMainDat.push(
|
||
// 1) 품명만 다름 (내역 규격은 동일하므로 품명만 동기화)
|
||
{ Code: "TEST_L1", LinkIndex: 6, Name: "내역 보통인부(오타)", Size: "원천 규격A", Unite: "인" },
|
||
// 2) 규격 차이 (원천 규격으로 동기화)
|
||
{ Code: "TEST_L2", LinkIndex: 6, Name: "원천 특별인부", Size: "내역 구규격", Unite: "인" },
|
||
// 3) 단위 차이 (일괄 반영 대상에서 제외되어야 함)
|
||
{ Code: "TEST_L3", LinkIndex: 6, Name: "원천 용접공", Size: "원천 규격C", Unite: "시간" }
|
||
);
|
||
}, false);
|
||
EstApp.navigate('issues');
|
||
}""")
|
||
|
||
page.wait_for_function("() => EstApp.state.view === 'issues'")
|
||
|
||
# 1) '불일치 원천 명칭/규격 일괄 반영' 버튼 클릭
|
||
sync_all_btn = page.locator("button[data-issue-sync-all='1']")
|
||
assert sync_all_btn.count() >= 1
|
||
sync_all_btn.click()
|
||
|
||
# 일괄 반영 후 결과 점검
|
||
state1 = page.evaluate("""() => {
|
||
const est = EstApp.state.project.tables.EstMainDat;
|
||
const r1 = est.find(r => r.Code === "TEST_L1");
|
||
const r2 = est.find(r => r.Code === "TEST_L2");
|
||
const r3 = est.find(r => r.Code === "TEST_L3");
|
||
return {
|
||
r1: { Name: r1.Name, Size: r1.Size, Unite: r1.Unite },
|
||
r2: { Name: r2.Name, Size: r2.Size, Unite: r2.Unite },
|
||
r3: { Name: r3.Name, Size: r3.Size, Unite: r3.Unite }
|
||
};
|
||
}""")
|
||
|
||
# TEST_L1: 품명은 원천으로 동기화, 규격/단위는 일치 유지
|
||
assert state1["r1"]["Name"] == "원천 보통인부"
|
||
assert state1["r1"]["Size"] == "원천 규격A"
|
||
assert state1["r1"]["Unite"] == "인"
|
||
|
||
# TEST_L2: 유효한 원천 규격으로 동기화됨
|
||
assert state1["r2"]["Size"] == "원천 규격B(신규)"
|
||
|
||
# TEST_L3: 단위 불일치는 일괄 반영에서 제외되어 "시간" 그대로 유지됨
|
||
assert state1["r3"]["Unite"] == "시간"
|
||
|
||
# 2) TEST_L3의 '원천단위 반영' 개별 버튼 명시적 클릭 확인
|
||
sync_unit_btn = page.locator("button[data-issue-sync-msg='연결 단위 차이']")
|
||
assert sync_unit_btn.count() >= 1
|
||
assert "원천단위 반영" in sync_unit_btn.first.inner_text()
|
||
sync_unit_btn.first.click()
|
||
|
||
# 개별 반영 후 단위가 동기화되었는지 확인
|
||
r3_unite_after = page.evaluate("""() => {
|
||
const est = EstApp.state.project.tables.EstMainDat;
|
||
const r3 = est.find(r => r.Code === "TEST_L3");
|
||
return r3.Unite;
|
||
}""")
|
||
assert r3_unite_after == "인"
|
||
|
||
# 3) 실행 취소(Undo)로 이전 복원 검증
|
||
page.evaluate("EstActions.undo()")
|
||
r3_unite_undone = page.evaluate("""() => {
|
||
const est = EstApp.state.project.tables.EstMainDat;
|
||
const r3 = est.find(r => r.Code === "TEST_L3");
|
||
return r3.Unite;
|
||
}""")
|
||
assert r3_unite_undone == "시간"
|
||
|
||
browser.close()
|
||
|
||
|
||
def test_data_issues_detect_percentage_unit_mismatch():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p = structuredClone(project);
|
||
p.tables.HeadMainDat.push(
|
||
{ Index: 0, Level: 0, LinkIndex: 58, Code: "R1", Name: "비율항목1", Unite: "식", GN: 5 },
|
||
{ Index: 0, Level: 0, LinkIndex: 62, Code: "R2", Name: "구간비율2", Unite: "M", GN: 10 },
|
||
{ Index: 0, Level: 0, LinkIndex: 52, Code: "R3", Name: "낙찰률3", Unite: "%", GN: 87.745 },
|
||
{ Index: 0, Level: 0, LinkIndex: 67, Code: "R4", Name: "제잡비4", Unite: "%", GN: 3 }
|
||
);
|
||
p.tables.EstMainDat.push(
|
||
{ Level: 0, LinkIndex: 57, Code: "R5", Name: "조달수수료5", Unite: "원", GN: 1 }
|
||
);
|
||
const issues = EstIssues.scan(p).filter(x => x.message === "단위 백분율(%) 불일치");
|
||
return issues.map(x => ({ table: x.table, code: x.code, message: x.message, rowUnite: x.rowUnite }));
|
||
})()""",
|
||
)
|
||
assert len(result) == 3
|
||
items = {(x["table"], x["code"], x["rowUnite"]) for x in result}
|
||
assert ("HeadMainDat", "R1", "식") in items
|
||
assert ("HeadMainDat", "R2", "M") in items
|
||
assert ("EstMainDat", "R5", "원") in items
|
||
|