1259 lines
54 KiB
JavaScript
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;
|
|
}
|
|
})();
|