297 lines
9.9 KiB
JavaScript
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',
|
|
),
|
|
);
|
|
});
|