53 lines
14 KiB
JavaScript
53 lines
14 KiB
JavaScript
// Only the remaining lease branch; reuse prior sale/materiality/coediting/A/B/C37 evidence.
|
||
const {start,assert,DIR,fs,path,sanitized}=require('./portal-functional-qa-lib.cjs');
|
||
const crypto=require('node:crypto');
|
||
void(async()=>{
|
||
const q=await start('functional-lease-workflow-chromium-20261011'),sessions=new Map();let page,p;
|
||
const id=54,title='Arrendamento — SIMULAÇÃO QA 11/10/2026, sem efeito jurídico';
|
||
const plan=JSON.parse(fs.readFileSync(path.join(DIR,'portal-stability-latest.json'),'utf8'));
|
||
const source=fs.readFileSync(path.join(plan.source,'resources/js/components/opportunity-canonical-fields.tsx'),'utf8');
|
||
const labelBlock=source.slice(source.indexOf('const labels:'),source.indexOf('function',source.indexOf('const labels:')));
|
||
const labels=Object.fromEntries([...labelBlock.matchAll(/^\s*(\w+):\s*'([^']+)'/gm)].map(m=>[m[1],m[2]]));
|
||
q.proof.opportunity_id=id;q.proof.documents=q.prior.documents||{};q.proof.location=q.prior.location;q.proof.checks={...(q.prior.checks||{})};q.proof.transitions=q.prior.transitions||[];
|
||
q.proof.reused=['Sale case31','6-category materiality/conditions','coediting QA22','multiple-role self-approval rejection','A/B and C37 closures'];
|
||
async function actor(role,fresh=false){if(fresh&&sessions.has(role)){await sessions.get(role).ctx.close();sessions.delete(role);}if(!sessions.has(role)){const s=await q.context(role);sessions.set(role,s);await q.login(s.page,role);}page=sessions.get(role).page;}
|
||
async function open(route=`/minhas-oportunidades/${id}/view`){const r=await q.open(page,route);assert.equal(r.status(),200);const html=await r.text(),a=html.match(/data-page="([^"]+)"/);p=JSON.parse(a&&a[1].startsWith('{')?a[1].replace(/"/g,'"').replace(/&/g,'&').replace(/'/g,"'"):html.match(/<script[^>]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/)[1]).props;await page.locator('main').first().waitFor();return p;}
|
||
async function rent(stage){await open();assert.equal(p.values.canonical.real_estate.rent,4500);assert.equal(p.values.canonical.real_estate.rent_periodicity,'monthly');assert.equal(p.values.canonical.currency,'MZN');const api=await q.json(page,`/minhas-oportunidades/${id}/response`);assert.equal(api.status,200);q.proof.transitions.push({stage,state:p.workflowState,rent:4500,periodicity:'monthly',currency:'MZN',response_status:200,version:p.canonicalResponse.data.version});q.save();}
|
||
async function action(name,gate){await open();assert(p.profileActions.includes(name),'Authorized '+name);const box=page.getByRole('region',{name:'Workflow interno',exact:true});await box.getByLabel('Fundamentação',{exact:true}).fill('SIMULAÇÃO QA54 de '+name+': preservar renda 4500 MZN/mês, documentos próprios e revisão independente; sem direito real.');
|
||
if(name==='request_correction')await box.getByLabel('Campos ou documentos a corrigir, um por linha',{exact:true}).fill('canonical.real_estate.title_holder_name — explicitar que o titular e o documento são simulações QA sem efeito jurídico.');
|
||
if(gate){await box.getByRole('combobox',{name:/^Revisão/}).selectOption(gate);await box.getByRole('combobox',{name:/^Decisão/}).selectOption('approved');await box.getByLabel(q.proof.documents[{property_review:'valuation',registry_urban_review:'title',building_compliance_review:'license'}[gate]].title,{exact:true}).check();}
|
||
if(name==='publish')await box.getByLabel(/Confirmei a pré-visualização/).check();
|
||
const names={submit:'Submeter para validação',start_review:'Iniciar validação',request_correction:'Solicitar correções',review_gate:'Guardar revisão especializada',approve:'Aprovar versão',publish:'Disponibilizar com acesso restrito'};
|
||
const pending=page.waitForResponse(r=>r.request().method()==='POST'&&new URL(r.url()).pathname===`/minhas-oportunidades/${id}/workflow`);await box.getByRole('button',{name:names[name],exact:true}).click();const r=await pending,body=await r.json();assert.equal(r.status(),200,JSON.stringify(body));await rent(name+(gate?':'+gate:''));
|
||
}
|
||
async function field(path_,value){const el=page.locator(`[id="${path_}"]`);await el.waitFor({state:'attached'});const tag=await el.evaluate(e=>e.tagName);if(tag==='SELECT')await el.selectOption(String(value));else await el.fill(String(value));}
|
||
async function values(data,prefix='canonical'){
|
||
for(const [key,v]of Object.entries(data)){if(['opportunity_category','promoter_id'].includes(key))continue;const here=prefix+'.'+key;
|
||
if(prefix==='canonical'&&['sector_id','subsector_id','economic_activity_id','internal_contact_id','public_contact_id'].includes(key)){await page.getByRole('combobox',{name:new RegExp('^'+labels[key])}).selectOption(String(v));continue;}
|
||
if(prefix==='canonical'&&['documents','locations','images'].includes(key)){const rows=key==='locations'?[q.proof.location]:Object.values(q.proof.documents);for(const row of rows)await page.getByLabel(row.title,{exact:true}).check();continue;}
|
||
if(Array.isArray(v)){const label=labels[key]??key.replaceAll('_',' '),box=page.locator('fieldset').filter({has:page.locator('legend').filter({hasText:new RegExp('^'+label.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')+'$')})}).filter({has:page.getByRole('button',{name:'Adicionar linha',exact:true})}).last();
|
||
for(let i=0;i<v.length;i++){const probe=typeof v[i]==='object'?Object.keys(v[i])[0]:null;const exists=await page.locator(`[id="${here}.${i}${probe?'.'+probe:''}"]`).count();if(!exists)await box.locator(':scope > button').filter({hasText:'Adicionar linha'}).click();if(typeof v[i]==='object')await values(v[i],here+'.'+i);else await field(here+'.'+i,v[i]);}continue;}
|
||
if(v&&typeof v==='object'){await values(v,here);continue;}await field(here,typeof v==='boolean'?String(v):v);
|
||
}
|
||
}
|
||
async function save(){await page.getByLabel('Motivo da alteração',{exact:true}).fill('SIMULAÇÃO QA54: arrendamento com documentos próprios; conservar a renda 4500 MZN/mês e os IDs existentes.');const pending=page.waitForResponse(r=>r.request().method()==='PUT'&&new URL(r.url()).pathname===`/minhas-oportunidades/${id}/draft`);await page.getByRole('button',{name:'Guardar contrato no rascunho',exact:true}).click();const r=await pending;assert.equal(r.status(),200,JSON.stringify(await r.json()));}
|
||
async function binary(url){return page.evaluate(async u=>{const r=await fetch(u);const bytes=await r.arrayBuffer();const hash=await crypto.subtle.digest('SHA-256',bytes);return{status:r.status,sha256:[...new Uint8Array(hash)].map(b=>b.toString(16).padStart(2,'0')).join('')};},url);}
|
||
try{
|
||
await actor('promoter');q.phase('Lease own documents and real QA scanning');
|
||
for(const kind of ['title','license','valuation']){if(q.proof.documents[kind])continue;await open(`/minhas-oportunidades/${id}/resources`);await page.getByRole('button',{name:'Documentos',exact:true}).click();const docTitle='SIMULAÇÃO QA54 — '+{title:'título e registo fictícios',license:'licença de utilização fictícia',valuation:'avaliação indicativa fictícia'}[kind];const file=path.join(DIR,`functional-lease-${kind}-20261011.pdf`),sha=crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');await page.getByLabel('Título',{exact:true}).fill(docTitle);await page.getByLabel('Classificação documental',{exact:true}).selectOption('restricted');await page.getByLabel('Âmbito de consulta',{exact:true}).selectOption('internal');await page.locator('input[type="file"]').setInputFiles(file);await page.getByLabel('Motivo (20–1000 caracteres)',{exact:true}).fill('SIMULAÇÃO QA54 sem efeito jurídico: ficheiro próprio para verificar a condição documental de arrendamento.');const pending=page.waitForResponse(r=>r.request().method()==='POST'&&new URL(r.url()).pathname.endsWith('/resources/documents'));await page.getByRole('button',{name:'Guardar recurso',exact:true}).click();const r=await pending;assert.equal(r.status(),201);const resource=(await r.json()).id;q.proof.documents[kind]={id:resource,title:docTitle,sha256:sha};q.save();const url=`/minhas-oportunidades/${id}/resources/documents/${resource}/download`;let found;for(let i=0;i<40;i++){found=await binary(url);if(found.status===200)break;assert.equal(found.status,423);await page.waitForTimeout(1000);}assert.equal(found.status,200);assert.equal(found.sha256,sha);q.proof.documents[kind].clean_download_verified=true;q.save();}
|
||
if(!q.proof.location){q.phase('Lease own location and administrative map');await open(`/minhas-oportunidades/${id}/resources`);const choose=async(label,re)=>{const s=page.getByRole('combobox',{name:label,exact:true}),o=s.locator('option').filter({hasText:re}).first();await o.waitFor({state:'attached'});await s.selectOption(await o.getAttribute('value'));};await choose('Província',/^Cabo Delgado/i);await choose('Distrito',/^Ancuabe/i);await page.getByLabel('Papel da localização',{exact:true}).selectOption('storage');await page.getByLabel('Natureza da relação territorial',{exact:true}).selectOption('context');await page.getByLabel('Localização principal',{exact:true}).check();await page.getByLabel('Título',{exact:true}).fill('SIMULAÇÃO QA54 — contexto administrativo Cabo Delgado/Ancuabe');await page.getByLabel('Motivo (20–1000 caracteres)',{exact:true}).fill('SIMULAÇÃO QA54: contexto administrativo sem atribuição de localização real nem direito territorial.');const pending=page.waitForResponse(r=>r.request().method()==='POST'&&new URL(r.url()).pathname.endsWith('/resources/locations'));await page.getByRole('button',{name:'Guardar recurso',exact:true}).click();const r=await pending;assert.equal(r.status(),201);q.proof.location={id:(await r.json()).id,title:'SIMULAÇÃO QA54 — contexto administrativo Cabo Delgado/Ancuabe'};q.save();}
|
||
if(!q.proof.checks.complete_contract_saved){q.phase('Lease canonical fields through the editor');await open(`/minhas-oportunidades/${id}/edit`);const baseline=JSON.parse(fs.readFileSync(path.join(DIR,'functional-lease-context-qa-20261011.json'),'utf8'))['31'].canonical;
|
||
const data={...baseline,official_title:title,executive_summary:'SIMULAÇÃO QA de arrendamento sem efeito jurídico. '.repeat(10),demand_statement:'Pretensão fictícia exclusivamente para verificar o workflow independente de arrendamento. '.repeat(3),review_due_date:'2027-01-01',documents:Object.values(q.proof.documents).map(d=>d.id),locations:[q.proof.location.id],evidences:Object.entries(q.proof.documents).map(([kind,d])=>({title:d.title,evidence_type:'qa-synthetic-'+kind,document_id:d.id})),real_estate:{...baseline.real_estate,transaction_type:'lease',rent:4500,rent_periodicity:'monthly',registration_number:'SIMULAÇÃO-QA54',title_holder_name:'Titular fictício da organização QA',title_document_id:q.proof.documents.title.id,use_license_document_id:q.proof.documents.license.id,valuation:{date:'2026-10-11',value:1000,method:'qa-simulation-only',appraiser:'SIMULAÇÃO QA54',document_id:q.proof.documents.valuation.id}}};delete data.real_estate.price;await values(data);await save();q.proof.checks.complete_contract_saved=true;q.save();}
|
||
await actor('promoter',true);await rent('complete_draft_new_session');for(const d of Object.values(q.proof.documents)){assert.equal((await binary(`/minhas-oportunidades/${id}/resources/documents/${d.id}/download`)).sha256,d.sha256);}q.proof.checks.documents_recovered_new_session=true;
|
||
if(!q.proof.checks.first_submission){q.phase('Lease submission');await action('submit');q.proof.checks.first_submission=true;q.save();}
|
||
if(!q.proof.checks.correction_requested){await actor('validator');q.phase('Independent validator requests correction');await action('start_review');await action('request_correction');assert.equal(p.workflowState,'needs_information');q.proof.checks.correction_requested=true;q.save();}
|
||
if(!q.proof.checks.corrected_and_resubmitted){await actor('promoter',true);await rent('correction_recovered_new_session');await open(`/minhas-oportunidades/${id}/edit`);await field('canonical.real_estate.title_holder_name','SIMULAÇÃO QA54 — titular fictício sem direito real atribuído');await save();await rent('corrected_draft');await action('submit');q.proof.checks.corrected_and_resubmitted=true;q.save();}
|
||
if(!q.proof.checks.approved){await actor('validator');await open();if(p.workflowState==='submitted')await action('start_review');for(const gate of ['property_review','registry_urban_review','building_compliance_review']){if(p.profileContext.reviews?.[gate]?.decision!=='approved')await action('review_gate',gate);}await action('approve');assert.equal(p.profileContext.approval.actor_id,24);q.proof.checks.approved=true;q.save();}
|
||
if(!q.proof.checks.published_restricted){await actor('publisher');q.phase('Explicit restricted publication, no public catalogue');await action('publish');assert.equal(p.workflowState,'published');assert.equal(p.publicationState.public_catalog,false);assert.equal(p.profileContext.publication.actor_id,25);q.proof.checks.published_restricted=true;await q.shot(page,'lease-publication-restricted',false);q.save();}
|
||
await actor('promoter',true);await rent('published_recovered_new_session');assert.equal(p.workflowState,'published');assert(!p.profileActions.includes('approve'));assert(!p.profileActions.includes('publish'));assert.equal(p.values.canonical.real_estate.title_document_id,q.proof.documents.title.id);assert.equal(p.values.canonical.real_estate.use_license_document_id,q.proof.documents.license.id);await q.shot(page,'lease-published-recovered',false);q.proof.checks.creator_review_buttons_denied=true;
|
||
await actor('outsider');assert.equal((await q.open(page,`/minhas-oportunidades/${id}/edit`)).status(),403);const denied=await binary(`/minhas-oportunidades/${id}/resources/documents/${q.proof.documents.title.id}/download`);assert([403,404].includes(denied.status));q.proof.checks.outside_organization_denied=true;
|
||
q.proof.result='passed';q.save();console.log(JSON.stringify({result:'passed',id,checks:q.proof.checks,transitions:q.proof.transitions.length,active_writes:0}));
|
||
}catch(e){q.proof.result='failed';q.proof.failure=sanitized(e);if(page&&!page.isClosed()){q.proof.last_route=new URL(page.url()).pathname;q.proof.visible_alerts=await page.getByRole('alert').allTextContents();}q.save();console.log(JSON.stringify({result:'failed',failure:q.proof.failure,route:q.proof.last_route,alerts:q.proof.visible_alerts}));process.exitCode=1;}
|
||
finally{for(const s of sessions.values())await s.ctx.close();await q.browser.close();}
|
||
})();
|