// Incremental UI proofs; use the established local QA Chromium, never active data. const { start, BASE, DIR, actors: _actors, assert, fs, path, sanitized, } = require('./portal-visual-qa-lib.cjs'); const mode = process.argv[2] || 'inspect'; void (async () => { const q = await start('consumer-' + mode + '-chromium-20261010'); let page, ctx; async function bootstrap(route) { const r = await q.open(page, route), html = await r.text(); const attr = html.match(/data-page="([^"]+)"/); const raw = attr && attr[1].startsWith('{') ? attr[1] .replace(/"/g, '"') .replace(/&/g, '&') .replace(/'/g, "'") : html.match( /]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/, )?.[1]; assert(raw, 'Inertia bootstrap'); return JSON.parse(raw).props; } async function mutation(url, data, method = 'POST') { return page.evaluate( async ({ url, data, method }) => { const cookie = 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': cookie ? decodeURIComponent(cookie.slice(11)) : '', }, ...(method === 'GET' || method === 'HEAD' ? {} : { body: JSON.stringify(data) }), }); return { status: r.status, data: await r.json() }; }, { url, data, method }, ); } async function clickSave(button, url, method) { const pending = page.waitForResponse( (r) => new URL(r.url()).pathname === url && r.request().method() === method, ); await button.click(); const r = await pending; assert( [200, 201, 302, 303].includes(r.status()), 'Expected saved response ' + r.status(), ); await page.waitForLoadState('networkidle'); return r.status(); } async function inspect(role, routes) { ({ page, ctx } = await q.context(role)); await q.login(page, role); for (const route of routes) { q.phase(role + ' ' + route); const props = await bootstrap(route); await page.getByRole('heading').first().waitFor(); (q.proof.views ||= []).push({ role, route, keys: Object.keys(props), labels: await page.locator('label').allTextContents(), buttons: await page.getByRole('button').allTextContents(), links: await page.locator('main a').evaluateAll((ns) => ns.map((n) => ({ text: n.textContent, href: n.getAttribute('href'), })), ), company_ids: props.companies?.map((c) => ({ id: c.id, name: c.payload.name, incorporation: c.payload.incorporation_country_code, })), definition: props.definition, services: props.services, profileActions: props.profileActions, workflowState: props.workflowState, canonicalOptions: props.canonicalOptions ? { gates: props.canonicalOptions.gates, resource_counts: Object.fromEntries( Object.entries( props.canonicalOptions.resources || {}, ).map(([k, v]) => [k, v.length]), ), } : undefined, }); q.save(); } await ctx.close(); } try { if (mode === 'inspect') { await inspect('investor18', [ '/investidor/profile', '/balcao-virtual/dossiers/73bf1982-d182-4588-a153-07e933bdbe12', '/balcao-virtual/pedidos/8e6511a8-c15d-46b7-8eb5-c34b6e31eb15', ]); await inspect('manager', [ '/management/projects/create', '/management/interests', ]); await inspect('promoter', ['/minhas-oportunidades/30/edit']); } else if (mode === 'entity') { q.phase( 'QA18: entity incorporation FR distinct from existing residence PT', ); ({ page, ctx } = await q.context('investor18')); await q.login(page, 'investor18'); let props = await bootstrap('/investidor/profile'); assert.equal(props.account.payload.residence_country_code, 'PT'); const name = 'QA entidade — país distinto'; let company = props.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('FR'); await box .getByLabel('Função ou relação com a empresa', { exact: true, }) .fill('Representante sintético QA, sem direito real'); await box .getByLabel('Actividade e observações', { exact: true }) .fill( 'Ensaio QA: constituição FR, residência PT; a origem da solicitação é independente.', ); await clickSave( box.getByRole('button', { name: 'Guardar declaração da empresa', exact: true, }), '/investidor/companies', 'POST', ); props = await bootstrap('/investidor/profile'); company = props.companies.find((c) => c.payload.name === name); assert(company); } q.proof.entity = { id: company.id, version: Number(company.version), incorporation: company.payload.incorporation_country_code, residence: props.account.payload.residence_country_code, }; q.save(); assert.equal(company.payload.incorporation_country_code, 'FR'); const box = page.locator('section').filter({ has: page.getByRole('heading', { name, exact: true }), }); assert.equal( await box .locator('select[id^="incorporation_country_code"]') .inputValue(), 'FR', ); await q.shot(page, 'company-country-distinct'); await ctx.close(); ({ page, ctx } = await q.context('investor18')); await q.login(page, 'investor18'); props = await bootstrap('/investidor/profile'); company = props.companies.find((c) => c.id === q.proof.entity.id); assert.equal(company.payload.incorporation_country_code, 'FR'); assert.equal(Number(company.version), q.proof.entity.version); assert.equal(props.account.payload.residence_country_code, 'PT'); await q.shot(page, 'company-new-session'); q.proof.checks.saved_and_recovered_new_session = true; await ctx.close(); q.phase('Other account cannot read or edit company declaration'); ({ page, ctx } = await q.context('outsider')); await q.login(page, 'outsider'); props = await bootstrap('/investidor/profile'); assert(!props.companies.some((c) => c.id === company.id)); const denied = await mutation( '/investidor/companies/' + company.id, { version: company.version, payload: company.payload }, ); assert.equal(denied.status, 404); q.proof.checks.other_account_denied = true; await ctx.close(); } else if (mode === 'stats') { q.proof.stats = q.prior.stats || {}; const s = q.proof.stats; ({ page, ctx } = await q.context('manager')); await q.login(page, 'manager'); let p = await bootstrap('/management/interests?origin=ZA'); const count = p.originStatistics.desk.find((r) => r.code === 'ZA')?.count || 0; s.before ??= count; await ctx.close(); q.phase('Separate QA Balcao request; leave A/B drafts intact'); ({ page, ctx } = await q.context('investor18')); await q.login(page, 'investor18'); if (!s.id) { await bootstrap( '/balcao-virtual/dossiers/73bf1982-d182-4588-a153-07e933bdbe12', ); await page .getByRole('button', { name: 'Preparar rascunho de pedido', exact: true, }) .click(); await page.waitForURL( /\/balcao-virtual\/pedidos\/[0-9a-f-]{36}$/, ); s.id = new URL(page.url()).pathname.split('/').pop(); q.save(); } const route = '/balcao-virtual/pedidos/' + s.id; p = (await q.json(page, route)).data; await q.open(page, route); await page.getByRole('heading').first().waitFor(); if (p.request.status === 'draft') { await page.locator('#request-country').selectOption('PT'); await page .locator('#declared-origin-country') .selectOption('ZA'); await page .getByLabel(/^Nome do projeto(?: \*)?$/) .fill('QA estatísticas após submissão — pedido separado'); await page .getByLabel(/^Descrição e objetivo \*/) .fill( 'Ensaio sintético QA de submissão, repetição idempotente e estatísticas; sem pedido real a entidade externa.', ); await page.getByLabel(/^Moeda \*/).selectOption('MZN'); await page .getByLabel(/^Valor do investimento(?: \*)?$/) .fill('1000'); await page .getByLabel(/^Regime declarado/) .selectOption('unknown'); await page .getByLabel( 'Revi o conteúdo e autorizo a submissão para análise interna.', { exact: true }, ) .check(); const pending = page.waitForResponse( (r) => new URL(r.url()).pathname === route && r.request().method() === 'PUT', ); const original = page.waitForRequest( (r) => new URL(r.url()).pathname === route && r.method() === 'PUT', ); await page .getByRole('button', { name: 'Submeter este pedido', exact: true, }) .click(); s.replay_input = (await original).postDataJSON(); assert([302, 303].includes((await pending).status())); await page.waitForLoadState('networkidle'); p = (await q.json(page, route)).data; assert.equal(p.request.status, 'submitted'); s.protocol = p.request.protocol; s.version = p.request.version; q.save(); } assert( s.replay_input, 'Prior submitted request must preserve its exact replay input', ); s.protocol ??= p.request.protocol; s.version ??= p.request.version; q.save(); assert.equal(p.request.payload.country, 'PT'); assert.equal(p.request.payload.declared_origin_country_code, 'ZA'); const replay = await mutation(route, s.replay_input, 'PUT'); assert.equal(replay.status, 200); assert.equal(replay.data.protocol, s.protocol); assert.equal(replay.data.version, s.version); s.replay = replay.data; await q.shot(page, 'submitted-protocol'); await ctx.close(); ({ page, ctx } = await q.context('investor18')); await q.login(page, 'investor18'); p = await bootstrap(route); assert.equal(p.request.protocol, s.protocol); assert.equal(p.request.status, 'submitted'); q.proof.checks.submitted_recovered_new_session = true; await ctx.close(); ({ page, ctx } = await q.context('manager')); await q.login(page, 'manager'); p = await bootstrap('/management/interests?origin=ZA'); s.after = p.originStatistics.desk.find((r) => r.code === 'ZA')?.count || 0; assert.equal(s.after, s.before + 1); await page .getByText( 'Estatísticas de solicitações por origem declarada', { exact: true }, ) .click(); const group = page.locator('details section').filter({ has: page.getByRole('heading', { name: 'Pedidos do Balcão', exact: true, }), }); assert( (await group.innerText()).includes('África do Sul: ' + s.after), ); await q.shot(page, 'desk-statistic-once'); q.proof.checks.statistic_incremented_once_after_replay = true; await ctx.close(); } else if (mode === 'structures') { q.proof.structures = q.prior.structures || {}; ({ page, ctx } = await q.context('manager')); await q.login(page, 'manager'); for (const kind of ['projects', 'clusters', 'corridors']) { q.phase('Save and recover territorial consumer ' + kind); const item = (q.proof.structures[kind] ||= { code: 'QA-CONSUMER-' + kind.toUpperCase(), name: 'QA consumidor territorial ' + kind, }); if (!item.id) { const list = await bootstrap( '/management/' + kind + '?q=' + encodeURIComponent(item.code), ); const found = list.records.data.find( (r) => r.code === item.code, ); if (found) item.id = found.id; } if (!item.id) { await bootstrap('/management/' + kind + '/create'); const province = page.getByRole('combobox', { name: 'Província', exact: true, }); await province .locator('option') .filter({ hasText: /Cabo Delgado/i }) .waitFor({ state: 'attached' }); item.province = await province .locator('option') .filter({ hasText: /Cabo Delgado/i }) .getAttribute('value'); await province.selectOption(item.province); const district = page.getByRole('combobox', { name: 'Distrito', exact: true, }); await district .locator('option') .filter({ hasText: /^ANCUABE(?: \(|$)/i }) .waitFor({ state: 'attached' }); item.district = await district .locator('option') .filter({ hasText: /^ANCUABE(?: \(|$)/i }) .getAttribute('value'); await district.selectOption(item.district); await page .getByLabel('Código', { exact: true }) .fill(item.code); await page .getByLabel('Nome', { exact: true }) .fill(item.name); if (kind === 'clusters') await page .getByLabel('Sector do cluster', { exact: true }) .fill('Ensaio sintético QA'); await clickSave( page.getByRole('button', { name: 'Guardar', exact: true, }), '/management/' + kind, 'POST', ); await page.waitForURL((u) => u.pathname.endsWith('/edit')); item.id = new URL(page.url()).pathname.split('/')[3]; q.save(); } await bootstrap( '/management/' + kind + '/' + item.id + '/edit', ); const region = page.getByRole('region', { name: 'Localização do investimento', exact: true, }); await region .getByRole('combobox', { name: 'Distrito', exact: true }) .locator('option') .filter({ hasText: /^ANCUABE(?: \(|$)/i }) .waitFor({ state: 'attached' }); assert.equal( await region .getByRole('combobox', { name: 'Província', exact: true, }) .inputValue(), item.province, ); assert.equal( await region .getByRole('combobox', { name: 'Distrito', exact: true, }) .inputValue(), item.district, ); const map = region.getByRole('region', { name: 'Cobertura cartográfica da seleção', }); await map .locator('.leaflet-overlay-pane svg path') .first() .waitFor({ state: 'attached' }); item.polygons = await map .locator('.leaflet-overlay-pane svg path') .count(); await q.shot(page, kind + '-saved-map'); q.save(); } await ctx.close(); ({ page, ctx } = await q.context('manager')); await q.login(page, 'manager'); for (const [kind, item] of Object.entries(q.proof.structures)) { const p = await bootstrap( '/management/' + kind + '/' + item.id + '/edit', ); assert.equal(p.record.primary_territory_id, item.district); const region = page.getByRole('region', { name: 'Localização do investimento', exact: true, }); await region .getByRole('combobox', { name: 'Distrito', exact: true }) .locator('option') .filter({ hasText: /^ANCUABE(?: \(|$)/i }) .waitFor({ state: 'attached' }); assert.equal( await region .getByRole('combobox', { name: 'Distrito', exact: true, }) .inputValue(), item.district, ); await region .locator('.leaflet-overlay-pane svg path') .first() .waitFor({ state: 'attached' }); item.new_session_recovered = true; } q.proof.checks.three_consumers_saved_recovered_mapped = true; await ctx.close(); } else if (mode === 'doc-before') { q.phase('Read-only print presentation before correction'); ({ page, ctx } = await q.context('promoter')); await q.login(page, 'promoter'); const ids = JSON.parse( fs.readFileSync( path.join( DIR, 'independent-documents-http-receipts-qa.json', ), 'utf8', ), ); await bootstrap( '/investidor/terra-duat/' + ids.case_id + '/imprimir', ); await page.getByRole('heading').first().waitFor(); q.proof.presentation = { route: new URL(page.url()).pathname, heading: await page.getByRole('heading').first().innerText(), encoding_corrupted: await page .locator('main') .innerText() .then((t) => /[ÃÂâ][\u0080-\u00bf€œž”˜™]/.test(t)), has_country: await page .locator('main') .innerText() .then((t) => t.includes('País do requerente')), }; await q.shot(page, 'print-before', false); q.proof.result = 'completed_with_findings'; await ctx.close(); } else if (mode === 'structures-deep') { const prior = JSON.parse( fs.readFileSync( path.join( DIR, 'consumer-structures-chromium-20261010.json', ), 'utf8', ), ); assert.equal(prior.result, 'passed'); q.proof.structures = q.prior.structures || prior.structures; ({ page, ctx } = await q.context('manager')); await q.login(page, 'manager'); for (const [kind, item] of Object.entries(q.proof.structures)) { q.phase( 'Remaining post/locality selector and geometry fallback ' + kind, ); await bootstrap( '/management/' + kind + '/' + item.id + '/edit', ); const region = page.getByRole('region', { name: 'Localização do investimento', exact: true, }); const district = region.getByRole('combobox', { name: 'Distrito', exact: true, }); await district .locator('option[value="' + item.district + '"]') .waitFor({ state: 'attached' }); const post = region.getByRole('combobox', { name: 'Posto administrativo', exact: true, }); await post .locator('option') .nth(1) .waitFor({ state: 'attached' }); if (!item.lower_saved) { const _option = post .locator('option') .filter({ hasText: /^(?!Seleccione|Selecione|A carregar)/, }) .nth(0); const values = await post .locator('option') .evaluateAll((ns) => ns .filter((n) => n.value) .map((n) => ({ id: n.value, name: n.textContent, })), ); assert(values.length); item.post = values[0]; await post.selectOption(item.post.id); const local = region.getByRole('combobox', { name: 'Localidade', exact: true, }); await local .locator('option') .nth(1) .waitFor({ state: 'attached' }); const locals = await local .locator('option') .evaluateAll((ns) => ns .filter((n) => n.value) .map((n) => ({ id: n.value, name: n.textContent, })), ); assert(locals.length); item.locality = locals[0]; await local.selectOption(item.locality.id); const pending = page.waitForResponse( (r) => new URL(r.url()).pathname === '/management/territories/' + kind + '/' + item.id && r.request().method() === 'POST', ); await region .getByRole('button', { name: 'Guardar localização', exact: true, }) .click(); const r = await pending; assert.equal(r.status(), 200); item.version = (await r.json()).version; item.lower_saved = true; q.save(); } const geometry = await q.json( page, '/management/territory-geometries?territory_id=' + item.locality.id, ); assert.equal(geometry.status, 200); item.geometry = { selected_available: geometry.data.selected_geometry_available, context_count: geometry.data.context_geometry_count, selected_type: 'locality', }; const box = region.getByRole('region', { name: 'Cobertura cartográfica da seleção', }); await box .getByText( /Sem limite administrativo acessível|Limite administrativo disponível/, ) .waitFor(); if (geometry.data.areas.length) await box .locator('.leaflet-overlay-pane svg path') .first() .waitFor({ state: 'attached' }); item.geometry.rendered_polygon_paths = await box .locator('.leaflet-overlay-pane svg path') .count(); await q.shot(page, kind + '-deep-selectors'); q.save(); } await ctx.close(); ({ page, ctx } = await q.context('manager')); await q.login(page, 'manager'); for (const [kind, item] of Object.entries(q.proof.structures)) { await bootstrap( '/management/' + kind + '/' + item.id + '/edit', ); const data = await q.json( page, '/management/territories/' + kind + '/' + item.id, ); assert.equal(data.status, 200); assert.deepEqual( data.data.path .filter((n) => n.location_type !== 'country') .reverse() .map((n) => n.id), [ item.province, item.district, item.post.id, item.locality.id, ], ); const region = page.getByRole('region', { name: 'Localização do investimento', exact: true, }); await region .getByRole('combobox', { name: 'Localidade', exact: true }) .locator('option[value="' + item.locality.id + '"]') .waitFor({ state: 'attached' }); assert.equal( await region .getByRole('combobox', { name: 'Localidade', exact: true, }) .inputValue(), item.locality.id, ); item.four_levels_recovered_new_session = true; } q.proof.checks.all_four_levels_three_consumers_saved_recovered = true; await ctx.close(); } else if (mode === 'business') { q.proof.business = q.prior.business || { id: '4f927885-4e30-42d6-a9ed-7c983f5af149', }; const b = q.proof.business; ({ page, ctx } = await q.context('manager')); await q.login(page, 'manager'); let p = await bootstrap('/management/interests?origin=FR'); b.before ??= p.originStatistics.businesses.find((r) => r.code === 'FR') ?.count || 0; await ctx.close(); q.phase( 'Complete only remaining submission criteria of existing synthetic business', ); ({ page, ctx } = await q.context('investor18')); await q.login(page, 'investor18'); const route = '/investidor/negocios/' + b.id; p = await bootstrap(route); assert.equal(p.record.payload.country, 'PT'); assert.equal(p.record.payload.declared_origin_country_code, 'FR'); assert(/QA/i.test(p.record.payload.title)); if (p.record.status === 'draft') { const targets = { proponent_type: 'person', proponent_name: 'Pessoa sintética QA — país/origem', contact_name: 'Contacto sintético QA', contact_email: 'investor-mfa-qa@example.invalid', acting_capacity: 'holder', legitimacy_declaration: 'yes', opportunity_type: 'investment_project', description: 'Ensaio sintético QA do negócio existente: gravação, recuperação, submissão e estatísticas sem direitos ou oferta real.', project_stage: 'idea', availability: 'available', value_status: 'unknown', investment_total: '1000', amount_secured: '0', amount_sought: '1000', geography_scope: 'digital', source_reference: 'Ensaio sintético QA 10/10/2026, sem validade oficial.', source_provider: 'Equipa sintética de ensaio QA', source_date: '2026-10-10', confirm_information: 'yes', confirm_disclosure_authority: 'yes', confirm_no_publication_guarantee: 'yes', }; const schema = p.formSchema; for (let step = 1; step <= 8; step++) { await page .getByRole('list', { name: 'Etapas', exact: true }) .getByRole('button') .nth(step - 1) .click(); const keys = Object.keys(targets).filter( (k) => schema[k]?.step === step, ); for (const key of keys) { const field = schema[key]; const escaped = field.pt.replace( /[.*+?^${}()|[\]\\]/g, '\\$&', ); const control = page.getByLabel( new RegExp('^' + escaped), ); if (field.type === 'checkbox') await control.check(); else if (field.type === 'select') await control.selectOption(targets[key]); else await control.fill(targets[key]); } if (step === 3) { const select = page.getByLabel(/^Sector de actividade/); await select.selectOption( String( p.sectors.find((s) => /sintético QA/.test(s.name), )?.id || p.sectors[0].id, ), ); } } await page .getByLabel(/^Digite o código acima para confirmar/) .fill(p.record.code); const checkboxes = page.locator('input[type=checkbox]'); const consent = checkboxes.last(); await consent.check(); const pending = page.waitForResponse( (r) => r.request().method() === 'POST' && new URL(r.url()).pathname === '/api/v1/existing-business/listings/' + b.id + '/transitions', ); const original = page.waitForRequest( (r) => r.method() === 'POST' && new URL(r.url()).pathname === '/api/v1/existing-business/listings/' + b.id + '/transitions', ); await page .getByRole('button', { name: 'Confirmar e submeter para análise', exact: true, }) .click(); b.replay_input = (await original).postDataJSON(); const r = await pending; b.submission_status = r.status(); q.save(); assert.equal(r.status(), 200); await page.waitForLoadState('networkidle'); } p = ( await q.json(page, '/api/v1/existing-business/listings/' + b.id) ).data; assert.equal(p.record.status, 'submitted'); assert.equal(p.record.payload.country, 'PT'); assert.equal(p.record.payload.declared_origin_country_code, 'FR'); b.code = p.record.code; b.version = p.record.version_no; assert(b.replay_input); const replay = await mutation( '/api/v1/existing-business/listings/' + b.id + '/transitions', b.replay_input, ); assert.equal(replay.status, 409); b.replay_status = replay.status; await q.open(page, route); await page.getByRole('heading').first().waitFor(); await q.shot(page, 'business-submitted', false); await ctx.close(); ({ page, ctx } = await q.context('investor18')); await q.login(page, 'investor18'); p = await bootstrap(route); assert.equal(p.record.status, 'submitted'); assert.equal(p.record.code, b.code); assert.equal(p.record.version_no, b.version); assert.equal(p.record.payload.country, 'PT'); assert.equal(p.record.payload.declared_origin_country_code, 'FR'); q.proof.checks.business_submitted_recovered = true; await ctx.close(); ({ page, ctx } = await q.context('manager')); await q.login(page, 'manager'); p = await bootstrap('/management/interests?origin=FR'); b.after = p.originStatistics.businesses.find((r) => r.code === 'FR') ?.count || 0; assert.equal(b.after, b.before + 1); await page .getByText( 'Estatísticas de solicitações por origem declarada', { exact: true }, ) .click(); const group = page.locator('details section').filter({ has: page.getByRole('heading', { name: 'Negócios submetidos', exact: true, }), }); assert((await group.innerText()).includes('França: ' + b.after)); await q.shot(page, 'business-statistic-once'); q.proof.checks.business_replay_no_duplicate_statistic = true; await ctx.close(); } else if (mode === 'corridor-label') { q.phase( 'C96: corridor description label, read-only verification of existing fixture', ); const prior = JSON.parse( fs.readFileSync( path.join( DIR, 'consumer-structures-deep-chromium-20261010.json', ), 'utf8', ), ); assert.equal(prior.result, 'passed'); ({ page, ctx } = await q.context('manager')); await q.login(page, 'manager'); const item = prior.structures.corridors; const p = await bootstrap( '/management/corridors/' + item.id + '/edit', ); await page.getByLabel('Descrição', { exact: true }).waitFor(); assert( !(await page.locator('main').innerText()).includes('DescriÃ'), ); assert.equal(p.record.primary_territory_id, item.locality.id); q.proof.checks.corridor_label_corrected_data_preserved = true; await q.shot(page, 'corridor-label-corrected', false); await ctx.close(); } else if (mode === 'reviewer-files') { q.phase( 'Independent validator can consult resources while editing remains blocked', ); const w = JSON.parse( fs.readFileSync( path.join(DIR, 'consumer-workflow-chromium-20261010.json'), 'utf8', ), ).workflow; ({ page, ctx } = await q.context('validator')); await q.login(page, 'validator'); const p = await bootstrap( '/minhas-oportunidades/' + w.id + '/resources', ); assert(p.readOnly); await page .getByRole('button', { name: 'Documentos', exact: true }) .click(); assert( await page .getByRole('link', { name: 'Consultar original autorizado', exact: true, }) .isVisible(), ); await page .getByRole('button', { name: 'Ver recurso', exact: true }) .click(); assert( await page .getByRole('button', { name: 'Guardar recurso', exact: true, }) .isDisabled(), ); const read = await page.request.get( BASE + '/minhas-oportunidades/' + w.id + '/resources/documents/' + w.document + '/download', ); assert.equal(read.status(), 200); q.proof.checks.validator_document_read_not_edit = true; await q.shot(page, 'validator-document-read-only', false); await page .getByRole('button', { name: 'Imagens', exact: true }) .click(); const image = page.getByRole('img', { name: 'Retângulo verde com identificação de ensaio sintético QA', exact: true, }); await image.waitFor(); await image.evaluate( (n) => new Promise((resolve, reject) => { if (n.complete) return n.naturalWidth ? resolve() : reject(Error('Preview failed')); n.addEventListener('load', resolve, { once: true }); n.addEventListener( 'error', () => reject(Error('Preview failed')), { once: true }, ); }), ); assert( await page .getByRole('button', { name: 'Guardar recurso', exact: true, }) .isDisabled(), ); q.proof.checks.validator_derivative_read_not_edit = true; await q.shot(page, 'validator-image-read-only', false); await ctx.close(); } else if ( mode === 'workflow-files-before' || mode === 'workflow-files' ) { const w = JSON.parse( fs.readFileSync( path.join(DIR, 'consumer-workflow-chromium-20261010.json'), 'utf8', ), ).workflow; q.phase( 'Private original and derivative permissions for published QA opportunity', ); ({ page, ctx } = await q.context('promoter')); await q.login(page, 'promoter'); let p = await bootstrap( '/minhas-oportunidades/' + w.id + '/resources', ); const image = p.resources.images.find((r) => r.id === w.image); assert(image.derivativePreviews.length); const preview = image.derivativePreviews[0].url; const download = '/minhas-oportunidades/' + w.id + '/resources/documents/' + w.document + '/download'; if (mode === 'workflow-files') { await page .getByRole('button', { name: 'Documentos', exact: true }) .click(); const pending = page.waitForEvent('download'); await page .getByRole('link', { name: 'Consultar original autorizado', exact: true, }) .click(); const original = await pending; const file = path.join( DIR, 'consumer-workflow-recovered-original.pdf', ); await original.saveAs(file); const crypto = require('node:crypto'); assert.equal( crypto .createHash('sha256') .update(fs.readFileSync(file)) .digest('hex'), p.resources.documents.find((r) => r.id === w.document).file .sha256, ); q.proof.checks.original_download_hash_preserved = true; await page .getByRole('button', { name: 'Imagens', exact: true }) .click(); const displayed = page.getByRole('img', { name: 'Retângulo verde com identificação de ensaio sintético QA', exact: true, }); await displayed.waitFor(); await displayed.evaluate( (n) => new Promise((resolve, reject) => { if (n.complete) return n.naturalWidth ? resolve() : reject(Error('Preview failed')); n.addEventListener('load', resolve, { once: true }); n.addEventListener( 'error', () => reject(Error('Preview failed')), { once: true }, ); }), ); q.proof.derivatives = { count: image.derivativePreviews.length, rendered_width: await displayed.evaluate( (n) => n.naturalWidth, ), }; await q.shot(page, 'image-derived-presented', false); q.proof.checks.derivative_displayed = true; } await ctx.close(); ({ page, ctx } = await q.context('outsider')); await q.login(page, 'outsider'); q.proof.denials = []; for (const [url, name] of [ [download, 'private_original'], [preview, 'signed_derivative'], ]) { const r = await page.request.get( url.startsWith('http') ? url : BASE + url, { headers: { Accept: 'application/json' } }, ); assert.equal( r.headers()['x-investmoz-qa-target'], q.proof.target, ); q.proof.denials.push({ operation: name, status: r.status() }); if (mode === 'workflow-files') assert.equal(r.status(), 403); } await ctx.close(); if (mode === 'workflow-files-before') { q.proof.result = q.proof.denials.some((r) => r.status === 200) ? 'completed_with_findings' : 'passed'; } else { q.phase( 'Anonymous public catalogue, private binary and dossier remain separate', ); ({ page, ctx } = await q.context('public')); p = await bootstrap( '/oportunidades?q=' + encodeURIComponent(w.title), ); const record = p.results.data.find( (r) => Number(r.id) === w.id, ); assert(record); assert(!p.auth.user); await page .getByRole('link', { name: w.title, exact: true }) .click(); await page.waitForURL( (u) => u.pathname === '/oportunidades/' + record.slug, ); await page .getByRole('heading', { name: w.title, exact: true }) .waitFor(); const html = await page.content(); assert( !html.includes(w.document) && !html.includes('management_draft') && !html.includes('workflow_context'), ); q.proof.public = { id: w.id, route: new URL(page.url()).pathname, authenticated: false, private_metadata_omitted: true, }; await q.shot(page, 'public-projection', false); for (const url of [download, preview]) { const r = await page.request.get( url.startsWith('http') ? url : BASE + url, { headers: { Accept: 'application/json' } }, ); assert.equal(r.status(), 401); } q.proof.checks.public_catalog_private_files_separated = true; await ctx.close(); } } else if (mode === 'documents') { q.phase( 'Filled land PDF: recovered saved data, actual print action and browser PDF', ); ({ page, ctx } = await q.context('promoter')); await q.login(page, 'promoter'); const ids = JSON.parse( fs.readFileSync( path.join( DIR, 'independent-documents-http-receipts-qa.json', ), 'utf8', ), ); await bootstrap('/investidor/terra-duat/' + ids.case_id); await page .getByRole('link', { name: 'Imprimir / guardar cópia preenchida como PDF', exact: true, }) .click(); await page.waitForURL( BASE + '/investidor/terra-duat/' + ids.case_id + '/imprimir', ); await page.getByRole('heading').first().waitFor(); const text = await page.locator('main').innerText(); assert( text.includes( 'Terra e DUAT — dados preenchidos pelo requerente', ), ); assert(text.includes('Moçambique (MZ)')); assert(text.includes('Portugal (PT)')); assert(!/[ÃÂâ][\u0080-\u00bf€œž”˜™]/.test(text)); await page.evaluate(() => { window.qaPrintCalls = 0; window.print = () => window.qaPrintCalls++; }); await page .getByRole('button', { name: 'Imprimir / guardar como PDF', exact: true, }) .click(); assert.equal(await page.evaluate(() => window.qaPrintCalls), 1); await q.shot(page, 'land-print-corrected', false); const file = path.join(DIR, 'consumer-land-derived-20261010.pdf'); await page.pdf({ path: file, format: 'A4', printBackground: true, margin: { top: '12mm', bottom: '12mm', left: '10mm', right: '10mm', }, displayHeaderFooter: false, }); q.proof.pdf = { path: path .relative(path.resolve(__dirname, '..'), file) .replaceAll('\\', '/'), source_route: new URL(page.url()).pathname, source_case: ids.case_id, generated_by: 'Chromium print CSS', print_action_triggered: true, country: 'MZ', declared_origin: 'PT', originals_replaced: false, }; await ctx.close(); q.phase('Print source permissions: other owner and manager denied'); for (const role of ['outsider', 'manager']) { ({ page, ctx } = await q.context(role)); await q.login(page, role); const r = await q.open( page, '/investidor/terra-duat/' + ids.case_id + '/imprimir', ); assert.equal(r.status(), 404); (q.proof.denials ||= []).push({ role, status: 404 }); await ctx.close(); } q.proof.checks.print_presented_countries_encoding_and_authorization = true; } else throw Error('Unknown mode'); 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_state = { route: new URL(page.url()).pathname, labels: await page.locator('label').allTextContents(), alerts: await page.getByRole('alert').allTextContents(), }; await q.shot(page, 'failure').catch(() => {}); } } finally { q.proof.finished_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, receipt: q.receipt, }), ); if (q.proof.result === 'failed') process.exitCode = 1; } })();