352 lines
12 KiB
JavaScript
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&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);
|
|
});
|