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

1259 lines
54 KiB
JavaScript

// Incremental UI proofs; use the established local QA Chromium, never active data.
const {
start,
BASE,
DIR,
actors: _actors,
assert,
fs,
path,
sanitized,
} = require('./portal-visual-qa-lib.cjs');
const mode = process.argv[2] || 'inspect';
void (async () => {
const q = await start('consumer-' + mode + '-chromium-20261010');
let page, ctx;
async function bootstrap(route) {
const r = await q.open(page, route),
html = await r.text();
const attr = html.match(/data-page="([^"]+)"/);
const raw =
attr && attr[1].startsWith('{')
? attr[1]
.replace(/"/g, '"')
.replace(/&/g, '&')
.replace(/'/g, "'")
: html.match(
/<script[^>]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/,
)?.[1];
assert(raw, 'Inertia bootstrap');
return JSON.parse(raw).props;
}
async function mutation(url, data, method = 'POST') {
return page.evaluate(
async ({ url, data, method }) => {
const cookie = document.cookie
.split('; ')
.find((c) => c.startsWith('XSRF-TOKEN='));
const r = await fetch(url, {
method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-XSRF-TOKEN': cookie
? decodeURIComponent(cookie.slice(11))
: '',
},
...(method === 'GET' || method === 'HEAD'
? {}
: { body: JSON.stringify(data) }),
});
return { status: r.status, data: await r.json() };
},
{ url, data, method },
);
}
async function clickSave(button, url, method) {
const pending = page.waitForResponse(
(r) =>
new URL(r.url()).pathname === url &&
r.request().method() === method,
);
await button.click();
const r = await pending;
assert(
[200, 201, 302, 303].includes(r.status()),
'Expected saved response ' + r.status(),
);
await page.waitForLoadState('networkidle');
return r.status();
}
async function inspect(role, routes) {
({ page, ctx } = await q.context(role));
await q.login(page, role);
for (const route of routes) {
q.phase(role + ' ' + route);
const props = await bootstrap(route);
await page.getByRole('heading').first().waitFor();
(q.proof.views ||= []).push({
role,
route,
keys: Object.keys(props),
labels: await page.locator('label').allTextContents(),
buttons: await page.getByRole('button').allTextContents(),
links: await page.locator('main a').evaluateAll((ns) =>
ns.map((n) => ({
text: n.textContent,
href: n.getAttribute('href'),
})),
),
company_ids: props.companies?.map((c) => ({
id: c.id,
name: c.payload.name,
incorporation: c.payload.incorporation_country_code,
})),
definition: props.definition,
services: props.services,
profileActions: props.profileActions,
workflowState: props.workflowState,
canonicalOptions: props.canonicalOptions
? {
gates: props.canonicalOptions.gates,
resource_counts: Object.fromEntries(
Object.entries(
props.canonicalOptions.resources || {},
).map(([k, v]) => [k, v.length]),
),
}
: undefined,
});
q.save();
}
await ctx.close();
}
try {
if (mode === 'inspect') {
await inspect('investor18', [
'/investidor/profile',
'/balcao-virtual/dossiers/73bf1982-d182-4588-a153-07e933bdbe12',
'/balcao-virtual/pedidos/8e6511a8-c15d-46b7-8eb5-c34b6e31eb15',
]);
await inspect('manager', [
'/management/projects/create',
'/management/interests',
]);
await inspect('promoter', ['/minhas-oportunidades/30/edit']);
} else if (mode === 'entity') {
q.phase(
'QA18: entity incorporation FR distinct from existing residence PT',
);
({ page, ctx } = await q.context('investor18'));
await q.login(page, 'investor18');
let props = await bootstrap('/investidor/profile');
assert.equal(props.account.payload.residence_country_code, 'PT');
const name = 'QA entidade — país distinto';
let company = props.companies.find((c) => c.payload.name === name);
if (!company) {
const box = page.locator('section').filter({
has: page.getByRole('heading', {
name: 'Acrescentar empresa',
exact: true,
}),
});
await box
.getByLabel('Nome da empresa', { exact: true })
.fill(name);
await box
.locator('select[id^="incorporation_country_code"]')
.selectOption('FR');
await box
.getByLabel('Função ou relação com a empresa', {
exact: true,
})
.fill('Representante sintético QA, sem direito real');
await box
.getByLabel('Actividade e observações', { exact: true })
.fill(
'Ensaio QA: constituição FR, residência PT; a origem da solicitação é independente.',
);
await clickSave(
box.getByRole('button', {
name: 'Guardar declaração da empresa',
exact: true,
}),
'/investidor/companies',
'POST',
);
props = await bootstrap('/investidor/profile');
company = props.companies.find((c) => c.payload.name === name);
assert(company);
}
q.proof.entity = {
id: company.id,
version: Number(company.version),
incorporation: company.payload.incorporation_country_code,
residence: props.account.payload.residence_country_code,
};
q.save();
assert.equal(company.payload.incorporation_country_code, 'FR');
const box = page.locator('section').filter({
has: page.getByRole('heading', { name, exact: true }),
});
assert.equal(
await box
.locator('select[id^="incorporation_country_code"]')
.inputValue(),
'FR',
);
await q.shot(page, 'company-country-distinct');
await ctx.close();
({ page, ctx } = await q.context('investor18'));
await q.login(page, 'investor18');
props = await bootstrap('/investidor/profile');
company = props.companies.find((c) => c.id === q.proof.entity.id);
assert.equal(company.payload.incorporation_country_code, 'FR');
assert.equal(Number(company.version), q.proof.entity.version);
assert.equal(props.account.payload.residence_country_code, 'PT');
await q.shot(page, 'company-new-session');
q.proof.checks.saved_and_recovered_new_session = true;
await ctx.close();
q.phase('Other account cannot read or edit company declaration');
({ page, ctx } = await q.context('outsider'));
await q.login(page, 'outsider');
props = await bootstrap('/investidor/profile');
assert(!props.companies.some((c) => c.id === company.id));
const denied = await mutation(
'/investidor/companies/' + company.id,
{ version: company.version, payload: company.payload },
);
assert.equal(denied.status, 404);
q.proof.checks.other_account_denied = true;
await ctx.close();
} else if (mode === 'stats') {
q.proof.stats = q.prior.stats || {};
const s = q.proof.stats;
({ page, ctx } = await q.context('manager'));
await q.login(page, 'manager');
let p = await bootstrap('/management/interests?origin=ZA');
const count =
p.originStatistics.desk.find((r) => r.code === 'ZA')?.count ||
0;
s.before ??= count;
await ctx.close();
q.phase('Separate QA Balcao request; leave A/B drafts intact');
({ page, ctx } = await q.context('investor18'));
await q.login(page, 'investor18');
if (!s.id) {
await bootstrap(
'/balcao-virtual/dossiers/73bf1982-d182-4588-a153-07e933bdbe12',
);
await page
.getByRole('button', {
name: 'Preparar rascunho de pedido',
exact: true,
})
.click();
await page.waitForURL(
/\/balcao-virtual\/pedidos\/[0-9a-f-]{36}$/,
);
s.id = new URL(page.url()).pathname.split('/').pop();
q.save();
}
const route = '/balcao-virtual/pedidos/' + s.id;
p = (await q.json(page, route)).data;
await q.open(page, route);
await page.getByRole('heading').first().waitFor();
if (p.request.status === 'draft') {
await page.locator('#request-country').selectOption('PT');
await page
.locator('#declared-origin-country')
.selectOption('ZA');
await page
.getByLabel(/^Nome do projeto(?: \*)?$/)
.fill('QA estatísticas após submissão — pedido separado');
await page
.getByLabel(/^Descrição e objetivo \*/)
.fill(
'Ensaio sintético QA de submissão, repetição idempotente e estatísticas; sem pedido real a entidade externa.',
);
await page.getByLabel(/^Moeda \*/).selectOption('MZN');
await page
.getByLabel(/^Valor do investimento(?: \*)?$/)
.fill('1000');
await page
.getByLabel(/^Regime declarado/)
.selectOption('unknown');
await page
.getByLabel(
'Revi o conteúdo e autorizo a submissão para análise interna.',
{ exact: true },
)
.check();
const pending = page.waitForResponse(
(r) =>
new URL(r.url()).pathname === route &&
r.request().method() === 'PUT',
);
const original = page.waitForRequest(
(r) =>
new URL(r.url()).pathname === route &&
r.method() === 'PUT',
);
await page
.getByRole('button', {
name: 'Submeter este pedido',
exact: true,
})
.click();
s.replay_input = (await original).postDataJSON();
assert([302, 303].includes((await pending).status()));
await page.waitForLoadState('networkidle');
p = (await q.json(page, route)).data;
assert.equal(p.request.status, 'submitted');
s.protocol = p.request.protocol;
s.version = p.request.version;
q.save();
}
assert(
s.replay_input,
'Prior submitted request must preserve its exact replay input',
);
s.protocol ??= p.request.protocol;
s.version ??= p.request.version;
q.save();
assert.equal(p.request.payload.country, 'PT');
assert.equal(p.request.payload.declared_origin_country_code, 'ZA');
const replay = await mutation(route, s.replay_input, 'PUT');
assert.equal(replay.status, 200);
assert.equal(replay.data.protocol, s.protocol);
assert.equal(replay.data.version, s.version);
s.replay = replay.data;
await q.shot(page, 'submitted-protocol');
await ctx.close();
({ page, ctx } = await q.context('investor18'));
await q.login(page, 'investor18');
p = await bootstrap(route);
assert.equal(p.request.protocol, s.protocol);
assert.equal(p.request.status, 'submitted');
q.proof.checks.submitted_recovered_new_session = true;
await ctx.close();
({ page, ctx } = await q.context('manager'));
await q.login(page, 'manager');
p = await bootstrap('/management/interests?origin=ZA');
s.after =
p.originStatistics.desk.find((r) => r.code === 'ZA')?.count ||
0;
assert.equal(s.after, s.before + 1);
await page
.getByText(
'Estatísticas de solicitações por origem declarada',
{ exact: true },
)
.click();
const group = page.locator('details section').filter({
has: page.getByRole('heading', {
name: 'Pedidos do Balcão',
exact: true,
}),
});
assert(
(await group.innerText()).includes('África do Sul: ' + s.after),
);
await q.shot(page, 'desk-statistic-once');
q.proof.checks.statistic_incremented_once_after_replay = true;
await ctx.close();
} else if (mode === 'structures') {
q.proof.structures = q.prior.structures || {};
({ page, ctx } = await q.context('manager'));
await q.login(page, 'manager');
for (const kind of ['projects', 'clusters', 'corridors']) {
q.phase('Save and recover territorial consumer ' + kind);
const item = (q.proof.structures[kind] ||= {
code: 'QA-CONSUMER-' + kind.toUpperCase(),
name: 'QA consumidor territorial ' + kind,
});
if (!item.id) {
const list = await bootstrap(
'/management/' +
kind +
'?q=' +
encodeURIComponent(item.code),
);
const found = list.records.data.find(
(r) => r.code === item.code,
);
if (found) item.id = found.id;
}
if (!item.id) {
await bootstrap('/management/' + kind + '/create');
const province = page.getByRole('combobox', {
name: 'Província',
exact: true,
});
await province
.locator('option')
.filter({ hasText: /Cabo Delgado/i })
.waitFor({ state: 'attached' });
item.province = await province
.locator('option')
.filter({ hasText: /Cabo Delgado/i })
.getAttribute('value');
await province.selectOption(item.province);
const district = page.getByRole('combobox', {
name: 'Distrito',
exact: true,
});
await district
.locator('option')
.filter({ hasText: /^ANCUABE(?: \(|$)/i })
.waitFor({ state: 'attached' });
item.district = await district
.locator('option')
.filter({ hasText: /^ANCUABE(?: \(|$)/i })
.getAttribute('value');
await district.selectOption(item.district);
await page
.getByLabel('Código', { exact: true })
.fill(item.code);
await page
.getByLabel('Nome', { exact: true })
.fill(item.name);
if (kind === 'clusters')
await page
.getByLabel('Sector do cluster', { exact: true })
.fill('Ensaio sintético QA');
await clickSave(
page.getByRole('button', {
name: 'Guardar',
exact: true,
}),
'/management/' + kind,
'POST',
);
await page.waitForURL((u) => u.pathname.endsWith('/edit'));
item.id = new URL(page.url()).pathname.split('/')[3];
q.save();
}
await bootstrap(
'/management/' + kind + '/' + item.id + '/edit',
);
const region = page.getByRole('region', {
name: 'Localização do investimento',
exact: true,
});
await region
.getByRole('combobox', { name: 'Distrito', exact: true })
.locator('option')
.filter({ hasText: /^ANCUABE(?: \(|$)/i })
.waitFor({ state: 'attached' });
assert.equal(
await region
.getByRole('combobox', {
name: 'Província',
exact: true,
})
.inputValue(),
item.province,
);
assert.equal(
await region
.getByRole('combobox', {
name: 'Distrito',
exact: true,
})
.inputValue(),
item.district,
);
const map = region.getByRole('region', {
name: 'Cobertura cartográfica da seleção',
});
await map
.locator('.leaflet-overlay-pane svg path')
.first()
.waitFor({ state: 'attached' });
item.polygons = await map
.locator('.leaflet-overlay-pane svg path')
.count();
await q.shot(page, kind + '-saved-map');
q.save();
}
await ctx.close();
({ page, ctx } = await q.context('manager'));
await q.login(page, 'manager');
for (const [kind, item] of Object.entries(q.proof.structures)) {
const p = await bootstrap(
'/management/' + kind + '/' + item.id + '/edit',
);
assert.equal(p.record.primary_territory_id, item.district);
const region = page.getByRole('region', {
name: 'Localização do investimento',
exact: true,
});
await region
.getByRole('combobox', { name: 'Distrito', exact: true })
.locator('option')
.filter({ hasText: /^ANCUABE(?: \(|$)/i })
.waitFor({ state: 'attached' });
assert.equal(
await region
.getByRole('combobox', {
name: 'Distrito',
exact: true,
})
.inputValue(),
item.district,
);
await region
.locator('.leaflet-overlay-pane svg path')
.first()
.waitFor({ state: 'attached' });
item.new_session_recovered = true;
}
q.proof.checks.three_consumers_saved_recovered_mapped = true;
await ctx.close();
} else if (mode === 'doc-before') {
q.phase('Read-only print presentation before correction');
({ page, ctx } = await q.context('promoter'));
await q.login(page, 'promoter');
const ids = JSON.parse(
fs.readFileSync(
path.join(
DIR,
'independent-documents-http-receipts-qa.json',
),
'utf8',
),
);
await bootstrap(
'/investidor/terra-duat/' + ids.case_id + '/imprimir',
);
await page.getByRole('heading').first().waitFor();
q.proof.presentation = {
route: new URL(page.url()).pathname,
heading: await page.getByRole('heading').first().innerText(),
encoding_corrupted: await page
.locator('main')
.innerText()
.then((t) => /[ÃÂâ][\u0080-\u00bf€œž”˜™]/.test(t)),
has_country: await page
.locator('main')
.innerText()
.then((t) => t.includes('País do requerente')),
};
await q.shot(page, 'print-before', false);
q.proof.result = 'completed_with_findings';
await ctx.close();
} else if (mode === 'structures-deep') {
const prior = JSON.parse(
fs.readFileSync(
path.join(
DIR,
'consumer-structures-chromium-20261010.json',
),
'utf8',
),
);
assert.equal(prior.result, 'passed');
q.proof.structures = q.prior.structures || prior.structures;
({ page, ctx } = await q.context('manager'));
await q.login(page, 'manager');
for (const [kind, item] of Object.entries(q.proof.structures)) {
q.phase(
'Remaining post/locality selector and geometry fallback ' +
kind,
);
await bootstrap(
'/management/' + kind + '/' + item.id + '/edit',
);
const region = page.getByRole('region', {
name: 'Localização do investimento',
exact: true,
});
const district = region.getByRole('combobox', {
name: 'Distrito',
exact: true,
});
await district
.locator('option[value="' + item.district + '"]')
.waitFor({ state: 'attached' });
const post = region.getByRole('combobox', {
name: 'Posto administrativo',
exact: true,
});
await post
.locator('option')
.nth(1)
.waitFor({ state: 'attached' });
if (!item.lower_saved) {
const _option = post
.locator('option')
.filter({
hasText: /^(?!Seleccione|Selecione|A carregar)/,
})
.nth(0);
const values = await post
.locator('option')
.evaluateAll((ns) =>
ns
.filter((n) => n.value)
.map((n) => ({
id: n.value,
name: n.textContent,
})),
);
assert(values.length);
item.post = values[0];
await post.selectOption(item.post.id);
const local = region.getByRole('combobox', {
name: 'Localidade',
exact: true,
});
await local
.locator('option')
.nth(1)
.waitFor({ state: 'attached' });
const locals = await local
.locator('option')
.evaluateAll((ns) =>
ns
.filter((n) => n.value)
.map((n) => ({
id: n.value,
name: n.textContent,
})),
);
assert(locals.length);
item.locality = locals[0];
await local.selectOption(item.locality.id);
const pending = page.waitForResponse(
(r) =>
new URL(r.url()).pathname ===
'/management/territories/' +
kind +
'/' +
item.id && r.request().method() === 'POST',
);
await region
.getByRole('button', {
name: 'Guardar localização',
exact: true,
})
.click();
const r = await pending;
assert.equal(r.status(), 200);
item.version = (await r.json()).version;
item.lower_saved = true;
q.save();
}
const geometry = await q.json(
page,
'/management/territory-geometries?territory_id=' +
item.locality.id,
);
assert.equal(geometry.status, 200);
item.geometry = {
selected_available:
geometry.data.selected_geometry_available,
context_count: geometry.data.context_geometry_count,
selected_type: 'locality',
};
const box = region.getByRole('region', {
name: 'Cobertura cartográfica da seleção',
});
await box
.getByText(
/Sem limite administrativo acessível|Limite administrativo disponível/,
)
.waitFor();
if (geometry.data.areas.length)
await box
.locator('.leaflet-overlay-pane svg path')
.first()
.waitFor({ state: 'attached' });
item.geometry.rendered_polygon_paths = await box
.locator('.leaflet-overlay-pane svg path')
.count();
await q.shot(page, kind + '-deep-selectors');
q.save();
}
await ctx.close();
({ page, ctx } = await q.context('manager'));
await q.login(page, 'manager');
for (const [kind, item] of Object.entries(q.proof.structures)) {
await bootstrap(
'/management/' + kind + '/' + item.id + '/edit',
);
const data = await q.json(
page,
'/management/territories/' + kind + '/' + item.id,
);
assert.equal(data.status, 200);
assert.deepEqual(
data.data.path
.filter((n) => n.location_type !== 'country')
.reverse()
.map((n) => n.id),
[
item.province,
item.district,
item.post.id,
item.locality.id,
],
);
const region = page.getByRole('region', {
name: 'Localização do investimento',
exact: true,
});
await region
.getByRole('combobox', { name: 'Localidade', exact: true })
.locator('option[value="' + item.locality.id + '"]')
.waitFor({ state: 'attached' });
assert.equal(
await region
.getByRole('combobox', {
name: 'Localidade',
exact: true,
})
.inputValue(),
item.locality.id,
);
item.four_levels_recovered_new_session = true;
}
q.proof.checks.all_four_levels_three_consumers_saved_recovered = true;
await ctx.close();
} else if (mode === 'business') {
q.proof.business = q.prior.business || {
id: '4f927885-4e30-42d6-a9ed-7c983f5af149',
};
const b = q.proof.business;
({ page, ctx } = await q.context('manager'));
await q.login(page, 'manager');
let p = await bootstrap('/management/interests?origin=FR');
b.before ??=
p.originStatistics.businesses.find((r) => r.code === 'FR')
?.count || 0;
await ctx.close();
q.phase(
'Complete only remaining submission criteria of existing synthetic business',
);
({ page, ctx } = await q.context('investor18'));
await q.login(page, 'investor18');
const route = '/investidor/negocios/' + b.id;
p = await bootstrap(route);
assert.equal(p.record.payload.country, 'PT');
assert.equal(p.record.payload.declared_origin_country_code, 'FR');
assert(/QA/i.test(p.record.payload.title));
if (p.record.status === 'draft') {
const targets = {
proponent_type: 'person',
proponent_name: 'Pessoa sintética QA — país/origem',
contact_name: 'Contacto sintético QA',
contact_email: 'investor-mfa-qa@example.invalid',
acting_capacity: 'holder',
legitimacy_declaration: 'yes',
opportunity_type: 'investment_project',
description:
'Ensaio sintético QA do negócio existente: gravação, recuperação, submissão e estatísticas sem direitos ou oferta real.',
project_stage: 'idea',
availability: 'available',
value_status: 'unknown',
investment_total: '1000',
amount_secured: '0',
amount_sought: '1000',
geography_scope: 'digital',
source_reference:
'Ensaio sintético QA 10/10/2026, sem validade oficial.',
source_provider: 'Equipa sintética de ensaio QA',
source_date: '2026-10-10',
confirm_information: 'yes',
confirm_disclosure_authority: 'yes',
confirm_no_publication_guarantee: 'yes',
};
const schema = p.formSchema;
for (let step = 1; step <= 8; step++) {
await page
.getByRole('list', { name: 'Etapas', exact: true })
.getByRole('button')
.nth(step - 1)
.click();
const keys = Object.keys(targets).filter(
(k) => schema[k]?.step === step,
);
for (const key of keys) {
const field = schema[key];
const escaped = field.pt.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&',
);
const control = page.getByLabel(
new RegExp('^' + escaped),
);
if (field.type === 'checkbox') await control.check();
else if (field.type === 'select')
await control.selectOption(targets[key]);
else await control.fill(targets[key]);
}
if (step === 3) {
const select = page.getByLabel(/^Sector de actividade/);
await select.selectOption(
String(
p.sectors.find((s) =>
/sintético QA/.test(s.name),
)?.id || p.sectors[0].id,
),
);
}
}
await page
.getByLabel(/^Digite o código acima para confirmar/)
.fill(p.record.code);
const checkboxes = page.locator('input[type=checkbox]');
const consent = checkboxes.last();
await consent.check();
const pending = page.waitForResponse(
(r) =>
r.request().method() === 'POST' &&
new URL(r.url()).pathname ===
'/api/v1/existing-business/listings/' +
b.id +
'/transitions',
);
const original = page.waitForRequest(
(r) =>
r.method() === 'POST' &&
new URL(r.url()).pathname ===
'/api/v1/existing-business/listings/' +
b.id +
'/transitions',
);
await page
.getByRole('button', {
name: 'Confirmar e submeter para análise',
exact: true,
})
.click();
b.replay_input = (await original).postDataJSON();
const r = await pending;
b.submission_status = r.status();
q.save();
assert.equal(r.status(), 200);
await page.waitForLoadState('networkidle');
}
p = (
await q.json(page, '/api/v1/existing-business/listings/' + b.id)
).data;
assert.equal(p.record.status, 'submitted');
assert.equal(p.record.payload.country, 'PT');
assert.equal(p.record.payload.declared_origin_country_code, 'FR');
b.code = p.record.code;
b.version = p.record.version_no;
assert(b.replay_input);
const replay = await mutation(
'/api/v1/existing-business/listings/' + b.id + '/transitions',
b.replay_input,
);
assert.equal(replay.status, 409);
b.replay_status = replay.status;
await q.open(page, route);
await page.getByRole('heading').first().waitFor();
await q.shot(page, 'business-submitted', false);
await ctx.close();
({ page, ctx } = await q.context('investor18'));
await q.login(page, 'investor18');
p = await bootstrap(route);
assert.equal(p.record.status, 'submitted');
assert.equal(p.record.code, b.code);
assert.equal(p.record.version_no, b.version);
assert.equal(p.record.payload.country, 'PT');
assert.equal(p.record.payload.declared_origin_country_code, 'FR');
q.proof.checks.business_submitted_recovered = true;
await ctx.close();
({ page, ctx } = await q.context('manager'));
await q.login(page, 'manager');
p = await bootstrap('/management/interests?origin=FR');
b.after =
p.originStatistics.businesses.find((r) => r.code === 'FR')
?.count || 0;
assert.equal(b.after, b.before + 1);
await page
.getByText(
'Estatísticas de solicitações por origem declarada',
{ exact: true },
)
.click();
const group = page.locator('details section').filter({
has: page.getByRole('heading', {
name: 'Negócios submetidos',
exact: true,
}),
});
assert((await group.innerText()).includes('França: ' + b.after));
await q.shot(page, 'business-statistic-once');
q.proof.checks.business_replay_no_duplicate_statistic = true;
await ctx.close();
} else if (mode === 'corridor-label') {
q.phase(
'C96: corridor description label, read-only verification of existing fixture',
);
const prior = JSON.parse(
fs.readFileSync(
path.join(
DIR,
'consumer-structures-deep-chromium-20261010.json',
),
'utf8',
),
);
assert.equal(prior.result, 'passed');
({ page, ctx } = await q.context('manager'));
await q.login(page, 'manager');
const item = prior.structures.corridors;
const p = await bootstrap(
'/management/corridors/' + item.id + '/edit',
);
await page.getByLabel('Descrição', { exact: true }).waitFor();
assert(
!(await page.locator('main').innerText()).includes('DescriÃ'),
);
assert.equal(p.record.primary_territory_id, item.locality.id);
q.proof.checks.corridor_label_corrected_data_preserved = true;
await q.shot(page, 'corridor-label-corrected', false);
await ctx.close();
} else if (mode === 'reviewer-files') {
q.phase(
'Independent validator can consult resources while editing remains blocked',
);
const w = JSON.parse(
fs.readFileSync(
path.join(DIR, 'consumer-workflow-chromium-20261010.json'),
'utf8',
),
).workflow;
({ page, ctx } = await q.context('validator'));
await q.login(page, 'validator');
const p = await bootstrap(
'/minhas-oportunidades/' + w.id + '/resources',
);
assert(p.readOnly);
await page
.getByRole('button', { name: 'Documentos', exact: true })
.click();
assert(
await page
.getByRole('link', {
name: 'Consultar original autorizado',
exact: true,
})
.isVisible(),
);
await page
.getByRole('button', { name: 'Ver recurso', exact: true })
.click();
assert(
await page
.getByRole('button', {
name: 'Guardar recurso',
exact: true,
})
.isDisabled(),
);
const read = await page.request.get(
BASE +
'/minhas-oportunidades/' +
w.id +
'/resources/documents/' +
w.document +
'/download',
);
assert.equal(read.status(), 200);
q.proof.checks.validator_document_read_not_edit = true;
await q.shot(page, 'validator-document-read-only', false);
await page
.getByRole('button', { name: 'Imagens', exact: true })
.click();
const image = page.getByRole('img', {
name: 'Retângulo verde com identificação de ensaio sintético QA',
exact: true,
});
await image.waitFor();
await image.evaluate(
(n) =>
new Promise((resolve, reject) => {
if (n.complete)
return n.naturalWidth
? resolve()
: reject(Error('Preview failed'));
n.addEventListener('load', resolve, { once: true });
n.addEventListener(
'error',
() => reject(Error('Preview failed')),
{ once: true },
);
}),
);
assert(
await page
.getByRole('button', {
name: 'Guardar recurso',
exact: true,
})
.isDisabled(),
);
q.proof.checks.validator_derivative_read_not_edit = true;
await q.shot(page, 'validator-image-read-only', false);
await ctx.close();
} else if (
mode === 'workflow-files-before' ||
mode === 'workflow-files'
) {
const w = JSON.parse(
fs.readFileSync(
path.join(DIR, 'consumer-workflow-chromium-20261010.json'),
'utf8',
),
).workflow;
q.phase(
'Private original and derivative permissions for published QA opportunity',
);
({ page, ctx } = await q.context('promoter'));
await q.login(page, 'promoter');
let p = await bootstrap(
'/minhas-oportunidades/' + w.id + '/resources',
);
const image = p.resources.images.find((r) => r.id === w.image);
assert(image.derivativePreviews.length);
const preview = image.derivativePreviews[0].url;
const download =
'/minhas-oportunidades/' +
w.id +
'/resources/documents/' +
w.document +
'/download';
if (mode === 'workflow-files') {
await page
.getByRole('button', { name: 'Documentos', exact: true })
.click();
const pending = page.waitForEvent('download');
await page
.getByRole('link', {
name: 'Consultar original autorizado',
exact: true,
})
.click();
const original = await pending;
const file = path.join(
DIR,
'consumer-workflow-recovered-original.pdf',
);
await original.saveAs(file);
const crypto = require('node:crypto');
assert.equal(
crypto
.createHash('sha256')
.update(fs.readFileSync(file))
.digest('hex'),
p.resources.documents.find((r) => r.id === w.document).file
.sha256,
);
q.proof.checks.original_download_hash_preserved = true;
await page
.getByRole('button', { name: 'Imagens', exact: true })
.click();
const displayed = page.getByRole('img', {
name: 'Retângulo verde com identificação de ensaio sintético QA',
exact: true,
});
await displayed.waitFor();
await displayed.evaluate(
(n) =>
new Promise((resolve, reject) => {
if (n.complete)
return n.naturalWidth
? resolve()
: reject(Error('Preview failed'));
n.addEventListener('load', resolve, { once: true });
n.addEventListener(
'error',
() => reject(Error('Preview failed')),
{ once: true },
);
}),
);
q.proof.derivatives = {
count: image.derivativePreviews.length,
rendered_width: await displayed.evaluate(
(n) => n.naturalWidth,
),
};
await q.shot(page, 'image-derived-presented', false);
q.proof.checks.derivative_displayed = true;
}
await ctx.close();
({ page, ctx } = await q.context('outsider'));
await q.login(page, 'outsider');
q.proof.denials = [];
for (const [url, name] of [
[download, 'private_original'],
[preview, 'signed_derivative'],
]) {
const r = await page.request.get(
url.startsWith('http') ? url : BASE + url,
{ headers: { Accept: 'application/json' } },
);
assert.equal(
r.headers()['x-investmoz-qa-target'],
q.proof.target,
);
q.proof.denials.push({ operation: name, status: r.status() });
if (mode === 'workflow-files') assert.equal(r.status(), 403);
}
await ctx.close();
if (mode === 'workflow-files-before') {
q.proof.result = q.proof.denials.some((r) => r.status === 200)
? 'completed_with_findings'
: 'passed';
} else {
q.phase(
'Anonymous public catalogue, private binary and dossier remain separate',
);
({ page, ctx } = await q.context('public'));
p = await bootstrap(
'/oportunidades?q=' + encodeURIComponent(w.title),
);
const record = p.results.data.find(
(r) => Number(r.id) === w.id,
);
assert(record);
assert(!p.auth.user);
await page
.getByRole('link', { name: w.title, exact: true })
.click();
await page.waitForURL(
(u) => u.pathname === '/oportunidades/' + record.slug,
);
await page
.getByRole('heading', { name: w.title, exact: true })
.waitFor();
const html = await page.content();
assert(
!html.includes(w.document) &&
!html.includes('management_draft') &&
!html.includes('workflow_context'),
);
q.proof.public = {
id: w.id,
route: new URL(page.url()).pathname,
authenticated: false,
private_metadata_omitted: true,
};
await q.shot(page, 'public-projection', false);
for (const url of [download, preview]) {
const r = await page.request.get(
url.startsWith('http') ? url : BASE + url,
{ headers: { Accept: 'application/json' } },
);
assert.equal(r.status(), 401);
}
q.proof.checks.public_catalog_private_files_separated = true;
await ctx.close();
}
} else if (mode === 'documents') {
q.phase(
'Filled land PDF: recovered saved data, actual print action and browser PDF',
);
({ page, ctx } = await q.context('promoter'));
await q.login(page, 'promoter');
const ids = JSON.parse(
fs.readFileSync(
path.join(
DIR,
'independent-documents-http-receipts-qa.json',
),
'utf8',
),
);
await bootstrap('/investidor/terra-duat/' + ids.case_id);
await page
.getByRole('link', {
name: 'Imprimir / guardar cópia preenchida como PDF',
exact: true,
})
.click();
await page.waitForURL(
BASE + '/investidor/terra-duat/' + ids.case_id + '/imprimir',
);
await page.getByRole('heading').first().waitFor();
const text = await page.locator('main').innerText();
assert(
text.includes(
'Terra e DUAT — dados preenchidos pelo requerente',
),
);
assert(text.includes('Moçambique (MZ)'));
assert(text.includes('Portugal (PT)'));
assert(!/[ÃÂâ][\u0080-\u00bf€œž”˜™]/.test(text));
await page.evaluate(() => {
window.qaPrintCalls = 0;
window.print = () => window.qaPrintCalls++;
});
await page
.getByRole('button', {
name: 'Imprimir / guardar como PDF',
exact: true,
})
.click();
assert.equal(await page.evaluate(() => window.qaPrintCalls), 1);
await q.shot(page, 'land-print-corrected', false);
const file = path.join(DIR, 'consumer-land-derived-20261010.pdf');
await page.pdf({
path: file,
format: 'A4',
printBackground: true,
margin: {
top: '12mm',
bottom: '12mm',
left: '10mm',
right: '10mm',
},
displayHeaderFooter: false,
});
q.proof.pdf = {
path: path
.relative(path.resolve(__dirname, '..'), file)
.replaceAll('\\', '/'),
source_route: new URL(page.url()).pathname,
source_case: ids.case_id,
generated_by: 'Chromium print CSS',
print_action_triggered: true,
country: 'MZ',
declared_origin: 'PT',
originals_replaced: false,
};
await ctx.close();
q.phase('Print source permissions: other owner and manager denied');
for (const role of ['outsider', 'manager']) {
({ page, ctx } = await q.context(role));
await q.login(page, role);
const r = await q.open(
page,
'/investidor/terra-duat/' + ids.case_id + '/imprimir',
);
assert.equal(r.status(), 404);
(q.proof.denials ||= []).push({ role, status: 404 });
await ctx.close();
}
q.proof.checks.print_presented_countries_encoding_and_authorization = true;
} else throw Error('Unknown 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,
)
) {
q.proof.failure_state = {
route: 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;
}
})();