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

503 lines
20 KiB
JavaScript

const {
start,
BASE,
DIR,
assert,
fs,
path,
sanitized,
} = require('./portal-visual-qa-lib.cjs');
const crypto = require('node:crypto');
const mode = process.argv[2];
void (async () => {
const q = await start('next-' + mode + '-chromium-20261010');
let page, ctx;
async function open(route) {
const r = await q.open(page, route);
const html = await r.text();
const script = html.match(
/<script[^>]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/,
);
const attr = html.match(/data-page="([^"]+)"/);
const raw =
attr && attr[1].startsWith('{')
? attr[1]
.replace(/&quot;/g, '"')
.replace(/&amp;/g, '&')
.replace(/&#039;/g, "'")
: script?.[1];
assert(raw, 'Inertia props');
return JSON.parse(raw).props;
}
async function actor(role) {
if (ctx) await ctx.close();
({ ctx, page } = await q.context(role));
await q.login(page, role);
}
async function mutation(url, data, method = 'POST') {
return page.evaluate(
async ({ url, data, method }) => {
const c = 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': c
? decodeURIComponent(c.slice(11))
: '',
},
...(method === 'GET' || method === 'HEAD'
? {}
: { body: JSON.stringify(data) }),
});
return { status: r.status, data: await r.json() };
},
{ url, data, method },
);
}
async function save(button, url, method) {
const wait = page.waitForResponse(
(r) =>
new URL(r.url()).pathname === url &&
r.request().method() === method,
);
await button.click();
const r = await wait;
assert([200, 201, 302, 303].includes(r.status()));
await page.waitForLoadState('networkidle');
return r.status();
}
try {
if (mode === 'organization') {
q.proof.fixture = q.prior.fixture || {};
const f = q.proof.fixture;
await actor('promoter');
let p = await open('/investidor/profile');
assert(p.organizationCountryAvailable);
const org = p.organizations.find(
(o) => o.id === '99964753-2632-4658-a687-8ebc6a178296',
);
assert(org?.canEdit);
f.organization = org.id;
const name = 'QA reconciliação país mestre — cópia explícita';
let company = p.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('MZ');
await box
.getByLabel('Função ou relação com a empresa', {
exact: true,
})
.fill('Declaração sintética QA, sem mandato real');
await save(
box.getByRole('button', {
name: 'Guardar declaração da empresa',
exact: true,
}),
'/investidor/companies',
'POST',
);
p = await open('/investidor/profile');
company = p.companies.find((c) => c.payload.name === name);
}
f.company = company.id;
q.save();
if (!f.old_submission) {
const section = page.locator('section').filter({
has: page.getByRole('heading', { name, exact: true }),
});
await save(
section.getByRole('button', {
name: 'Preparar manifestação desta empresa',
exact: true,
}),
'/investidor/manifestacoes',
'POST',
);
await page.waitForURL((u) =>
/^\/investidor\/manifestacoes\/.+/.test(u.pathname),
);
f.old_submission = new URL(page.url()).pathname
.split('/')
.pop();
q.save();
}
let old = await open(
'/investidor/manifestacoes/' + f.old_submission,
);
assert.equal(
old.submission.payload.incorporation_country_code,
'MZ',
);
f.old_version = old.submission.version;
q.save();
p = await open('/investidor/profile');
const section = page.getByRole('region', {
name: 'Cadastro mestre: Synthetic QA organization',
exact: true,
});
if (!f.master_saved) {
q.phase(
'C97: master country with explicit source, validation error and keyboard focus',
);
await section
.getByRole('combobox', {
name: 'País de constituição da organização',
exact: true,
})
.selectOption('');
await section
.getByLabel('Fonte ou referência deste país', {
exact: true,
})
.fill('');
await save(
section.getByRole('button', {
name: 'Guardar país no cadastro mestre',
exact: true,
}),
'/investidor/organizations/' + org.id + '/country',
'PUT',
);
const invalid = section.getByRole('combobox', {
name: 'País de constituição da organização',
exact: true,
});
await page.waitForFunction(
() =>
document.activeElement?.getAttribute('aria-invalid') ===
'true',
);
assert.equal(
await invalid.getAttribute('aria-invalid'),
'true',
);
assert(
await invalid.evaluate((n) => document.activeElement === n),
);
assert(await invalid.getAttribute('aria-describedby'));
q.proof.checks.master_errors_named_and_focused = true;
await q.shot(page, 'master-error-focused', false);
await invalid.selectOption('FR');
await section
.getByLabel('Fonte ou referência deste país', {
exact: true,
})
.fill(
'Declaração sintética QA do representante, 10/10/2026; sem registo ou decisão real.',
);
await save(
section.getByRole('button', {
name: 'Guardar país no cadastro mestre',
exact: true,
}),
'/investidor/organizations/' + org.id + '/country',
'PUT',
);
p = await open('/investidor/profile');
assert.equal(
p.organizations.find((o) => o.id === org.id).country,
'FR',
);
f.master_saved = true;
f.master_version = p.organizations.find(
(o) => o.id === org.id,
).version;
q.save();
}
if (!f.reused) {
const section = page.locator('section').filter({
has: page.getByRole('heading', { name, exact: true }),
});
await save(
section.getByRole('button', {
name: 'Reutilizar país de Synthetic QA organization',
exact: true,
}),
'/investidor/companies/' +
f.company +
'/organization-country',
'POST',
);
p = await open('/investidor/profile');
company = p.companies.find((c) => c.id === f.company);
assert.equal(company.payload.incorporation_country_code, 'FR');
assert.equal(company.countrySource.organization_id, org.id);
f.reused = true;
q.save();
}
if (!f.new_submission) {
const section = page.locator('section').filter({
has: page.getByRole('heading', { name, exact: true }),
});
await save(
section.getByRole('button', {
name: 'Preparar manifestação desta empresa',
exact: true,
}),
'/investidor/manifestacoes',
'POST',
);
await page.waitForURL((u) =>
/^\/investidor\/manifestacoes\/.+/.test(u.pathname),
);
f.new_submission = new URL(page.url()).pathname
.split('/')
.pop();
q.save();
}
const newSubmission = await open(
'/investidor/manifestacoes/' + f.new_submission,
);
assert.equal(
newSubmission.submission.payload.incorporation_country_code,
'FR',
);
assert.equal(
newSubmission.submission.payload.organization_country_snapshot
.organization_version,
f.master_version,
);
if (!f.master_changed) {
p = await open('/investidor/profile');
const box = page.getByRole('region', {
name: 'Cadastro mestre: Synthetic QA organization',
exact: true,
});
await box
.getByRole('combobox', {
name: 'País de constituição da organização',
exact: true,
})
.selectOption('ZA');
await box
.getByLabel('Fonte ou referência deste país', {
exact: true,
})
.fill(
'Segunda declaração sintética QA: ensaio de cópias históricas preservadas.',
);
await save(
box.getByRole('button', {
name: 'Guardar país no cadastro mestre',
exact: true,
}),
'/investidor/organizations/' + org.id + '/country',
'PUT',
);
f.master_changed = true;
q.save();
}
q.phase(
'Fresh session: current master ZA, prior copies MZ/FR and provenance preserved',
);
await actor('promoter');
p = await open('/investidor/profile');
assert.equal(
p.organizations.find((o) => o.id === org.id).country,
'ZA',
);
assert.equal(
p.companies.find((c) => c.id === f.company).payload
.incorporation_country_code,
'FR',
);
await q.shot(page, 'master-versus-explicit-copy');
old = await open('/investidor/manifestacoes/' + f.old_submission);
assert.equal(
old.submission.payload.incorporation_country_code,
'MZ',
);
assert.equal(old.submission.version, f.old_version);
const later = await open(
'/investidor/manifestacoes/' + f.new_submission,
);
assert.equal(
later.submission.payload.incorporation_country_code,
'FR',
);
assert.equal(
later.submission.payload.organization_country_snapshot
.organization_version,
f.master_version,
);
q.proof.checks.new_session_no_historical_rewrite = true;
await actor('outsider');
p = await open('/investidor/profile');
assert(!p.organizations.some((o) => o.id === org.id));
const denied = await mutation(
'/investidor/organizations/' + org.id + '/country',
{
version: f.master_version + 1,
country: 'MZ',
source_reference:
'Tentativa sintética de outra organização',
},
'PUT',
);
assert.equal(denied.status, 403);
q.proof.checks.other_organization_denied = true;
} else if (mode === 'sharing') {
const old = JSON.parse(
fs.readFileSync(
path.join(DIR, 'independent-documents-http-qa.json'),
'utf8',
),
);
const room = old.receipts.room_id;
assert.equal(room, 1);
q.proof.documents = [];
await actor('promoter');
let p = await open('/parcerias/salas/' + room);
assert.equal(p.room.id, room);
const owner = { page, ctx };
({ page, ctx } = await q.context('colleague'));
await q.login(page, 'colleague');
let candidate = { page, ctx };
const base = '/parcerias/salas/' + room;
for (const d of old.room) {
q.phase('Visual explicit sharing and revocation ' + d.type);
page = owner.page;
ctx = owner.ctx;
p = await open(base);
const record = p.documents.find(
(x) => x.id === d.attachment_id,
);
assert(record && !record.shared);
const article = page.locator('article').filter({
has: page.getByText(record.file_name, { exact: false }),
});
await save(
article.getByRole('button', {
name: 'Autorizar partilha com a contraparte',
exact: true,
}),
base + '/documentos/' + d.attachment_id + '/partilha',
'POST',
);
page = candidate.page;
ctx = candidate.ctx;
p = await open(base);
assert(p.documents.some((x) => x.id === d.attachment_id));
const response = await page.request.get(
BASE + base + '/documentos/' + d.attachment_id,
);
assert.equal(response.status(), 200);
assert.equal(
crypto
.createHash('sha256')
.update(await response.body())
.digest('hex'),
d.sha256,
);
await q.shot(page, 'shared-' + d.type, false);
page = owner.page;
ctx = owner.ctx;
await open(base);
const card = page.locator('article').filter({
has: page.getByText(record.file_name, { exact: false }),
});
await save(
card.getByRole('button', {
name: 'Retirar autorização de partilha',
exact: true,
}),
base + '/documentos/' + d.attachment_id + '/partilha',
'POST',
);
page = candidate.page;
ctx = candidate.ctx;
const denied = await page.request.get(
BASE + base + '/documentos/' + d.attachment_id,
);
assert.equal(denied.status(), 404);
p = await open(base);
assert(!p.documents.some((x) => x.id === d.attachment_id));
await q.shot(page, 'revoked-' + d.type, false);
q.proof.documents.push({
type: d.type,
id: d.attachment_id,
sha256: d.sha256,
previously_allowed: 200,
same_session_after_revocation: 404,
listing_removed: true,
});
q.save();
}
await candidate.ctx.close();
({ page, ctx } = await q.context('colleague'));
await q.login(page, 'colleague');
p = await open(base);
for (const d of q.proof.documents) {
const denied = await page.request.get(
BASE + base + '/documentos/' + d.id,
);
assert.equal(denied.status(), 404);
assert(!p.documents.some((x) => x.id === d.id));
d.fresh_session_after_revocation = 404;
}
await ctx.close();
page = owner.page;
ctx = owner.ctx;
for (const d of q.proof.documents) {
const r = await page.request.get(
BASE + base + '/documentos/' + d.id,
);
assert.equal(r.status(), 200);
assert.equal(
crypto
.createHash('sha256')
.update(await r.body())
.digest('hex'),
d.sha256,
);
}
q.proof.checks.visual_revocation_previously_granted_urls = true;
q.proof.checks.owner_originals_preserved = true;
}
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 = {
route: new URL(page.url()).pathname,
alerts: await page.getByRole('alert').allTextContents(),
buttons: await page.getByRole('button').allTextContents(),
};
await q.shot(page, 'failure', false).catch(() => {});
}
} finally {
q.proof.completed_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,
error: q.proof.error,
}),
);
if (q.proof.result === 'failed') process.exitCode = 1;
})();