Files
SGF/scripts/sgf-statements-ui.mjs
2026-11-11 09:00:17 +02:00

8 lines
10 KiB
JavaScript

import fs from 'node:fs';import assert from 'node:assert/strict';import {pathToFileURL} from 'node:url';import {transformWithOxc} from 'vite';import {checkedStatements} from '../resources/js/internal-statements-view.js';
const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',sample=JSON.parse(fs.readFileSync(root+'/statements-ui-sample-private.json')),checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);};let slots=[],cursor=0,effects=[],tree,calls=[],uncertain=true;
const history={policy:'SGF-INTERNAL-STATEMENTS-QA-v1',view:'history',records:{data:[],current_page:1,last_page:1,total:0},mapping_latest:null,mapping_approved:null,result_latest:null,result_approved:null,capabilities:{prepare:true,review:false}},same=(a,b)=>a&&b&&a.length===b.length&&a.every((v,i)=>v===b[i]);let state=structuredClone(history);const R={createElement:(type,props,...children)=>({type,props:props??{},children}),Fragment:Symbol('fragment'),useRef(value){const i=cursor++;slots[i]??={current:value};return slots[i];},useState(initial){const i=cursor++;slots[i]??={value:initial};return[slots[i].value,v=>{slots[i].value=typeof v==='function'?v(slots[i].value):v;}];},useMemo(fn,deps){const i=cursor++;if(!same(slots[i]?.deps,deps))slots[i]={value:fn(),deps};return slots[i].value;},useEffect(fn,deps){const i=cursor++;if(!same(slots[i]?.deps,deps)){slots[i]?.cleanup?.();slots[i]={deps};effects.push(()=>slots[i].cleanup=fn());}}};globalThis.__statementsHooks=R;const stub='const r=globalThis.__statementsHooks;export default r;export const useState=r.useState,useMemo=r.useMemo,useEffect=r.useEffect,useRef=r.useRef;';let src=fs.readFileSync('resources/js/InternalStatementsWorkspace.jsx','utf8').replace("from 'react'",'from '+JSON.stringify('data:text/javascript;base64,'+Buffer.from(stub).toString('base64')));for(const f of ['statement-download.js','workshop-stock-operation.js','ledger-report-view.js','internal-statements-view.js'])src=src.replace("from './"+f+"'",'from '+JSON.stringify(pathToFileURL(process.cwd()+'/resources/js/'+f).href));const compiled=await transformWithOxc(src,'InternalStatementsWorkspace.jsx',{jsx:{runtime:'classic'}}),{default:View,StatementGroups,StatementLines}=await import('data:text/javascript;base64,'+Buffer.from(compiled.code).toString('base64'));
const api=async(path,method='GET',body,token,key)=>{calls.push({path,method,body:structuredClone(body),key});if(path.endsWith('/preview'))return{preview:'encrypted-test-cut',impact:{kind:body.kind,before:{version:body.version,state:null},after:{version:body.version+1,state:'prepared'},data:body.kind==='mapping'?{entries:body.data.entries.map(e=>({...e,account:{code:sample.source.rows.find(r=>r.account_id===e.account_id).code}}))}:{summary:sample.summary,csv_sha256:'a'.repeat(64)},new_journals:0}};if(path.endsWith('/confirm')){if(uncertain){uncertain=false;const e=Error('QA response lost');e.status=503;throw e;}return{id:123,kind:'mapping',version:1,state:'prepared',replayed:true};}if(path.includes('view=result'))return JSON.parse(fs.readFileSync(root+'/statements-sample-private.json'));if(path.includes('view=accounts'))return{...history,view:'accounts',records:{data:sample.source.rows.map(r=>({id:r.account_id,code:r.code,label:r.label,currency:r.currency})),current_page:1,last_page:1,total:sample.source.rows.length}};return structuredClone(state);};const props={api,context:{token:'qa-statements'}},render=()=>{cursor=0;tree=View(props);const jobs=effects;effects=[];jobs.forEach(f=>f());return tree;},tick=async()=>{await new Promise(r=>setTimeout(r,0));render();},descend=n=>Array.isArray(n)?n.flatMap(descend):n&&typeof n==='object'?[n,...descend(n.children)]:[],nodes=(n=tree)=>descend(n),text=n=>Array.isArray(n)?n.map(text).join(''):n&&typeof n==='object'?text(typeof n.type==='function'?n.type(n.props):n.children):String(n??''),find=(tag,label)=>nodes().find(n=>n.type===tag&&text(n).includes(label)),click=label=>{const b=find('button',label);assert(b,label);assert(!b.props.disabled,label+' disabled');b.props.onClick();render();};
render();await tick();ok(text(tree).includes('Sem classificação automática')&&text(tree).includes('modelo legal'),'scope explicit');click('Preparar correspondências explícitas');await tick();const selects=nodes().filter(n=>n.type==='select');ok(selects.filter(n=>n.props.value==='').length>=2,'account and category not automatically chosen');ok(nodes().filter(n=>n.type==='textarea').every(n=>n.props.value===''),'new evidence reason blank');selects.find(n=>n.children.flat(Infinity).some(c=>c?.props?.children)||n.props.value==='').props.onChange({target:{value:String(sample.source.rows[0].account_id)}});render();const category=nodes().filter(n=>n.type==='select').find(n=>n.children.flat(Infinity).some(c=>c?.props?.value==='asset'));assert(category);category.props.onChange({target:{value:'asset'}});render();click('Adicionar/substituir esta correspondência');ok(text(tree).includes('Conta#'+sample.source.rows[0].account_id),'only selected account added');let areas=nodes().filter(n=>n.type==='textarea');areas[0].props.onChange({target:{value:'QA explicit evidence for classification.'}});render();areas=nodes().filter(n=>n.type==='textarea');areas[1].props.onChange({target:{value:'QA individual reason for presentation.'}});render();await find('form','Conferir impacto').props.onSubmit({preventDefault(){}});render();ok(calls.find(c=>c.path.endsWith('/preview')).body.data.entries.length===1,'preview sends explicit single mapping only');ok(text(tree).includes('Impacto a confirmar')&&text(tree).includes(sample.source.rows[0].code),'preview shows real selected account and impact');click('Confirmar esta decisão individual');await tick();ok(text(tree).includes('Resultado incerto')&&find('button','Descartar formulário').props.disabled,'uncertain operation preserves draft and prevents discard');click('Recuperar a mesma confirmação');await tick();const sent=calls.filter(c=>c.path.endsWith('/confirm'));ok(sent.length===2&&JSON.stringify(sent[0])===JSON.stringify(sent[1]),'recovery keeps body and key');ok(text(tree).includes('recibo recuperado'),'recovery acknowledged');
state={...history,mapping_approved:{id:55,version:2,data:{entries:[]}}};slots=[];effects=[];calls=[];render();await tick();click('Preparar resultado com datas explícitas');ok(nodes().some(n=>n.type==='select'&&n.props.required&&n.props.value===''),'approved mapping version still explicitly chosen');ok(nodes().filter(n=>n.type==='input'&&n.props.type==='date').every(n=>n.props.value===''&&n.props.required),'both report dates explicit empty');const groupsText=text(StatementGroups({summary:sample.summary}));ok(groupsText.includes('Balanço interno parcial')&&groupsText.includes('Resultados internos parciais')&&groupsText.includes('Contas por classificar'),'balance income and unresolved sections separate');ok(groupsText.includes('Capital próprio: indeterminado')&&groupsText.includes('Não é resultado da empresa'),'capital and subtotal honestly labelled');ok(groupsText.includes('386,21')&&groupsText.includes('9 conta(s) por classificar'),'accepted source classification subtotal and nine pending');const emptySummary={currencies:{},accounts:0};ok(text(StatementGroups({summary:emptySummary})).includes('Não se fabricam rubricas com saldo zero'),'empty source no invented amounts');const noIncome=structuredClone(sample.summary);noIncome.currencies.MZN.groups.income.presentation_minor=null;noIncome.currencies.MZN.groups.income.accounts=0;ok(text(StatementGroups({summary:noIncome})).includes('Sem conta observada'),'no rubric account distinct from zero');const lines=StatementLines({rows:sample.rows,dates:sample.summary.query});const anchor=nodes(lines).find(n=>n.type==='a');ok(anchor.props.href.includes('account_id='+sample.rows[0].account_id)&&anchor.props.href.includes('from_on=2026-11-10'),'drilldown preserves account and dates');
const q={view:'result',page:1,per_page:25},contract={...history,view:'result',snapshot:{summary:sample.summary}};ok(checkedStatements(contract,q)===contract,'result page contract accepted');assert.throws(()=>checkedStatements({...contract,records:{...contract.records,current_page:2}},q));ok(true,'late wrong page refused');const actual=JSON.parse(fs.readFileSync(root+'/statements-sample-private.json')),progress=JSON.parse(fs.readFileSync(root+'/statements-http-progress.json'));state={...history,records:{data:[{id:progress.resultReview.result.id,kind:'result',version:2,state:'approved',host_actor_id:16,created_at:'QA',reason:'QA reviewed snapshot',data:actual.snapshot}],current_page:1,last_page:1,total:1}};slots=[];effects=[];calls=[];render();await tick();click('Abrir resultado revisto desta versão');await tick();const downloadButton=find('button','Descarregar CSV congelado desta revisão');ok(downloadButton&&!nodes().some(n=>n.type==='a'&&String(n.props.href).includes('/statements/exports/')),'download uses guarded action not raw API link');const bytes=fs.readFileSync(root+'/statements-frozen-export-private.csv'),oldFetch=globalThis.fetch,oldDocument=globalThis.document;let clicked=0,requestHeaders;globalThis.fetch=async(path,options)=>{requestHeaders=options.headers;return new Response(bytes,{headers:{'Content-Type':'text/csv; charset=UTF-8','X-Content-SHA256':actual.snapshot.csv_sha256}});};globalThis.document={createElement(tag){assert.equal(tag,'a');return{click(){clicked++;}};}};try{await downloadButton.props.onClick();render();ok(clicked===1,'actual download handler saves exactly once after verification');ok(requestHeaders['X-SGF-Context']===props.context.token,'component forwards actual context token');ok(text(tree).includes('CSV congelado descarregado e SHA verificado.'),'download success shown only after SHA check');}finally{globalThis.fetch=oldFetch;globalThis.document=oldDocument;}const file=root+'/statements-ui-20261111.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,actual_component_handlers:true,api_mocked:true,browser_visual_review:false,business_commands:0},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));