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

683 lines
29 KiB
JavaScript

// Only remaining criteria. Closed A/B and C37 are not repeated.
const {
start,
DIR,
assert,
fs,
path,
sanitized,
} = require('./portal-visual-qa-lib.cjs');
const mode = process.argv[2] || 'land';
void (async () => {
const q = await start('revision-' + mode + '-chromium-20261011');
let page, ctx;
async function actor(role) {
if (ctx) await ctx.close();
({ ctx, page } = await q.context(role));
if (role !== 'public') await q.login(page, role);
}
async function open(route) {
const r = await q.open(page, route);
const html = await r.text();
const attr = html.match(/data-page="([^"]+)"/);
const raw = attr?.[1].startsWith('{')
? attr[1]
.replace(/"/g, '"')
.replace(/&/g, '&')
.replace(/'/g, "'")
: html.match(
/<script[^>]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/,
)?.[1];
assert(raw, 'Inertia props');
return JSON.parse(raw).props;
}
async function a11y(name) {
await page.addScriptTag({
path: path.join(DIR, 'qa-a11y-toolkit/package/axe.min.js'),
});
const findings = await page.evaluate(async () => {
const result = await axe.run(document, {
runOnly: {
type: 'tag',
values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'],
},
});
return result.violations.map((v) => ({
id: v.id,
impact: v.impact,
nodes: v.nodes.map((n) => ({
target: n.target,
summary: n.failureSummary,
})),
}));
});
(q.proof.accessibility ||= []).push({
route: new URL(page.url()).pathname,
name,
violations: findings,
});
q.save();
return findings;
}
try {
if (mode === 'land') {
await actor('promoter');
const id = '01a12568-c330-73ec-a1ac-543ac0116a26';
const route = '/investidor/terra-duat/' + id;
let p = await open(route);
q.proof.case = {
id,
status_before: p.case.status,
version_before: p.case.version,
country: p.case.payload.country,
origin: p.case.payload.declared_origin_country_code,
};
assert.equal(p.case.payload.country, 'MZ');
assert.equal(p.case.payload.declared_origin_country_code, 'PT');
if (p.case.status === 'draft') {
q.phase('Land: support conversion from existing draft, UI');
await open(route + '/editar');
await page
.getByLabel(/Pretende apoio para/)
.selectOption('investment');
await page
.getByLabel(/Descrição do pedido/)
.fill(
'Ensaio sintético QA: orientação interna e preservação da origem declarada.',
);
await page
.getByLabel(/Categoria do requerente/)
.selectOption('individual');
await page
.getByLabel(/Nome ou denominação/)
.fill('Promotor sintético QA21 — ensaio de apoio');
await page.getByLabel(/Canal preferido/).selectOption('email');
await page
.getByRole('button', { name: 'Continuar', exact: true })
.click();
await page
.getByLabel(/Situação da localização/)
.selectOption('location_support');
await page
.locator('#land-field-purpose')
.fill(
'Preparação de investimento sintético em QA; sem decisão externa.',
);
await page
.getByRole('button', { name: 'Anterior', exact: true })
.click();
for (let i = 0; i < 3; i++)
await page
.getByRole('button', { name: 'Continuar', exact: true })
.click();
await page
.getByLabel(/Confirmo que as respostas correspondem/)
.check();
await page
.getByLabel(/Autorizo o contacto para responder/)
.check();
await a11y('land-support-final-step');
const pending = page.waitForResponse(
(r) =>
new URL(r.url()).pathname ===
route + '/solicitar-apoio' &&
r.request().method() === 'POST',
);
await page
.getByRole('button', {
name: 'Solicitar apoio',
exact: true,
})
.click();
const response = await pending;
q.proof.support_status = response.status();
await page.waitForLoadState('networkidle');
await page.waitForFunction(
() =>
!Array.from(document.querySelectorAll('button')).some(
(b) =>
b.textContent.trim() === 'Solicitar apoio' &&
b.disabled,
),
);
if (
![302, 303, 202].includes(response.status()) ||
new URL(page.url()).pathname.endsWith('/editar')
) {
q.proof.field_errors = await page
.locator('[role="alert"]')
.allTextContents();
await q.shot(page, 'support-errors');
throw Error(
'Support conversion not demonstrated; inspect field errors and response status',
);
}
}
p = await open(route);
assert.equal(p.case.status, 'support_requested');
assert(p.supportTicket?.reference);
q.proof.case.support_reference = p.supportTicket.reference;
await q.shot(page, 'land-support-recovered');
await a11y('land-support-read');
q.phase('Land: new session recovery and permission');
await actor('promoter');
p = await open(route);
assert.equal(p.case.payload.declared_origin_country_code, 'PT');
assert.equal(p.case.status, 'support_requested');
await actor('outsider');
q.proof.outsider_status = (await q.open(page, route)).status();
assert([403, 404].includes(q.proof.outsider_status));
q.phase('Land: manager recovery and origin statistics');
await actor('manager');
p = await open('/management/terra-duat/' + id);
assert.equal(p.case.payload.country, 'MZ');
assert.equal(p.case.payload.declared_origin_country_code, 'PT');
await q.shot(page, 'manager-support-country-origin');
const stats = await open('/management/interests?origin=PT');
q.proof.management_props = Object.keys(stats);
q.proof.statistics =
stats.declaredOriginStatistics ||
stats.originStatistics ||
null;
await q.shot(page, 'support-statistics');
await a11y('management-statistics');
q.proof.criteria = {
support_ui: true,
country_origin_preserved: true,
new_session: true,
manager_recovery: true,
outsider_denied: true,
idempotent_conversion:
'Separate guarded persistence/replay proof required; no second UI submission',
};
} else if (mode === 'land-errors') {
await actor('promoter');
q.phase('Land: incomplete draft and accessible support errors');
let route = q.prior.fixture?.route;
if (!route) {
await open('/investidor/terra-duat/novo');
await page
.getByLabel(/Nome ou denominação/)
.fill('Ensaio sintético QA — erros acessíveis');
const navigation = page.waitForURL((u) =>
/^\/investidor\/terra-duat\/[a-f0-9-]+$/.test(u.pathname),
);
await page
.getByRole('button', {
name: 'Guardar rascunho',
exact: true,
})
.click();
await navigation;
route = new URL(page.url()).pathname;
q.proof.fixture = { route };
q.save();
} else q.proof.fixture = { route };
await open(route + '/editar');
for (let i = 0; i < 3; i++)
await page
.getByRole('button', { name: 'Continuar', exact: true })
.click();
await page
.getByRole('button', { name: 'Solicitar apoio', exact: true })
.click();
await page.waitForFunction(
() => document.activeElement?.id === 'land-field-support_kind',
);
assert.equal(
await page
.locator('#land-field-support_kind')
.getAttribute('aria-describedby'),
'land-error-support_kind',
);
assert(await page.locator('#land-error-support_kind').isVisible());
q.proof.criteria = {
incomplete_draft_recoverable: true,
invalid_request_not_submitted: true,
first_error_focused: true,
field_error_associated: true,
summary_names_fields: await page
.locator('#land-error-summary')
.innerText(),
};
await a11y('land-invalid-support');
await q.shot(page, 'land-errors-focused');
const stored = await open(route);
assert.equal(stored.case.status, 'draft');
q.proof.criteria.invalid_remains_draft = true;
} else if (mode === 'land-replay') {
await actor('promoter');
q.phase('Land: API repetition of already submitted support');
const route =
'/investidor/terra-duat/01a12568-c330-73ec-a1ac-543ac0116a26';
const before = await open(route);
assert.equal(before.case.status, 'support_requested');
const errorsFixture = JSON.parse(
fs.readFileSync(
path.join(
DIR,
'revision-land-errors-chromium-20261011.json',
),
'utf8',
),
);
await open(errorsFixture.fixture.route + '/editar');
assert(
await page
.locator('p')
.filter({ hasText: /^País do requerente \(opcional\)$/ })
.isVisible(),
);
assert(
await page
.getByLabel('País do requerente (opcional)', {
exact: true,
})
.isVisible(),
);
assert(
await page
.getByLabel(
'País de origem declarado da solicitação (opcional)',
{ exact: true },
)
.isVisible(),
);
await a11y('land-country-labels');
await q.shot(page, 'land-country-origin-distinct');
const status = await page.evaluate(
async ({ route, record }) => {
const token = document.cookie
.split('; ')
.find((c) => c.startsWith('XSRF-TOKEN='));
return (
await fetch(route + '/solicitar-apoio', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-XSRF-TOKEN': token
? decodeURIComponent(token.slice(11))
: '',
},
body: JSON.stringify({
version: record.version,
payload: record.payload,
}),
})
).status;
},
{ route, record: before.case },
);
assert.equal(status, 409);
const after = await open(route);
assert.equal(after.case.version, before.case.version);
assert.equal(
after.supportTicket.reference,
before.supportTicket.reference,
);
q.proof.criteria = {
api_replay_status: status,
version_unchanged: true,
support_reference_unchanged: true,
visual_already_proved: 'revision-land-chromium-20261011.json',
};
const master = await open('/investidor/profile');
assert(master.organizationCountryAvailable);
q.proof.country_master_current_build = {
available: master.organizationCountryAvailable,
represented_count: master.organizations.length,
values_recovered: master.organizations.map((o) => ({
id: o.id,
country: o.country,
version: o.version,
})),
};
} else if (mode === 'zones-map') {
q.proof.consumers = [];
for (const role of ['public', 'manager', 'geo']) {
await actor(role);
const route =
role === 'public'
? '/zonas/ed4d2fba-95d5-42e3-8ca7-1f520de1036a'
: '/management/investment/zones/ed4d2fba-95d5-42e3-8ca7-1f520de1036a';
q.phase('Zone own geometry map: ' + role);
if (role === 'manager') {
const denied = await q.open(page, route);
assert.equal(denied.status(), 403);
assert(
(await denied.text()).includes('Configure e confirme'),
);
q.proof.consumers.push({
route,
role,
status: 403,
reason: 'Existing QA14 lacks confirmed MFA required by RequireZoneMfa; no privileges or MFA changed.',
});
continue;
}
if (role === 'geo') {
const props = await open(route);
assert.equal(props.management, true);
const region = page.getByRole('region', {
name: 'Localização do investimento',
exact: true,
});
const province = region.getByLabel('Província', {
exact: true,
});
await province
.locator(
'option[value="059b02ca-7095-577a-8705-4d4c46896747"]',
)
.waitFor({ state: 'attached' });
const pending = page.waitForResponse(
(r) =>
new URL(r.url()).pathname ===
'/management/territory-geometries',
);
await province.selectOption(
'059b02ca-7095-577a-8705-4d4c46896747',
);
const response = await pending;
assert.equal(response.status(), 200);
const data = await response.json();
assert.equal(data.selected_geometry_available, true);
await page
.getByRole('region', {
name: 'Mapa dos limites administrativos',
exact: true,
})
.last()
.waitFor();
await page.setViewportSize({ width: 390, height: 844 });
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth > innerWidth,
);
q.proof.consumers.push({
route,
role,
api_status: 200,
administrative_geometry_available: true,
administrative_geometry_count:
data.context_geometry_count,
selection_only_not_saved: true,
mobile390_overflow: overflow,
own_zone_map_not_present_in_editor: true,
});
await a11y('zone-territory-manager');
await q.shot(page, 'zone-territory-manager');
continue;
}
await open(route);
const pending = page.waitForResponse(
(r) =>
new URL(r.url()).pathname ===
'/api/v2/investment/zone-map',
);
await page
.getByRole('button', {
name: 'Consultar mapa',
exact: true,
})
.focus();
await page.keyboard.press('Enter');
const response = await pending;
assert.equal(response.status(), 200);
const data = await response.json();
assert(Array.isArray(data.features));
if (data.features.length)
await page
.getByRole('img', {
name: 'Esquema das geometrias autorizadas da zona',
})
.waitFor();
else
await page
.getByText(
'Não existe geometria autorizada para divulgação nesta ficha.',
{ exact: true },
)
.waitFor();
await page.setViewportSize({ width: 390, height: 844 });
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth > innerWidth,
);
assert.equal(
overflow,
false,
'The map must fit the 390px viewport',
);
q.proof.consumers.push({
route,
role,
api_status: 200,
own_geometry_count: data.features.length,
keyboard_action: true,
empty_state_visible: data.features.length === 0,
mobile390_overflow: overflow,
not_administrative_parcel_equivalence: true,
});
await a11y('zone-own-map-' + role);
await q.shot(page, 'zone-map-' + role);
}
} else if (mode === 'zones') {
await actor('public');
q.phase('Zones: public selectors and geometry consumer');
const p = await open('/zonas');
q.proof.props = Object.keys(p);
q.proof.selector_names = await page
.locator('select')
.evaluateAll((nodes) =>
nodes.map((n) => ({
name: n.name,
id: n.id,
label: n.labels?.[0]?.textContent?.trim(),
options: n.options.length,
})),
);
await a11y('zone-search');
await q.shot(page, 'zone-search');
q.proof.records =
p.records?.data?.map((r) => ({ id: r.id, name: r.name })) || [];
const province = page.getByLabel('Província', { exact: true });
await province.selectOption('059b02ca-7095-577a-8705-4d4c46896747');
await page.waitForFunction(
() =>
document.querySelector('input[name="province_id"]')
.value === '059b02ca-7095-577a-8705-4d4c46896747',
);
const navigation = page.waitForURL(
(u) =>
u.searchParams.get('province_id') ===
'059b02ca-7095-577a-8705-4d4c46896747',
);
await page
.getByRole('button', { name: 'Pesquisar', exact: true })
.click();
await navigation;
await page.waitForLoadState('networkidle');
assert(
new URL(page.url()).searchParams.get('province_id') ===
'059b02ca-7095-577a-8705-4d4c46896747',
);
await q.shot(page, 'zone-filter-cabo-delgado');
await a11y('zone-filtered');
q.proof.criteria = {
public_filter: true,
selectors_filled: true,
map_functional: false,
note: 'Inspect map presentation separately; a filter response is not geometry coverage',
};
} else if (mode === 'library') {
await actor('manager');
q.phase(
'Library: locate synthetic published resource without reimport',
);
const p = await open('/management/portal-services');
q.proof.synthetic_candidates = p.resources
.filter(
(r) =>
r.published &&
/qa|sint[eé]tic|fixture|demonstra/i.test(r.title),
)
.map((r) => ({
id: r.id,
title: r.title,
published: r.published,
document: r.document_id,
}));
await a11y('library-management');
await q.shot(page, 'library-management');
const legacy = p.resources.find(
(r) =>
r.published &&
/agroneg[oó]cio/i.test(r.title) &&
r.source_url?.startsWith('https://investemoz.tempo.co.mz/'),
);
const selected =
q.prior.selected ||
q.proof.synthetic_candidates[0] ||
(legacy && {
id: legacy.id,
title: legacy.title,
published: true,
document: legacy.document_id,
scope: 'Already disclosed legacy Agronegocio QA; temporary withdrawal with restoration',
});
assert(
selected,
'No published synthetic resource; do not withdraw arbitrary content',
);
q.proof.selected = selected;
const resource = p.resources.find((r) => r.id === selected.id);
assert(resource);
const doc = p.documents.find((d) => d.id === resource.document_id);
q.proof.original = {
id: doc.id,
sha256: doc.sha256,
scan: doc.malware_scan_status,
};
assert.equal(doc.malware_scan_status, 'clean');
const publicContext = await q.context('public');
const publicPage = publicContext.page;
const url = '/documentos/' + resource.id + '/download';
const before = await publicPage.request.get(
'http://127.0.0.1:18097' + url,
);
assert.equal(before.status(), 200);
q.proof.public_hash_before = require('node:crypto')
.createHash('sha256')
.update(await before.body())
.digest('hex');
const article = page.locator('article').filter({
has: page.getByRole('heading', {
name: resource.title + ' · Publicado',
exact: true,
}),
});
const pending = page.waitForResponse(
(r) =>
new URL(r.url()).pathname ===
'/management/portal-services/resources/' +
resource.id && r.request().method() === 'POST',
);
await article
.getByRole('button', {
name: 'Retirar da publicação',
exact: true,
})
.click();
assert([302, 303].includes((await pending).status()));
await page.waitForLoadState('networkidle');
q.proof.revoked_same_session = (
await publicPage.request.get('http://127.0.0.1:18097' + url)
).status();
assert([403, 404].includes(q.proof.revoked_same_session));
await publicContext.ctx.close();
const newer = await q.context('public');
q.proof.revoked_new_session = (
await newer.page.request.get('http://127.0.0.1:18097' + url)
).status();
assert([403, 404].includes(q.proof.revoked_new_session));
await newer.ctx.close();
const original = await page.request.get(
'http://127.0.0.1:18097/management/documents/' +
doc.id +
'/download',
);
assert.equal(original.status(), 200);
assert.equal(
require('node:crypto')
.createHash('sha256')
.update(await original.body())
.digest('hex'),
q.proof.public_hash_before,
);
q.proof.original_preserved = true;
await q.shot(page, 'library-withdrawn');
const withdrawn = page.locator('article').filter({
has: page.getByRole('heading', {
name: resource.title + ' · Rascunho',
exact: true,
}),
});
const restore = page.waitForResponse(
(r) =>
new URL(r.url()).pathname ===
'/management/portal-services/resources/' +
resource.id && r.request().method() === 'POST',
);
await withdrawn
.getByRole('button', { name: 'Publicar', exact: true })
.click();
assert([302, 303].includes((await restore).status()));
await page.waitForLoadState('networkidle');
const restoredPublic = await q.context('public');
const restored = await restoredPublic.page.request.get(
'http://127.0.0.1:18097' + url,
);
assert.equal(restored.status(), 200);
assert.equal(
require('node:crypto')
.createHash('sha256')
.update(await restored.body())
.digest('hex'),
q.proof.public_hash_before,
);
await restoredPublic.ctx.close();
q.proof.publication_restored = true;
q.proof.criteria = {
public_revocation: true,
same_session_denied: true,
new_session_denied: true,
original_preserved: true,
};
} else throw Error('Unknown bounded mode');
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,
)
)
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,
error: q.proof.error,
receipt: q.receipt,
}),
);
if (q.proof.result === 'failed') process.exitCode = 1;
}
})();