581 lines
23 KiB
JavaScript
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;
|
|
}
|
|
})();
|