Files
Investidor-V2/tools/portal-ab-visual-qa.cjs
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

581 lines
23 KiB
JavaScript

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;
}
})();