341 lines
11 KiB
JavaScript
341 lines
11 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import { compileFunction } from 'node:vm';
|
|
import { createRequire } from 'node:module';
|
|
import React from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import ts from 'typescript';
|
|
const require = createRequire(import.meta.url);
|
|
const Map = () => React.createElement('div', { 'data-test-map': true });
|
|
function setup() {
|
|
let theme;
|
|
let mounted = false;
|
|
const source = fs.readFileSync(
|
|
new URL(
|
|
'../../resources/js/components/thematic-investment-map.tsx',
|
|
import.meta.url,
|
|
),
|
|
'utf8',
|
|
);
|
|
const output = ts.transpileModule(source, {
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.ES2022,
|
|
jsx: ts.JsxEmit.ReactJSX,
|
|
},
|
|
}).outputText;
|
|
const module = { exports: {} };
|
|
compileFunction(output, ['require', 'module', 'exports'])(
|
|
(name) =>
|
|
name === 'react'
|
|
? {
|
|
...React,
|
|
useEffect: (effect) => {
|
|
if (!mounted) {
|
|
mounted = true;
|
|
effect();
|
|
}
|
|
},
|
|
useState: (initial) => {
|
|
if (theme === undefined)
|
|
theme =
|
|
typeof initial === 'function'
|
|
? initial()
|
|
: initial;
|
|
return [
|
|
theme,
|
|
(next) => {
|
|
theme = next;
|
|
},
|
|
];
|
|
},
|
|
}
|
|
: name === './investment-map-indicators'
|
|
? { default: () => null }
|
|
: name === './catalogue-investment-map'
|
|
? { default: Map }
|
|
: require(name),
|
|
module,
|
|
module.exports,
|
|
);
|
|
return (...args) => {
|
|
module.exports.default(...args);
|
|
return module.exports.default(...args);
|
|
};
|
|
}
|
|
const flat = (node) =>
|
|
!node || typeof node !== 'object'
|
|
? []
|
|
: [node, ...React.Children.toArray(node.props.children).flatMap(flat)];
|
|
const proof = (category) => ({
|
|
id: category,
|
|
category,
|
|
statement: '<script>synthetic</script>',
|
|
source: 'Synthetic source',
|
|
locator: 'Table 4',
|
|
period: '2024',
|
|
scope: 'District A',
|
|
coverage: 'Partial coverage',
|
|
verified_on: '2026-10-01',
|
|
});
|
|
const records = [
|
|
{
|
|
id: 1,
|
|
title: 'Resource fixture',
|
|
url: '/fixture/1',
|
|
location: 'District A',
|
|
state: 'hypothesis',
|
|
limitations: 'No availability confirmed',
|
|
evidence: [proof('resource')],
|
|
},
|
|
{
|
|
id: 2,
|
|
title: 'Infrastructure fixture',
|
|
url: '/fixture/2',
|
|
location: 'Province B',
|
|
state: 'hypothesis',
|
|
limitations: null,
|
|
evidence: [proof('infrastructure')],
|
|
},
|
|
];
|
|
const points = [1, 3].map((id) => ({
|
|
geometry: { coordinates: [32, -25] },
|
|
properties: {
|
|
id,
|
|
title: 'Site ' + id,
|
|
url: '/fixture/' + id,
|
|
location: 'Confirmed opportunity site',
|
|
},
|
|
}));
|
|
const areas = [1, 2, 3].map((id) => ({
|
|
opportunity_id: id,
|
|
title: 'Area ' + id,
|
|
geometry: { type: 'Polygon', coordinates: [] },
|
|
}));
|
|
const props = {
|
|
points,
|
|
areas,
|
|
records,
|
|
territoryId: 'province-a',
|
|
truncated: false,
|
|
};
|
|
|
|
test('default preserves every opportunity and one map, with sourced thematic evidence and no new location claims', () => {
|
|
const Component = setup(),
|
|
tree = Component(props),
|
|
nodes = flat(tree);
|
|
const map = nodes.find((node) => node.type === Map);
|
|
assert.equal(nodes.filter((node) => node.type === Map).length, 1);
|
|
assert.equal(map.props.points, points);
|
|
assert.equal(map.props.areas, areas);
|
|
assert.equal(map.props.territoryId, 'province-a');
|
|
const html = renderToStaticMarkup(tree);
|
|
for (const text of [
|
|
'Synthetic source',
|
|
'Table 4',
|
|
'2024',
|
|
'District A',
|
|
'Partial coverage',
|
|
'2026-10-01',
|
|
'não representam a posição exacta',
|
|
'sem índice',
|
|
])
|
|
assert.ok(html.includes(text), text);
|
|
assert.match(html, /2 fichas com evidência temática pública/);
|
|
assert.match(html, /1 sem ponto público utilizável/);
|
|
assert.doesNotMatch(html, /<script>/);
|
|
});
|
|
|
|
test('choosing resources filters existing points and opportunity areas, retaining administrative identity and source context', () => {
|
|
const Component = setup();
|
|
flat(Component(props))
|
|
.find((node) => node.type === 'select')
|
|
.props.onChange({ target: { value: 'resource' } });
|
|
const tree = Component(props),
|
|
map = flat(tree).find((node) => node.type === Map);
|
|
assert.deepEqual(map.props.points, [points[0]]);
|
|
assert.deepEqual(map.props.areas, [areas[0]]);
|
|
assert.equal(map.props.territoryId, 'province-a');
|
|
const html = renderToStaticMarkup(tree);
|
|
assert.match(html, /Resource fixture/);
|
|
assert.doesNotMatch(html, /Infrastructure fixture/);
|
|
flat(tree)
|
|
.find((node) => node.type === 'select')
|
|
.props.onChange({ target: { value: 'all' } });
|
|
assert.equal(
|
|
flat(Component(props)).find((node) => node.type === Map).props.points,
|
|
points,
|
|
);
|
|
});
|
|
|
|
test('infrastructure without confirmed coordinates remains consultable without borrowing another opportunity point', () => {
|
|
const Component = setup();
|
|
flat(Component(props))
|
|
.find((node) => node.type === 'select')
|
|
.props.onChange({ target: { value: 'infrastructure' } });
|
|
const tree = Component(props),
|
|
map = flat(tree).find((node) => node.type === Map);
|
|
assert.deepEqual(map.props.points, []);
|
|
assert.deepEqual(map.props.areas, [areas[1]]);
|
|
assert.match(renderToStaticMarkup(tree), /Infrastructure fixture/);
|
|
assert.match(renderToStaticMarkup(tree), /Sem ponto público utilizável/);
|
|
assert.ok(
|
|
flat(tree).some(
|
|
(node) => node.type === 'a' && node.props.href === '/fixture/2',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('changing query data never retains previous evidence; loading and incomplete coverage are explicit', () => {
|
|
const Component = setup();
|
|
flat(Component(props))
|
|
.find((node) => node.type === 'select')
|
|
.props.onChange({ target: { value: 'resource' } });
|
|
const loading = Component({
|
|
...props,
|
|
records: [],
|
|
points: null,
|
|
areas: [],
|
|
});
|
|
assert.equal(
|
|
flat(loading).find((node) => node.type === Map).props.points,
|
|
null,
|
|
);
|
|
assert.equal(
|
|
flat(loading).find((node) => node.type === 'select').props.disabled,
|
|
true,
|
|
);
|
|
assert.doesNotMatch(renderToStaticMarkup(loading), /Resource fixture/);
|
|
const html = renderToStaticMarkup(
|
|
Component({
|
|
...props,
|
|
points: [],
|
|
records: [],
|
|
areas: [],
|
|
truncated: true,
|
|
}),
|
|
);
|
|
assert.match(html, /Sem evidência pública local/);
|
|
assert.match(html, /A resposta está limitada/);
|
|
assert.match(html, /não significa ausência de recursos/);
|
|
});
|
|
|
|
test('theme selection survives reload in the tab and unavailable or invalid storage does not break the map', () => {
|
|
const previous = globalThis.window;
|
|
let saved = 'resource';
|
|
globalThis.window = {
|
|
sessionStorage: {
|
|
getItem: () => saved,
|
|
setItem: (key, value) => {
|
|
assert.equal(key, 'investmoz-map-theme-v1');
|
|
saved = value;
|
|
},
|
|
},
|
|
};
|
|
try {
|
|
const Component = setup();
|
|
let select = flat(Component(props)).find(
|
|
(node) => node.type === 'select',
|
|
);
|
|
assert.equal(select.props.value, 'resource');
|
|
select.props.onChange({ target: { value: 'infrastructure' } });
|
|
assert.equal(saved, 'infrastructure');
|
|
assert.equal(
|
|
flat(setup()(props)).find((node) => node.type === 'select').props
|
|
.value,
|
|
'infrastructure',
|
|
);
|
|
saved = 'invalid';
|
|
assert.equal(
|
|
flat(setup()(props)).find((node) => node.type === 'select').props
|
|
.value,
|
|
'all',
|
|
);
|
|
globalThis.window = {
|
|
sessionStorage: {
|
|
getItem: () => {
|
|
throw new Error('Storage unavailable');
|
|
},
|
|
setItem: () => {
|
|
throw new Error('Storage unavailable');
|
|
},
|
|
},
|
|
};
|
|
const WithoutStorage = setup();
|
|
select = flat(WithoutStorage(props)).find(
|
|
(node) => node.type === 'select',
|
|
);
|
|
assert.equal(select.props.value, 'all');
|
|
select.props.onChange({ target: { value: 'resource' } });
|
|
assert.equal(
|
|
flat(WithoutStorage(props)).find((node) => node.type === 'select')
|
|
.props.value,
|
|
'resource',
|
|
);
|
|
} finally {
|
|
if (previous === undefined) delete globalThis.window;
|
|
else globalThis.window = previous;
|
|
}
|
|
});
|
|
|
|
test('workforce and economic context preserve source categories and incompatible periods without inferred totals or borrowed points', () => {
|
|
const Component = setup();
|
|
const extra = [
|
|
{
|
|
...records[0],
|
|
id: 4,
|
|
title: 'Workforce evidence',
|
|
evidence: [{ ...proof('workforce'), source_category: 'workforce' }],
|
|
},
|
|
{
|
|
...records[1],
|
|
id: 5,
|
|
title: 'Economic context',
|
|
evidence: [
|
|
{
|
|
...proof('economic_potential'),
|
|
id: 'supply',
|
|
source_category: 'supply',
|
|
period: '2023',
|
|
},
|
|
{
|
|
...proof('economic_potential'),
|
|
id: 'demand',
|
|
source_category: 'demand',
|
|
period: '2026',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
const data = { ...props, records: [...records, ...extra] };
|
|
flat(Component(data))
|
|
.find((node) => node.type === 'select')
|
|
.props.onChange({ target: { value: 'workforce' } });
|
|
let tree = Component(data);
|
|
assert.deepEqual(
|
|
flat(tree).find((node) => node.type === Map).props.points,
|
|
[],
|
|
);
|
|
let html = renderToStaticMarkup(tree);
|
|
assert.match(html, /Workforce evidence/);
|
|
assert.match(html, /Categoria de origem: Mão-de-obra/);
|
|
assert.match(html, /não mede trabalhadores disponíveis/);
|
|
flat(tree)
|
|
.find((node) => node.type === 'select')
|
|
.props.onChange({ target: { value: 'economic_potential' } });
|
|
tree = Component(data);
|
|
html = renderToStaticMarkup(tree);
|
|
assert.match(html, /Economic context/);
|
|
assert.match(html, /Categoria de origem: Oferta/);
|
|
assert.match(html, /Categoria de origem: Procura/);
|
|
assert.match(html, /2023/);
|
|
assert.match(html, /2026/);
|
|
assert.match(html, /sem índice, previsão de retorno ou viabilidade/);
|
|
assert.doesNotMatch(html, /Workforce evidence/);
|
|
assert.deepEqual(
|
|
flat(tree).find((node) => node.type === Map).props.points,
|
|
[],
|
|
);
|
|
});
|