Files
Investidor-V2/tests/frontend/investment-guides.test.mjs
T

432 lines
14 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/investment-guides.ts');
const guide = {
slug: 'capitalizacao',
version: 'SYNTHETIC-v1',
sha256: 'a'.repeat(64),
title: 'SYNTHETIC capital guide',
reviewed_on: '2026-11-02',
scope: 'SYNTHETIC information, not approval',
instruments: {
debt: {
title: 'SYNTHETIC debt',
description: 'SYNTHETIC repayments',
risks: ['SYNTHETIC default'],
prepare: ['SYNTHETIC cashflows'],
sources: ['primary'],
},
equity: {
title: 'SYNTHETIC equity',
description: 'SYNTHETIC ownership',
risks: ['SYNTHETIC dilution'],
prepare: ['SYNTHETIC ownership'],
sources: ['primary'],
},
},
sources: {
primary: {
title: 'Primary <SYNTHETIC>',
url: 'https://primary.test/page',
edition: 'SYNTHETIC edition',
locator: 'SYNTHETIC paragraph',
consulted_on: '2026-11-02',
sha256: 'b'.repeat(64),
},
},
eligibility: [
{ text: 'SYNTHETIC provider criteria', sources: ['primary'] },
],
preparation: [
{
title: 'SYNTHETIC documents',
text: 'SYNTHETIC proposed list',
sources: ['primary'],
},
],
next: ['SYNTHETIC next action'],
limits: ['SYNTHETIC no automatic submission'],
};
const props = {
guide,
instruments: [],
note: '',
case: null,
saved: null,
canWrite: false,
historical: false,
olderEdition: false,
currentAvailable: true,
workingCopy: false,
contextChanged: false,
unavailableInstruments: [],
history: [],
historyNext: null,
};
const dossier = {
id: 'SYNTHETIC-case',
title: 'SYNTHETIC destination',
version: 1,
};
function nodes(tree) {
if (!tree || typeof tree !== 'object') return [];
if (Array.isArray(tree)) return tree.flatMap(nodes);
return [tree, ...nodes(tree.props?.children)];
}
const byName = (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,
withdrawn = false;
const values = [],
refs = [],
requests = [],
visits = [];
class ValidationError extends Error {}
const hooks = {
useState(initial) {
const i = cursor++;
if (!(i in values))
values[i] = structuredClone(
typeof initial === 'function' ? initial() : initial,
);
return [
values[i],
(v) => {
values[i] = typeof v === 'function' ? 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 component = load('../../resources/js/pages/investment/guide.tsx', {
react: hooks,
'@inertiajs/react': {
Link: 'a',
router: { visit: (...args) => visits.push(args) },
},
'./shared': { Frame: 'main', input: '', card: '', button: '' },
'@/lib/investment-guides': 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: { ...dossier, version: 4 },
canWrite: true,
olderEdition: withdrawn,
currentAvailable: !withdrawn,
note: 'SYNTHETIC saved by coeditor',
};
if (reject) throw new Error('SYNTHETIC conflict');
return { saved: true };
},
},
});
return {
render(extra = {}) {
cursor = 0;
const outer = component.default({ ...props, ...extra });
return outer.type(outer.props);
},
refs,
requests,
visits,
fail() {
reject = true;
},
withdraw() {
withdrawn = true;
},
};
}
async function withData(data, fn) {
const original = globalThis.FormData;
globalThis.FormData = class {
get(key) {
return data[key] ?? null;
}
getAll(key) {
return data[key] ?? [];
}
};
try {
return await fn();
} finally {
globalThis.FormData = original;
}
}
await test('public guide distinguishes instruments, risks, provider criteria, documents and exact source edition without identity fields', () => {
const tree = harness().render(),
html = renderToStaticMarkup(tree);
for (const text of [
'SYNTHETIC debt',
'SYNTHETIC equity',
'SYNTHETIC default',
'SYNTHETIC dilution',
'SYNTHETIC provider criteria',
'SYNTHETIC documents',
'SYNTHETIC next action',
'SYNTHETIC edition',
'SYNTHETIC paragraph',
])
assert.match(html, new RegExp(text));
assert.match(html, /Primary &lt;SYNTHETIC&gt;/);
assert.match(html, /https:\/\/primary.test\/page/);
assert.equal(byName(tree, 'note'), undefined);
assert.doesNotMatch(html, /name="(name|email|nuit|amount|return)"/);
});
await test('selection filters only the visible instruments and can return to the whole guide', () => {
const h = harness();
const checkbox = nodes(h.render()).find((n) => n.props?.value === 'debt');
checkbox.props.onChange({ target: { checked: true } });
let html = renderToStaticMarkup(h.render());
assert.match(html, /SYNTHETIC default/);
assert.doesNotMatch(html, /SYNTHETIC dilution/);
nodes(h.render())
.find((n) => n.props?.value === 'debt')
.props.onChange({ target: { checked: false } });
html = renderToStaticMarkup(h.render());
assert.match(html, /SYNTHETIC dilution/);
});
await test('authentication URLs carry only coded choices and edition pins, including an explicitly empty selection', async () => {
const h = harness();
h.render();
h.refs[0].current = {};
await withData(
{ 'instruments[]': ['equity'], note: 'PRIVATE MUST NOT TRAVEL' },
async () => {
nodes(h.render())
.find((n) => n.type === 'form')
.props.onSubmit({ preventDefault() {} });
},
);
const url = new URL(h.visits[0][0], 'https://portal.test');
assert.equal(url.pathname, '/investimentos');
assert.equal(
url.searchParams.get('investment_guide[sha256]'),
guide.sha256,
);
assert.deepEqual(
url.searchParams.getAll('investment_guide[instruments][]'),
['equity'],
);
assert.doesNotMatch(url.href, /PRIVATE|note/);
const empty = new URL(
lib.guideUrl('/investimentos/id/guias/capitalizacao', {
slug: guide.slug,
sha256: guide.sha256,
instruments: [],
}),
'https://portal.test',
);
assert.equal(empty.searchParams.get('current'), '1');
assert.equal(empty.searchParams.get('selection'), '1');
assert.equal(empty.searchParams.get('sha256'), guide.sha256);
});
await test('private saves use actual browser values and return to the same dossier without putting notes in URLs', async () => {
const h = harness(),
extra = { case: dossier, canWrite: true };
h.render(extra);
h.refs[0].current = {};
await withData(
{
'instruments[]': ['debt'],
note: 'SYNTHETIC autofilled private note',
},
async () => {
nodes(h.render(extra))
.find((n) => n.type === 'form')
.props.onSubmit({ preventDefault() {} });
await tick();
},
);
assert.equal(h.requests[0][1].note, 'SYNTHETIC autofilled private note');
assert.deepEqual(h.requests[0][1].instruments, ['debt']);
assert.equal(h.requests[0][1].sha256, guide.sha256);
assert.deepEqual(h.visits, [
[
'/investimentos/SYNTHETIC-case/guias/capitalizacao',
{ preserveState: false },
],
]);
});
await test('conflicts preserve private notes and require explicit comparison before adopting the current dossier version', async () => {
const h = harness(),
extra = { case: dossier, canWrite: true };
h.render(extra);
h.refs[0].current = {};
h.fail();
await withData(
{ 'instruments[]': ['debt'], note: 'SYNTHETIC unsaved note' },
async () => {
nodes(h.render(extra))
.find((n) => n.type === 'form')
.props.onSubmit({ preventDefault() {} });
await tick();
},
);
assert.equal(
byName(h.render(extra), 'note').props.value,
'SYNTHETIC unsaved note',
);
assert.equal(h.visits.length, 0);
await button(
h.render(extra),
'Conferir a referência actual sem perder a nota',
).props.onClick();
assert.equal(h.requests[1][2], 'GET');
assert.match(
renderToStaticMarkup(h.render(extra)),
/SYNTHETIC saved by coeditor/,
);
button(
h.render(extra),
'Conferi: manter a minha preparação numa nova revisão',
).props.onClick();
await withData({ 'instruments[]': ['debt'] }, async () => {
nodes(h.render(extra))
.find((n) => n.type === 'form')
.props.onSubmit({ preventDefault() {} });
await tick();
});
assert.equal(h.requests[2][1].version, 4);
assert.equal(h.requests[2][1].note, 'SYNTHETIC unsaved note');
h.withdraw();
await button(
h.render(extra),
'Conferir a referência actual sem perder a nota',
).props.onClick();
assert.equal(
button(
h.render(extra),
'Conferi: manter a minha preparação numa nova revisão',
),
undefined,
);
assert.equal(
byName(h.render(extra), 'note').props.value,
'SYNTHETIC unsaved note',
);
assert.match(
renderToStaticMarkup(h.render(extra)),
/guia utilizado foi alterado/,
);
});
await test('historical or withdrawn guides and consultation members cannot write or silently upgrade a saved edition', async () => {
for (const flags of [
{ historical: true },
{ olderEdition: true, currentAvailable: false },
{ canWrite: false },
]) {
const h = harness(),
tree = h.render({ case: dossier, canWrite: true, ...flags });
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);
if (flags.currentAvailable === false) {
const html = renderToStaticMarkup(tree);
assert.match(html, /retirado da consulta actual/);
assert.doesNotMatch(html, /\?current=1/);
}
}
});
await test('dossier chooser preserves the selected source edition on existing and newly created investment paths', () => {
let created;
const selection = {
slug: guide.slug,
sha256: guide.sha256,
instruments: ['debt'],
};
const module = load('../../resources/js/pages/investor-path/index.tsx', {
react: { useState: (fn) => [typeof fn === 'function' ? fn() : fn] },
'@inertiajs/react': {
Head: 'title',
Link: 'a',
router: {},
useForm: (data) => {
created = data;
return { data, errors: {}, processing: false };
},
},
'@/lib/investment-guides': lib,
'@/lib/operation-key': {
createOperationKey: () => 'SYNTHETIC-operation',
},
'@/lib/company-comparison': {},
});
const tree = module.default({
contexts: [{ ...dossier, updated_at: '2026-11-02' }],
guideSelection: selection,
});
assert.deepEqual(created.investment_guide, selection);
const link = nodes(tree).find(
(n) =>
typeof n.props?.href === 'string' &&
n.props.href.includes('/guias/'),
);
const url = new URL(link.props.href, 'https://portal.test');
assert.equal(
url.pathname,
'/investimentos/SYNTHETIC-case/guias/capitalizacao',
);
assert.equal(url.searchParams.get('sha256'), guide.sha256);
assert.match(
renderToStaticMarkup(tree),
/só serão guardados quando confirmar/,
);
});