// Remaining A/B acceptance only: reuse the demonstrated dossiers, do not repeat C37 or create fixtures. const { start, BASE, DIR, fs, path, assert, sanitized, } = require('./portal-visual-qa-lib.cjs'); void (async () => { const q = await start('investor-ab-remaining-chromium-20261010'); const previous = JSON.parse( fs.readFileSync( path.join(DIR, 'investor-ab-chromium-20261010.json'), 'utf8', ), ); assert.equal(previous.result, 'passed'); q.proof.previous_full_path_receipt = 'investor-ab-chromium-20261010.json'; q.proof.scope = 'Remaining visual A/B criteria, existing dossiers. Authentication is a prerequisite; C37 acceptance is not repeated.'; q.proof.scenarios = {}; q.proof.accessibility = []; let page, _ctx; async function regionShot(locator, name) { await locator.scrollIntoViewIfNeeded(); const file = path.join( DIR, 'investor-ab-remaining-' + q.stamp + '-' + name + '.png', ); await locator.screenshot({ path: file }); q.proof.screenshots.push( path .relative(path.resolve(__dirname, '..'), file) .replaceAll('\\', '/'), ); q.save(); } async function audit(route, width) { await page.addScriptTag({ path: path.join(DIR, 'qa-a11y-toolkit/package/axe.min.js'), }); const result = await page.evaluate(async () => { const r = await axe.run(document, { runOnly: { type: 'tag', values: [ 'wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice', ], }, }); return { violations: r.violations.map((v) => ({ id: v.id, impact: v.impact, targets: v.nodes.map((n) => n.target), })), incomplete: r.incomplete.map((v) => v.id), viewport: innerWidth, scrollWidth: document.documentElement.scrollWidth, }; }); q.proof.accessibility.push({ route, width, ...result }); q.save(); assert( result.scrollWidth <= width + 1, 'Horizontal overflow on the demonstrated A/B page', ); assert.equal( result.violations.length, 0, 'Accessibility findings need separate correction', ); } try { q.phase('Remaining A/B criteria in fresh Chromium session, 390px'); ({ page, ctx: _ctx } = await q.context('investor18-remaining-ab', 390)); await q.login(page, 'investor18'); for (const name of ['A', 'B']) { const entry = previous.scenarios[name]; q.phase( 'Scenario ' + name + ': applicability, correct financial result, existing Balcao preparation', ); await q.open(page, '/investimentos/' + entry.id); await page .getByRole('heading', { name: entry.title, exact: true }) .waitFor(); const data = (await q.json(page, '/investimentos/' + entry.id)) .data; assert.equal(data.case.id, entry.id); assert.equal(await page.locator('article[id^="S"]').count(), 19); assert.equal( await page .getByRole('heading', { name: /^Fase \d+ ·/ }) .count(), 5, ); const decisions = page.locator('form').filter({ has: page.getByRole('heading', { name: 'Decisões do promotor e aplicabilidade', exact: true, }), }); const expected = name === 'A' ? 'no' : 'yes'; assert.equal( await decisions.getByLabel(/^Precisa de terra/).inputValue(), expected, ); assert.equal( await decisions .getByLabel(/^Precisa de financiamento externo/) .inputValue(), expected, ); const land = data.steps.find((s) => s.code === 'S10'); assert.equal( land.state, name === 'A' ? 'dispensed' : 'waiting_external', ); assert.equal(land.preparation_allowed, name === 'B'); await page .locator('#S10') .getByText( name === 'A' ? /Dispensa apenas de preparação deste ramo/ : /Aguarda informação do terreno QA/, ) .waitFor(); await regionShot(decisions, name + '-applicability-390'); await regionShot(page.locator('#S10'), name + '-land-390'); const financial = page.locator('section').filter({ has: page.getByRole('heading', { name: 'Resultados devolvidos ao dossier', exact: true, }), }); const result = data.results.find((r) => r.id === entry.result_id); assert(result && result.draft_id === entry.draft_id); const other = previous.scenarios[name === 'A' ? 'B' : 'A']; assert( !data.results.some( (r) => r.id === other.result_id || r.draft_id === other.draft_id, ), 'No financial result from the other dossier', ); // Preserve older result versions; assert the exact previously demonstrated evidence, not an arbitrary first row. const financialEntry = financial .locator('li') .filter({ hasText: entry.result_id }); assert.equal(await financialEntry.count(), 1); await financialEntry .getByText('Ponto de equilíbrio: 20 unidades', { exact: true }) .waitFor(); await financialEntry .getByText( new RegExp( '^Necessidade de financiamento: ' + entry.expected_funding_gap + '(?:\\.00)?$', ), ) .waitFor(); await regionShot(financial, name + '-financial-390'); const tasks = page.locator('section').filter({ has: page.getByRole('heading', { name: 'Acompanhamento interno', exact: true, }), }); const savedTask = data.tasks.find((t) => t.id === entry.task_id); assert.equal(savedTask.status, 'done'); const item = tasks .locator('li') .filter({ hasText: savedTask.title }); await item.getByText(/done/).waitFor(); if (name === 'B') { const independent = data.steps.find((s) => s.code === 'S02'); assert.equal(independent.saved_state, 'preparing'); assert.equal(independent.preparation_allowed, true); const stage = page.locator('#S02'); await stage.locator('summary').click(); assert.equal( await stage .getByRole('button', { name: 'Guardar registo', exact: true, }) .isEnabled(), true, ); assert.equal( await tasks .getByLabel(/^Etapa/) .locator('option[value="S02"]') .count(), 1, ); await regionShot(stage, 'B-independent-stage-enabled-390'); } await regionShot(item, name + '-internal-completion-390'); await audit('/investimentos/' + entry.id, 390); const support = page.locator('section').filter({ has: page.getByRole('heading', { name: 'Apoio no Balcão', exact: true, }), }); const request = data.requests.find( (r) => r.id === entry.request_id, ); assert.equal(request.status, 'draft'); assert.equal(request.protocol, null); await support .locator( 'a[href="/balcao-virtual/pedidos/' + entry.request_id + '"]', ) .click(); await page.waitForURL( BASE + '/balcao-virtual/pedidos/' + entry.request_id, ); await page .getByRole('heading', { name: 'Rascunho — ainda não submetido', exact: true, }) .waitFor(); assert.equal( await page.locator('#request-country').inputValue(), 'PT', ); assert.equal( await page.locator('#declared-origin-country').inputValue(), 'ZA', ); await page .getByRole('link', { name: 'Voltar ao dossier ' + entry.title, exact: true, }) .waitFor(); const prepared = ( await q.json( page, '/balcao-virtual/pedidos/' + entry.request_id, ) ).data; if (!prepared.request.payload.description && name === 'A') { const source = prepared.toolPrefill; assert(source && source.draft_id === entry.draft_id); const reuse = page.getByRole('region', { name: 'Descrição guardada no projeto', }); await reuse .getByText(source.description, { exact: true }) .waitFor(); await regionShot(reuse, 'A-C88-explicit-reuse-preview'); const saved = page.waitForResponse( (r) => r.request().method() === 'PUT' && new URL(r.url()).pathname === '/balcao-virtual/pedidos/' + entry.request_id, ); await reuse .getByRole('button', { name: 'Reutilizar descrição guardada e guardar rascunho', exact: true, }) .click(); assert([200, 302, 303].includes((await saved).status())); await page.reload({ waitUntil: 'domcontentloaded' }); const recovered = ( await q.json( page, '/balcao-virtual/pedidos/' + entry.request_id, ) ).data; assert.equal( recovered.request.payload.description, source.description, ); assert.equal( recovered.request.version, prepared.request.version + 1, ); assert.equal(recovered.request.status, 'draft'); assert.equal(recovered.request.payload.country, 'PT'); assert.equal( recovered.request.payload.declared_origin_country_code, 'ZA', ); assert( recovered.revisions.some( (r) => Number(r.version) === Number(prepared.request.version) && r.snapshot.payload.description === '', ), ); assert( recovered.events.some( (e) => e.action === 'request_prefill_reused' && e.payload.draft_id === entry.draft_id, ), ); q.proof.C88 = { request_id: entry.request_id, draft_id: source.draft_id, draft_version: source.draft_version, request_version_before: prepared.request.version, request_version_after: recovered.request.version, old_empty_description_preserved_in_revision: true, explicit_preview_and_ui_save: true, source_recorded_in_audit: true, }; q.proof.business_data_writes = 1; q.save(); } else if (name === 'A') { const reused = prepared.events.find( (e) => e.action === 'request_prefill_reused' && e.payload.draft_id === entry.draft_id, ); assert( reused, 'Recovered reuse must have provenance in the existing audit trail', ); assert( prepared.revisions.some( (r) => r.snapshot.payload.description === '', ), 'Empty original preserved in a previous revision', ); q.proof.C88 = { request_id: entry.request_id, draft_id: reused.payload.draft_id, draft_version: reused.payload.draft_version, recovered_request_version: prepared.request.version, old_empty_description_preserved_in_revision: true, source_recorded_in_audit: true, explicit_preview_and_ui_save_receipt: 'investor-ab-remaining-chromium-20261010-attempt-' + q.stamp + '.json', mutation_attempt_started_at: q.prior.started_at, mutation_attempt_result: q.prior.result, mutation_attempt_harness_limit: 'revision version is serialized as text; corrected comparison uses Number and does not repeat the save', }; q.save(); } const populated = await page .locator( 'textarea, input:not([type="hidden"]):not([type="checkbox"])', ) .evaluateAll((nodes) => nodes .filter((n) => n.value) .map((n) => ({ label: n.labels?.[0]?.textContent?.trim(), value: n.value, })) .filter((n) => /nome.*proje|título|resumo|descrição/i.test( n.label || '', ), ), ); q.proof.prefill_probe = { scenario: name, fields: populated }; q.save(); assert( populated.some((n) => n.value === entry.title), 'Balcao title must remain the correct dossier title', ); assert( populated.some( (n) => n.value === 'Descrição sintética ' + name + ' QA para recuperar no Balcão.', ), 'Balcao summary must come from the contextual saved tool', ); await q.shot(page, name + '-balcao-prepared-390'); await audit('/balcao-virtual/pedidos/' + entry.request_id, 390); q.proof.scenarios[name] = { case_id: entry.id, draft_id: entry.draft_id, result_id: entry.result_id, facts: data.facts, land: { state: land.state, saved_state: land.saved_state, preparation_allowed: land.preparation_allowed, }, break_even_units: 20, funding_gap: entry.expected_funding_gap, currency: result.currency, result_only_in_correct_dossier: true, preserved_result_history_count: data.results.length, request_id: entry.request_id, request_status: request.status, request_country: 'PT', request_origin: 'ZA', prefilled_fields: populated, task_id: savedTask.id, task_status: savedTask.status, task_title: savedTask.title, independent_stage_enabled: name === 'B', case_version_before: data.case.version, }; q.save(); } q.phase('320px A/B dossier reflow, same data and no repeated mutation'); await page.setViewportSize({ width: 320, height: 844 }); for (const name of ['A', 'B']) { const entry = previous.scenarios[name]; await q.open(page, '/investimentos/' + entry.id); await page .getByRole('heading', { name: entry.title, exact: true }) .waitFor(); await audit('/investimentos/' + entry.id, 320); const data = (await q.json(page, '/investimentos/' + entry.id)) .data; assert.equal( data.case.version, q.proof.scenarios[name].case_version_before, ); q.proof.scenarios[name].case_version_after = data.case.version; await regionShot( page.locator('section').filter({ has: page.getByRole('heading', { name: 'Resultados devolvidos ao dossier', exact: true, }), }), name + '-financial-320', ); } q.proof.checks = { applicability_A_no_land_no_funding: true, applicability_B_land_and_funding: true, financial_results_in_correct_dossier: true, prepared_balcao_drafts_recovered: true, independent_task_completed_while_land_remains_waiting: true, independent_preparation_enabled: true, mobile_390_320_no_overflow_or_axe_violations: true, preserved_existing_dossiers_and_versions: true, }; q.proof.business_data_writes ||= 0; q.proof.scope_limit = 'Internal A/B preparation and follow-up only; no Balcao submission or external land/finance decision. Real devices and screen-reader acceptance remain separate.'; q.proof.result = 'passed'; } catch (error) { q.proof.result = 'failed'; q.proof.error = sanitized(error); if ( page && !['/login', '/two-factor-challenge'].includes( new URL(page.url()).pathname, ) ) { q.proof.failure_state = { path: 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 !== 'passed') process.exitCode = 1; } })();