Files
Investidor-V2/tests/frontend/similar-businesses.test.mjs
T

352 lines
12 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import { compileFunction } from 'node:vm';
import { renderToStaticMarkup } from 'react-dom/server';
import ts from 'typescript';
const require = createRequire(import.meta.url);
function harness() {
let cursor = 0,
effect;
const state = [],
requests = [];
const hooks = {
useState(initial) {
const index = cursor++;
if (!(index in state)) state[index] = initial;
return [
state[index],
(value) => {
state[index] =
typeof value === 'function'
? value(state[index])
: value;
},
];
},
useEffect(fn) {
effect = fn;
},
};
const output = ts.transpileModule(
fs.readFileSync(
new URL(
'../../resources/js/components/similar-businesses.tsx',
import.meta.url,
),
'utf8',
),
{
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'
? hooks
: name === '@inertiajs/react'
? { Link: 'a' }
: name === '@/lib/portal-api'
? {
portalRequest: (url, options) =>
new Promise((resolve, reject) =>
requests.push({
url,
options,
resolve,
reject,
}),
),
}
: require(name),
module,
module.exports,
);
const render = (props) => {
cursor = 0;
return module.exports.default(props);
};
return { render, start: () => effect(), requests };
}
const props = {
currentId: 'current',
catalogueUrl: '/negocios?sector=4&modality=company_sale',
criteria: ['Venda da empresa', 'Agricultura'],
};
const flush = () => new Promise((resolve) => setImmediate(resolve));
function nodes(tree) {
if (!tree || typeof tree !== 'object') return [];
if (Array.isArray(tree)) return tree.flatMap(nodes);
return [tree, ...nodes(tree.props?.children)];
}
await test('radius replaces only the area, combines public classification, preserves links and reports missing point coverage', async () => {
const h = harness();
const scoped = {
...props,
radiusAvailable: true,
areaLabel: 'Synthetic province',
catalogueUrl:
props.catalogueUrl +
'&province_id=qa-province&district_id=qa-district',
classifications: [
{ target_type: 'sector', target_id: '4', label: 'QA sector' },
],
};
let tree = h.render(scoped);
const radius = nodes(tree).find((node) => node.type === 'fieldset').props
.children[1].props.children[1];
radius.props.onChange({ target: { value: '25' } });
const classification = nodes(tree).find(
(node) =>
node.type === 'select' &&
node.props.children[0]?.props?.children ===
'Sem classificação adicional',
);
classification.props.onChange({ target: { value: 'sector:4:' } });
tree = h.render(scoped);
h.start();
const url = new URL(h.requests[0].url, 'https://portal.invalid');
assert.equal(url.searchParams.get('near_business'), 'current');
assert.equal(url.searchParams.get('radius_km'), '25');
assert.equal(url.searchParams.has('province_id'), false);
assert.equal(url.searchParams.has('district_id'), false);
assert.equal(url.searchParams.get('sector'), '4');
assert.equal(url.searchParams.get('association_target'), '4');
h.requests[0].resolve({
data: [
{
id: 'near',
slug: 'near',
title: 'Nearby',
distance_km: 1.5,
location_recorded_at: '2026-10-02T10:00:00Z',
},
],
geographicCoverage: {
origin_id: 'current',
origin_title: 'Public reference',
radius_km: 25,
origin_recorded_at: '2026-10-01T10:00:00Z',
with_public_point: 4,
without_public_point: 8,
source: 'Published location register',
method: 'Geodesic, not road',
},
});
await flush();
let html = renderToStaticMarkup(h.render(scoped));
assert.match(html, /Raio de 25 km/);
assert.match(html, /4 com ponto utilizável; 8 sem ponto público/);
assert.match(html, /Localização registada em/);
assert.match(html, /Published location register/);
assert.match(html, /near_business=current&radius_km=25/);
assert.match(html, /Geodesic, not road/);
tree = h.render(scoped);
nodes(tree)
.find((node) => node.type === 'fieldset')
.props.children[1].props.children[1].props.onChange({
target: { value: '' },
});
html = renderToStaticMarkup(h.render(scoped));
assert.match(html, /A consultar/);
assert.doesNotMatch(html, /Nearby|8 sem ponto/);
h.start();
assert.equal(
new URL(h.requests[1].url, 'https://portal.invalid').searchParams.get(
'province_id',
),
'qa-province',
);
assert.equal(
new URL(h.requests[1].url, 'https://portal.invalid').searchParams.has(
'radius_km',
),
false,
);
const changed = { ...scoped, currentId: 'new-reference' };
h.render(changed);
h.start();
assert.equal(
new URL(h.requests[2].url, 'https://portal.invalid').searchParams.has(
'near_business',
),
false,
);
});
await test('missing point reference has no radius selector, and a radius response without coverage is a recoverable failure', async () => {
const h = harness();
assert.match(
renderToStaticMarkup(h.render(props)),
/requer um ponto primário público exacto/,
);
assert.equal(
nodes(h.render(props)).some((node) => node.type === 'fieldset'),
false,
);
const scoped = { ...props, radiusAvailable: true };
const tree = h.render(scoped);
nodes(tree)
.find((node) => node.type === 'fieldset')
.props.children[1].props.children[1].props.onChange({
target: { value: '10' },
});
h.render(scoped);
h.start();
h.requests[0].resolve({ data: [] });
await flush();
const html = renderToStaticMarkup(h.render(scoped));
assert.match(html, /Tentar novamente/);
assert.doesNotMatch(html, /Não foram encontrados/);
});
await test('explicit public classification changes matching criteria and is cleared when the source record changes', async () => {
const h = harness();
const row = {
target_type: 'chain',
target_id: 'synthetic-chain',
purpose: 'coverage',
purpose_label: 'Abrangencia documentada',
label: 'Cadeia sintética',
source_reference: 'Fonte sintética divulgada',
};
const scoped = {
...props,
classifications: [
row,
row,
{ ...row, purpose: 'location', purpose_label: 'Localização' },
],
};
let tree = h.render(scoped);
const chooser = tree.props.children.find((child) => child?.type === 'label')
.props.children[1];
chooser.props.onChange({
target: { value: 'chain:synthetic-chain:coverage' },
});
tree = h.render(scoped);
h.start();
const url = new URL(h.requests[0].url, 'https://portal.invalid');
assert.equal(url.searchParams.get('association_type'), 'chain');
assert.equal(url.searchParams.get('association_target'), 'synthetic-chain');
assert.equal(url.searchParams.get('association_purpose'), 'coverage');
assert.match(renderToStaticMarkup(tree), /Fonte sintética divulgada/);
assert.match(
renderToStaticMarkup(tree),
/value="chain:synthetic-chain:location"/,
);
assert.equal(
(
renderToStaticMarkup(tree).match(
/value="chain:synthetic-chain:coverage"/g,
) ?? []
).length,
1,
);
h.requests[0].resolve({ data: [] });
await flush();
const changed = { ...scoped, currentId: 'other-record' };
const next = renderToStaticMarkup(h.render(changed));
assert.doesNotMatch(next, /Fonte sintética divulgada/);
assert.match(next, /A consultar/);
h.start();
assert.equal(
new URL(h.requests[1].url, 'https://portal.invalid').searchParams.has(
'association_target',
),
false,
);
});
await test('similar businesses reuse public criteria, exclude self and duplicates, bound the preview and preserve filters in detail links', async () => {
const h = harness();
assert.match(renderToStaticMarkup(h.render(props)), /A consultar/);
h.start();
assert.equal(
h.requests[0].url,
'/api/v1/business-opportunities?sector=4&modality=company_sale&per_page=7&page=1',
);
h.requests[0].resolve({
data: [
{ id: 'current', slug: 'self', title: 'Self' },
{
id: 'a',
slug: 'a',
title: 'A',
published_at: '2026-11-01T12:00:00Z',
},
{ id: 'a', slug: 'duplicate', title: 'Duplicate' },
...Array.from({ length: 8 }, (_, i) => ({
id: `b${i}`,
slug: `b${i}`,
title: `Business ${i}`,
})),
],
});
await flush();
const html = renderToStaticMarkup(h.render(props));
assert.doesNotMatch(html, />Self<|>Duplicate<|Business 7/);
assert.equal((html.match(/<li>/g) ?? []).length, 6);
assert.match(html, /\/negocios\/a\?sector=4&amp;modality=company_sale/);
assert.match(html, /Agricultura/);
assert.match(html, /proximidade geográfica/);
assert.match(html, /Fonte: catálogo público/);
assert.match(html, /datetime="2026-11-01T12:00:00Z"/i);
assert.match(html, /Data de publicação não indicada/);
});
await test('malformed catalogue records are a recoverable error, never proof of no matching businesses', async () => {
const h = harness();
h.render(props);
h.start();
h.requests[0].resolve({
data: [{ id: 'a', slug: null, title: 'Malformed' }],
});
await flush();
const html = renderToStaticMarkup(h.render(props));
assert.match(html, /Tentar novamente/);
assert.doesNotMatch(html, /Não foram encontrados/);
});
await test('empty catalogue differs from failed lookup; changing context hides old rows and ignores late responses', async () => {
const h = harness();
h.render(props);
const cleanup = h.start();
cleanup();
const next = { ...props, catalogueUrl: '/negocios?modality=real_estate' };
assert.match(renderToStaticMarkup(h.render(next)), /A consultar/);
h.start();
h.requests[0].resolve({
data: [{ id: 'old', slug: 'old', title: 'Old response' }],
});
await flush();
assert.doesNotMatch(renderToStaticMarkup(h.render(next)), /Old response/);
h.requests[1].resolve({ data: [] });
await flush();
assert.match(
renderToStaticMarkup(h.render(next)),
/não demonstra ausência/,
);
h.render(props);
h.start();
h.requests[2].reject(new Error('network'));
await flush();
const tree = h.render(props);
const html = renderToStaticMarkup(tree);
assert.match(html, /Tentar novamente/);
assert.doesNotMatch(html, /não demonstra ausência/);
const button = tree.props.children[2].props.children[1];
button.props.onClick();
assert.match(renderToStaticMarkup(h.render(props)), /A consultar/);
h.start();
assert.equal(h.requests.length, 4);
});