// 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( /]*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), }), ); } })();