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

497 lines
20 KiB
JavaScript

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