Files
Investidor-V2/tests/frontend/catalogue-map-layers.test.mjs
T

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/);
});