const { start, BASE, DIR, assert, fs, path, sanitized, } = require('./portal-visual-qa-lib.cjs'); const crypto = require('node:crypto'); const mode = process.argv[2]; void (async () => { const q = await start('next-' + mode + '-chromium-20261010'); let page, ctx; async function open(route) { const r = await q.open(page, route); const html = await r.text(); const script = html.match( /]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/, ); const attr = html.match(/data-page="([^"]+)"/); const raw = attr && attr[1].startsWith('{') ? attr[1] .replace(/"/g, '"') .replace(/&/g, '&') .replace(/'/g, "'") : script?.[1]; assert(raw, 'Inertia props'); return JSON.parse(raw).props; } async function actor(role) { if (ctx) await ctx.close(); ({ ctx, page } = await q.context(role)); await q.login(page, role); } async function mutation(url, data, method = 'POST') { return page.evaluate( async ({ url, data, method }) => { const c = document.cookie .split('; ') .find((c) => c.startsWith('XSRF-TOKEN=')); const r = await fetch(url, { method, headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-XSRF-TOKEN': c ? decodeURIComponent(c.slice(11)) : '', }, ...(method === 'GET' || method === 'HEAD' ? {} : { body: JSON.stringify(data) }), }); return { status: r.status, data: await r.json() }; }, { url, data, method }, ); } async function save(button, url, method) { const wait = page.waitForResponse( (r) => new URL(r.url()).pathname === url && r.request().method() === method, ); await button.click(); const r = await wait; assert([200, 201, 302, 303].includes(r.status())); await page.waitForLoadState('networkidle'); return r.status(); } try { if (mode === 'organization') { q.proof.fixture = q.prior.fixture || {}; const f = q.proof.fixture; await actor('promoter'); let p = await open('/investidor/profile'); assert(p.organizationCountryAvailable); const org = p.organizations.find( (o) => o.id === '99964753-2632-4658-a687-8ebc6a178296', ); assert(org?.canEdit); f.organization = org.id; const name = 'QA reconciliação país mestre — cópia explícita'; let company = p.companies.find((c) => c.payload.name === name); if (!company) { const box = page.locator('section').filter({ has: page.getByRole('heading', { name: 'Acrescentar empresa', exact: true, }), }); await box .getByLabel('Nome da empresa', { exact: true }) .fill(name); await box .locator('select[id^="incorporation_country_code"]') .selectOption('MZ'); await box .getByLabel('Função ou relação com a empresa', { exact: true, }) .fill('Declaração sintética QA, sem mandato real'); await save( box.getByRole('button', { name: 'Guardar declaração da empresa', exact: true, }), '/investidor/companies', 'POST', ); p = await open('/investidor/profile'); company = p.companies.find((c) => c.payload.name === name); } f.company = company.id; q.save(); if (!f.old_submission) { const section = page.locator('section').filter({ has: page.getByRole('heading', { name, exact: true }), }); await save( section.getByRole('button', { name: 'Preparar manifestação desta empresa', exact: true, }), '/investidor/manifestacoes', 'POST', ); await page.waitForURL((u) => /^\/investidor\/manifestacoes\/.+/.test(u.pathname), ); f.old_submission = new URL(page.url()).pathname .split('/') .pop(); q.save(); } let old = await open( '/investidor/manifestacoes/' + f.old_submission, ); assert.equal( old.submission.payload.incorporation_country_code, 'MZ', ); f.old_version = old.submission.version; q.save(); p = await open('/investidor/profile'); const section = page.getByRole('region', { name: 'Cadastro mestre: Synthetic QA organization', exact: true, }); if (!f.master_saved) { q.phase( 'C97: master country with explicit source, validation error and keyboard focus', ); await section .getByRole('combobox', { name: 'País de constituição da organização', exact: true, }) .selectOption(''); await section .getByLabel('Fonte ou referência deste país', { exact: true, }) .fill(''); await save( section.getByRole('button', { name: 'Guardar país no cadastro mestre', exact: true, }), '/investidor/organizations/' + org.id + '/country', 'PUT', ); const invalid = section.getByRole('combobox', { name: 'País de constituição da organização', exact: true, }); await page.waitForFunction( () => document.activeElement?.getAttribute('aria-invalid') === 'true', ); assert.equal( await invalid.getAttribute('aria-invalid'), 'true', ); assert( await invalid.evaluate((n) => document.activeElement === n), ); assert(await invalid.getAttribute('aria-describedby')); q.proof.checks.master_errors_named_and_focused = true; await q.shot(page, 'master-error-focused', false); await invalid.selectOption('FR'); await section .getByLabel('Fonte ou referência deste país', { exact: true, }) .fill( 'Declaração sintética QA do representante, 10/10/2026; sem registo ou decisão real.', ); await save( section.getByRole('button', { name: 'Guardar país no cadastro mestre', exact: true, }), '/investidor/organizations/' + org.id + '/country', 'PUT', ); p = await open('/investidor/profile'); assert.equal( p.organizations.find((o) => o.id === org.id).country, 'FR', ); f.master_saved = true; f.master_version = p.organizations.find( (o) => o.id === org.id, ).version; q.save(); } if (!f.reused) { const section = page.locator('section').filter({ has: page.getByRole('heading', { name, exact: true }), }); await save( section.getByRole('button', { name: 'Reutilizar país de Synthetic QA organization', exact: true, }), '/investidor/companies/' + f.company + '/organization-country', 'POST', ); p = await open('/investidor/profile'); company = p.companies.find((c) => c.id === f.company); assert.equal(company.payload.incorporation_country_code, 'FR'); assert.equal(company.countrySource.organization_id, org.id); f.reused = true; q.save(); } if (!f.new_submission) { const section = page.locator('section').filter({ has: page.getByRole('heading', { name, exact: true }), }); await save( section.getByRole('button', { name: 'Preparar manifestação desta empresa', exact: true, }), '/investidor/manifestacoes', 'POST', ); await page.waitForURL((u) => /^\/investidor\/manifestacoes\/.+/.test(u.pathname), ); f.new_submission = new URL(page.url()).pathname .split('/') .pop(); q.save(); } const newSubmission = await open( '/investidor/manifestacoes/' + f.new_submission, ); assert.equal( newSubmission.submission.payload.incorporation_country_code, 'FR', ); assert.equal( newSubmission.submission.payload.organization_country_snapshot .organization_version, f.master_version, ); if (!f.master_changed) { p = await open('/investidor/profile'); const box = page.getByRole('region', { name: 'Cadastro mestre: Synthetic QA organization', exact: true, }); await box .getByRole('combobox', { name: 'País de constituição da organização', exact: true, }) .selectOption('ZA'); await box .getByLabel('Fonte ou referência deste país', { exact: true, }) .fill( 'Segunda declaração sintética QA: ensaio de cópias históricas preservadas.', ); await save( box.getByRole('button', { name: 'Guardar país no cadastro mestre', exact: true, }), '/investidor/organizations/' + org.id + '/country', 'PUT', ); f.master_changed = true; q.save(); } q.phase( 'Fresh session: current master ZA, prior copies MZ/FR and provenance preserved', ); await actor('promoter'); p = await open('/investidor/profile'); assert.equal( p.organizations.find((o) => o.id === org.id).country, 'ZA', ); assert.equal( p.companies.find((c) => c.id === f.company).payload .incorporation_country_code, 'FR', ); await q.shot(page, 'master-versus-explicit-copy'); old = await open('/investidor/manifestacoes/' + f.old_submission); assert.equal( old.submission.payload.incorporation_country_code, 'MZ', ); assert.equal(old.submission.version, f.old_version); const later = await open( '/investidor/manifestacoes/' + f.new_submission, ); assert.equal( later.submission.payload.incorporation_country_code, 'FR', ); assert.equal( later.submission.payload.organization_country_snapshot .organization_version, f.master_version, ); q.proof.checks.new_session_no_historical_rewrite = true; await actor('outsider'); p = await open('/investidor/profile'); assert(!p.organizations.some((o) => o.id === org.id)); const denied = await mutation( '/investidor/organizations/' + org.id + '/country', { version: f.master_version + 1, country: 'MZ', source_reference: 'Tentativa sintética de outra organização', }, 'PUT', ); assert.equal(denied.status, 403); q.proof.checks.other_organization_denied = true; } else if (mode === 'sharing') { const old = JSON.parse( fs.readFileSync( path.join(DIR, 'independent-documents-http-qa.json'), 'utf8', ), ); const room = old.receipts.room_id; assert.equal(room, 1); q.proof.documents = []; await actor('promoter'); let p = await open('/parcerias/salas/' + room); assert.equal(p.room.id, room); const owner = { page, ctx }; ({ page, ctx } = await q.context('colleague')); await q.login(page, 'colleague'); let candidate = { page, ctx }; const base = '/parcerias/salas/' + room; for (const d of old.room) { q.phase('Visual explicit sharing and revocation ' + d.type); page = owner.page; ctx = owner.ctx; p = await open(base); const record = p.documents.find( (x) => x.id === d.attachment_id, ); assert(record && !record.shared); const article = page.locator('article').filter({ has: page.getByText(record.file_name, { exact: false }), }); await save( article.getByRole('button', { name: 'Autorizar partilha com a contraparte', exact: true, }), base + '/documentos/' + d.attachment_id + '/partilha', 'POST', ); page = candidate.page; ctx = candidate.ctx; p = await open(base); assert(p.documents.some((x) => x.id === d.attachment_id)); const response = await page.request.get( BASE + base + '/documentos/' + d.attachment_id, ); assert.equal(response.status(), 200); assert.equal( crypto .createHash('sha256') .update(await response.body()) .digest('hex'), d.sha256, ); await q.shot(page, 'shared-' + d.type, false); page = owner.page; ctx = owner.ctx; await open(base); const card = page.locator('article').filter({ has: page.getByText(record.file_name, { exact: false }), }); await save( card.getByRole('button', { name: 'Retirar autorização de partilha', exact: true, }), base + '/documentos/' + d.attachment_id + '/partilha', 'POST', ); page = candidate.page; ctx = candidate.ctx; const denied = await page.request.get( BASE + base + '/documentos/' + d.attachment_id, ); assert.equal(denied.status(), 404); p = await open(base); assert(!p.documents.some((x) => x.id === d.attachment_id)); await q.shot(page, 'revoked-' + d.type, false); q.proof.documents.push({ type: d.type, id: d.attachment_id, sha256: d.sha256, previously_allowed: 200, same_session_after_revocation: 404, listing_removed: true, }); q.save(); } await candidate.ctx.close(); ({ page, ctx } = await q.context('colleague')); await q.login(page, 'colleague'); p = await open(base); for (const d of q.proof.documents) { const denied = await page.request.get( BASE + base + '/documentos/' + d.id, ); assert.equal(denied.status(), 404); assert(!p.documents.some((x) => x.id === d.id)); d.fresh_session_after_revocation = 404; } await ctx.close(); page = owner.page; ctx = owner.ctx; for (const d of q.proof.documents) { const r = await page.request.get( BASE + base + '/documentos/' + d.id, ); assert.equal(r.status(), 200); assert.equal( crypto .createHash('sha256') .update(await r.body()) .digest('hex'), d.sha256, ); } q.proof.checks.visual_revocation_previously_granted_urls = true; q.proof.checks.owner_originals_preserved = true; } q.proof.result = 'passed'; } catch (e) { q.proof.result = 'failed'; q.proof.error = sanitized(e); if ( page && !['/login', '/two-factor-challenge'].includes( new URL(page.url()).pathname, ) ) { q.proof.failure = { route: new URL(page.url()).pathname, alerts: await page.getByRole('alert').allTextContents(), buttons: await page.getByRole('button').allTextContents(), }; await q.shot(page, 'failure', false).catch(() => {}); } } finally { q.proof.completed_at = new Date().toISOString(); q.save(); await q.browser.close(); } console.log( JSON.stringify({ result: q.proof.result, phase: q.proof.phase, checks: q.proof.checks, error: q.proof.error, }), ); if (q.proof.result === 'failed') process.exitCode = 1; })();