Files
SGF/scripts/stock-trace-ui.mjs
2026-11-04 09:37:20 +02:00

47 lines
8.8 KiB
JavaScript

import fs from 'node:fs';import path from 'node:path';import assert from 'node:assert/strict';
import React from 'react';import {renderToStaticMarkup} from 'react-dom/server';import {createServer} from 'vite';
import {openStockReceipt,receiptMovementHref} from '../resources/js/stock-receipt-trace.js';
import {readFocus} from '../public/purchasing-focus.mjs';
import {openReceiptMovement} from '../public/stock-receipt-focus.mjs';
const tag=process.argv[2];assert(/^[a-f0-9]{12}$/.test(tag??''));const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=path.join(run.root,'private/stock-trace-read-'+tag),out=path.join(root,'stock-trace-ui.json');assert(!fs.existsSync(out));
const checks=[],ok=(v,name)=>{assert(v,name);checks.push(name);};
const context={module_id:'stock',token:'old',company:{tenant_id:2,id:3},unit:{id:4},purchasing:{receipts:true},modules:[{id:'stock'},{id:'purchasing'}],grants:[{permission:'stock.view',unit_id:4},{permission:'purchasing_receipts.view',unit_id:4}]};
const next={context:{...context,module_id:'purchasing',token:'new'}},calls=[];
const options={movement:71,context,active:()=>true,select:async body=>{calls.push(['select',body]);return next;},commit:v=>calls.push(['commit',v.context.token]),go:href=>calls.push(['go',href])};
await openStockReceipt(options);
ok(JSON.stringify(calls)===JSON.stringify([['select',{tenant_id:2,company_id:3,unit_id:4,module_id:'purchasing'}],['commit','new'],['go','/purchasing/receipts?movement=71']]),'explicit handoff selects the same company/unit before committing token and navigating locally');
for(const id of [0,-1,1.5,'71',Number.MAX_SAFE_INTEGER+1]){assert.throws(()=>receiptMovementHref(id));}ok(true,'invalid movement identifiers cannot become navigation targets');
for(const mutation of [c=>c.grants.splice(0,1),c=>c.grants.splice(1,1),c=>c.unit.id=5,c=>c.module_id='purchasing',c=>c.modules=[],c=>c.purchasing.receipts=false]){const c=structuredClone(context);mutation(c);let selected=false;await assert.rejects(openStockReceipt({...options,context:c,select:async()=>{selected=true;}}));assert(!selected);}ok(true,'missing rights unit module or capability cannot initiate a context change');
calls.length=0;await assert.rejects(openStockReceipt({...options,active:()=>false}));ok(calls.length===0,'stale source context is rejected before requesting a transition');
calls.length=0;await assert.rejects(openStockReceipt({...options,select:async()=>{throw Error('403');}}));ok(calls.length===0,'refused module transition leaves navigation and committed token unchanged');
let active=true;calls.length=0;await assert.rejects(openStockReceipt({...options,active:()=>active,select:async()=>{active=false;return next;}}));ok(calls.length===0,'late transition response cannot navigate after another context wins');
calls.length=0;await assert.rejects(openStockReceipt({...options,select:async()=>({context:{...next.context,unit:{id:8}}})}));ok(calls.length===0,'unexpected destination scope cannot be committed or navigated');
ok(readFocus('receipts','?movement=71').movement===71&&readFocus('receipts','?order=41&receipt=91').receipt===91,'movement startup and existing order/receipt deep links remain explicit');
for(const query of ['?movement=71&movement=72','?movement=71&order=41','?movement=71&receipt=91','?movement=01','?movement=71&url=https://example.com'])assert.throws(()=>readFocus('receipts',query));ok(true,'ambiguous mixed or external origin hints are rejected');
let epoch=1,opened=[],problem='';await openReceiptMovement({movement:71,current:()=>epoch,resolve:async()=>({movement_id:71,order_id:41,receipt_id:91,read_only:true}),open:async(...ids)=>opened.push(ids),error:e=>problem=e});ok(JSON.stringify(opened)==='[[41,91]]'&&!problem,'destination resolves the exact current parent and receipt before opening evidence');
opened=[];await openReceiptMovement({movement:71,current:()=>epoch,resolve:async()=>{epoch++;return {movement_id:71,order_id:41,receipt_id:91,read_only:true};},open:async(...ids)=>opened.push(ids),error:e=>problem=e});ok(opened.length===0,'source resolution arriving after another selection cannot open an old receipt');
problem='';await openReceiptMovement({movement:71,current:()=>epoch,resolve:async()=>({movement_id:72,order_id:41,receipt_id:91,read_only:true}),open:async(...ids)=>opened.push(ids),error:e=>problem=e});ok(opened.length===0&&problem.includes('conferência'),'inconsistent resolver response opens no fallback receipt');
const server=await createServer({configFile:false,server:{middlewareMode:true,watch:null},appType:'custom',optimizeDeps:{noDiscovery:true}});
try{const {StockReceiptOrigin}=await server.ssrLoadModule('/resources/js/StockReadWorkspace.jsx');let chosen=null;const props={row:{id:71,movement_type:'receipt_in'},enabled:true,onOpen:id=>chosen=id,loading:false};
const button=StockReceiptOrigin(props);button.props.onClick();ok(chosen===71&&renderToStaticMarkup(React.createElement(StockReceiptOrigin,props)).includes('Conferir recepção em Compras'),'actual React origin action uses only the displayed movement id');
ok(StockReceiptOrigin({...props,enabled:false})===null&&StockReceiptOrigin({...props,row:{id:71,movement_type:'workshop_return_in'}})===null&&StockReceiptOrigin({...props,row:{id:'71',movement_type:'receipt_in'}})===null,'actual origin action is absent without capability for other movement kinds or invalid ids');
ok(StockReceiptOrigin({...props,loading:true}).props.disabled===true,'origin action is disabled while its source query is loading');
}finally{await server.close();}
class Element{constructor(tag,text){this.tag=tag;this.textContent=text??'';this.children=[];this.dataset={focusEnabled:'true',context:'qa-context',csrf:'qa-csrf'};this.isConnected=true;}append(...nodes){this.children.push(...nodes);}prepend(...nodes){this.children.unshift(...nodes);}replaceChildren(...nodes){this.children=[...nodes];}setAttribute(k,v){this[k]=v;}dispatchEvent(){}}
globalThis.Option=class extends Element{constructor(text,value){super('option',text);this.value=value;}};globalThis.FormData=class{*[Symbol.iterator](){}};
let elements,requests,fail=false,wrongParent=false,serial=0;const walk=e=>[e,...e.children.flatMap(walk)],paginator=data=>({data,current_page:1,last_page:1,total:data.length});
globalThis.document={createElement:tag=>new Element(tag),querySelector:selector=>{if(!elements.has(selector))elements.set(selector,new Element(selector));return elements.get(selector);}};
globalThis.fetch=async(url,options={})=>{requests.push({url,method:options.method??'GET'});let value;
if(url==='/api/purchasing-receipts/movements/71/origin'){if(fail)return {ok:false,json:async()=>({message:'Origem recusada'})};value={movement_id:71,order_id:41,receipt_id:91,read_only:true};}
else if(url==='/api/purchasing-receipts/orders/41')value={order:{id:41,number:'OC-TRACE'},progress:{state:'complete'},history:paginator([]),can_confirm:false};
else if(url==='/api/purchasing-receipts/history/91')value={receipt:{id:91,purchase_order_id:wrongParent?42:41,number:'REC-TRACE'},documents_enabled:false};
else if(url==='/api/purchasing-receipts?page=1')value=paginator([]);else throw Error('Unexpected request '+url);
return {ok:true,json:async()=>structuredClone(value)};};
const settle=()=>new Promise(resolve=>setTimeout(resolve,10));async function mount(search){elements=new Map;requests=[];globalThis.location={search};await import('../public/receipt-workspace.mjs?trace='+tag+'-'+serial++);await settle();await settle();return {requests:[...requests],nodes:walk(document.querySelector('#detail')),message:document.querySelector('#message').textContent};}
let result=await mount('?movement=71');ok(result.nodes.some(e=>e.textContent.includes('REC-TRACE'))&&result.requests.some(r=>r.url==='/api/purchasing-receipts/movements/71/origin'),'actual receipt workspace opens resolved evidence even outside its first list page');
ok(result.requests.every(r=>r.method==='GET'),'actual movement-focused startup performs no preview or business confirmation');
fail=true;result=await mount('?movement=71');ok(result.message==='Origem recusada'&&!result.requests.some(r=>r.url.includes('/orders/')),'actual startup refusal preserves empty evidence and chooses no fallback order');fail=false;
wrongParent=true;result=await mount('?movement=71');ok(result.message.includes('não pertence')&&!result.nodes.some(e=>e.textContent.includes('REC-TRACE')),'actual receipt workspace still rejects a parent mismatch before rendering evidence');wrongParent=false;
result=await mount('?movement=71&order=41');ok(result.message.includes('Atalho inválido')&&result.requests.length===1,'actual mixed link lists only and never resolves a guessed source');
fs.writeFileSync(out,JSON.stringify({passed:true,checks,simulated_dom:true,react_component:true,context_selection_only:true,business_commands:0,visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length}));