Files
Investidor-V2/tests/frontend/public-information.test.mjs
T
2026-11-15 18:56:39 +02:00

589 lines
22 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
import { compileFunction } from 'node:vm';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import ts from 'typescript';
const require = createRequire(import.meta.url);
const root = path.resolve('resources/js');
function load(file, mocks = {}) {
const absolute = path.resolve(root, file);
const module = { exports: {} };
const code = ts.transpileModule(fs.readFileSync(absolute, 'utf8'), {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
jsx: ts.JsxEmit.ReactJSX,
},
}).outputText;
compileFunction(code, ['require', 'module', 'exports'])(
(id) => {
if (id in mocks) return mocks[id];
if (id === '@inertiajs/react')
return {
Link: ({ children, ...props }) =>
React.createElement('a', props, children),
usePage: () => ({ url: '/sectores/test' }),
};
if (id.startsWith('.') || id.startsWith('@/')) {
const base = id.startsWith('@/')
? path.resolve(root, id.slice(2))
: path.resolve(path.dirname(absolute), id);
const found = [base, base + '.tsx', base + '.ts'].find(
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
);
if (found) return load(path.relative(root, found), mocks);
}
return require(id);
},
module,
module.exports,
);
return module.exports;
}
const render = (file, props) =>
renderToStaticMarkup(React.createElement(load(file).default, props));
test('demographic comparison retains four saved places and requires a province before searching districts', () => {
const Page = load('pages/investment/demography.tsx', {
'./shared': {
Frame: ({ children }) =>
React.createElement('main', null, children),
button: '',
},
'@/components/demographic-evidence': { default: () => null },
}).default;
const territories = [
{
id: 'p1',
name: 'Província A',
location_type: 'province',
parent_id: null,
},
{
id: 'p2',
name: 'Província B',
location_type: 'province',
parent_id: null,
},
{
id: 'd1',
name: 'Distrito A',
location_type: 'district',
parent_id: 'p1',
},
{
id: 'd2',
name: 'Distrito B',
location_type: 'district',
parent_id: 'p2',
},
];
const html = renderToStaticMarkup(
React.createElement(Page, {
evidence: { territories: [] },
filters: {
territories: territories.map((t) => t.id),
start_year: 2026,
end_year: 2027,
},
territories,
sectors: { general: 'Geral' },
}),
);
assert.match(html, /aria-label="Pesquisar Província da comparação"/);
assert.match(
html,
/aria-label="Pesquisar Distrito da comparação \(opcional\)"[^>]*disabled/,
);
assert.doesNotMatch(html, /<select[^>]*multiple/);
for (const t of territories)
assert.ok(html.includes(`Retirar ${t.name} —`));
assert.match(html, /disabled=""[^>]*>Acrescentar à comparação/);
assert.match(html, /value="2026"/);
assert.match(html, /value="2027"/);
});
test('the real preparation editor opens an own idea with its budget and no mandatory advert or duplicate reference selector', () => {
const Empty = () => null;
const Editor = load('pages/investor-path/context.tsx', {
'@/pages/investment/shared': {
Frame: ({ children }) =>
React.createElement('main', null, children),
card: '',
button: '',
input: '',
},
'@/components/navigation-context': { useNavigationContext: () => ({}) },
'@/components/investment-territory': { TerritoryFields: Empty },
'@/components/private-investment-preparation': {
PreparationEditor: Empty,
PreparationImpact: Empty,
},
'@/components/investment-complements': {
default: Empty,
SelectedComplements: Empty,
complementInput: (value) => value,
},
'@/components/context-documents': { default: Empty },
'@/components/investment-opportunity-choices': { default: Empty },
'@/components/investment-requirements': { default: Empty },
'@/lib/operation-key': { createOperationKey: () => 'synthetic-key' },
}).default;
const html = renderToStaticMarkup(
React.createElement(Editor, {
initial: {
case: {
id: 'synthetic-hotel',
title: 'Criar Hotel',
version: 1,
},
context: {
revision: 0,
fields: { project_title: 'Criar Hotel' },
selection: {},
provenance: {},
opportunity_choices: [],
},
private_investment: null,
can_write: true,
results: [],
requests: [],
preparation_documents: [],
},
source: null,
options: {
countries: [
{ code: 'MZ', name: 'Moçambique', name_en: 'Mozambique' },
],
sectors: [{ id: 7, name: 'Turismo QA' }],
parks: [],
search_limited: false,
opportunities: [
{
key: 'opportunity:2',
title: 'Açúcar sem pertinência',
kind: 'opportunity',
},
],
},
entryIdea: {
sector_id: '7',
currency: 'MZN',
investment_budget_min: '1000000',
investment_budget_max: '5000000',
},
}),
);
assert.match(html, /Criar Hotel/);
assert.match(html, /value="1000000"/);
assert.match(html, /value="5000000"/);
assert.match(html, /value="sector:7" selected=""/);
assert.match(html, /Oportunidades relacionadas \(opcional\)/);
assert.doesNotMatch(html, /Referência individual|Açúcar sem pertinência/);
if (process.env.INVESTMOZ_INTENT_PREVIEW === '1') {
const manifest = JSON.parse(
fs.readFileSync(
'public/build-qa-opportunities/manifest.json',
'utf8',
),
);
const css = fs.readFileSync(
'public/build-qa-opportunities/' +
manifest['resources/css/app.css'].file,
'utf8',
);
fs.writeFileSync(
'outputs/investor-intent-20261115/editor-preview.html',
`<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>InvesteMoz — prévia sintética de preparação</title><style>${css}</style><body><div class="mx-auto max-w-4xl p-4"><p class="rounded border p-3">QA sintético: editor renderizado, sem autenticação, gravação ou consultas. A cartografia foi substituída no ensaio. Esta prévia não demonstra o percurso autenticado.</p>${html}</div></body></html>`,
'utf8',
);
}
});
test('changing the consulted territory remounts its evidence and aborts a late response', async () => {
const state = [],
refs = [];
let cursor = 0,
refCursor = 0,
cleanup;
const hooks = {
useState(initial) {
const index = cursor++;
if (!(index in state)) state[index] = initial;
return [
state[index],
(next) => {
state[index] = next;
},
];
},
useRef(initial) {
const index = refCursor++;
refs[index] ??= { current: initial };
return refs[index];
},
useEffect(callback) {
cleanup = callback();
},
};
const Component = load('components/demographic-context.tsx', {
react: hooks,
}).default;
const a = Component({ territoryId: 'synthetic-A' });
const b = Component({ territoryId: 'synthetic-B' });
assert.notEqual(a.key, b.key);
assert.equal(Component({ territoryId: null }), null);
const tree = a.type(a.props);
const nodes = (n) =>
!n || typeof n !== 'object'
? []
: Array.isArray(n)
? n.flatMap(nodes)
: [n, ...nodes(n.props?.children)];
const button = nodes(tree).find((n) => n.type === 'button');
const originalFetch = globalThis.fetch;
let resolve, signal;
globalThis.fetch = (url, options) => {
assert.match(url, /synthetic-A/);
signal = options.signal;
return new Promise((done) => {
resolve = done;
});
};
try {
const pending = button.props.onClick();
cleanup();
assert.equal(signal.aborted, true);
resolve({ ok: true, json: async () => ({ territories: ['old A'] }) });
await pending;
assert.equal(
state[0],
null,
'late evidence must not be attached to another territory',
);
} finally {
globalThis.fetch = originalFetch;
}
});
test('shared evidence is closed by default, preserves content and wraps long references', () => {
const html = render('components/evidence-details.tsx', {
children: 'source-version-hash',
title: 'Fonte',
});
assert.match(html, /<details[^>]*>/);
assert.doesNotMatch(html, /<details[^>]*\bopen/);
assert.match(html, /source-version-hash/);
assert.match(html, /overflow-wrap:anywhere/);
});
test('public document keeps applicability, usage limitations and actions visible, with source metadata in detail', () => {
const { PublicDocumentContent } = load(
'components/public-document-content.tsx',
);
const resource = {
id: 1,
title: 'Diploma QA',
description: 'Objecto documentado',
issuer: null,
document_number: null,
document_date: null,
applicable_from: '2025-01-01',
applicable_until: null,
source_url: null,
source_confirmation: 'unknown',
usage_notice: 'Confirmar o âmbito aplicável',
view_url: null,
download_url: '/documents/1',
context_actions: [],
};
const html = renderToStaticMarkup(
React.createElement(PublicDocumentContent, { resource }),
);
assert.match(html, /2025-01-01/);
assert.match(html, /Confirmar o âmbito aplicável/);
assert.match(html, /href="\/documents\/1"/);
assert.doesNotMatch(html, /Entidade emissora|Consultar ficha/);
assert.ok(html.indexOf('Período indicado') > html.indexOf('</details>'));
});
test('territorial references retain exact citation and provenance without a primary internal-process block', () => {
const source = {
association: 'Corredor QA',
role: 'Ligação',
source_reference: 'Diploma QA §2',
source_quote: 'Relação documentada',
supports: '',
traceability: 'original hash/version',
official_source_accepted: true,
existing_validation: 'validated',
};
const html = render('components/territorial-source-evidence.tsx', {
sources: [source],
});
assert.match(html, /Diploma QA §2/);
assert.match(html, /original hash\/version/);
assert.doesNotMatch(
html,
/não retira a aceitação existente|campo específico/,
);
assert.equal(
render('components/territorial-source-evidence.tsx', { sources: [] }),
'',
);
});
test('empty published catalogue does not claim absent companies, resources or economic potential', () => {
const html = render('components/published-opportunities-empty.tsx', {
href: '/oportunidades?sector=qa',
});
assert.match(html, /Sem anúncios publicados/);
assert.match(
html,
/não indica ausência de empresas, recursos ou potencial/,
);
assert.match(html, /sector=qa/);
});
const value = (
year,
age_group = 'all',
data_kind = 'projected',
unit = 'pessoas',
) => ({
year,
indicator: 'population',
age_group,
residence: 'all',
value: '1234',
original_value: '1 234',
unit,
data_kind,
sheet: 'sheet',
cell: 'B' + year,
validation_status: 'valid',
});
test('demographic highlights never sum dimensions, mix units or silently replace a source/version', () => {
const { demographicHighlights } = load(
'components/demographic-evidence.tsx',
);
const values = [
value(2024),
value(2025),
value(2026, '0-4'),
value(2024, 'all', 'observed'),
value(2023, 'all', 'projected', 'unspecified'),
];
assert.deepEqual(demographicHighlights(values), [
values[1],
values[3],
values[4],
]);
assert.deepEqual(demographicHighlights([value(2025, '0-4')]), []);
});
test('demography shows original latest total, period, projection and decision warning; technical cells remain closed', () => {
const evidence = {
state: 'available',
start_year: 2024,
end_year: 2025,
limitations: ['Não prova procura'],
comparisons: [],
differences: [],
territories: [
{
id: 'qa',
name: 'Território QA',
level: 'province',
factors_to_assess: [],
series: [
{
import_id: 'qa',
source: 'Fonte QA',
version: 'Edição QA',
sha256: '123abc',
file_name: 'QA.xlsx',
missing_years: [],
issue_counts: {},
values: [value(2024), value(2025)],
},
],
},
],
};
const html = render('components/demographic-evidence.tsx', { evidence });
assert.match(html, /1 234/);
assert.match(html, /não demonstram procura ou rentabilidade/);
assert.match(html, /Projecção/);
assert.match(html, /SHA256/);
assert.ok(html.indexOf('SHA256') > html.indexOf('<summary'));
assert.doesNotMatch(html, /<details[^>]*\bopen/);
});
test('cluster analysis omits empty field blocks but keeps documented evidence and uncertainty', () => {
const { forces } = load('lib/cluster-competitiveness.ts');
const rows = Object.fromEntries(
forces.map((f) => [
f,
{
pressure: '',
evidence: 'Facto QA',
source: '',
period: '',
rationale: '',
limitations: 'Por confirmar',
opportunity: '',
},
]),
);
const html = render('components/cluster-competitiveness-public.tsx', {
analysis: {
version: 1,
at: '2026-11-15',
draft: { sector: 'QA', region: 'Local QA', forces: rows },
},
});
assert.match(html, /Facto QA/);
assert.match(html, /Por confirmar/);
assert.doesNotMatch(html, /<dt[^>]*>Fonte e referência/);
});
test('simulator keeps original calculation correspondence and source behind explicit details', () => {
const { SimulationModeloA } = load('components/simulation-modelo-a.tsx');
const html = renderToStaticMarkup(
React.createElement(SimulationModeloA, { draft: {} }),
);
assert.match(html, /Modelo A/);
assert.match(html, /SHA256/);
assert.ok(
html.indexOf('SHA256') >
html.indexOf('Exemplar e integridade da fonte'),
);
assert.match(html, /não é imposto final/);
});
// Optional visual fixture: the real components with explicit synthetic data,
// not an authenticated application or an operational publication.
if (process.env.INVESTMOZ_RECONCILIATION_PREVIEW === '1') {
const population = (year, age, amount, residence = 'all') => ({
...value(year, age, 'projected', 'pessoas'),
residence,
value: String(amount),
original_value: String(amount),
cell: `${year}-${age}-${residence}`,
});
const values = [
population(2026, 'all', 1000),
population(2028, 'all', 1200),
population(2028, 'all', 480, 'urban'),
population(2028, 'all', 720, 'rural'),
];
for (const age of ['0-4', '5-9', '10-14'])
values.push(population(2028, age, 120));
for (const age of [
'15-19',
'20-24',
'25-29',
'30-34',
'35-39',
'40-44',
'45-49',
'50-54',
'55-59',
'60-64',
])
values.push(population(2028, age, 60));
const evidence = {
state: 'available',
start_year: 2026,
end_year: 2028,
sector: 'general',
limitations: [
'Exclusivamente dados sintéticos. Não representam população real.',
],
comparisons: [],
differences: [],
territories: [
{
id: 'synthetic-qa',
name: 'Território sintético QA',
level: 'district',
factors_to_assess: [],
series: [
{
import_id: 'qa',
source: 'Fonte sintética de ensaio',
version: 'QA 2026',
sha256: 'a'.repeat(64),
file_name: 'exemplo-sintetico.xlsx',
missing_years: [2027],
issue_counts: {},
values,
},
],
},
],
};
const demographicHtml = render('components/demographic-evidence.tsx', {
evidence,
});
const emptyHtml = render('components/published-opportunities-empty.tsx', {
href: '#demografia',
label: 'Estudar a localização',
});
const { PublicDocumentContent } = load(
'components/public-document-content.tsx',
);
const documentHtml = renderToStaticMarkup(
React.createElement(PublicDocumentContent, {
resource: {
id: 1,
title: 'Guia sintético QA',
description:
'Documento de ensaio para conferir a apresentação.',
issuer: 'Entidade sintética',
document_number: 'QA-1',
document_date: '2026-11-15',
applicable_from: '2026-11-15',
applicable_until: null,
source_url: null,
source_confirmation: 'unknown',
usage_notice:
'Exemplo de limitação relevante, conservada junto do documento.',
view_url: null,
download_url: '#documento',
context_actions: [],
},
}),
);
const sourceHtml = render('components/territorial-source-evidence.tsx', {
sources: [
{
association: 'Relação territorial sintética',
role: 'Contexto',
source_reference: 'Fonte sintética §1',
source_quote: 'Relação apenas para ensaio.',
supports: '',
traceability: 'Versão QA e original conservados',
official_source_accepted: false,
existing_validation: null,
},
],
});
const { SimulationModeloA } = load('components/simulation-modelo-a.tsx');
const simulatorHtml = renderToStaticMarkup(
React.createElement(SimulationModeloA, { draft: {} }),
);
const manifest = JSON.parse(
fs.readFileSync('public/build-qa-opportunities/manifest.json', 'utf8'),
);
const css = fs.readFileSync(
'public/build-qa-opportunities/' +
manifest['resources/css/app.css'].file,
'utf8',
);
fs.writeFileSync(
'outputs/demography-reconciliation-20261115/visual-components.html',
`<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>InvesteMoz QA — conferência de componentes</title><style>${css}</style><body class="bg-slate-50 text-slate-900"><main class="mx-auto max-w-5xl space-y-8 p-4"><header><h1 class="text-2xl font-bold">InvesteMoz — prévia QA</h1><p>Componentes reais com dados sintéticos. Não é uma sessão autenticada nem publicação no portal.</p><nav class="flex flex-wrap gap-4 py-4"><a href="#demografia">Demografia</a><a href="#catalogo">Catálogo</a><a href="#territorio">Território</a><a href="#documento">Biblioteca</a><a href="#simulador">Simulador</a></nav></header><section id="demografia">${demographicHtml}</section><section id="catalogo"><h2 class="text-xl font-semibold">Catálogo — sem anúncios publicados</h2>${emptyHtml}</section><section id="territorio"><h2 class="text-xl font-semibold">Contexto territorial</h2>${sourceHtml}</section><section id="documento">${documentHtml}</section><section id="simulador">${simulatorHtml}</section></main></body></html>`,
'utf8',
);
}