Files
Investidor-V2/tests/frontend/investor-usability-components.test.mjs
T

361 lines
12 KiB
JavaScript

import { compileFunction } from 'node:vm';
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import ts from 'typescript';
import * as flow from '../../resources/js/lib/opportunity-fill-flow.ts';
import { fiscalPreparationGuide } from '../../resources/js/lib/fiscal-preparation-guide.ts';
const require = createRequire(import.meta.url);
function load(path, mocks = {}, exportName = 'default', extra = '') {
const source = fs.readFileSync(new URL(path, import.meta.url), 'utf8');
const output = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
jsx: ts.JsxEmit.ReactJSX,
},
}).outputText;
const module = { exports: {} };
compileFunction(output + extra, ['require', 'module', 'exports'])(
(name) =>
mocks[name] ??
(name.startsWith('@/') ? { default: () => null } : require(name)),
module,
module.exports,
);
return module.exports[exportName];
}
function stateHooks() {
const memory = [];
let cursor = 0;
return {
hooks: {
...React,
useState(initial) {
const i = cursor++;
if (!(i in memory))
memory[i] =
typeof initial === 'function' ? initial() : initial;
return [
memory[i],
(next) =>
(memory[i] =
typeof next === 'function'
? next(memory[i])
: next),
];
},
useRef: () => ({ current: null }),
useEffect: () => {},
},
reset: () => (cursor = 0),
};
}
const flat = (node) =>
!node || typeof node !== 'object'
? []
: [node, ...React.Children.toArray(node.props?.children).flatMap(flat)];
void test('free navigation keeps every field mounted and reports resource work in the current group', () => {
const state = stateHooks();
const Component = load(
'../../resources/js/components/opportunity-fill-flow.tsx',
{ react: state.hooks, '@/lib/opportunity-fill-flow': flow },
);
const groups = flow.opportunityFillGroups([
'official_title',
'locations',
'documents',
]);
const saved = {
official_title: 'Deliberate user title',
locations: ['saved-site'],
};
const opened = [];
let tree;
const render = () => {
state.reset();
tree = Component({
groups,
errors: {},
renderField: (field) =>
React.createElement('input', {
name: field,
value: JSON.stringify(saved[field] ?? ''),
readOnly: true,
}),
onOpenResources: (group) => opened.push(group),
});
};
const priorWindow = globalThis.window,
priorFrame = globalThis.requestAnimationFrame;
globalThis.window = {
history: {
state: { existing: 'inertia-state' },
replaceState(state, _title, url) {
assert.deepEqual(state, { existing: 'inertia-state' });
this.url = url;
},
},
};
globalThis.requestAnimationFrame = (callback) => callback();
try {
render();
assert.equal(
flat(tree).filter((node) => node.type === 'input').length,
3,
);
flat(tree)
.find(
(node) =>
node.type === 'a' && node.props.href === '#fill-territory',
)
.props.onClick({ preventDefault() {} });
render();
const sections = flat(tree).filter((node) => node.type === 'section');
assert.equal(
sections.find((node) => node.props.id === 'fill-territory').props
.hidden,
false,
);
assert.equal(
sections.find((node) => node.props.id === 'fill-idea').props.hidden,
true,
);
assert.equal(
flat(tree).filter((node) => node.type === 'input').length,
3,
);
flat(tree)
.find((node) => node.type === 'button')
.props.onClick();
assert.deepEqual(opened, ['territory']);
assert.equal(saved.official_title, 'Deliberate user title');
assert.deepEqual(saved.locations, ['saved-site']);
assert.equal(globalThis.window.history.url, '#fill-territory');
} finally {
globalThis.window = priorWindow;
globalThis.requestAnimationFrame = priorFrame;
}
});
void test('profile request renders readable labels without dumping every available document into the form', () => {
const Picker = load(
'../../resources/js/components/profile-proof-picker.tsx',
);
const Page = load('../../resources/js/pages/profiles/request.tsx', {
'@/lib/operation-key': { createOperationKey: () => 'synthetic-key' },
'@inertiajs/react': {
Head: () => null,
Link: ({ href, children }) =>
React.createElement('a', { href }, children),
router: {},
},
'@/components/profile-navigation': { default: () => null },
'@/lib/profile-json': {},
'@/pages/management/shared': { button: 'button', control: 'control' },
'@/components/opportunity-search-filters': {
modalityLabels: {
investment_project: 'Projecto de investimento',
land_rights: 'Direitos fundiários',
},
},
'@/components/profile-proof-picker': { default: Picker },
});
const html = renderToStaticMarkup(
React.createElement(Page, {
requests: [],
types: ['investment_project', 'land_rights'],
organizations: [],
documents: Array.from({ length: 80 }, (_, i) => ({
id: `doc-${i}`,
file_name: `UNSELECTED-DOCUMENT-${i}.txt`,
})),
}),
);
assert.match(html, /Em nome próprio/);
assert.match(html, /Entidade pública/);
assert.match(html, /Projecto de investimento/);
assert.match(html, /Direitos fundiários/);
assert.doesNotMatch(html, /UNSELECTED-DOCUMENT/);
assert.match(html, /Pesquisar nos meus ficheiros/);
const visible = html.replace(/<[^>]+>/g, '');
assert.doesNotMatch(
visible,
/investment_project|land_rights|public_entity/,
);
});
void test('proof selection is explicit and retained when the filename search changes', () => {
const state = stateHooks();
const Component = load(
'../../resources/js/components/profile-proof-picker.tsx',
{ react: state.hooks },
);
const documents = [
{ id: 'identity', file_name: 'Documento próprio' },
{ id: 'law', file_name: 'Lei.pdf' },
];
const values = [];
let tree;
const render = () => {
state.reset();
tree = Component({
documents,
value: values,
onChange: (ids) => {
values.splice(0, values.length, ...ids);
},
});
};
render();
assert.equal(
flat(tree).filter(
(node) => node.type === 'input' && node.props.type === 'checkbox',
).length,
0,
);
flat(tree)
.find((node) => node.type === 'input')
.props.onChange({ target: { value: 'próprio' } });
render();
flat(tree)
.find((node) => node.type === 'input' && node.props.type === 'checkbox')
.props.onChange();
render();
assert.deepEqual(values, ['identity']);
flat(tree)
.find((node) => node.type === 'input' && node.props.type === 'search')
.props.onChange({ target: { value: 'Lei' } });
render();
assert.deepEqual(values, ['identity']);
assert.equal(
flat(tree).filter(
(node) => node.type === 'input' && node.props.type === 'checkbox',
)[0].props.checked,
false,
);
});
void test('document entry leads to existing fillable preparation and keeps sources optional', () => {
const context = '10000000-0000-4000-8000-000000000001';
const inertia = {
Link: ({ href, children }) =>
React.createElement('a', { href }, children),
usePage: () => ({ url: `/documentos?context=${context}` }),
};
const Hub = load(
'../../resources/js/components/journey-document-resources.tsx',
{
'@inertiajs/react': inertia,
'@/lib/fiscal-preparation-guide': { fiscalPreparationGuide },
},
'JourneyDocumentResources',
);
const hub = renderToStaticMarkup(React.createElement(Hub, {}));
assert.match(
hub,
new RegExp(
`/investimentos/${context}/contexto/editar#preparation-questions`,
),
);
assert.match(hub, /href="\/terra-duat"/);
assert.match(hub, /Preparação digital e exemplar oficial/);
assert.doesNotMatch(hub, /<details[^>]*\bopen\b/);
const Editor = load(
'../../resources/js/components/private-investment-preparation.tsx',
{
'@inertiajs/react': inertia,
'@/pages/investment/shared': { card: 'card', input: 'input' },
},
'PreparationEditor',
);
const html = renderToStaticMarkup(
React.createElement(Editor, {
data: {
questions: [
{
key: 'investment_preparation',
label: 'Preparar pedido?',
options: { yes: 'Sim', no: 'Não' },
source: 'SYNTHETIC QA',
},
{
key: 'declared_amount',
label: 'Valor do investimento',
type: 'number',
source: 'SYNTHETIC QA',
},
],
requirements: [],
enabled: true,
mode: 'authorization',
missing_fields: ['declared_amount'],
},
fields: { investment_preparation: 'yes' },
changes: {},
documents: [],
links: {},
onChange() {},
onLink() {},
disabled: false,
}),
);
assert.match(html, /<input/);
assert.match(html, /Valor do investimento/);
assert.match(html, /id="preparation-questions"/);
});
void test('investment stages expose readable next actions while internal codes and sources remain consultable', () => {
const useForm = (data) => ({
data,
errors: {},
processing: false,
setData() {},
transform() {
return this;
},
post() {},
});
const Stage = load(
'../../resources/js/pages/investor-path/show.tsx',
{
'@inertiajs/react': {
useForm,
Link: ({ href, children }) =>
React.createElement('a', { href }, children),
},
'@/lib/investor-path': {
pathStates: { not_started: 'Por iniciar' },
},
},
'Stage',
'\nmodule.exports.Stage=Stage;',
);
const step = {
code: 'S01',
title: 'Sistematizar a ideia',
state: 'not_started',
detail: { scope: 'promoter_preparation' },
preparation_allowed: true,
action_gaps: {},
source_paragraph: 22,
applicability: 'applicable',
};
const html = renderToStaticMarkup(
React.createElement(Stage, {
step,
path: {
case: { id: 'synthetic', version: 1 },
next: { code: 'S01' },
canWrite: true,
canReview: false,
steps: [step],
documents: [],
},
}),
);
assert.match(html, /<details[^>]*id="S01"[^>]*open/);
assert.match(html, /<summary[^>]*>Sistematizar a ideia/);
assert.doesNotMatch(html, /<summary[^>]*>S01/);
assert.match(html, /Fonte e referência desta etapa/);
});