const { start, BASE, assert, sanitized, } = require('./portal-visual-qa-lib.cjs'); void (async () => { const q = await start('investor-ab-chromium-20261010'); q.proof.scenarios = q.prior.scenarios || {}; q.proof.checks = { ...q.prior.checks }; q.proof.prior_attempt_started_at = q.prior.started_at || null; let page, ctx; async function view(id) { const r = await q.json(page, '/investimentos/' + id); assert.equal(r.status, 200); return r.data; } async function mutate(path, action) { const response = page.waitForResponse( (r) => new URL(r.url()).pathname === path && r.request().method() === 'POST', ); await action(); const r = await response; assert( [200, 201, 302, 303].includes(r.status()), 'Expected successful UI mutation', ); await page.waitForLoadState('domcontentloaded'); } async function choice(label, text) { const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const control = page.getByRole('combobox', { name: new RegExp('^' + escaped + '(?: \\(obrigatório\\))?$'), }); await control.fill(text); await page .getByRole('option') .filter({ hasText: text }) .first() .click(); } async function state(e, code, value, reason) { const box = page.locator('#' + code); await box.locator('summary').click(); await box.getByLabel(/^Estado/).selectOption(value); await box.getByLabel(/^Acção/).selectOption('prepare'); await box.getByLabel('Fundamento', { exact: true }).fill(reason); await mutate('/investimentos/' + e.id + '/etapas/' + code, () => box .getByRole('button', { name: 'Guardar registo', exact: true }) .click(), ); await page.reload({ waitUntil: 'domcontentloaded' }); assert.equal( (await view(e.id)).steps.find((s) => s.code === code).saved_state, value, ); } try { if (!q.proof.checks.new_session_applicability_parallel) { q.phase('A/B creation through interface'); ({ page, ctx } = await q.context('investor18')); await q.login(page, 'investor18'); for (const name of ['A', 'B']) { const e = (q.proof.scenarios[name] ||= { title: 'QA visual ' + name + ' — ' + q.stamp, }); await q.open(page, '/investimentos'); if (!e.id) { await page .getByLabel('Título', { exact: true }) .fill(e.title); await page .getByLabel( 'Autorizo reutilizar o meu perfil neste dossier privado.', ) .check(); await page .getByRole('button', { name: 'Criar e guardar dossier', exact: true, }) .click(); await page.waitForURL(/\/investimentos\/[0-9a-f-]{36}$/); e.id = new URL(page.url()).pathname.split('/').pop(); e.created_via_ui = true; q.save(); } await q.open(page, '/investimentos/' + e.id); await page .getByRole('heading', { name: e.title, exact: true }) .waitFor(); assert.equal( await page.locator('article[id^="S"]').count(), 19, ); assert.equal( await page .getByRole('heading', { name: /^Fase \d+ ·/ }) .count(), 5, ); } assert.notEqual(q.proof.scenarios.A.id, q.proof.scenarios.B.id); q.proof.checks.two_independent_dossiers_5_phases_19_steps = true; q.save(); await q.logout(page); await ctx.close(); q.phase( 'Fresh authenticated session, applicability and parallel preparation', ); ({ page, ctx } = await q.context('investor18-new-session')); await q.login(page, 'investor18'); for (const name of ['A', 'B']) { const e = q.proof.scenarios[name]; await q.open(page, '/investimentos'); await page .getByRole('link', { name: e.title, exact: true }) .click(); await page.waitForURL(BASE + '/investimentos/' + e.id); e.recovered_new_session = true; const form = page.locator('form').filter({ has: page.getByRole('heading', { name: 'Decisões do promotor e aplicabilidade', exact: true, }), }); await form .getByLabel(/^Precisa de terra/) .selectOption(name === 'A' ? 'no' : 'yes'); await form .getByLabel(/^Precisa de financiamento externo/) .selectOption(name === 'A' ? 'no' : 'yes'); await form .getByLabel(/^Decisão sobre viabilidade/) .selectOption('proceed'); await form .getByLabel('Fundamento', { exact: true }) .fill( 'Declaração sintética ' + name + ' QA; sem decisão externa.', ); await mutate('/investimentos/' + e.id + '/aplicabilidade', () => form .getByRole('button', { name: 'Guardar decisões', exact: true, }) .click(), ); await page.reload({ waitUntil: 'domcontentloaded' }); const v = await view(e.id); assert.equal(v.facts.land_needed, name === 'A' ? 'no' : 'yes'); assert( name === 'A' ? v.steps.find((s) => s.code === 'S10').state === 'dispensed' : ['not_started', 'waiting_external'].includes( v.steps.find((s) => s.code === 'S10').state, ), ); e.applicability_recovered = true; if (name === 'B') await state( e, 'S10', 'waiting_external', 'Aguarda informação do terreno QA; mercado independente.', ); await state( e, 'S02', 'preparing', 'Preparação independente de mercado QA.', ); await q.shot(page, 'dossier-' + name + '-applicability', false); q.save(); } q.proof.checks.new_session_applicability_parallel = true; } else { q.phase( 'Resume demonstrated A/B, preserving prior evidence and persisted states', ); ({ page, ctx } = await q.context('investor18')); await q.login(page, 'investor18'); } q.phase( 'Contextual tool, saved financial result and return to dossier', ); for (const name of ['A', 'B']) { const e = q.proof.scenarios[name]; await q.open(page, '/investimentos/' + e.id); await page .getByRole('link', { name: 'Plano e simulador', exact: true }) .click(); await page.waitForURL( (u) => u.searchParams.get('context') === e.id, ); await page .getByText('Dossier privado recuperado da sua conta.', { exact: true, }) .waitFor(); await choice( 'O que pretende calcular ou planear? Escolha o percurso', 'Avaliar o meu investimento', ); await choice('Nível de detalhe', 'Simples'); await choice('O que pretende fazer?', 'Apenas simular'); await choice('Idioma preferido', 'Português (Moçambique)'); await page .getByRole('navigation', { name: 'Etapas do formulário' }) .getByRole('button', { name: /Projecto ou negócio/ }) .click(); await page .getByLabel('Título do projecto', { exact: true }) .fill('Plano financeiro ' + name + ' QA'); await page .getByLabel('Resumo executivo', { exact: true }) .fill( 'Descrição sintética ' + name + ' QA para recuperar no Balcão.', ); await page .getByRole('navigation', { name: 'Etapas do formulário' }) .getByRole('button', { name: /Estrutura financeira/ }) .click(); // UI probes contain field names only, no entered credentials or OTP. q.proof.tool_probe = { scenario: name, path: new URL(page.url()).pathname, labels: await page.locator('label').allTextContents(), alerts: await page.getByRole('alert').allTextContents(), }; q.save(); const financial = page.getByRole('region', { name: 'Ferramentas de planeamento financeiro', }); await choice('Moeda', 'MZN'); assert.equal( await financial .getByLabel('Moeda dos valores (código ISO)') .inputValue(), 'MZN', ); await financial.getByLabel(/^Custos fixos/).fill('1000'); await financial .getByLabel('Preço por unidade', { exact: true }) .fill('100'); await financial .getByLabel('Custo variável por unidade', { exact: true }) .fill('50'); await financial .getByLabel('Investimento total', { exact: true }) .fill('100000'); await financial .getByLabel('Montante já mobilizado', { exact: true }) .fill(name === 'A' ? '100000' : '30000'); await financial .getByLabel(/^Valor da receita/) .first() .fill('100'); await financial .getByLabel(/^Valor da despesa/) .first() .fill('50'); await financial.getByText(/Unidades: 20;/).waitFor(); assert.equal( await financial .getByLabel('Moeda dos valores (código ISO)') .inputValue(), 'MZN', ); await page .getByRole('button', { name: 'Guardar e continuar mais tarde', exact: true, }) .click(); await page .getByText( 'Dossier guardado no espaço privado da sua conta. Pode continuar mais tarde ao iniciar sessão.', { exact: true }, ) .waitFor(); await page .getByRole('button', { name: 'Guardar resultado financeiro e voltar ao dossier', exact: true, }) .click(); await page.waitForURL(BASE + '/investimentos/' + e.id); const v = await view(e.id); assert(v.results.length > 0); e.result_id = v.results.at(-1).id; e.draft_id = v.results.at(-1).draft_id; e.tool_returned_via_ui = true; await page .getByText('Ponto de equilíbrio: 20 unidades', { exact: true }) .first() .waitFor(); await page .getByText( new RegExp( '^Necessidade de financiamento: ' + (name === 'A' ? '0' : '70000') + '(?:\\.00)?$', ), ) .first() .waitFor(); e.expected_break_even = 20; e.expected_funding_gap = name === 'A' ? 0 : 70000; await q.shot(page, 'dossier-' + name + '-result'); q.save(); const support = page.locator('section').filter({ has: page.getByRole('heading', { name: 'Apoio no Balcão', exact: true, }), }); const requests = (await view(e.id)).requests; if (!requests.length) { await support.getByLabel(/^Serviço/).selectOption({ index: 0 }); await support .getByRole('button', { name: 'Preparar pedido com dados do dossier', exact: true, }) .click(); await page.waitForURL( /\/balcao-virtual\/pedidos\/[0-9a-f-]{36}$/, ); e.request_id = new URL(page.url()).pathname.split('/').pop(); } else { e.request_id = requests[0].id; await support .locator( 'a[href="/balcao-virtual/pedidos/' + e.request_id + '"]', ) .click(); await page.waitForURL( BASE + '/balcao-virtual/pedidos/' + e.request_id, ); } q.save(); await page.locator('#request-country').selectOption('PT'); await page.locator('#declared-origin-country').selectOption('ZA'); const supportSave = page.waitForResponse( (r) => r.request().method() === 'PUT' && new URL(r.url()).pathname === '/balcao-virtual/pedidos/' + e.request_id, ); await page .getByRole('button', { name: 'Guardar rascunho', exact: true }) .click(); assert([200, 302, 303].includes((await supportSave).status())); await page.reload({ waitUntil: 'domcontentloaded' }); assert.equal( await page.locator('#request-country').inputValue(), 'PT', ); assert.equal( await page.locator('#declared-origin-country').inputValue(), 'ZA', ); e.support_ui_saved = true; await q.shot(page, 'balcao-' + name); await q.open(page, '/investimentos/' + e.id); const task = page.locator('section').filter({ has: page.getByRole('heading', { name: 'Acompanhamento interno', exact: true, }), }); if (!(await view(e.id)).tasks.length) { await task.getByLabel(/^Etapa/).selectOption('S02'); await task .getByLabel('Tarefa', { exact: true }) .fill('Mercado paralelo ' + name + ' QA'); await task .getByLabel('Prazo', { exact: true }) .fill('2026-10-20T12:00'); await task .getByLabel('Assumo esta tarefa no acompanhamento interno.') .check(); await mutate('/investimentos/' + e.id + '/tarefas', () => task .getByRole('button', { name: 'Guardar tarefa', exact: true, }) .click(), ); await page.reload({ waitUntil: 'domcontentloaded' }); } e.task_id = (await view(e.id)).tasks[0].id; q.save(); } q.proof.checks.contextual_tool_results_support_tasks = true; await q.logout(page); await ctx.close(); q.phase('Management follow-up with private financial data protected'); ({ page, ctx } = await q.context('manager')); await q.login(page, 'manager'); for (const name of ['A', 'B']) { const e = q.proof.scenarios[name]; await q.open(page, '/investimentos/' + e.id); await page .getByText( 'Nenhum resultado financeiro disponível para esta conta neste dossier.', { exact: true }, ) .waitFor(); assert.equal((await view(e.id)).results.length, 0); if ((await view(e.id)).tasks[0].status !== 'done') { await page .getByLabel('Fundamento do resultado ou atribuição') .fill('Acompanhamento interno sintético ' + name + ' QA.'); for (const action of [ 'Assumir acompanhamento', 'Iniciar tarefa', 'Registar conclusão', ]) { await mutate( '/balcao-virtual/dossiers/' + e.id + '/tarefas/' + e.task_id, () => page .getByRole('button', { name: action, exact: true, }) .click(), ); await page.reload({ waitUntil: 'domcontentloaded' }); await page .getByLabel('Fundamento do resultado ou atribuição') .fill( 'Acompanhamento interno sintético ' + name + ' QA.', ); } } assert.equal((await view(e.id)).tasks[0].status, 'done'); e.management_ui_completed = true; await q.shot(page, 'management-' + name); q.save(); } await q.logout(page); await ctx.close(); q.phase('Investor recovery, no project mixing, outsider denial'); ({ page, ctx } = await q.context('investor18-recovery')); await q.login(page, 'investor18'); for (const name of ['A', 'B']) { const e = q.proof.scenarios[name]; await q.open(page, '/investimentos/' + e.id); const v = await view(e.id); assert( v.results.some( (r) => r.id === e.result_id && r.draft_id === e.draft_id, ), ); assert.equal(v.tasks[0].status, 'done'); assert.equal(v.requests[0].id, e.request_id); await page .getByRole('heading', { name: 'Próxima acção', exact: true }) .waitFor(); assert.equal( v.steps.find((s) => s.code === 'S10').state, name === 'A' ? 'dispensed' : 'waiting_external', ); e.final_ui_recovery = true; await q.shot(page, 'final-' + name); } for (const [name, e] of Object.entries(q.proof.scenarios)) { await q.open(page, '/simulador-fiscal?context=' + e.id); await page .getByText('Dossier privado recuperado da sua conta.', { exact: true, }) .waitFor(); await page .getByRole('navigation', { name: 'Etapas do formulário' }) .getByRole('button', { name: /Projecto ou negócio/ }) .click(); assert.equal( await page .getByLabel('Título do projecto', { exact: true }) .inputValue(), 'Plano financeiro ' + name + ' QA', ); await page .getByRole('navigation', { name: 'Etapas do formulário' }) .getByRole('button', { name: /Estrutura financeira/ }) .click(); const tool = page.getByRole('region', { name: 'Ferramentas de planeamento financeiro', }); assert.equal( await tool.getByLabel(/^Custos fixos/).inputValue(), '1000', ); assert.equal( await tool .getByLabel('Moeda dos valores (código ISO)') .inputValue(), 'MZN', ); assert.equal( await tool .getByLabel('Montante já mobilizado', { exact: true }) .inputValue(), name === 'A' ? '100000' : '30000', ); e.tool_inputs_recovered_new_session = true; await q.shot(page, 'tool-recovery-' + name); } q.proof.checks.tool_inputs_recovered_new_session = true; assert.notEqual( q.proof.scenarios.A.draft_id, q.proof.scenarios.B.draft_id, ); await q.logout(page); await ctx.close(); ({ page, ctx } = await q.context('outsider')); await q.login(page, 'outsider'); for (const e of Object.values(q.proof.scenarios)) { assert.equal( (await q.open(page, '/investimentos/' + e.id)).status(), 403, ); assert.equal( (await q.json(page, '/api/v1/simulation-drafts/' + e.draft_id)) .status, 404, ); } await ctx.close(); q.proof.checks.final_recovery_financial_privacy_unauthorized_denied = 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_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; } })();