361 lines
12 KiB
JavaScript
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/);
|
|
});
|