507 lines
17 KiB
JavaScript
507 lines
17 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);
|
|
function load(file, mocks) {
|
|
const output = ts.transpileModule(
|
|
fs
|
|
.readFileSync(
|
|
new URL(
|
|
'../../resources/js/components/' + file,
|
|
import.meta.url,
|
|
),
|
|
'utf8',
|
|
)
|
|
.replaceAll('import.meta.env.VITE_MAP_TILE_URL', 'undefined'),
|
|
{
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.ES2022,
|
|
jsx: ts.JsxEmit.ReactJSX,
|
|
},
|
|
},
|
|
).outputText;
|
|
const module = { exports: {} };
|
|
compileFunction(output, ['require', 'module', 'exports'])(
|
|
(name) => mocks[name] ?? require(name),
|
|
module,
|
|
module.exports,
|
|
);
|
|
return module.exports;
|
|
}
|
|
const polygon = (x, y) => ({
|
|
type: 'Polygon',
|
|
coordinates: [
|
|
[
|
|
[x, y],
|
|
[x + 1, y],
|
|
[x + 1, y + 1],
|
|
[x, y + 1],
|
|
[x, y],
|
|
],
|
|
],
|
|
});
|
|
const boundary = {
|
|
geometry: polygon(30, -20),
|
|
title: 'Província sintética',
|
|
purpose: 'administrative_boundary',
|
|
selected_territory: true,
|
|
location_type: 'province',
|
|
url: '/territorio',
|
|
source_reference: 'Fonte sintética',
|
|
source_date: '2026-01-01',
|
|
};
|
|
const point = {
|
|
geometry: { coordinates: [40, -10] },
|
|
properties: {
|
|
id: 1,
|
|
title: 'Investimento sintético',
|
|
url: '/oportunidades/1',
|
|
location: 'Local confirmado',
|
|
},
|
|
};
|
|
const flatten = (node) =>
|
|
!node || typeof node !== 'object'
|
|
? []
|
|
: [
|
|
node,
|
|
...React.Children.toArray(node.props.children).flatMap(flatten),
|
|
];
|
|
|
|
await test('catalogue composes one map with the same opportunity data and separate administrative context, including no selected territory', () => {
|
|
const Map = () => null,
|
|
Info = () => null;
|
|
const requests = [];
|
|
const Component = load('catalogue-investment-map.tsx', {
|
|
'./interactive-investment-map': { default: Map },
|
|
'./territory-geometry-preview': { TerritoryGeometryDetails: Info },
|
|
'./use-territory-geometry': {
|
|
useTerritoryGeometry: (id, endpoint, children) => {
|
|
assert.equal(endpoint, undefined);
|
|
assert.equal(children, true);
|
|
requests.push(id);
|
|
return { context: { areas: [boundary] }, error: '' };
|
|
},
|
|
},
|
|
}).default;
|
|
const points = [point],
|
|
areas = [];
|
|
const tree = Component({ points, areas, territoryId: 'province-1' });
|
|
assert.equal(tree.type, Map);
|
|
assert.equal(tree.props.points, points);
|
|
assert.equal(tree.props.areas, areas);
|
|
assert.deepEqual(tree.props.administrativeAreas, [boundary]);
|
|
assert.equal(tree.props.layerControls, true);
|
|
assert.equal(tree.props.administrativeInfo.type, Info);
|
|
assert.deepEqual(requests, ['province-1']);
|
|
assert.equal(
|
|
Component({ points: [], areas }).props.administrativeInfo,
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
await test('territorial request changes discard old boundaries immediately and ignore late responses; clearing selection removes context', async () => {
|
|
const previousFetch = globalThis.fetch;
|
|
const pending = [],
|
|
memory = [];
|
|
let cursor = 0,
|
|
effect;
|
|
const hooks = {
|
|
useState(initial) {
|
|
const index = cursor++;
|
|
if (!(index in memory)) memory[index] = initial;
|
|
return [memory[index], (value) => (memory[index] = value)];
|
|
},
|
|
useEffect(fn) {
|
|
effect = fn;
|
|
},
|
|
};
|
|
globalThis.fetch = (url, options) =>
|
|
new Promise((resolve) => pending.push({ url, options, resolve }));
|
|
try {
|
|
const { useTerritoryGeometry } = load('use-territory-geometry.ts', {
|
|
react: hooks,
|
|
});
|
|
const render = (id, children = false) => {
|
|
cursor = 0;
|
|
return useTerritoryGeometry(id, undefined, children);
|
|
};
|
|
render('a');
|
|
const cleanup = effect();
|
|
cleanup();
|
|
render('b');
|
|
effect();
|
|
pending[0].resolve({
|
|
ok: true,
|
|
json: async () => ({ areas: [boundary] }),
|
|
});
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(render('b').context, null);
|
|
pending[1].resolve({
|
|
ok: true,
|
|
json: async () => ({ areas: [{ ...boundary, title: 'B' }] }),
|
|
});
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(render('b').context.areas[0].title, 'B');
|
|
assert.equal(render('c').context, null);
|
|
assert.equal(render(undefined).context, null);
|
|
assert.equal(pending[0].options.signal.aborted, true);
|
|
assert.equal(pending.length, 2);
|
|
assert.equal(render('b', true).context, null);
|
|
effect();
|
|
assert.match(pending[2].url, /include_children=1/);
|
|
pending[2].resolve({
|
|
ok: true,
|
|
json: async () => ({
|
|
areas: [boundary],
|
|
child_coverage: { registered_count: 2 },
|
|
}),
|
|
});
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(
|
|
render('b', true).context.child_coverage.registered_count,
|
|
2,
|
|
);
|
|
assert.equal(render('b', false).context, null);
|
|
} finally {
|
|
globalThis.fetch = previousFetch;
|
|
}
|
|
});
|
|
|
|
await test('Leaflet uses one instance, independent layers and selected boundary bounds; restoring hidden points from the list never creates positions', async () => {
|
|
const previousDocument = globalThis.document,
|
|
previousObserver = globalThis.ResizeObserver;
|
|
let effect, overlayOptions, overlayEvents, fitted;
|
|
const refs = [],
|
|
instances = [],
|
|
groups = [];
|
|
globalThis.document = {
|
|
createElement: () => ({ append() {}, textContent: '', style: {} }),
|
|
};
|
|
globalThis.ResizeObserver = class {
|
|
observe() {}
|
|
disconnect() {}
|
|
};
|
|
const instance = {
|
|
setView() {
|
|
return this;
|
|
},
|
|
fitBounds(value) {
|
|
fitted = value;
|
|
},
|
|
on(_event, fn) {
|
|
overlayEvents = fn;
|
|
return this;
|
|
},
|
|
addLayer(group) {
|
|
group.visible = true;
|
|
return this;
|
|
},
|
|
invalidateSize() {},
|
|
remove() {},
|
|
};
|
|
const layer = () => ({
|
|
addTo(group) {
|
|
group.items.push(this);
|
|
return this;
|
|
},
|
|
bindPopup() {
|
|
return this;
|
|
},
|
|
on() {
|
|
return this;
|
|
},
|
|
getLatLng() {
|
|
return [
|
|
point.geometry.coordinates[1],
|
|
point.geometry.coordinates[0],
|
|
];
|
|
},
|
|
openPopup() {},
|
|
});
|
|
const L = {
|
|
map() {
|
|
instances.push(instance);
|
|
return instance;
|
|
},
|
|
tileLayer: () => ({
|
|
on() {
|
|
return this;
|
|
},
|
|
addTo() {},
|
|
}),
|
|
layerGroup() {
|
|
const group = {
|
|
items: [],
|
|
addTo() {
|
|
this.visible = true;
|
|
return this;
|
|
},
|
|
};
|
|
groups.push(group);
|
|
return group;
|
|
},
|
|
control: {
|
|
scale: () => ({ addTo() {} }),
|
|
layers(_base, overlays) {
|
|
overlayOptions = overlays;
|
|
return { addTo() {} };
|
|
},
|
|
},
|
|
geoJSON(geometry) {
|
|
const item = layer();
|
|
const coords = geometry.coordinates[0];
|
|
item.getBounds = () => ({
|
|
isValid: () => true,
|
|
getSouth: () => Math.min(...coords.map((c) => c[1])),
|
|
getNorth: () => Math.max(...coords.map((c) => c[1])),
|
|
getWest: () => Math.min(...coords.map((c) => c[0])),
|
|
getEast: () => Math.max(...coords.map((c) => c[0])),
|
|
});
|
|
return item;
|
|
},
|
|
divIcon: (value) => value,
|
|
marker: () => layer(),
|
|
};
|
|
const hooks = {
|
|
...React,
|
|
useMemo: (fn) => fn(),
|
|
useState: (initial) => [initial, () => {}],
|
|
useRef(initial) {
|
|
const ref = { current: refs.length === 0 ? {} : initial };
|
|
refs.push(ref);
|
|
return ref;
|
|
},
|
|
useEffect(fn) {
|
|
effect = fn;
|
|
},
|
|
};
|
|
try {
|
|
const Component = load('interactive-investment-map.tsx', {
|
|
react: hooks,
|
|
leaflet: L,
|
|
'leaflet/dist/leaflet.css': {},
|
|
}).default;
|
|
const area = {
|
|
...boundary,
|
|
title: 'Área sintética',
|
|
purpose: 'investment_area',
|
|
selected_territory: false,
|
|
};
|
|
const tree = Component({
|
|
points: [point],
|
|
areas: [area, boundary],
|
|
administrativeAreas: [boundary],
|
|
layerControls: true,
|
|
});
|
|
const cleanup = effect();
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(instances.length, 1);
|
|
assert.equal(groups[0].items.length, 1);
|
|
assert.equal(groups[1].items.length, 1);
|
|
assert.equal(groups[2].items.length, 1); // Same boundary from both APIs is not doubled.
|
|
assert.deepEqual(fitted, [
|
|
[-20, 30],
|
|
[-19, 31],
|
|
]); // No national parent/outside point overrides the selection.
|
|
assert.equal(Object.keys(overlayOptions).length, 3);
|
|
overlayEvents({ layer: groups[0], type: 'overlayremove' });
|
|
assert.equal(refs[4].current.points, false);
|
|
const button = flatten(tree).find((n) => n.type === 'button');
|
|
button.props.onClick();
|
|
assert.equal(groups[0].visible, true);
|
|
assert.equal(refs[4].current.points, true);
|
|
assert(
|
|
flatten(tree).some(
|
|
(n) => n.type === 'a' && n.props.href === '/territorio',
|
|
),
|
|
);
|
|
assert(flatten(tree).some((n) => n.type === 'details'));
|
|
cleanup();
|
|
refs.length = 0;
|
|
groups.length = 0;
|
|
Component({
|
|
points: [],
|
|
areas: [],
|
|
administrativeAreas: [
|
|
{ ...boundary, selected_territory: false },
|
|
{
|
|
...boundary,
|
|
geometry: polygon(0, 0),
|
|
selected_territory: false,
|
|
location_type: 'country',
|
|
title: 'País sintético',
|
|
},
|
|
],
|
|
layerControls: true,
|
|
});
|
|
const cleanupEmpty = effect();
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(groups[0].items.length, 0);
|
|
assert.deepEqual(fitted, [
|
|
[-20, 30],
|
|
[-19, 31],
|
|
]);
|
|
cleanupEmpty();
|
|
} finally {
|
|
globalThis.document = previousDocument;
|
|
globalThis.ResizeObserver = previousObserver;
|
|
}
|
|
});
|
|
|
|
await test('administrative failure does not remove investment results or add another map', () => {
|
|
const Map = () => null,
|
|
Info = () => null;
|
|
const Component = load('catalogue-investment-map.tsx', {
|
|
'./interactive-investment-map': { default: Map },
|
|
'./territory-geometry-preview': { TerritoryGeometryDetails: Info },
|
|
'./use-territory-geometry': {
|
|
useTerritoryGeometry: () => ({
|
|
context: null,
|
|
error: 'Limites indisponíveis',
|
|
}),
|
|
},
|
|
}).default;
|
|
const points = [point];
|
|
const tree = Component({ points, areas: [], territoryId: 'a' });
|
|
assert.equal(tree.type, Map);
|
|
assert.equal(tree.props.points, points);
|
|
assert.equal(tree.props.administrativeAreas, undefined);
|
|
assert(flatten(tree.props.notice).some((n) => n.props.role === 'alert'));
|
|
});
|
|
|
|
await test('declared parcels and documented source locations are never labelled as administrative boundaries or investments', () => {
|
|
const hooks = {
|
|
...React,
|
|
useMemo: (fn) => fn(),
|
|
useState: (value) => [value, () => {}],
|
|
useRef: (value) => ({ current: value }),
|
|
useEffect: () => {},
|
|
};
|
|
const Component = load('interactive-investment-map.tsx', {
|
|
react: hooks,
|
|
'leaflet/dist/leaflet.css': {},
|
|
}).default;
|
|
const parcel = renderToStaticMarkup(
|
|
Component({
|
|
points: [],
|
|
areas: [
|
|
{
|
|
...boundary,
|
|
purpose: 'declared_parcel',
|
|
title: 'Parcela sintética',
|
|
},
|
|
],
|
|
contextOnly: true,
|
|
mapKind: 'declared_parcel',
|
|
}),
|
|
);
|
|
assert.match(parcel, />Perímetro declarado pelo Promotor<\/h2>/);
|
|
assert.match(parcel, /não é limite administrativo/);
|
|
assert.doesNotMatch(
|
|
parcel,
|
|
/>Contexto administrativo<\/h2>|limites administrativos disponíveis/,
|
|
);
|
|
const documented = renderToStaticMarkup(
|
|
Component({
|
|
points: [],
|
|
areas: [],
|
|
mapKind: 'documented_locations',
|
|
layerControls: true,
|
|
}),
|
|
);
|
|
assert.match(documented, />Mapa dos locais documentados<\/h2>/);
|
|
assert.match(documented, /Local documentado com coordenadas/);
|
|
assert.doesNotMatch(documented, /Oportunidade com coordenadas confirmadas/);
|
|
const administrative = renderToStaticMarkup(
|
|
Component({ points: [], areas: [boundary], contextOnly: true }),
|
|
);
|
|
assert.match(administrative, />Contexto administrativo<\/h2>/);
|
|
});
|
|
|
|
await test('partnership catalogue has one contextual map and keeps the applied territory while filter edits are pending', () => {
|
|
let draft;
|
|
const hooks = {
|
|
...React,
|
|
useState(initial) {
|
|
draft ??= initial;
|
|
return [
|
|
draft,
|
|
(value) => {
|
|
draft = value;
|
|
},
|
|
];
|
|
},
|
|
};
|
|
const Map = () => null;
|
|
const Page = load('../pages/partnerships/catalogue.tsx', {
|
|
react: hooks,
|
|
'@inertiajs/react': { Link: 'a', router: { get() {} } },
|
|
'@/components/catalogue-investment-map': { default: Map },
|
|
'./shared': { Frame: 'main', Field: 'label', control: '', button: '' },
|
|
}).default;
|
|
const props = {
|
|
types: {},
|
|
sectors: [],
|
|
territories: [{ id: 'a', name: 'A', location_type: 'province' }],
|
|
filters: { territory_id: 'a' },
|
|
mapPoints: [point],
|
|
results: {
|
|
data: [],
|
|
total: 0,
|
|
prev_page_url: null,
|
|
next_page_url: null,
|
|
},
|
|
};
|
|
let tree = Page(props);
|
|
const maps = () => flatten(tree).filter((node) => node.type === Map);
|
|
assert.equal(maps().length, 1);
|
|
assert.equal(maps()[0].props.mapKind, 'documented_locations');
|
|
assert.equal(maps()[0].props.points, props.mapPoints);
|
|
flatten(tree)
|
|
.find((node) => node.type === 'select' && node.props.value === 'a')
|
|
.props.onChange({ target: { value: 'b' } });
|
|
tree = Page(props);
|
|
assert.equal(maps()[0].props.territoryId, 'a');
|
|
assert.equal(maps()[0].props.points, props.mapPoints);
|
|
});
|
|
|
|
await test('administrative coverage identifies lower-level subdivisions and distinguishes absent geometry from absent territory', () => {
|
|
const Details = load('territory-geometry-preview.tsx', {
|
|
'./interactive-investment-map': { default: () => null },
|
|
'./use-territory-geometry': {},
|
|
}).TerritoryGeometryDetails;
|
|
const html = renderToStaticMarkup(
|
|
React.createElement(Details, {
|
|
error: '',
|
|
context: {
|
|
selected_geometry_available: false,
|
|
context_geometry_count: 1,
|
|
truncated: true,
|
|
child_coverage: {
|
|
location_type: 'district',
|
|
registered_count: 3,
|
|
represented_count: 1,
|
|
},
|
|
areas: [
|
|
{
|
|
...boundary,
|
|
selected_territory: false,
|
|
child_territory: true,
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
);
|
|
assert.match(html, /1 de 3 referências acessíveis/);
|
|
assert.match(html, /Subdivisão do território seleccionado/);
|
|
assert.match(html, /não significa inexistência do território/);
|
|
assert.match(html, /100 geometrias e 500 subdivisões/);
|
|
assert.doesNotMatch(html, /apenas limites dos níveis superiores/);
|
|
});
|