Files
Investidor-V2/tests/frontend/bvm-admission-guide.test.mjs
T

297 lines
9.9 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { execFileSync } from 'node:child_process';
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;
}
// Pure config export: no application bootstrap, connection or active database access.
const guide = JSON.parse(
execFileSync(
'php',
[
'-r',
"echo json_encode(require 'config/bvm-admission-guide.php', JSON_THROW_ON_ERROR);",
],
{ cwd: new URL('../..', import.meta.url), encoding: 'utf8' },
),
);
guide.sha256 = 'b'.repeat(64);
const lib = load('../../resources/js/lib/investment-guides.ts');
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,
};
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() {
let cursor = 0;
const values = [],
requests = [];
class ValidationError extends Error {}
const component = load('../../resources/js/pages/investment/guide.tsx', {
react: {
useState(initial) {
const i = cursor++;
if (!(i in values)) values[i] = structuredClone(initial);
return [
values[i],
(value) => {
values[i] =
typeof value === 'function'
? value(values[i])
: value;
},
];
},
useRef(initial) {
const i = cursor++;
if (!(i in values)) values[i] = { current: initial };
return values[i];
},
},
'@inertiajs/react': { Link: 'a', router: { visit() {} } },
'./shared': { Frame: 'main', card: '', input: '', button: '' },
'@/lib/investment-guides': lib,
'@/lib/operation-key': {
createOperationKey: () => 'SYNTHETIC-operation',
},
'@/lib/profile-json': {
ProfileValidationError: ValidationError,
profileJson: async (...args) => {
requests.push(args);
return { saved: true };
},
},
});
return {
render(extra = {}) {
cursor = 0;
const outer = component.default({ ...props, ...extra });
return outer.type(outer.props);
},
requests,
};
}
function headingCount(tree, label) {
return nodes(tree).filter(
(n) =>
['h2', 'h3', 'h4'].includes(n.type) && n.props.children === label,
).length;
}
await test('actual source guide renders five paths, scoped gaps and article/page links with explicit historical minimums', () => {
const tree = harness().render(),
html = renderToStaticMarkup(tree);
assert.equal(
nodes(tree).filter(
(n) => n.type === 'input' && n.props.name === 'instruments[]',
).length,
5,
);
assert.match(html, /Que segmento e tipo de valor pretende preparar/);
assert.match(html, /Requisitos por segmento e tipo de valor/);
assert.match(html, /Período indicado até 2025-03-31/);
assert.match(html, /fonte histórica, sem aplicação como mínimo actual/);
assert.match(html, /Por esclarecer/);
assert.match(html, /Regra com dispensa ou condição específica/);
assert.match(html, /Regulamento 04\/2021/);
assert.ok(
nodes(tree).some(
(n) =>
n.type === 'a' &&
n.props.href ===
'https://www.bvm.co.mz/documentos/regulamentos/RegulamentoTerceiroMercado.pdf#page=2',
),
);
assert.match(html, /Art. 2\(1\)\(c\) e art. 8/);
assert.equal(headingCount(tree, 'Preparar para discussão'), 0);
assert.doesNotMatch(html, /name="(note|nuit|email|amount|capital)"/);
});
await test('third-market selection hides the entry audit document while keeping OCAM, declaration and conditional waiver explanations', () => {
const h = harness();
nodes(h.render())
.find((n) => n.props?.value === 'third_shares')
.props.onChange({ target: { checked: true } });
const tree = h.render(),
html = renderToStaticMarkup(tree);
assert.equal(headingCount(tree, 'Auditoria ou certificação aplicável'), 0);
assert.equal(
headingCount(
tree,
'Demonstrações financeiras assinadas por contabilista certificado',
),
1,
);
assert.equal(
headingCount(tree, 'Declaração de actualidade das informações'),
1,
);
assert.equal(
headingCount(tree, 'Detentores do capital e participações'),
1,
);
assert.match(html, /sem extensão automática/);
assert.match(html, /demonstrações e relatório de evolução semestrais/);
assert.doesNotMatch(html, /Existência jurídica de um ano, não derrogável/);
assert.doesNotMatch(
html,
/Relatórios de gestão e contas anuais dos dois exercícios anteriores/,
);
});
await test('multiple paths share common documents once while retaining their different accounts and prospectus conditions', () => {
const h = harness();
for (const code of ['official_shares', 'second_shares'])
nodes(h.render())
.find((n) => n.props?.value === code)
.props.onChange({ target: { checked: true } });
const tree = h.render(),
html = renderToStaticMarkup(tree);
for (const title of [
'Deliberações, estatutos e certidão',
'Relatórios, contas, fiscalização e publicações',
'Auditoria ou certificação aplicável',
'Projecto de prospecto conforme o segmento',
])
assert.equal(headingCount(tree, title), 1);
assert.equal(
headingCount(
tree,
'Demonstrações financeiras assinadas por contabilista certificado',
),
0,
);
assert.match(html, /dois exercícios anteriores/);
assert.match(html, /pelo menos nove meses/);
assert.match(html, /Anexo A do Regulamento do Segundo Mercado/);
assert.doesNotMatch(
html,
/Documentos específicos do empréstimo obrigacionista/,
);
});
await test('bonds use their own document path and external handoffs never receive private note or dossier parameters', () => {
const dossier = {
id: 'SYNTHETIC-private-dossier',
title: 'SYNTHETIC private context',
version: 1,
};
const tree = harness().render({
case: dossier,
canWrite: true,
instruments: ['second_bonds'],
note: 'SYNTHETIC PRIVATE NOTE',
});
assert.equal(
headingCount(
tree,
'Documentos específicos do empréstimo obrigacionista',
),
1,
);
assert.equal(
headingCount(tree, 'Detentores do capital e participações'),
0,
);
assert.ok(
nodes(tree).some(
(n) =>
n.props?.href ===
'/balcao-virtual/dossiers/SYNTHETIC-private-dossier',
),
);
const external = nodes(tree).filter(
(n) => n.type === 'a' && n.props.href?.startsWith('https://'),
);
assert.ok(external.length > 10);
for (const link of external) {
assert.doesNotMatch(
link.props.href,
/SYNTHETIC|note=|context=|dossier=/,
);
assert.equal(new URL(link.props.href).search, '');
assert.equal(link.props.rel, 'noopener noreferrer');
}
assert.ok(
external.some(
(n) => n.props.href === 'https://www.bvm.co.mz/empresas/0/0',
),
);
});
await test('historical BVM references render frozen sources and conditions without reopening a write or mixing guide history', async () => {
const h = harness();
const oldGuide = { ...guide, version: 'SYNTHETIC-old-BVM-edition' };
const tree = h.render({
guide: oldGuide,
instruments: ['third_shares'],
case: { id: 'SYNTHETIC-case', title: 'SYNTHETIC', version: 3 },
saved: { revision: 1, at: 'SYNTHETIC-date', context: { revision: 0 } },
canWrite: true,
historical: true,
history: [
{
id: 'SYNTHETIC-event',
revision: 1,
edition: 'SYNTHETIC-old-BVM-edition',
at: 'SYNTHETIC-date',
},
],
});
assert.equal(
nodes(tree).find((n) => n.type === 'fieldset').props.disabled,
true,
);
nodes(tree)
.find((n) => n.type === 'form')
.props.onSubmit({ preventDefault() {} });
await new Promise((resolve) => setImmediate(resolve));
assert.equal(h.requests.length, 0);
assert.match(renderToStaticMarkup(tree), /SYNTHETIC-old-BVM-edition/);
assert.ok(
nodes(tree).some(
(n) =>
n.props?.href ===
'/investimentos/SYNTHETIC-case/guias/bolsa?revision=SYNTHETIC-event',
),
);
});