543 lines
22 KiB
JavaScript
543 lines
22 KiB
JavaScript
// Countries/origin per remaining consumer. Synthetic public intakes only; no notifications or external workers are invoked.
|
|
const {
|
|
start,
|
|
BASE,
|
|
DIR,
|
|
fs,
|
|
path,
|
|
assert,
|
|
sanitized,
|
|
} = require('./portal-visual-qa-lib.cjs');
|
|
void (async () => {
|
|
const q = await start(
|
|
process.argv.includes('--search-only')
|
|
? 'country-search-chromium-20261010'
|
|
: 'country-paths-chromium-20261010',
|
|
);
|
|
q.proof.forms = q.prior.forms || {};
|
|
q.proof.recovery = [];
|
|
let page, ctx, management;
|
|
async function props(response) {
|
|
const html = await response.text(),
|
|
encoded = html.match(/data-page="([^"]+)"/);
|
|
if (encoded && encoded[1].startsWith('{'))
|
|
return JSON.parse(
|
|
encoded[1]
|
|
.replace(/"/g, '"')
|
|
.replace(/&/g, '&')
|
|
.replace(/'/g, "'"),
|
|
).props;
|
|
const script = html.match(
|
|
/<script[^>]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/,
|
|
);
|
|
assert(script);
|
|
return JSON.parse(script[1]).props;
|
|
}
|
|
async function countStats(p) {
|
|
return (
|
|
p.originStatistics.interests.find((r) => r.code === 'ZA')?.count ||
|
|
0
|
|
);
|
|
}
|
|
async function audit(route) {
|
|
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,
|
|
targets: v.nodes.map((n) => n.target),
|
|
})),
|
|
viewport: innerWidth,
|
|
scrollWidth: document.documentElement.scrollWidth,
|
|
};
|
|
});
|
|
(q.proof.accessibility ||= []).push({ route, ...result });
|
|
q.save();
|
|
assert.equal(result.violations.length, 0);
|
|
assert(result.scrollWidth <= result.viewport + 1);
|
|
}
|
|
try {
|
|
const routes = [
|
|
'/formularios/investidor',
|
|
'/formularios/empresa',
|
|
'/apoio',
|
|
'/manifestar-interesse',
|
|
];
|
|
if (process.argv.includes('--search-only')) {
|
|
q.phase(
|
|
'Remaining country-search criteria only; no submissions or authentication',
|
|
);
|
|
({ page, ctx } = await q.context('anonymous-country-search', 390));
|
|
q.proof.search = [];
|
|
for (const route of routes) {
|
|
await q.open(page, route);
|
|
await page.getByRole('heading').first().waitFor();
|
|
const country = page.getByRole('combobox', {
|
|
name: 'País do requerente',
|
|
exact: true,
|
|
});
|
|
const origin = page.getByRole('combobox', {
|
|
name: 'País de origem declarado desta solicitação',
|
|
exact: true,
|
|
});
|
|
await page
|
|
.getByRole('textbox', {
|
|
name: 'Pesquisar país do requerente',
|
|
exact: true,
|
|
})
|
|
.fill('Portugal');
|
|
assert.equal(await country.locator('option').count(), 2);
|
|
await country.selectOption('PT');
|
|
await page
|
|
.getByRole('textbox', {
|
|
name: 'Pesquisar país do requerente',
|
|
exact: true,
|
|
})
|
|
.fill('França');
|
|
assert.equal(await country.inputValue(), 'PT');
|
|
assert.equal(
|
|
await country.locator('option[value="FR"]').count(),
|
|
1,
|
|
);
|
|
await page
|
|
.getByRole('textbox', {
|
|
name: 'Pesquisar país de origem declarado desta solicitação',
|
|
exact: true,
|
|
})
|
|
.fill('África do Sul');
|
|
assert.equal(await origin.locator('option').count(), 2);
|
|
await origin.selectOption('ZA');
|
|
assert.equal(await country.inputValue(), 'PT');
|
|
assert.equal(await origin.inputValue(), 'ZA');
|
|
q.proof.search.push({
|
|
route,
|
|
country_query: 'Portugal',
|
|
country_result: 'PT',
|
|
changed_query: 'França',
|
|
previous_selection_preserved: 'PT',
|
|
origin_query: 'África do Sul',
|
|
origin_result: 'ZA',
|
|
});
|
|
q.save();
|
|
}
|
|
q.proof.business_data_writes = 0;
|
|
q.proof.result = 'passed_read_only_four_country_searches';
|
|
} else if (process.argv.includes('--probe-labels')) {
|
|
q.phase(
|
|
'Read-only accessible names for country and declared origin before correction',
|
|
);
|
|
({ page, ctx } = await q.context('anonymous-country-labels', 390));
|
|
q.proof.labels = [];
|
|
for (const route of routes) {
|
|
await q.open(page, route);
|
|
await page.getByRole('heading').first().waitFor();
|
|
const controls = await page
|
|
.locator('select')
|
|
.evaluateAll((nodes) =>
|
|
nodes
|
|
.filter((n) =>
|
|
Array.from(n.options).some(
|
|
(o) => o.value === 'PT',
|
|
),
|
|
)
|
|
.map((n) => ({
|
|
accessible_label: n.getAttribute('aria-label'),
|
|
options: n.options.length,
|
|
visual_label: n
|
|
.closest('label')
|
|
?.textContent?.trim()
|
|
.slice(0, 150),
|
|
})),
|
|
);
|
|
q.proof.labels.push({ route, controls });
|
|
q.save();
|
|
}
|
|
q.proof.result = 'diagnosed_C89_indistinguishable_country_names';
|
|
} else {
|
|
q.phase(
|
|
'Management baseline for positive declared-origin statistics',
|
|
);
|
|
management = await q.context('manager-country-stats', 1440);
|
|
await q.login(management.page, 'manager');
|
|
const baseline = await props(
|
|
await q.open(
|
|
management.page,
|
|
'/management/interests?origin=ZA',
|
|
),
|
|
);
|
|
q.proof.baseline_known_origin =
|
|
q.prior.baseline_known_origin ?? (await countStats(baseline));
|
|
q.save();
|
|
({ page, ctx } = await q.context(
|
|
'anonymous-country-submissions',
|
|
390,
|
|
));
|
|
for (const [i, route] of routes.entries()) {
|
|
const entry = (q.proof.forms[route] ||= {
|
|
name: 'QA países origem formulário ' + i + ' 2026-10-10',
|
|
email: 'qa-ab-countries-' + i + '-20261010@example.invalid',
|
|
country: 'PT',
|
|
origin: 'ZA',
|
|
});
|
|
// The public API requires a DNS mail domain. Invalid/reserved no-mail domains are correctly rejected; QA outgoing notifications are disabled.
|
|
if (route === '/manifestar-interesse' && !entry.reference) {
|
|
entry.rejected_synthetic_email = entry.email.endsWith(
|
|
'.invalid',
|
|
)
|
|
? entry.email
|
|
: entry.rejected_synthetic_email;
|
|
entry.email =
|
|
'qa-investmoz-countries-' +
|
|
i +
|
|
'-20261010-9bd17fc0482e@gmail.com';
|
|
}
|
|
if (entry.reference) continue;
|
|
// If a prior attempt saved before capturing its confirmation, recover by this exact synthetic identity before considering another submission.
|
|
const existing = await props(
|
|
await q.open(
|
|
management.page,
|
|
'/management/interests?q=' +
|
|
encodeURIComponent(entry.email),
|
|
),
|
|
);
|
|
const found = existing.requests.data.find(
|
|
(r) => r.email === entry.email,
|
|
);
|
|
if (found) {
|
|
entry.reference = found.reference;
|
|
entry.id = found.id;
|
|
entry.recovered_existing_attempt = true;
|
|
q.save();
|
|
continue;
|
|
}
|
|
q.phase(
|
|
'Public form ' +
|
|
route +
|
|
': country/origin -> submission -> visible protocol',
|
|
);
|
|
await q.open(page, route);
|
|
await page.getByRole('heading').first().waitFor();
|
|
const form = page.locator('form').filter({
|
|
has: page.getByRole('button', {
|
|
name:
|
|
route === '/manifestar-interesse'
|
|
? 'Enviar manifestação de interesse'
|
|
: 'Enviar e obter protocolo',
|
|
exact: true,
|
|
}),
|
|
});
|
|
await form
|
|
.getByLabel(
|
|
route === '/manifestar-interesse'
|
|
? /^Nome completo/
|
|
: /^Nome do representante/,
|
|
)
|
|
.fill(entry.name);
|
|
await form.getByLabel(/^Email/).fill(entry.email);
|
|
const country = form.getByRole('combobox', {
|
|
name: 'País do requerente',
|
|
exact: true,
|
|
});
|
|
const origin = form.getByRole('combobox', {
|
|
name: 'País de origem declarado desta solicitação',
|
|
exact: true,
|
|
});
|
|
assert.equal(await country.locator('option').count(), 250);
|
|
await country.selectOption('PT');
|
|
await origin.selectOption('ZA');
|
|
if (route === '/manifestar-interesse') {
|
|
await form
|
|
.getByLabel(/^Tipo de investidor/)
|
|
.selectOption('individual');
|
|
await form
|
|
.getByLabel(/^Sector de interesse/)
|
|
.selectOption({ index: 1 });
|
|
await form
|
|
.getByLabel(/^Mensagem/)
|
|
.fill(
|
|
'Ensaio sintético QA de países e origem declarada; nenhum pedido de comunicação externa.',
|
|
);
|
|
} else {
|
|
await form
|
|
.getByLabel('Sector ou assunto', { exact: true })
|
|
.fill(
|
|
'Ensaio sintético QA de cobertura por formulário',
|
|
);
|
|
if (route === '/formularios/empresa')
|
|
await form
|
|
.getByLabel('Nome da empresa', { exact: true })
|
|
.fill('Empresa sintética QA países 20261010');
|
|
await form
|
|
.getByLabel('Descreva o pedido', { exact: true })
|
|
.fill(
|
|
'Ensaio sintético QA de países e origem declarada; nenhum pedido de comunicação externa.',
|
|
);
|
|
}
|
|
await form.getByRole('checkbox').check();
|
|
await q.shot(page, 'form-' + i + '-country-origin-390');
|
|
await audit(route);
|
|
const posted = page.waitForResponse(
|
|
(r) =>
|
|
r.request().method() === 'POST' &&
|
|
new URL(r.url()).pathname ===
|
|
(route === '/manifestar-interesse'
|
|
? '/api/v1/interest-requests'
|
|
: '/apoio'),
|
|
);
|
|
await form
|
|
.getByRole('button', {
|
|
name:
|
|
route === '/manifestar-interesse'
|
|
? 'Enviar manifestação de interesse'
|
|
: 'Enviar e obter protocolo',
|
|
exact: true,
|
|
})
|
|
.click();
|
|
const response = await posted;
|
|
if (![200, 201, 302, 303].includes(response.status())) {
|
|
q.proof.rejected_form = {
|
|
route,
|
|
status: response.status(),
|
|
validation: await response.json().catch(() => null),
|
|
};
|
|
q.save();
|
|
throw Error(
|
|
'Public form rejected; inspect sanitized validation evidence',
|
|
);
|
|
}
|
|
if (route === '/manifestar-interesse')
|
|
entry.reference = (await response.json()).data.reference;
|
|
else {
|
|
await page
|
|
.getByText(/Pedido registado\. Protocolo:/)
|
|
.waitFor();
|
|
entry.reference = (
|
|
await page
|
|
.getByText(/Pedido registado\. Protocolo:/)
|
|
.innerText()
|
|
).match(/AP-\d{4}-[A-Z0-9]+/)[0];
|
|
}
|
|
await page
|
|
.getByText(new RegExp(entry.reference))
|
|
.first()
|
|
.waitFor();
|
|
entry.submitted_via_ui = true;
|
|
entry.post_status = response.status();
|
|
q.save();
|
|
await q.shot(page, 'form-' + i + '-protocol');
|
|
}
|
|
await ctx.close();
|
|
q.phase(
|
|
'Recovery by authorized management, country/origin filters and positive statistics',
|
|
);
|
|
({ page, ctx } = management);
|
|
for (const [_route, entry] of Object.entries(q.proof.forms)) {
|
|
const result = await props(
|
|
await q.open(
|
|
page,
|
|
'/management/interests?q=' +
|
|
encodeURIComponent(entry.reference) +
|
|
'&country=PT&origin=ZA',
|
|
),
|
|
);
|
|
assert.equal(result.requests.data.length, 1);
|
|
const row = result.requests.data[0];
|
|
assert.equal(row.email, entry.email);
|
|
assert.equal(row.declaredOrigin.code, 'ZA');
|
|
entry.id = row.id;
|
|
entry.filtered_recovery = true;
|
|
const link = page.getByRole('link', {
|
|
name: entry.reference,
|
|
exact: true,
|
|
});
|
|
await link.click();
|
|
await page.waitForURL(BASE + '/management/interests/' + row.id);
|
|
await page
|
|
.getByText('África do Sul (ZA)', { exact: true })
|
|
.waitFor();
|
|
await page.getByText('PT', { exact: true }).waitFor();
|
|
entry.management_detail_presented = true;
|
|
await q.shot(page, 'detail-' + row.id);
|
|
q.save();
|
|
}
|
|
const positive = await props(
|
|
await q.open(
|
|
page,
|
|
'/management/interests?country=PT&origin=ZA',
|
|
),
|
|
);
|
|
q.proof.known_origin_after = await countStats(positive);
|
|
assert.equal(
|
|
q.proof.known_origin_after,
|
|
q.proof.baseline_known_origin + 4,
|
|
);
|
|
await page
|
|
.getByText(
|
|
'Estatísticas de solicitações por origem declarada',
|
|
{ exact: true },
|
|
)
|
|
.click();
|
|
await page
|
|
.getByText('África do Sul: ' + q.proof.known_origin_after, {
|
|
exact: true,
|
|
})
|
|
.waitFor();
|
|
await q.shot(page, 'management-positive-origin-statistics');
|
|
await page
|
|
.getByRole('combobox', {
|
|
name: 'Filtrar país do requerente',
|
|
exact: true,
|
|
})
|
|
.selectOption('ZA');
|
|
await page
|
|
.getByRole('button', { name: 'Pesquisar', exact: true })
|
|
.click();
|
|
await page.waitForURL(
|
|
(u) => u.searchParams.get('country') === 'ZA',
|
|
);
|
|
for (const entry of Object.values(q.proof.forms))
|
|
assert.equal(
|
|
await page
|
|
.getByRole('link', {
|
|
name: entry.reference,
|
|
exact: true,
|
|
})
|
|
.count(),
|
|
0,
|
|
);
|
|
q.proof.country_is_independent_from_declared_origin = true;
|
|
await ctx.close();
|
|
q.phase(
|
|
'Fresh QA18 session: recover previous profile, business and land drafts without rewriting',
|
|
);
|
|
({ page, ctx } = await q.context(
|
|
'investor18-country-recovery',
|
|
390,
|
|
));
|
|
await q.login(page, 'investor18');
|
|
const prior = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(
|
|
DIR,
|
|
'consumers-mobile-accessibility-chromium-20261010.json',
|
|
),
|
|
'utf8',
|
|
),
|
|
);
|
|
await q.open(page, '/investidor/profile');
|
|
assert.equal(
|
|
await page
|
|
.locator('#residence_country_codeaccount')
|
|
.inputValue(),
|
|
'PT',
|
|
);
|
|
q.proof.recovery.push({
|
|
route: '/investidor/profile',
|
|
country: 'PT',
|
|
fresh_session: true,
|
|
business_data_writes: 0,
|
|
});
|
|
for (const [route, countryName, originName] of [
|
|
[
|
|
'/investidor/negocios/' + prior.business_id,
|
|
'País de origem ou constituição',
|
|
'País de origem declarado desta solicitação (opcional)',
|
|
],
|
|
[
|
|
'/investidor/terra-duat/' + prior.land_id + '/editar',
|
|
'País, se aplicável',
|
|
'País de origem declarado da solicitação (opcional)',
|
|
],
|
|
]) {
|
|
await q.open(page, route);
|
|
await page
|
|
.getByRole('combobox', { name: countryName, exact: true })
|
|
.waitFor();
|
|
assert.equal(
|
|
await page
|
|
.getByRole('combobox', {
|
|
name: countryName,
|
|
exact: true,
|
|
})
|
|
.inputValue(),
|
|
'PT',
|
|
);
|
|
assert.equal(
|
|
await page
|
|
.getByRole('combobox', {
|
|
name: originName,
|
|
exact: true,
|
|
})
|
|
.inputValue(),
|
|
'FR',
|
|
);
|
|
q.proof.recovery.push({
|
|
route,
|
|
country: 'PT',
|
|
origin: 'FR',
|
|
fresh_session: true,
|
|
business_data_writes: 0,
|
|
});
|
|
await q.shot(
|
|
page,
|
|
route.includes('/negocios/')
|
|
? 'business-fresh-session-390'
|
|
: 'land-fresh-session-390',
|
|
);
|
|
q.save();
|
|
}
|
|
q.proof.checks = {
|
|
four_public_forms_submitted_and_recovered: true,
|
|
independent_country_origin_filters: true,
|
|
positive_known_origin_statistics: true,
|
|
previous_business_and_land_recovered_new_session: true,
|
|
};
|
|
q.proof.external_dispatches = 0;
|
|
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 === 'failed') process.exitCode = 1;
|
|
}
|
|
})();
|