390 lines
13 KiB
JavaScript
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, /<SYNTHETIC>/);
|
|
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,
|
|
);
|
|
});
|