Files
Investidor-V2/tests/frontend/company-preparation.test.mjs
T

390 lines
13 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 lib = load('../../resources/js/lib/company-preparation.ts');
const party = {
id: 'SYNTHETIC-party',
name: '<SYNTHETIC>',
kind: 'person',
source: null,
roles: ['subscriber'],
represented_id: null,
cash: '10',
in_kind: '0',
paid: '2',
share_count: '5',
work: null,
commitment: null,
};
const facts = {
form: 'quotas',
proposed_name: null,
seat: null,
activities: null,
regulated: 'unknown',
currency: 'MZN',
capital: '10',
nominal_value: '2',
management: null,
binding: null,
notes: null,
parties: [party],
};
const props = {
case: { id: 'SYNTHETIC-case', title: 'SYNTHETIC dossier', version: 1 },
canWrite: true,
historical: false,
definition: {
version: 'v1',
forms: {
individual: { title: 'Individual' },
quotas: { title: 'Quotas' },
anonymous: { title: 'Anónima' },
},
roles: {
subscriber: 'Sócio',
administrator: 'Administração',
representative: 'Representante',
beneficial_owner: 'Beneficiário',
},
template_notice: 'Exemplares pendentes; ficha de preparação.',
source: {
title: 'SYNTHETIC source',
edition: '2022',
url: 'https://source.test/code.pdf',
},
},
facts,
review: {
issues: ['SYNTHETIC pending'],
totals: { subscribed: '10', unpaid: '8' },
note: 'SYNTHETIC declared',
},
saved: null,
sources: {
case_proponent: {
name: 'SYNTHETIC reused',
kind: 'person',
origin: 'Proponente do dossier',
},
},
history: [],
historyNext: null,
};
function nodes(tree) {
if (!tree || typeof tree !== 'object') return [];
if (Array.isArray(tree)) return tree.flatMap(nodes);
return [tree, ...nodes(tree.props?.children)];
}
const named = (tree, name) => nodes(tree).find((n) => n.props?.name === name);
const button = (tree, label) =>
nodes(tree).find((n) => n.type === 'button' && n.props.children === label);
const tick = () => new Promise((resolve) => setImmediate(resolve));
function harness() {
let cursor = 0,
reject = false;
const values = [],
refs = [],
visits = [],
requests = [];
class ValidationError extends Error {
constructor(message) {
super(message);
this.fields = { 'facts.capital': 'SYNTHETIC amount error' };
}
}
const component = load(
'../../resources/js/pages/investment/company-preparation.tsx',
{
react: {
useState(initial) {
const i = cursor++;
if (!(i in values)) values[i] = structuredClone(initial);
return [
values[i],
(next) => {
values[i] =
typeof next === 'function'
? next(values[i])
: next;
},
];
},
useRef(initial) {
const i = cursor++;
if (!(i in values)) {
values[i] = { current: initial };
refs.push(values[i]);
}
return values[i];
},
},
'@inertiajs/react': {
Link: 'a',
router: { visit: (...args) => visits.push(args) },
},
'./shared': { Frame: 'main', card: '', input: '', button: '' },
'@/lib/company-preparation': lib,
'@/lib/operation-key': {
createOperationKey: () => 'SYNTHETIC-operation',
},
'@/lib/profile-json': {
ProfileValidationError: ValidationError,
profileJson: async (...args) => {
requests.push(args);
if (args[2] === 'GET')
return {
...props,
case: { ...props.case, version: 4 },
facts: {
...facts,
notes: 'SYNTHETIC concurrent edit',
},
};
if (reject) throw new ValidationError('SYNTHETIC conflict');
return { saved: true };
},
},
},
);
return {
render(extra = {}) {
cursor = 0;
const outer = component.default({ ...props, ...extra });
return outer.type(outer.props);
},
refs,
visits,
requests,
fail() {
reject = true;
},
};
}
async function withData(data, fn) {
const original = globalThis.FormData;
globalThis.FormData = class {
get(key) {
return Object.hasOwn(data, key) ? data[key] : null;
}
};
try {
return await fn();
} finally {
globalThis.FormData = original;
}
}
await test('preparation renders seven sections, escaped private names, source and explicit incomplete state', () => {
const tree = harness().render(),
html = renderToStaticMarkup(tree);
assert.equal(
nodes(tree).filter(
(n) => n.type === 'h2' && /^[1-7]\./.test(n.props.children),
).length,
7,
);
assert.match(html, /&lt;SYNTHETIC&gt;/);
assert.match(html, /Exemplares pendentes/);
assert.match(html, /SYNTHETIC pending/);
assert.match(html, /https:\/\/source.test\/code.pdf/);
assert.doesNotMatch(html, /name="(email|nuit|password)"/);
});
await test('changing form hides only inapplicable capital and preserves amounts for later recovery', () => {
const h = harness();
named(h.render(), 'form').props.onChange({
target: { value: 'individual' },
});
assert.equal(named(h.render(), 'capital'), undefined);
named(h.render(), 'form').props.onChange({
target: { value: 'anonymous' },
});
assert.equal(named(h.render(), 'capital').props.value, '10');
assert.equal(named(h.render(), 'nominal_value').props.value, '2');
assert.equal(named(h.render(), `${party.id}.share_count`).props.value, '5');
});
await test('explicit reuse fills source identity while roles remain independently selectable', () => {
const h = harness();
const source = nodes(h.render()).find(
(n) =>
n.type === 'select' &&
n.props.children?.[0]?.props?.children ===
'Informação nova desta preparação',
);
source.props.onChange({ target: { value: 'case_proponent' } });
assert.equal(
named(h.render(), `${party.id}.name`).props.value,
'SYNTHETIC reused',
);
assert.equal(named(h.render(), `${party.id}.name`).props.readOnly, true);
const checkboxes = nodes(h.render()).filter(
(n) => n.type === 'input' && n.props.type === 'checkbox',
);
assert.deepEqual(
checkboxes.map((n) => n.props.checked),
[true, false, false, false],
);
checkboxes[1].props.onChange({ target: { checked: true } });
assert.deepEqual(
nodes(h.render())
.filter((n) => n.type === 'input' && n.props.type === 'checkbox')
.map((n) => n.props.checked),
[true, true, false, false],
);
});
await test('actual browser values are sent privately and hidden values survive without forged source snapshots', async () => {
await withData(
{
proposed_name: 'SYNTHETIC autofill',
[`${party.id}.cash`]: '7,25',
capital: '',
},
() => {
const values = lib.preparationValues(
{},
{
...facts,
form: 'individual',
parties: [
{
...party,
source_snapshot: { reference: 'DO NOT POST' },
},
],
},
);
assert.equal(values.proposed_name, 'SYNTHETIC autofill');
assert.equal(values.capital, null);
assert.equal(values.parties[0].cash, '7,25');
assert.equal(values.parties[0].share_count, '5');
assert.equal('source_snapshot' in values.parties[0], false);
},
);
const h = harness();
h.render();
h.refs[0].current = {};
await withData({ notes: 'SYNTHETIC autofill' }, async () => {
nodes(h.render())
.find((n) => n.type === 'form')
.props.onSubmit({ preventDefault() {} });
await tick();
});
assert.equal(
h.requests[0][0],
'/investimentos/SYNTHETIC-case/preparar-empresa',
);
assert.equal(h.requests[0][1].facts.notes, 'SYNTHETIC autofill');
assert.deepEqual(h.visits, [
[
'/investimentos/SYNTHETIC-case/preparar-empresa',
{ preserveState: false },
],
]);
});
await test('failed save retains private edits, consults current version without URL data and requires explicit reconciliation', async () => {
const h = harness();
h.render();
h.refs[0].current = {};
h.fail();
await withData({ notes: 'SYNTHETIC unsaved' }, async () => {
nodes(h.render())
.find((n) => n.type === 'form')
.props.onSubmit({ preventDefault() {} });
await tick();
});
assert.equal(h.visits.length, 0);
assert.equal(named(h.render(), 'notes').props.value, 'SYNTHETIC unsaved');
assert.match(renderToStaticMarkup(h.render()), /SYNTHETIC amount error/);
await button(
h.render(),
'Conferir edição actual sem perder estes dados',
).props.onClick();
assert.equal(h.requests[1][2], 'GET');
assert.equal(named(h.render(), 'notes').props.value, 'SYNTHETIC unsaved');
const recoveryHtml = renderToStaticMarkup(h.render());
assert.match(recoveryHtml, /SYNTHETIC concurrent edit/);
assert.match(recoveryHtml, /Denominação proposta/);
const recoverySection = nodes(h.render()).find(
(n) =>
n.type === 'section' &&
nodes(n).some(
(child) =>
child.type === 'h2' &&
child.props.children ===
'Edição actualmente guardada no dossier',
),
);
assert.doesNotMatch(
renderToStaticMarkup(recoverySection),
/source_snapshot|proposed_name|SYNTHETIC-party/,
);
button(
h.render(),
'Conferi: manter os meus dados para uma nova revisão',
).props.onClick();
assert.equal(named(h.render(), 'notes').props.value, 'SYNTHETIC unsaved');
await withData({}, async () => {
nodes(h.render())
.find((n) => n.type === 'form')
.props.onSubmit({ preventDefault() {} });
await tick();
});
assert.equal(h.requests[2][1].version, 4);
});
await test('history and consultation keep all fields read-only and cannot submit a write', async () => {
const h = harness();
const tree = h.render({
historical: true,
saved: { revision: 1, at: 'SYNTHETIC-date' },
history: [{ id: 'old', revision: 1, at: 'SYNTHETIC-date' }],
historyNext: 7,
});
assert.equal(
nodes(tree).find((n) => n.type === 'fieldset').props.disabled,
true,
);
nodes(tree)
.find((n) => n.type === 'form')
.props.onSubmit({ preventDefault() {} });
await tick();
assert.equal(h.requests.length, 0);
assert.match(renderToStaticMarkup(tree), /preparar-empresa\?revision=old/);
assert.match(renderToStaticMarkup(tree), /history_before=7/);
const viewer = harness();
assert.equal(
nodes(viewer.render({ canWrite: false })).find(
(n) => n.type === 'fieldset',
).props.disabled,
true,
);
});