427 lines
14 KiB
JavaScript
427 lines
14 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 { createElement } 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(file, 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) => mocks[name] ?? require(name),
|
|
module,
|
|
module.exports,
|
|
);
|
|
return module.exports;
|
|
}
|
|
const locations = load('../../resources/js/lib/business-locations.ts');
|
|
const summaryModule = load(
|
|
'../../resources/js/components/business-point-summary.tsx',
|
|
{ '@/lib/business-locations': locations },
|
|
);
|
|
const Summary = summaryModule.default;
|
|
const sourceModule = load(
|
|
'../../resources/js/components/similar-businesses.tsx',
|
|
{ '@inertiajs/react': { Link: 'a' }, '@/lib/portal-api': {} },
|
|
);
|
|
const paths = [
|
|
{
|
|
province_id: 'qa-province',
|
|
district_id: 'qa-one',
|
|
admin_post_id: '',
|
|
locality_id: '',
|
|
},
|
|
{
|
|
province_id: 'qa-province',
|
|
district_id: 'qa-two',
|
|
admin_post_id: '',
|
|
locality_id: '',
|
|
},
|
|
];
|
|
const payload = {
|
|
title: 'QA private preparation',
|
|
wizard_version: '2',
|
|
wizard_step: '5',
|
|
geography_scope: 'multiple',
|
|
locations_json: JSON.stringify(paths),
|
|
location_unknown: 'no',
|
|
disclosure_precision: 'exact',
|
|
location_point_enabled: 'yes',
|
|
location_latitude: '-26.0001',
|
|
location_longitude: '32.0001',
|
|
location_point_source_mode: 'listing',
|
|
source_reference: '<QA source>',
|
|
source_date: '2026-10-02',
|
|
location_point_source_reference: 'Other private source',
|
|
location_point_source_date: '',
|
|
location_point_share_source: 'no',
|
|
};
|
|
function nodes(tree) {
|
|
if (!tree || typeof tree !== 'object') return [];
|
|
if (Array.isArray(tree)) return tree.flatMap(nodes);
|
|
return [tree, ...nodes(tree.props?.children)];
|
|
}
|
|
function harness(initial = payload) {
|
|
let cursor = 0;
|
|
const state = [],
|
|
requests = [];
|
|
const hooks = {
|
|
useState(initial) {
|
|
const index = cursor++;
|
|
if (!(index in state))
|
|
state[index] =
|
|
typeof initial === 'function' ? initial() : initial;
|
|
return [
|
|
state[index],
|
|
(value) => {
|
|
state[index] =
|
|
typeof value === 'function'
|
|
? value(state[index])
|
|
: value;
|
|
},
|
|
];
|
|
},
|
|
useRef(initial) {
|
|
const index = cursor++;
|
|
if (!(index in state)) state[index] = { current: initial };
|
|
return state[index];
|
|
},
|
|
useEffect() {},
|
|
useCallback(fn) {
|
|
return fn;
|
|
},
|
|
};
|
|
const active = {
|
|
geography_scope: ['specific', 'multiple'],
|
|
disclosure_precision: ['exact'],
|
|
location_unknown: ['no'],
|
|
location_point_enabled: ['yes'],
|
|
};
|
|
const schema = {
|
|
geography_scope: {
|
|
step: 5,
|
|
pt: 'Geographic scope',
|
|
en: 'Scope',
|
|
type: 'select',
|
|
max: 30,
|
|
options: {
|
|
multiple: 'Multiple',
|
|
specific: 'Specific',
|
|
national: 'National',
|
|
digital: 'Digital',
|
|
},
|
|
},
|
|
location_point_territory: {
|
|
step: 5,
|
|
pt: 'Primary site',
|
|
en: 'Primary site',
|
|
type: 'point_territory',
|
|
max: 150,
|
|
when: { ...active, geography_scope: ['multiple'] },
|
|
},
|
|
location_latitude: {
|
|
step: 5,
|
|
pt: 'Latitude',
|
|
en: 'Latitude',
|
|
type: 'decimal',
|
|
max: 16,
|
|
when: active,
|
|
},
|
|
location_longitude: {
|
|
step: 5,
|
|
pt: 'Longitude',
|
|
en: 'Longitude',
|
|
type: 'decimal',
|
|
max: 16,
|
|
when: active,
|
|
},
|
|
location_point_source_mode: {
|
|
step: 5,
|
|
pt: 'Coordinate source',
|
|
en: 'Coordinate source',
|
|
type: 'select',
|
|
max: 20,
|
|
options: { listing: 'Existing source', custom: 'Different source' },
|
|
when: active,
|
|
},
|
|
location_point_source_reference: {
|
|
step: 5,
|
|
pt: 'Point source',
|
|
en: 'Point source',
|
|
type: 'textarea',
|
|
max: 2000,
|
|
when: { ...active, location_point_source_mode: ['custom'] },
|
|
},
|
|
};
|
|
const record = {
|
|
id: 'qa-listing',
|
|
code: 'MNE-QA',
|
|
modality: 'company_sale',
|
|
classification: 'restricted',
|
|
version_no: 1,
|
|
payload: initial,
|
|
territory_paths: [
|
|
['QA province', 'QA one'],
|
|
['QA province', 'QA two'],
|
|
],
|
|
};
|
|
const Wizard = load(
|
|
'../../resources/js/pages/investment/business-wizard.tsx',
|
|
{
|
|
react: hooks,
|
|
'@inertiajs/react': { router: { visit() {}, reload() {} } },
|
|
'@/components/country-select': { default: () => null },
|
|
'@/components/business-point-summary': summaryModule,
|
|
'./shared': {
|
|
Frame: 'div',
|
|
Field: ({ label, children }) =>
|
|
createElement('label', null, label, children),
|
|
input: 'input',
|
|
button: 'button',
|
|
card: 'card',
|
|
api: async (url, body) => {
|
|
requests.push({ url, body });
|
|
return { ...record, payload: body.payload, version_no: 2 };
|
|
},
|
|
},
|
|
'./business-types': {
|
|
businessModalities: { company_sale: 'Sale' },
|
|
},
|
|
'./business-objects': { default: () => null },
|
|
'../../lib/business-locations': locations,
|
|
},
|
|
).default;
|
|
return {
|
|
render() {
|
|
cursor = 0;
|
|
return Wizard({
|
|
countries: [],
|
|
record,
|
|
formSchema: schema,
|
|
modalityFormType: 'company_sale',
|
|
modalities: ['company_sale'],
|
|
objects: [],
|
|
objectKinds: [],
|
|
sectors: [],
|
|
});
|
|
},
|
|
requests,
|
|
};
|
|
}
|
|
|
|
await test('the existing wizard requires an explicit primary path and keeps the selected path distinct from the first site', async () => {
|
|
const h = harness();
|
|
let tree = h.render();
|
|
const chooser = nodes(tree).find(
|
|
(node) =>
|
|
node.type === 'select' &&
|
|
nodes(node).some(
|
|
(child) =>
|
|
child.type === 'option' &&
|
|
child.props.value ===
|
|
locations.businessPointTerritoryKey(paths[1]),
|
|
),
|
|
);
|
|
assert.equal(chooser.props.value, '');
|
|
chooser.props.onChange({
|
|
target: { value: locations.businessPointTerritoryKey(paths[1]) },
|
|
});
|
|
tree = h.render();
|
|
const html = renderToStaticMarkup(tree);
|
|
assert.match(html, /QA two/);
|
|
assert.match(html, /Latitude \*/);
|
|
assert.match(html, /não publica o ponto/);
|
|
assert.match(html, /mantida no processo reservado/);
|
|
const save = nodes(tree).find(
|
|
(node) =>
|
|
node.type === 'button' && node.props.children === 'Guardar e sair',
|
|
);
|
|
await save.props.onClick();
|
|
assert.equal(
|
|
h.requests[0].url,
|
|
'/api/v1/existing-business/listings/qa-listing',
|
|
);
|
|
assert.equal(
|
|
h.requests[0].body.payload.location_point_territory,
|
|
locations.businessPointTerritoryKey(paths[1]),
|
|
);
|
|
assert.equal(h.requests[0].body.version_no, 1);
|
|
assert.equal(h.requests[0].body.payload.location_latitude, '-26.0001');
|
|
});
|
|
|
|
await test('switching source reuses existing fields and switching to national hides the supplement while preserving the private draft', async () => {
|
|
const h = harness();
|
|
let tree = h.render();
|
|
assert.equal(
|
|
nodes(tree).some((node) => node.type === 'textarea'),
|
|
false,
|
|
);
|
|
const source = nodes(tree).find(
|
|
(node) => node.type === 'select' && node.props.value === 'listing',
|
|
);
|
|
source.props.onChange({ target: { value: 'custom' } });
|
|
tree = h.render();
|
|
assert.equal(
|
|
nodes(tree).find((node) => node.type === 'textarea').props.value,
|
|
'Other private source',
|
|
);
|
|
const scope = nodes(tree).find(
|
|
(node) => node.type === 'select' && node.props.value === 'multiple',
|
|
);
|
|
scope.props.onChange({ target: { value: 'national' } });
|
|
tree = h.render();
|
|
assert.doesNotMatch(
|
|
renderToStaticMarkup(tree),
|
|
/Ponto em preparação|Other private source|Latitude WGS84/,
|
|
);
|
|
assert.equal(
|
|
nodes(tree).some((node) => node.type === 'textarea'),
|
|
false,
|
|
);
|
|
await nodes(tree)
|
|
.find(
|
|
(node) =>
|
|
node.type === 'button' &&
|
|
node.props.children === 'Guardar e sair',
|
|
)
|
|
.props.onClick();
|
|
assert.equal(h.requests[0].body.payload.location_latitude, '-26.0001');
|
|
assert.equal(
|
|
h.requests[0].body.payload.location_point_source_reference,
|
|
'Other private source',
|
|
);
|
|
assert.equal(
|
|
h.requests[0].body.payload.location_point_source_mode,
|
|
'custom',
|
|
);
|
|
assert.equal(h.requests[0].body.payload.geography_scope, 'national');
|
|
});
|
|
|
|
await test('stale point path stays visible for reconciliation instead of selecting another site', () => {
|
|
const h = harness({
|
|
...payload,
|
|
location_point_territory: 'qa-removed-site',
|
|
});
|
|
const html = renderToStaticMarkup(h.render());
|
|
assert.match(html, /Selecção guardada fora das localizações actuais/);
|
|
assert.match(html, /value="qa-removed-site" selected/);
|
|
});
|
|
|
|
await test('an incomplete earlier location does not change the index or name of a later selectable site', () => {
|
|
const h = harness({
|
|
...payload,
|
|
locations_json: JSON.stringify([
|
|
locations.emptyBusinessLocation(),
|
|
paths[1],
|
|
]),
|
|
});
|
|
const option = nodes(h.render()).find(
|
|
(node) =>
|
|
node.type === 'option' &&
|
|
node.props.value === locations.businessPointTerritoryKey(paths[1]),
|
|
);
|
|
const html = renderToStaticMarkup(option);
|
|
assert.match(html, /Localização 2/);
|
|
assert.match(html, /QA two/);
|
|
assert.doesNotMatch(html, /QA one/);
|
|
assert.equal(
|
|
locations.businessPointTerritoryLabel(
|
|
{
|
|
...payload,
|
|
location_point_territory: locations.businessPointTerritoryKey(
|
|
paths[1],
|
|
),
|
|
},
|
|
[['QA one'], ['QA two']],
|
|
),
|
|
'QA two',
|
|
);
|
|
});
|
|
|
|
await test('summary distinguishes registered, prepared and published data, escapes the reused source and reports an unavailable supplement', () => {
|
|
const registered = {
|
|
id: 'qa-existing',
|
|
is_public: false,
|
|
precision_level: 'exact',
|
|
has_geometry: true,
|
|
latitude: -27,
|
|
longitude: 33,
|
|
};
|
|
let html = renderToStaticMarkup(
|
|
Summary({ payload, registered, available: false }),
|
|
);
|
|
assert.match(html, /Latitude -27; longitude/);
|
|
assert.match(html, /Ponto em preparação/);
|
|
assert.match(html, /<QA source>/);
|
|
assert.match(html, /ainda não está instalado/);
|
|
assert.match(html, /substitui o primário cadastrado/);
|
|
html = renderToStaticMarkup(
|
|
Summary({ payload, registered, published: true }),
|
|
);
|
|
assert.match(html, /Ponto declarado na publicação/);
|
|
assert.match(html, /não reescreve o histórico/);
|
|
assert.equal(
|
|
Summary({ payload: { ...payload, location_unknown: 'yes' } }),
|
|
null,
|
|
);
|
|
html = renderToStaticMarkup(
|
|
Summary({
|
|
payload: { ...payload, location_point_enabled: 'no' },
|
|
registered,
|
|
}),
|
|
);
|
|
assert.match(
|
|
html,
|
|
/cadastro existente é conservado|cadastro\s+existente é conservado/,
|
|
);
|
|
assert.doesNotMatch(html, /-26.0001|<QA source>/);
|
|
});
|
|
|
|
await test('public source dates remain calendar dates in a western timezone and unknown dates are not replaced by the registration time', () => {
|
|
const previous = process.env.TZ;
|
|
try {
|
|
process.env.TZ = 'Pacific/Honolulu';
|
|
const html = renderToStaticMarkup(
|
|
sourceModule.BusinessLocationSource({
|
|
record: {
|
|
location_source_reference: '<Public source>',
|
|
location_source_date: '2026-10-02',
|
|
location_recorded_at: '2026-11-02T10:00:00Z',
|
|
},
|
|
showRecorded: true,
|
|
}),
|
|
);
|
|
assert.match(html, /<Public source>/);
|
|
assert.ok(
|
|
html.includes(
|
|
new Date('2026-10-02T00:00:00Z').toLocaleDateString('pt-MZ', {
|
|
timeZone: 'UTC',
|
|
}),
|
|
),
|
|
);
|
|
const unknown = renderToStaticMarkup(
|
|
sourceModule.BusinessLocationSource({
|
|
record: { location_recorded_at: '2026-11-02T10:00:00Z' },
|
|
}),
|
|
);
|
|
assert.match(unknown, /Data da fonte: desconhecida/);
|
|
assert.doesNotMatch(unknown, /2026/);
|
|
} finally {
|
|
if (previous === undefined) delete process.env.TZ;
|
|
else process.env.TZ = previous;
|
|
}
|
|
});
|