497 lines
20 KiB
JavaScript
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;
|
|
}
|
|
})();
|