from test_est_rea_parity import run 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.crypto={randomUUID:()=>String(++id)};globalThis.confirm=()=>true; const state={quantitySheet:'manual',project:{quantitySheets:[],tables:{EstMainDat:[{Code:'X',Level:0,GN:1}]}}}; globalThis.EstApp={state,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({quantityTarget:'0'},'X'); change({quantityLine:'0'},'2*3 = ⟦결과⟧0⟦결과⟧'); 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,remaining:state.project.quantitySheets.length,selection:state.quantitySheet}; })()""", ) assert result["saved"]["quantitySheets"][0]["Name"] == "Earthwork" assert result["saved"]["quantitySheets"][0]["items"][0]["Name"] == "excavation" 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}; globalThis.crypto={randomUUID:()=>String(++id)}; const state={quantitySheet:'manual',project:{quantitySheets:[],tables:{EstMainDat:[]}}}; globalThis.EstApp={state,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"])