336 lines
11 KiB
JavaScript
336 lines
11 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 { renderToStaticMarkup } from 'react-dom/server';
|
|
import ts from 'typescript';
|
|
const require = createRequire(import.meta.url);
|
|
function load(file, mocks = {}) {
|
|
const code = 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(code, ['require', 'module', 'exports'])(
|
|
(key) => mocks[key] ?? require(key),
|
|
module,
|
|
module.exports,
|
|
);
|
|
return module.exports;
|
|
}
|
|
const urls = load('../../resources/js/lib/company-comparison.ts');
|
|
function nodes(tree) {
|
|
if (!tree || typeof tree !== 'object') return [];
|
|
if (Array.isArray(tree)) return tree.flatMap(nodes);
|
|
return [tree, ...nodes(tree.props?.children)];
|
|
}
|
|
const questions = {
|
|
purpose: {
|
|
label: 'Finalidade',
|
|
options: {
|
|
new: 'Nova',
|
|
representation: 'Representação',
|
|
unknown: 'Por definir',
|
|
},
|
|
},
|
|
participants: {
|
|
label: 'Participantes',
|
|
options: { one: 'Um', several: 'Vários', unknown: 'Por definir' },
|
|
},
|
|
regulated: {
|
|
label: 'Regras específicas',
|
|
options: { unknown: 'Por conferir' },
|
|
},
|
|
preferred: {
|
|
label: 'Preferência',
|
|
options: { quotas: 'Quotas', unknown: 'Por definir' },
|
|
},
|
|
};
|
|
const props = {
|
|
definition: { version: 'v1', questions },
|
|
answers: {
|
|
purpose: 'new',
|
|
participants: 'one',
|
|
regulated: 'unknown',
|
|
preferred: 'quotas',
|
|
},
|
|
result: {
|
|
engine_version: 'v1',
|
|
questions,
|
|
active: Object.keys(questions),
|
|
pending: ['regulated'],
|
|
route: 'Compare alternativas.',
|
|
preparation: ['Preparar contrato.'],
|
|
scope: 'Edição indicada.',
|
|
source: {
|
|
title: 'Código Comercial',
|
|
edition: 'Edição consultada',
|
|
issued_on: '2022-05-25',
|
|
consulted_on: '2026-11-02',
|
|
url: 'https://tsrm.gov.mz/source.pdf',
|
|
},
|
|
forms: [
|
|
{
|
|
code: 'quotas',
|
|
title: 'Sociedade <SYNTHETIC>',
|
|
description: 'Capital em quotas',
|
|
articles: 'Artigo 281',
|
|
source_href: 'https://tsrm.gov.mz/source.pdf#page=42',
|
|
selected: true,
|
|
constraints: ['SYNTHETIC incompatibility'],
|
|
notes: ['Conferir modalidade.'],
|
|
},
|
|
],
|
|
},
|
|
case: null,
|
|
saved: null,
|
|
history: [],
|
|
historyNext: null,
|
|
historical: false,
|
|
workingCopy: false,
|
|
canWrite: false,
|
|
};
|
|
function harness() {
|
|
let cursor = 0;
|
|
const values = [],
|
|
refs = [],
|
|
visits = [],
|
|
requests = [];
|
|
let rejectSave = false;
|
|
const hooks = {
|
|
useState(initial) {
|
|
const i = cursor++;
|
|
if (!(i in values))
|
|
values[i] = typeof initial === 'function' ? initial() : initial;
|
|
return [
|
|
values[i],
|
|
(v) => {
|
|
values[i] = v;
|
|
},
|
|
];
|
|
},
|
|
useRef(initial) {
|
|
const i = cursor++;
|
|
if (!(i in values)) {
|
|
values[i] = { current: initial };
|
|
refs.push(values[i]);
|
|
}
|
|
return values[i];
|
|
},
|
|
};
|
|
const module = load(
|
|
'../../resources/js/pages/investment/company-comparison.tsx',
|
|
{
|
|
react: hooks,
|
|
'@inertiajs/react': {
|
|
Link: 'a',
|
|
router: { visit: (...args) => visits.push(args) },
|
|
},
|
|
'./shared': { Frame: 'main', card: '', input: '', button: '' },
|
|
'@/lib/company-comparison': urls,
|
|
'@/lib/operation-key': {
|
|
createOperationKey: () => 'SYNTHETIC-operation',
|
|
},
|
|
'@/lib/profile-json': {
|
|
profileJson: async (...args) => {
|
|
requests.push(args);
|
|
if (rejectSave)
|
|
throw new Error('SYNTHETIC conflict; keep edits');
|
|
return { saved: true };
|
|
},
|
|
},
|
|
},
|
|
);
|
|
return {
|
|
render(extra = {}) {
|
|
cursor = 0;
|
|
const outer = module.default({ ...props, ...extra });
|
|
return outer.type(outer.props);
|
|
},
|
|
refs,
|
|
visits,
|
|
requests,
|
|
fail() {
|
|
rejectSave = true;
|
|
},
|
|
};
|
|
}
|
|
const find = (tree, name) =>
|
|
nodes(tree).find((node) => node.props?.name === `answers[${name}]`);
|
|
const button = (tree, text) =>
|
|
nodes(tree).find(
|
|
(node) => node.type === 'button' && node.props.children === text,
|
|
);
|
|
await test('public comparison is a real GET form, safely renders sources and constraints, and asks no identity data', () => {
|
|
const h = harness(),
|
|
tree = h.render(),
|
|
html = renderToStaticMarkup(tree);
|
|
const form = nodes(tree).find((node) => node.type === 'form');
|
|
assert.equal(form.props.method, 'get');
|
|
assert.equal(form.props.action, '/formas-juridicas');
|
|
assert.match(html, /Sociedade <SYNTHETIC>/);
|
|
assert.match(html, /Incompatibilidade a resolver/);
|
|
assert.match(html, /Preferência para aprofundar/);
|
|
assert.match(html, /2022-05-25/);
|
|
assert.doesNotMatch(html, /name="(email|nuit|name|registration)"/);
|
|
});
|
|
await test('changing to representation hides inapplicable questions while keeping their answers for later recovery', () => {
|
|
const h = harness();
|
|
find(h.render(), 'purpose').props.onChange({
|
|
target: { value: 'representation' },
|
|
});
|
|
const tree = h.render();
|
|
assert.equal(find(tree, 'participants').type, 'input');
|
|
assert.equal(find(tree, 'participants').props.type, 'hidden');
|
|
assert.equal(find(tree, 'participants').props.value, 'one');
|
|
assert.equal(find(tree, 'preferred').props.value, 'quotas');
|
|
find(tree, 'purpose').props.onChange({ target: { value: 'new' } });
|
|
assert.equal(find(h.render(), 'participants').props.value, 'one');
|
|
});
|
|
await test('dossier choice uses current browser values and transports only answer codes through normal authentication', () => {
|
|
const h = harness(),
|
|
tree = h.render();
|
|
h.refs[0].current = {};
|
|
const old = globalThis.FormData;
|
|
globalThis.FormData = class {
|
|
get(key) {
|
|
return key === 'answers[participants]' ? 'several' : null;
|
|
}
|
|
};
|
|
try {
|
|
button(tree, 'Escolher dossier para guardar').props.onClick();
|
|
} finally {
|
|
globalThis.FormData = old;
|
|
}
|
|
const target = new URL(h.visits[0][0], 'https://portal.test');
|
|
assert.equal(target.pathname, '/investimentos');
|
|
assert.equal(
|
|
target.searchParams.get('company_comparison[participants]'),
|
|
'several',
|
|
);
|
|
assert.equal(
|
|
target.searchParams.get('company_comparison[preferred]'),
|
|
'quotas',
|
|
);
|
|
});
|
|
await test('a failed save keeps actual field values and context, and never navigates as though the comparison was saved', async () => {
|
|
const h = harness();
|
|
h.fail();
|
|
const extra = {
|
|
case: { id: 'dossier-a', title: 'SYNTHETIC dossier', version: 7 },
|
|
canWrite: true,
|
|
};
|
|
let tree = h.render(extra);
|
|
h.refs[0].current = {};
|
|
find(tree, 'participants').props.onChange({ target: { value: 'several' } });
|
|
tree = h.render(extra);
|
|
const old = globalThis.FormData;
|
|
globalThis.FormData = class {
|
|
get(key) {
|
|
return key === 'answers[participants]' ? 'several' : null;
|
|
}
|
|
};
|
|
try {
|
|
await button(tree, 'Guardar no dossier').props.onClick();
|
|
} finally {
|
|
globalThis.FormData = old;
|
|
}
|
|
assert.equal(h.requests[0][0], '/investimentos/dossier-a/forma-juridica');
|
|
assert.equal(h.requests[0][1].version, 7);
|
|
assert.equal(h.requests[0][1].answers.participants, 'several');
|
|
assert.equal(h.visits.length, 0);
|
|
const after = h.render(extra);
|
|
assert.equal(find(after, 'participants').props.value, 'several');
|
|
assert.match(renderToStaticMarkup(after), /SYNTHETIC conflict; keep edits/);
|
|
assert.equal(
|
|
button(after, 'Reabrir com estas respostas').props.type,
|
|
'submit',
|
|
);
|
|
assert.equal(
|
|
nodes(after).find((node) => node.type === 'form').props.method,
|
|
'get',
|
|
);
|
|
});
|
|
await test('historical and superseded results retain their own question labels and source and require explicit current preparation', () => {
|
|
const h = harness();
|
|
const historicalResult = {
|
|
...props.result,
|
|
engine_version: 'older',
|
|
questions: {
|
|
...questions,
|
|
participants: {
|
|
...questions.participants,
|
|
label: 'SYNTHETIC historical wording',
|
|
},
|
|
},
|
|
};
|
|
const tree = h.render({
|
|
case: { id: 'dossier-a', title: 'Dossier', version: 3 },
|
|
canWrite: true,
|
|
historical: true,
|
|
saved: { revision: 1, at: '2026-11-01', result: historicalResult },
|
|
result: historicalResult,
|
|
});
|
|
const html = renderToStaticMarkup(tree);
|
|
assert.match(html, /SYNTHETIC historical wording/);
|
|
assert.equal(nodes(tree).filter((node) => node.type === 'form').length, 0);
|
|
const link = nodes(tree).find(
|
|
(node) =>
|
|
node.props?.children ===
|
|
'Preparar nova comparação com estas respostas',
|
|
);
|
|
assert.equal(
|
|
new URL(link.props.href, 'https://portal.test').searchParams.get(
|
|
'answers[participants]',
|
|
),
|
|
'one',
|
|
);
|
|
assert.equal(
|
|
new URL(link.props.href, 'https://portal.test').searchParams.has(
|
|
'revision',
|
|
),
|
|
false,
|
|
);
|
|
});
|
|
await test('successful save recovers the saved dossier edition and history links retain the exact dossier scope', async () => {
|
|
const h = harness(),
|
|
extra = {
|
|
case: { id: 'dossier-a', title: 'Dossier', version: 5 },
|
|
canWrite: true,
|
|
history: [
|
|
{
|
|
id: 'event-a',
|
|
revision: 2,
|
|
at: '2026-11-02',
|
|
engine_version: 'v1',
|
|
},
|
|
],
|
|
historyNext: 100,
|
|
};
|
|
const tree = h.render(extra);
|
|
h.refs[0].current = {};
|
|
const old = globalThis.FormData;
|
|
globalThis.FormData = class {
|
|
get() {
|
|
return null;
|
|
}
|
|
};
|
|
try {
|
|
await button(tree, 'Guardar no dossier').props.onClick();
|
|
} finally {
|
|
globalThis.FormData = old;
|
|
}
|
|
assert.equal(h.visits[0][0], '/investimentos/dossier-a/forma-juridica');
|
|
assert.equal(h.visits[0][1].preserveState, false);
|
|
const html = renderToStaticMarkup(tree);
|
|
assert.match(html, /dossier-a\/forma-juridica\?revision=event-a/);
|
|
assert.match(html, /dossier-a\/forma-juridica\?history_before=100/);
|
|
});
|