from test_est_rea_parity import run def test_excel_quantity_cell_rejects_invalid_address_or_non_numeric_value(): result = run( "math", """(()=>{ let type='n',value='6.25'; const sheet={getAttribute:k=>k==='name'?'Data':null,getAttributeNS:()=> 'r1'}; const rel={getAttribute:k=>({Id:'r1',Target:'/xl/worksheets/sheet1.xml'})[k]}; const cell={getAttribute:k=>({r:'B2',t:type})[k],getElementsByTagNameNS:()=>[{textContent:value}]}; const doc=items=>({querySelector:()=>null,getElementsByTagNameNS:()=>items}); globalThis.DOMParser=class{parseFromString(x){return x==='workbook'?doc([sheet]):x==='rels'?doc([rel]):doc([cell])}}; const files={'xl/workbook.xml':'workbook','xl/_rels/workbook.xml.rels':'rels','xl/worksheets/sheet1.xml':'sheet'}; const valid=EstQuantity.excelCell(files,'Data','b2');let rejected=0; for(const [name,address,t,v] of [['Other','B2','n','1'],['Data','XFE1','n','1'],['Data','B3','n','1'],['Data','B2','s','1'],['Data','B2','n',''],['Data','B2','n','1e100']]){ type=t;value=v;try{EstQuantity.excelCell(files,name,address)}catch{rejected++} } type='n';value='8';const p={tables:{EstMainDat:[{PkID:1,GN:2},{PkID:2,GN:3}]},excelQuantityLinks:[{target:1,file:'a.xlsx',sheet:'Data',cell:'B2'},{target:2,file:'a.xlsx',sheet:'Data',cell:'B2'}]}; const updates=EstQuantity.excelUpdates(p,'a.xlsx',files).map(x=>x.value),before=p.tables.EstMainDat.map(x=>x.GN);let bad=0; p.excelQuantityLinks[1].cell='B3';try{EstQuantity.excelUpdates(p,'a.xlsx',files)}catch{bad++} p.tables.EstMainDat.push({PkID:1});try{EstQuantity.excelUpdates(p,'a.xlsx',files)}catch{bad++} try{EstQuantity.excelUpdates(p,'missing.xlsx',files)}catch{bad++} return {valid,rejected,updates,before,bad}; })()""", ) assert result == {"valid": 6.25, "rejected": 6, "updates": [8, 8], "before": [2, 3], "bad": 3} def test_excel_quantity_links_validate_and_survive_project_validation(): result = run( "math", """(()=>{ const p=EstNative.empty();p.excelQuantityLinks=[{target:1,file:'a.xlsx',sheet:'Data',cell:'B2'}]; const valid=EstModel.validate(p).excelQuantityLinks;let rejected=0; for(const bad of [[valid[0],valid[0]],[{...valid[0],target:'1'}],[{...valid[0],cell:'wrong'}],[{...valid[0],cell:'XFE1'}],[{...valid[0],cell:'A1048577'}]]){ const q=EstNative.empty();q.excelQuantityLinks=bad;try{EstModel.validate(q)}catch{rejected++} } return {valid,rejected}; })()""", ) assert result["valid"] == [{"target": 1, "file": "a.xlsx", "sheet": "Data", "cell": "B2"}] assert result["rejected"] == 5 def test_deleted_excel_link_target_id_is_reserved_for_new_estimate_rows(): result = run( "math", """(()=>{ const p=EstNative.empty();p.tables.EstMainDat=[{PkID:1,Code:'A'}]; p.excelQuantityLinks=[{target:2,file:'a.xlsx',sheet:'Data',cell:'B2'}]; const row=EstModel.newRow('EstMainDat',p.tables.EstMainDat,p); const other=EstModel.newRow('StuffcDat',[],p); return {next:row.PkID,other:other.PkID}; })()""", ) assert result == {"next": 3, "other": 1} def test_apply_all_quantities_sums_sheets_and_rejects_errors_atomically(): result = run( "math", """(()=>{ const p=EstNative.empty();p.tables.EstMainDat=[{PkID:1,Code:'A',GN:1},{PkID:2,Code:'B',GN:2}]; p.quantities=[{Code:'Q1',formula:'2',TargetKey:'id:1'}]; p.quantitySheets=[{Code:'S1',Name:'sheet',options:{},rows:[{LineData:'3 = \\x120\\x12',markers:['a']}], items:[{Code:'a',TargetKey:'id:1'}]}, {Code:'S2',Name:'sheet2',options:{},rows:[{LineData:'4 = \\x120\\x12',markers:['b']}], items:[{Code:'b',TargetKey:'id:2'}]}]; const values=EstQuantity.allTotals(p);EstQuantity.applyTotals(p,values); const updated=p.tables.EstMainDat.map(row=>row.GN),before=JSON.stringify(p.tables.EstMainDat); p.quantitySheets[1].rows[0].LineData='bad';let rejected=false; try{EstQuantity.applyTotals(p,EstQuantity.allTotals(p))}catch{rejected=true} return {updated,rejected,unchanged:before===JSON.stringify(p.tables.EstMainDat)}; })()""", ) assert result == {"updated": [5, 4], "rejected": True, "unchanged": True} def test_apply_all_quantities_rejects_duplicate_or_reserved_sheet_codes(): result = run( "math", """(()=>{ const p=EstNative.empty();p.tables.EstMainDat=[{PkID:1,Code:'A',GN:1}]; p.quantities=[{Code:'Q1',formula:'2',TargetKey:'id:1'}]; let rejected=0; for(const codes of [['S','S'],['manual'],['']]) { p.quantitySheets=codes.map(Code=>({Code,rows:[],items:[],options:{}})); try{EstQuantity.allTotals(p)}catch{rejected++} } return {rejected,GN:p.tables.EstMainDat[0].GN}; })()""", ) assert result == {"rejected": 3, "GN": 1} def test_quantity_apply_requires_one_detail_row_per_code(): result = run( "math", """(()=>{ function apply(rows,items=[{Code:'r1',GN:7,TargetCode:'X'}]){ const handlers={}; globalThis.document={addEventListener:(name,fn)=>handlers[name]=fn}; const sheet={Code:'Q',rows:[],items,options:{}}; const state={quantitySheet:'Q',project:{quantitySheets:[sheet],tables:{EstMainDat:rows}}}; globalThis.EstApp={state,change:fn=>fn(),render:()=>{}}; EstQuantity.install();handlers.click({target:{closest:selector=>selector==='[data-quantity-apply]'?{}:null}}); return {rows,notice:state.notice||''}; } return {duplicate:apply([{Code:'X',Level:0,GN:1},{Code:'X',Level:0,GN:2}]), heading:apply([{Code:'X',Level:1,GN:1}]), valid:apply([{Code:'X',Level:1,GN:1},{Code:'X',Level:0,GN:2}]), partial:apply([{Code:'X',Level:0,GN:1}],[{Code:'r1',GN:7,TargetCode:'X'},{Code:'r2',GN:4,TargetCode:'missing'}]), disconnected:apply([{Code:'X',Level:0,GN:1}],[{Code:'r1',GN:7,TargetCode:'',LinkCode:'X'}])}; })()""", ) assert [r["GN"] for r in result["duplicate"]["rows"]] == [1, 2] assert result["duplicate"]["notice"] assert result["heading"]["rows"][0]["GN"] == 1 assert result["heading"]["notice"] assert [r["GN"] for r in result["valid"]["rows"]] == [1, 7] assert result["valid"]["notice"] == "" assert result["partial"]["rows"][0]["GN"] == 1 assert result["partial"]["notice"] assert result["disconnected"]["rows"][0]["GN"] == 1 assert result["disconnected"]["notice"] def test_create_edit_apply_and_delete_quantity_sheet(): result = run( "math", """(()=>{ const handlers={};let id=0; globalThis.document={addEventListener:(name,fn)=>handlers[name]=fn}; globalThis.confirm=()=>true; const state={quantitySheet:'manual',project:{quantitySheets:[],tables:{EstMainDat:[{Code:'X',Level:0,GN:1}]}}}; globalThis.EstApp={state,uid:()=>String(++id),change:fn=>fn(),render:()=>{}}; EstQuantity.install(); const click=(selector,dataset={})=>handlers.click({target:{closest:s=>s===selector?{dataset}:null}}); const change=(dataset,value)=>handlers.change({target:{dataset,value}}); click('[data-quantity-new]');click('[data-quantity-add]'); change({quantityName:''},'Earthwork'); change({quantityItem:'Name',index:'0'},'excavation'); change({quantityItem:'Unite',index:'0'},'m3'); change({quantityTarget:'0'},'X'); change({quantityLine:'0'},'2*3 = ⟦결과⟧0⟦결과⟧'); click('[data-quantity-add-item]'); const withNewItem = state.project.quantitySheets[0].items.length; // 참조 중인 0번 항목 삭제 시도 -> 차단 및 오류 메시지 설정 확인 click('[data-quantity-delete-item]', { quantityDeleteItem: '0' }); const noticeBlocked = state.notice; const blockedCount = state.project.quantitySheets[0].items.length; // 미참조 항목인 1번 항목 삭제 시도 -> 정상 삭제 click('[data-quantity-delete-item]', { quantityDeleteItem: '1' }); const afterItemDeleted = state.project.quantitySheets[0].items.length; const saved=JSON.parse(JSON.stringify(state.project)); click('[data-quantity-apply]'); const applied=state.project.tables.EstMainDat[0].GN; const sheet=state.project.quantitySheets[0]; const computed=EstQuantity.calculate(sheet); click('[data-quantity-delete-row]',{quantityDeleteRow:'0'}); const empty=[sheet.rows.length,sheet.items.length]; click('[data-quantity-delete-sheet]'); return {saved,applied,computed,empty,withNewItem,blockedCount,afterItemDeleted,noticeBlocked,remaining:state.project.quantitySheets.length,selection:state.quantitySheet}; })()""", ) assert result["withNewItem"] == 2 assert result["blockedCount"] == 2 assert "연결된 산출 행을 삭제하거나" in result["noticeBlocked"] assert result["afterItemDeleted"] == 1 assert result["saved"]["quantitySheets"][0]["Name"] == "Earthwork" assert result["saved"]["quantitySheets"][0]["items"][0]["Name"] == "excavation" assert result["saved"]["quantitySheets"][0]["items"][0]["Unite"] == "m3" assert result["computed"]["errors"] == [] assert result["computed"]["items"][0]["GN"] == 6 assert result["applied"] == 6 assert result["empty"] == [0, 0] assert result["remaining"] == 0 assert result["selection"] == "manual" def test_insert_and_move_quantity_rows_keeps_item_results_linked(): result = run( "math", """(()=>{ const handlers={};let id=0; globalThis.document={addEventListener:(name,fn)=>handlers[name]=fn}; const state={quantitySheet:'manual',project:{quantitySheets:[],tables:{EstMainDat:[]}}}; globalThis.EstApp={state,uid:()=>String(++id),change:fn=>fn(),render:()=>{}};EstQuantity.install(); const click=(selector,dataset={})=>handlers.click({target:{closest:s=>s===selector?{dataset}:null}}); click('[data-quantity-new]');click('[data-quantity-add]');click('[data-quantity-add]'); const sheet=state.project.quantitySheets[0]; sheet.rows[0].LineData='1+2 = \\x120\\x12';sheet.rows[1].LineData='4+5 = \\x120\\x12'; click('[data-quantity-add]',{index:'0'}); sheet.rows[1].LineData='6+7 = \\x120\\x12'; const before=EstQuantity.calculate(sheet).items.map(x=>x.GN); click('[data-quantity-move]',{index:'2',quantityMove:'-1'}); const after=EstQuantity.calculate(sheet).items.map(x=>x.GN); const order=sheet.rows.map(x=>x.LineData); sheet.rows[0].LineData='A=2 = \\x120\\x12';sheet.rows[1].LineData='A*3 = \\x120\\x12'; const linked=EstQuantity.calculate(sheet); click('[data-quantity-move]',{index:'1',quantityMove:'-1'}); const forward=EstQuantity.calculate(sheet); return {before,after,order,linked,forward}; })()""", ) assert result["before"] == result["after"] == [3, 9, 13] assert result["order"][1].startswith("4+5") assert result["linked"]["errors"] == [] assert result["linked"]["items"][1]["GN"] == 6 assert result["forward"]["errors"] def test_manual_and_selected_sheet_totals_validate_before_application(): result = run( "math", """(()=>{ const p=EstNative.empty(); p.tables.EstMainDat=[{Code:'X',Level:0,GN:1,MulCode:{binary:'YWJj'}}]; p.quantities=[{Code:'A',formula:'2',TargetCode:'X'}, {Code:'B',formula:'[A]*3',TargetCode:'X'}]; const manual=EstQuantity.totals(p,'manual');EstQuantity.applyTotals(p,manual); const a=structuredClone(p.tables.EstMainDat[0]); p.quantitySheets=[{Code:'Q',options:{},rows:[{LineData:'4+5 = \\x120\\x12',markers:['r1']}], items:[{Code:'r1',GN:0,TargetCode:'X'}]}]; const sheet=EstQuantity.totals(p,'Q');EstQuantity.applyTotals(p,sheet); let invalid='';p.quantities.push({Code:'C',formula:'1',TargetCode:'missing'}); try{EstQuantity.totals(p,'manual')}catch(e){invalid=e.message} return {a,b:p.tables.EstMainDat[0],invalid}; })()""", ) assert result["a"]["GN"] == 8 assert result["a"]["MulCode"] == {"binary": "YWJj"} assert result["b"]["GN"] == 9 assert result["b"]["MulCode"] == {"binary": "YWJj"} assert result["invalid"] def test_new_quantity_code_does_not_reuse_existing_or_referenced_codes(): result = run( "math", """[EstQuantity.newCode([{Code:'Q2',formula:'[Q1]*3'}]), EstQuantity.newCode([{Code:'Q2',formula:'1'}]), EstQuantity.newCode([])]""", ) assert result == ["Q3", "Q1", "Q1"] def test_target_row_ids_support_blank_and_duplicate_codes_for_both_modes(): result = run( "math", """(()=>{ const p=EstNative.empty();p.tables.EstMainDat=[ {PkID:1,Code:'X',GN:1},{PkID:2,Code:'X',GN:2},{PkID:3,Code:'',GN:3}]; p.quantities=[{Code:'A',formula:'7',TargetCode:'X',TargetKey:'id:2'}, {Code:'B',formula:'4',TargetCode:'',TargetKey:'id:3'}]; EstQuantity.applyTotals(p,EstQuantity.totals(p,'manual')); const manual=p.tables.EstMainDat.map(row=>row.GN); p.quantitySheets=[{Code:'Q',options:{},rows:[{LineData:'2+3 = \\x120\\x12',markers:['r1']}], items:[{Code:'r1',GN:0,TargetCode:'X',TargetKey:'id:1'}]}]; p.tables.EstMainDat.reverse();EstQuantity.applyTotals(p,EstQuantity.totals(p,'Q')); const sheet=p.tables.EstMainDat.map(row=>[row.PkID,row.GN]); p.quantitySheets[0].items[0].TargetKey='id:missing';let error=''; try{EstQuantity.totals(p,'Q')}catch(e){error=e.message} return {manual,sheet,error}; })()""", ) assert result["manual"] == [1, 7, 4] assert result["sheet"] == [[3, 4], [2, 7], [1, 5]] assert result["error"] def test_missing_or_extra_result_markers_block_quantity_application(): result = run( "math", """(()=>{ const p=EstNative.empty();p.tables.EstMainDat=[{PkID:1,Code:'X',GN:1}]; p.quantitySheets=[{Code:'Q',options:{},rows:[{LineData:'2*3 = \\x120\\x12',markers:['r1']}], items:[{Code:'r1',GN:9,TargetKey:'id:1'}]}]; const sheet=p.quantitySheets[0],valid=EstQuantity.calculate(sheet); const bad=[];for(const line of ['2*3 = 0','2*3 = \\x120\\x12 + \\x120\\x12']){ sheet.rows[0].LineData=line;let blocked=false; try{EstQuantity.applyTotals(p,EstQuantity.totals(p,'Q'))}catch{blocked=true} bad.push({blocked,errors:EstQuantity.calculate(sheet).errors,quantity:p.tables.EstMainDat[0].GN}); } return {valid,bad}; })()""", ) assert result["valid"]["errors"] == [] assert result["valid"]["items"][0]["GN"] == 6 assert all(row["blocked"] and row["errors"] and row["quantity"] == 1 for row in result["bad"])