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

455 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Scoped P1: opportunity resource country/hierarchy, persistence and map recovery.
const crypto = require('node:crypto');
const {
start,
BASE,
TARGET,
DIR,
path,
fs: _fs,
assert,
sanitized,
} = require('./portal-functional-qa-lib.cjs');
async function browserRequest(page, method, url, options={}) {
assert.equal(new URL(url).origin,BASE);
const result=await page.evaluate(async ({method,url,options})=>{
const r=await fetch(url,{method,headers:{...options.headers,...(method==='POST'?{'Content-Type':'application/json'}:{})},...(method==='POST'?{body:JSON.stringify(options.data)}:{})});
return {status:r.status,headers:Object.fromEntries(r.headers.entries()),text:await r.text()};
},{method,url,options});
return {status:()=>result.status,headers:()=>result.headers,text:async()=>result.text,json:async()=>JSON.parse(result.text)};
}
void (async () => {
const screenshotOnly = process.argv.includes('--map-screenshot');
const q = await start(
screenshotOnly
? 'functional-coedit-map-chromium-20261011'
: 'functional-coedit-resources-chromium-20261011',
);
let page, ctx;
const route = '/minhas-oportunidades/15/resources';
const names = {
mz: 'QA contexto Cabo Delgado Ancuabe — 11/10/2026 coedição QA22',
pt: 'QA localização incompleta Portugal — 11/10/2026 coedição QA22',
};
async function requestHeaders() {
assert(ctx.qaConfirmed);
const cookie = (await ctx.cookies(BASE)).find(
(c) => c.name === 'XSRF-TOKEN',
);
assert(cookie, 'QA CSRF cookie required');
return {
Accept: 'application/json',
'X-XSRF-TOKEN': decodeURIComponent(cookie.value),
};
}
async function props() {
const r = await browserRequest(page, 'GET', BASE + route, {
headers: { Accept: 'text/html' },
});
assert.equal(r.headers()['x-investmoz-qa-target'], TARGET);
assert.equal(r.status(), 200);
const html = await r.text(),
encoded = html.match(/data-page="([^"]+)"/);
let data;
if (encoded && encoded[1].startsWith('{'))
data = JSON.parse(
encoded[1]
.replace(/"/g, '"')
.replace(/&/g, '&')
.replace(/'/g, "'"),
);
else {
const script = html.match(
/<script[^>]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/,
);
assert(script, 'Inertia bootstrap required');
data = JSON.parse(script[1]);
}
return data.props;
}
async function choose(label, pattern) {
const s = page.getByRole('combobox', { name: label, exact: true });
const option = s
.locator('option')
.filter({ hasText: new RegExp(pattern.source, 'i') })
.first();
await option.waitFor({ state: 'attached' });
const selected = await option.evaluate((o) => ({
id: o.value,
name: o.textContent,
}));
await s.selectOption(selected.id);
return selected;
}
async function savedLocation(title) {
const p = await props();
return p.resources.locations.find(
(r) => r.payload.title === title && !r.deleted_at,
);
}
async function edit(title) {
await q.open(page, route);
await page
.getByRole('article')
.filter({ hasText: title })
.getByRole('button', {
name: 'Ver / editar / restaurar',
exact: true,
})
.click();
await page
.getByRole('heading', {
name: 'Editar recurso / nova versão',
exact: true,
})
.waitFor();
}
async function map(id) {
const r = await q.json(
page,
'/minhas-oportunidades/territory-geometries?territory_id=' + id,
);
assert.equal(r.status, 200);
const box = page.getByRole('region', {
name: 'Cobertura cartográfica da seleção',
exact: true,
});
await box
.getByText(
/Limite administrativo disponível|Sem limite administrativo acessível/,
)
.waitFor();
if (r.data.areas.length)
await box
.locator('.leaflet-overlay-pane svg path')
.first()
.waitFor({ state: 'attached' });
const polygons = await box
.locator('.leaflet-overlay-pane svg path')
.count();
assert(r.data.areas.length === 0 || polygons > 0);
return {
selected_geometry_available: r.data.selected_geometry_available,
context_geometry_count: r.data.context_geometry_count,
rendered_polygon_paths: polygons,
external_tiles: 'blocked/not tested',
};
}
async function save(title) {
await page.getByLabel('Título', { exact: true }).fill(title);
await page
.getByLabel('Motivo (20–1000 caracteres)', { exact: true })
.fill(
'Ensaio territorial sintético QA, contexto administrativo sem atribuição de direitos.',
);
const response = page.waitForResponse(
(r) =>
r.request().method() === 'POST' &&
new URL(r.url()).pathname.includes('/resources/locations'),
);
await page
.getByRole('button', { name: 'Guardar recurso', exact: true })
.click();
const r = await response;
assert([200, 201].includes(r.status()));
await page.getByRole('article').filter({ hasText: title }).waitFor();
await page
.getByRole('heading', {
name: 'Novo recurso incompleto',
exact: true,
})
.waitFor();
return { status: r.status(), id: (await r.json()).id };
}
try {
q.phase('QA target and authorized QA21; no QA18 authentication retest');
({ ctx, page } = await q.context('colleague', 1440));
await q.login(page, 'colleague');
await q.open(page, route);
if (screenshotOnly) {
await page.setViewportSize({ width: 390, height: 1000 });
await edit(names.mz);
const mz = await savedLocation(names.mz);
q.proof.recovered_map = await map(mz.payload.district_id);
const box = page.getByRole('region', {
name: 'Cobertura cartográfica da seleção',
exact: true,
});
await box.scrollIntoViewIfNeeded();
assert.equal(
await box.getByText(/limites oficiais por verificar/).count(),
0,
);
assert.equal(
await box
.getByText(
/Limite administrativo da edição indicada; não representa um local de investimento/,
)
.count(),
2,
);
const file = path.join(
DIR,
'territory-resource-recovered-map-390-' + q.stamp + '.png',
);
await box.screenshot({ path: file });
q.proof.screenshots.push(
path
.relative(path.resolve(__dirname, '..'), file)
.replaceAll('\\', '/'),
);
const p = await props();
assert(
!p.assessment.some((issue) => /província|distrito/.test(issue)),
);
q.proof.assessment_foreign_hierarchy_not_required = true;
q.proof.administrative_edition_caption_correct = true;
q.proof.result = 'passed_read_only_map_screenshot';
return;
}
const baseline = await props();
q.proof.before = {
locations: baseline.resources.locations.length,
accepted_territorial_links: baseline.existingLocations.length,
};
q.proof.resources = {};
let mz = await savedLocation(names.mz);
if (!mz) {
const province = await choose('Província', /^Cabo Delgado/);
const district = await choose('Distrito', /^Ancuabe/);
q.proof.selection = {
province,
district,
initial_map: await map(district.id),
};
q.save();
await choose('Província', /^Nampula/);
assert.equal(
await page
.getByRole('combobox', { name: 'Distrito', exact: true })
.inputValue(),
'',
);
assert.equal(
await page
.getByRole('combobox', {
name: 'Posto administrativo',
exact: true,
})
.inputValue(),
'',
);
await choose('Província', /^Cabo Delgado/);
await choose('Distrito', /^Ancuabe/);
await page
.getByLabel('Papel da localização', { exact: true })
.selectOption('market');
await page
.getByLabel('Natureza da relação territorial', { exact: true })
.selectOption('context');
await save(names.mz);
mz = await savedLocation(names.mz);
}
assert.equal(mz.payload.country_code, 'MZ');
assert(mz.payload.province_id && mz.payload.district_id);
q.proof.resources.mz = {
id: mz.id,
version: mz.version,
payload: mz.payload,
};
// API rejection must preserve an already saved resource/version.
const current = await props();
assert(ctx.qaConfirmed);
const rejected = await browserRequest(page, 'POST',
BASE + route + '/locations/' + mz.id,
{
headers: await requestHeaders(),
data: {
etag: current.etag,
version: mz.version,
creation_key: crypto.randomUUID(),
reason: 'Ensaio QA de recusa da combinação país e hierarquia incompatíveis.',
payload: { ...mz.payload, country_code: 'PT' },
},
},
);
assert.equal(rejected.status(), 422);
assert((await rejected.json()).errors.country_code);
const unchanged = await savedLocation(names.mz);
assert.deepEqual(unchanged.payload, mz.payload);
assert.equal(unchanged.version, mz.version);
q.proof.rejected_foreign_hierarchy = {
status: 422,
error_field: 'country_code',
saved_payload_unchanged: true,
saved_version_unchanged: true,
};
q.phase(
'Foreign-country change clears only the unsaved hierarchy; save a recoverable incomplete location',
);
let pt = await savedLocation(names.pt);
if (!pt) {
await q.open(page, route);
await choose('Província', /^Cabo Delgado/);
await choose('Distrito', /^Ancuabe/);
await page
.getByLabel('País da localização', { exact: true })
.selectOption('PT');
for (const label of [
'Província',
'Distrito',
'Posto administrativo',
'Localidade',
]) {
const s = page.getByRole('combobox', {
name: label,
exact: true,
});
assert.equal(await s.inputValue(), '');
assert(await s.isDisabled());
}
assert.equal(
await page
.getByRole('region', {
name: 'Cobertura cartográfica da seleção',
})
.count(),
0,
);
await save(names.pt);
pt = await savedLocation(names.pt);
}
assert.equal(pt.payload.country_code, 'PT');
for (const key of [
'province_id',
'district_id',
'admin_post_id',
'locality_id',
])
assert(!pt.payload[key]);
q.proof.resources.pt = {
id: pt.id,
version: pt.version,
payload: pt.payload,
};
await ctx.close();
await new Promise((resolve) =>
setTimeout(resolve, 30000 - (Date.now() % 30000) + 1000),
);
q.phase(
'New QA21 browser context: recover saved paths, map and foreign draft; do not repeat C37',
);
({ ctx, page } = await q.context('promoter-new-context', 390));
await q.login(page, 'colleague');
await edit(names.mz);
assert.equal(
await page
.getByLabel('País da localização', { exact: true })
.inputValue(),
'MZ',
);
for (const [label, key] of [
['Província', 'province_id'],
['Distrito', 'district_id'],
])
assert.equal(
await page
.getByRole('combobox', { name: label, exact: true })
.inputValue(),
mz.payload[key],
);
q.proof.recovered_map = await map(mz.payload.district_id);
assert(q.proof.recovered_map.rendered_polygon_paths > 0);
await q.shot(page, 'saved-location-recovered-map-mobile', false);
await page.reload({ waitUntil: 'domcontentloaded' });
await page
.getByRole('article')
.filter({ hasText: names.mz })
.getByRole('button', {
name: 'Ver / editar / restaurar',
exact: true,
})
.click();
await map(mz.payload.district_id);
await edit(names.pt);
assert.equal(
await page
.getByLabel('País da localização', { exact: true })
.inputValue(),
'PT',
);
assert.equal(
await page
.getByRole('region', {
name: 'Cobertura cartográfica da seleção',
})
.count(),
0,
);
const final = await props();
assert.equal(
final.existingLocations.length,
baseline.existingLocations.length,
);
assert.equal(final.opportunity.status, baseline.opportunity.status);
q.proof.after = {
locations: final.resources.locations.length,
accepted_territorial_links: final.existingLocations.length,
opportunity_status_unchanged: true,
};
await ctx.close();
q.phase('Different organization QA23 denied view and write');
({ ctx, page } = await q.context('outsider', 1440));
await q.login(page, 'outsider');
assert.equal((await q.open(page, route)).status(), 403);
assert(ctx.qaConfirmed);
const denied = await browserRequest(page, 'POST',
BASE + route + '/locations/' + mz.id,
{
headers: await requestHeaders(),
data: {
etag: final.etag,
version: mz.version,
creation_key: crypto.randomUUID(),
reason: 'Ensaio sintético de autorização QA de organização diferente.',
payload: mz.payload,
},
},
);
assert.equal(denied.status(), 403);
q.proof.authorization = {
actor_id: 23,
view_status: 403,
write_status: 403,
};
q.proof.checks = {
country_hierarchy_server_rejection: true,
unsaved_country_change_clears_mozambique_hierarchy: true,
foreign_incomplete_draft_saved: true,
new_context_country_path_map_recovery: true,
parent_change_clears_descendants: true,
no_accepted_territorial_links_added: true,
no_publication_or_active_writes: true,
wrong_organization_denied: true,
no_qa18_authentication_retest: true,
};
q.proof.result = 'passed';
} catch (error) {
q.proof.result = 'failed';
q.proof.error = sanitized(error);
console.log(JSON.stringify({ error: q.proof.error }));
process.exitCode = 1;
} finally {
q.proof.finished_at = new Date().toISOString();
q.save();
await q.browser.close();
console.log(
JSON.stringify({
result: q.proof.result,
checks: q.proof.checks,
receipt: path.basename(q.receipt),
}),
);
}
})();