// Only remaining criteria. Closed A/B and C37 are not repeated. const { start, DIR, assert, fs, path, sanitized, } = require('./portal-visual-qa-lib.cjs'); const mode = process.argv[2] || 'land'; void (async () => { const q = await start('revision-' + mode + '-chromium-20261011'); let page, ctx; async function actor(role) { if (ctx) await ctx.close(); ({ ctx, page } = await q.context(role)); if (role !== 'public') await q.login(page, role); } async function open(route) { const r = await q.open(page, route); const html = await r.text(); const attr = html.match(/data-page="([^"]+)"/); const raw = attr?.[1].startsWith('{') ? attr[1] .replace(/"/g, '"') .replace(/&/g, '&') .replace(/'/g, "'") : html.match( /]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/, )?.[1]; assert(raw, 'Inertia props'); return JSON.parse(raw).props; } async function a11y(name) { await page.addScriptTag({ path: path.join(DIR, 'qa-a11y-toolkit/package/axe.min.js'), }); const findings = await page.evaluate(async () => { const result = await axe.run(document, { runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'], }, }); return result.violations.map((v) => ({ id: v.id, impact: v.impact, nodes: v.nodes.map((n) => ({ target: n.target, summary: n.failureSummary, })), })); }); (q.proof.accessibility ||= []).push({ route: new URL(page.url()).pathname, name, violations: findings, }); q.save(); return findings; } try { if (mode === 'land') { await actor('promoter'); const id = '01a12568-c330-73ec-a1ac-543ac0116a26'; const route = '/investidor/terra-duat/' + id; let p = await open(route); q.proof.case = { id, status_before: p.case.status, version_before: p.case.version, country: p.case.payload.country, origin: p.case.payload.declared_origin_country_code, }; assert.equal(p.case.payload.country, 'MZ'); assert.equal(p.case.payload.declared_origin_country_code, 'PT'); if (p.case.status === 'draft') { q.phase('Land: support conversion from existing draft, UI'); await open(route + '/editar'); await page .getByLabel(/Pretende apoio para/) .selectOption('investment'); await page .getByLabel(/Descrição do pedido/) .fill( 'Ensaio sintético QA: orientação interna e preservação da origem declarada.', ); await page .getByLabel(/Categoria do requerente/) .selectOption('individual'); await page .getByLabel(/Nome ou denominação/) .fill('Promotor sintético QA21 — ensaio de apoio'); await page.getByLabel(/Canal preferido/).selectOption('email'); await page .getByRole('button', { name: 'Continuar', exact: true }) .click(); await page .getByLabel(/Situação da localização/) .selectOption('location_support'); await page .locator('#land-field-purpose') .fill( 'Preparação de investimento sintético em QA; sem decisão externa.', ); await page .getByRole('button', { name: 'Anterior', exact: true }) .click(); for (let i = 0; i < 3; i++) await page .getByRole('button', { name: 'Continuar', exact: true }) .click(); await page .getByLabel(/Confirmo que as respostas correspondem/) .check(); await page .getByLabel(/Autorizo o contacto para responder/) .check(); await a11y('land-support-final-step'); const pending = page.waitForResponse( (r) => new URL(r.url()).pathname === route + '/solicitar-apoio' && r.request().method() === 'POST', ); await page .getByRole('button', { name: 'Solicitar apoio', exact: true, }) .click(); const response = await pending; q.proof.support_status = response.status(); await page.waitForLoadState('networkidle'); await page.waitForFunction( () => !Array.from(document.querySelectorAll('button')).some( (b) => b.textContent.trim() === 'Solicitar apoio' && b.disabled, ), ); if ( ![302, 303, 202].includes(response.status()) || new URL(page.url()).pathname.endsWith('/editar') ) { q.proof.field_errors = await page .locator('[role="alert"]') .allTextContents(); await q.shot(page, 'support-errors'); throw Error( 'Support conversion not demonstrated; inspect field errors and response status', ); } } p = await open(route); assert.equal(p.case.status, 'support_requested'); assert(p.supportTicket?.reference); q.proof.case.support_reference = p.supportTicket.reference; await q.shot(page, 'land-support-recovered'); await a11y('land-support-read'); q.phase('Land: new session recovery and permission'); await actor('promoter'); p = await open(route); assert.equal(p.case.payload.declared_origin_country_code, 'PT'); assert.equal(p.case.status, 'support_requested'); await actor('outsider'); q.proof.outsider_status = (await q.open(page, route)).status(); assert([403, 404].includes(q.proof.outsider_status)); q.phase('Land: manager recovery and origin statistics'); await actor('manager'); p = await open('/management/terra-duat/' + id); assert.equal(p.case.payload.country, 'MZ'); assert.equal(p.case.payload.declared_origin_country_code, 'PT'); await q.shot(page, 'manager-support-country-origin'); const stats = await open('/management/interests?origin=PT'); q.proof.management_props = Object.keys(stats); q.proof.statistics = stats.declaredOriginStatistics || stats.originStatistics || null; await q.shot(page, 'support-statistics'); await a11y('management-statistics'); q.proof.criteria = { support_ui: true, country_origin_preserved: true, new_session: true, manager_recovery: true, outsider_denied: true, idempotent_conversion: 'Separate guarded persistence/replay proof required; no second UI submission', }; } else if (mode === 'land-errors') { await actor('promoter'); q.phase('Land: incomplete draft and accessible support errors'); let route = q.prior.fixture?.route; if (!route) { await open('/investidor/terra-duat/novo'); await page .getByLabel(/Nome ou denominação/) .fill('Ensaio sintético QA — erros acessíveis'); const navigation = page.waitForURL((u) => /^\/investidor\/terra-duat\/[a-f0-9-]+$/.test(u.pathname), ); await page .getByRole('button', { name: 'Guardar rascunho', exact: true, }) .click(); await navigation; route = new URL(page.url()).pathname; q.proof.fixture = { route }; q.save(); } else q.proof.fixture = { route }; await open(route + '/editar'); for (let i = 0; i < 3; i++) await page .getByRole('button', { name: 'Continuar', exact: true }) .click(); await page .getByRole('button', { name: 'Solicitar apoio', exact: true }) .click(); await page.waitForFunction( () => document.activeElement?.id === 'land-field-support_kind', ); assert.equal( await page .locator('#land-field-support_kind') .getAttribute('aria-describedby'), 'land-error-support_kind', ); assert(await page.locator('#land-error-support_kind').isVisible()); q.proof.criteria = { incomplete_draft_recoverable: true, invalid_request_not_submitted: true, first_error_focused: true, field_error_associated: true, summary_names_fields: await page .locator('#land-error-summary') .innerText(), }; await a11y('land-invalid-support'); await q.shot(page, 'land-errors-focused'); const stored = await open(route); assert.equal(stored.case.status, 'draft'); q.proof.criteria.invalid_remains_draft = true; } else if (mode === 'land-replay') { await actor('promoter'); q.phase('Land: API repetition of already submitted support'); const route = '/investidor/terra-duat/01a12568-c330-73ec-a1ac-543ac0116a26'; const before = await open(route); assert.equal(before.case.status, 'support_requested'); const errorsFixture = JSON.parse( fs.readFileSync( path.join( DIR, 'revision-land-errors-chromium-20261011.json', ), 'utf8', ), ); await open(errorsFixture.fixture.route + '/editar'); assert( await page .locator('p') .filter({ hasText: /^País do requerente \(opcional\)$/ }) .isVisible(), ); assert( await page .getByLabel('País do requerente (opcional)', { exact: true, }) .isVisible(), ); assert( await page .getByLabel( 'País de origem declarado da solicitação (opcional)', { exact: true }, ) .isVisible(), ); await a11y('land-country-labels'); await q.shot(page, 'land-country-origin-distinct'); const status = await page.evaluate( async ({ route, record }) => { const token = document.cookie .split('; ') .find((c) => c.startsWith('XSRF-TOKEN=')); return ( await fetch(route + '/solicitar-apoio', { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-XSRF-TOKEN': token ? decodeURIComponent(token.slice(11)) : '', }, body: JSON.stringify({ version: record.version, payload: record.payload, }), }) ).status; }, { route, record: before.case }, ); assert.equal(status, 409); const after = await open(route); assert.equal(after.case.version, before.case.version); assert.equal( after.supportTicket.reference, before.supportTicket.reference, ); q.proof.criteria = { api_replay_status: status, version_unchanged: true, support_reference_unchanged: true, visual_already_proved: 'revision-land-chromium-20261011.json', }; const master = await open('/investidor/profile'); assert(master.organizationCountryAvailable); q.proof.country_master_current_build = { available: master.organizationCountryAvailable, represented_count: master.organizations.length, values_recovered: master.organizations.map((o) => ({ id: o.id, country: o.country, version: o.version, })), }; } else if (mode === 'zones-map') { q.proof.consumers = []; for (const role of ['public', 'manager', 'geo']) { await actor(role); const route = role === 'public' ? '/zonas/ed4d2fba-95d5-42e3-8ca7-1f520de1036a' : '/management/investment/zones/ed4d2fba-95d5-42e3-8ca7-1f520de1036a'; q.phase('Zone own geometry map: ' + role); if (role === 'manager') { const denied = await q.open(page, route); assert.equal(denied.status(), 403); assert( (await denied.text()).includes('Configure e confirme'), ); q.proof.consumers.push({ route, role, status: 403, reason: 'Existing QA14 lacks confirmed MFA required by RequireZoneMfa; no privileges or MFA changed.', }); continue; } if (role === 'geo') { const props = await open(route); assert.equal(props.management, true); const region = page.getByRole('region', { name: 'Localização do investimento', exact: true, }); const province = region.getByLabel('Província', { exact: true, }); await province .locator( 'option[value="059b02ca-7095-577a-8705-4d4c46896747"]', ) .waitFor({ state: 'attached' }); const pending = page.waitForResponse( (r) => new URL(r.url()).pathname === '/management/territory-geometries', ); await province.selectOption( '059b02ca-7095-577a-8705-4d4c46896747', ); const response = await pending; assert.equal(response.status(), 200); const data = await response.json(); assert.equal(data.selected_geometry_available, true); await page .getByRole('region', { name: 'Mapa dos limites administrativos', exact: true, }) .last() .waitFor(); await page.setViewportSize({ width: 390, height: 844 }); const overflow = await page.evaluate( () => document.documentElement.scrollWidth > innerWidth, ); q.proof.consumers.push({ route, role, api_status: 200, administrative_geometry_available: true, administrative_geometry_count: data.context_geometry_count, selection_only_not_saved: true, mobile390_overflow: overflow, own_zone_map_not_present_in_editor: true, }); await a11y('zone-territory-manager'); await q.shot(page, 'zone-territory-manager'); continue; } await open(route); const pending = page.waitForResponse( (r) => new URL(r.url()).pathname === '/api/v2/investment/zone-map', ); await page .getByRole('button', { name: 'Consultar mapa', exact: true, }) .focus(); await page.keyboard.press('Enter'); const response = await pending; assert.equal(response.status(), 200); const data = await response.json(); assert(Array.isArray(data.features)); if (data.features.length) await page .getByRole('img', { name: 'Esquema das geometrias autorizadas da zona', }) .waitFor(); else await page .getByText( 'Não existe geometria autorizada para divulgação nesta ficha.', { exact: true }, ) .waitFor(); await page.setViewportSize({ width: 390, height: 844 }); const overflow = await page.evaluate( () => document.documentElement.scrollWidth > innerWidth, ); assert.equal( overflow, false, 'The map must fit the 390px viewport', ); q.proof.consumers.push({ route, role, api_status: 200, own_geometry_count: data.features.length, keyboard_action: true, empty_state_visible: data.features.length === 0, mobile390_overflow: overflow, not_administrative_parcel_equivalence: true, }); await a11y('zone-own-map-' + role); await q.shot(page, 'zone-map-' + role); } } else if (mode === 'zones') { await actor('public'); q.phase('Zones: public selectors and geometry consumer'); const p = await open('/zonas'); q.proof.props = Object.keys(p); q.proof.selector_names = await page .locator('select') .evaluateAll((nodes) => nodes.map((n) => ({ name: n.name, id: n.id, label: n.labels?.[0]?.textContent?.trim(), options: n.options.length, })), ); await a11y('zone-search'); await q.shot(page, 'zone-search'); q.proof.records = p.records?.data?.map((r) => ({ id: r.id, name: r.name })) || []; const province = page.getByLabel('Província', { exact: true }); await province.selectOption('059b02ca-7095-577a-8705-4d4c46896747'); await page.waitForFunction( () => document.querySelector('input[name="province_id"]') .value === '059b02ca-7095-577a-8705-4d4c46896747', ); const navigation = page.waitForURL( (u) => u.searchParams.get('province_id') === '059b02ca-7095-577a-8705-4d4c46896747', ); await page .getByRole('button', { name: 'Pesquisar', exact: true }) .click(); await navigation; await page.waitForLoadState('networkidle'); assert( new URL(page.url()).searchParams.get('province_id') === '059b02ca-7095-577a-8705-4d4c46896747', ); await q.shot(page, 'zone-filter-cabo-delgado'); await a11y('zone-filtered'); q.proof.criteria = { public_filter: true, selectors_filled: true, map_functional: false, note: 'Inspect map presentation separately; a filter response is not geometry coverage', }; } else if (mode === 'library') { await actor('manager'); q.phase( 'Library: locate synthetic published resource without reimport', ); const p = await open('/management/portal-services'); q.proof.synthetic_candidates = p.resources .filter( (r) => r.published && /qa|sint[eé]tic|fixture|demonstra/i.test(r.title), ) .map((r) => ({ id: r.id, title: r.title, published: r.published, document: r.document_id, })); await a11y('library-management'); await q.shot(page, 'library-management'); const legacy = p.resources.find( (r) => r.published && /agroneg[oó]cio/i.test(r.title) && r.source_url?.startsWith('https://investemoz.tempo.co.mz/'), ); const selected = q.prior.selected || q.proof.synthetic_candidates[0] || (legacy && { id: legacy.id, title: legacy.title, published: true, document: legacy.document_id, scope: 'Already disclosed legacy Agronegocio QA; temporary withdrawal with restoration', }); assert( selected, 'No published synthetic resource; do not withdraw arbitrary content', ); q.proof.selected = selected; const resource = p.resources.find((r) => r.id === selected.id); assert(resource); const doc = p.documents.find((d) => d.id === resource.document_id); q.proof.original = { id: doc.id, sha256: doc.sha256, scan: doc.malware_scan_status, }; assert.equal(doc.malware_scan_status, 'clean'); const publicContext = await q.context('public'); const publicPage = publicContext.page; const url = '/documentos/' + resource.id + '/download'; const before = await publicPage.request.get( 'http://127.0.0.1:18097' + url, ); assert.equal(before.status(), 200); q.proof.public_hash_before = require('node:crypto') .createHash('sha256') .update(await before.body()) .digest('hex'); const article = page.locator('article').filter({ has: page.getByRole('heading', { name: resource.title + ' · Publicado', exact: true, }), }); const pending = page.waitForResponse( (r) => new URL(r.url()).pathname === '/management/portal-services/resources/' + resource.id && r.request().method() === 'POST', ); await article .getByRole('button', { name: 'Retirar da publicação', exact: true, }) .click(); assert([302, 303].includes((await pending).status())); await page.waitForLoadState('networkidle'); q.proof.revoked_same_session = ( await publicPage.request.get('http://127.0.0.1:18097' + url) ).status(); assert([403, 404].includes(q.proof.revoked_same_session)); await publicContext.ctx.close(); const newer = await q.context('public'); q.proof.revoked_new_session = ( await newer.page.request.get('http://127.0.0.1:18097' + url) ).status(); assert([403, 404].includes(q.proof.revoked_new_session)); await newer.ctx.close(); const original = await page.request.get( 'http://127.0.0.1:18097/management/documents/' + doc.id + '/download', ); assert.equal(original.status(), 200); assert.equal( require('node:crypto') .createHash('sha256') .update(await original.body()) .digest('hex'), q.proof.public_hash_before, ); q.proof.original_preserved = true; await q.shot(page, 'library-withdrawn'); const withdrawn = page.locator('article').filter({ has: page.getByRole('heading', { name: resource.title + ' · Rascunho', exact: true, }), }); const restore = page.waitForResponse( (r) => new URL(r.url()).pathname === '/management/portal-services/resources/' + resource.id && r.request().method() === 'POST', ); await withdrawn .getByRole('button', { name: 'Publicar', exact: true }) .click(); assert([302, 303].includes((await restore).status())); await page.waitForLoadState('networkidle'); const restoredPublic = await q.context('public'); const restored = await restoredPublic.page.request.get( 'http://127.0.0.1:18097' + url, ); assert.equal(restored.status(), 200); assert.equal( require('node:crypto') .createHash('sha256') .update(await restored.body()) .digest('hex'), q.proof.public_hash_before, ); await restoredPublic.ctx.close(); q.proof.publication_restored = true; q.proof.criteria = { public_revocation: true, same_session_denied: true, new_session_denied: true, original_preserved: true, }; } else throw Error('Unknown bounded 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, ) ) 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, error: q.proof.error, receipt: q.receipt, }), ); if (q.proof.result === 'failed') process.exitCode = 1; } })();