Files
Investidor-V2/tests/frontend/desk-closed-lifecycle.test.mjs
T

454 lines
15 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import { compileFunction } from 'node:vm';
import ts from 'typescript';
const require = createRequire(import.meta.url);
function load(file, mocks = {}, suffix = '') {
const code = ts.transpileModule(
fs.readFileSync(new URL(file, import.meta.url), 'utf8') + suffix,
{
compilerOptions: {
module: ts.ModuleKind.CommonJS,
jsx: ts.JsxEmit.ReactJSX,
target: ts.ScriptTarget.ES2022,
},
},
).outputText;
const module = { exports: {} };
compileFunction(code, ['require', 'module', 'exports'])(
(name) => mocks[name] ?? require(name),
module,
module.exports,
);
return module.exports;
}
function nodes(tree) {
if (Array.isArray(tree)) return tree.flatMap(nodes);
if (!tree || typeof tree !== 'object') return [];
return [tree, ...nodes(tree.props?.children)];
}
const text = (tree) =>
typeof tree === 'string'
? tree
: Array.isArray(tree)
? tree.map(text).join('')
: tree?.props
? text(tree.props.children)
: typeof tree === 'number'
? String(tree)
: '';
const Empty = () => null;
function harness(file, initial, extra = {}, suffix = '') {
const cells = [],
forms = [],
sent = [];
let cursor = 0,
formCursor = 0,
props = initial,
tree;
const mocks = {
react: {
useState(initial) {
const i = cursor++;
if (!(i in cells)) cells[i] = initial;
return [
cells[i],
(value) => {
cells[i] =
typeof value === 'function'
? value(cells[i])
: value;
},
];
},
useRef(initial) {
const i = cursor++;
cells[i] ??= { current: initial };
return cells[i];
},
},
'@inertiajs/react': {
Link: 'a',
router: { reload() {}, post() {} },
usePage: () => ({
url: '/synthetic',
props: { auth: { user: { id: 1 } } },
}),
useForm(initial) {
const i = formCursor++;
forms[i] ??= {
data: structuredClone(initial),
errors: {},
processing: false,
setData(key, value) {
if (typeof key === 'function')
this.data = key(this.data);
else if (typeof key === 'object') this.data = key;
else this.data[key] = value;
},
clearErrors() {},
setDefaults() {},
transform(callback) {
this.transformer = callback;
},
post(url, options) {
sent.push({
url,
options,
data: this.transformer
? this.transformer(this.data)
: this.data,
});
},
patch(url, options) {
sent.push({ url, options, data: this.data });
},
};
return forms[i];
},
},
'@/pages/investment/shared': {
Frame: 'main',
card: '',
input: '',
button: '',
},
'@/lib/operation-key': {
createOperationKey: () => 'synthetic-operation',
},
'@/lib/desk-field-values': load(
'../../resources/js/lib/desk-field-values.ts',
),
'@/lib/desk-field-visibility': load(
'../../resources/js/lib/desk-field-visibility.ts',
),
'@/components/dossier-prefill': { DossierPrefill: Empty },
'@/components/desk-typed-choice': { default: Empty },
'@/components/desk-draft-recovery': { default: Empty },
'@/components/desk-attachment-field': { default: Empty },
'@/components/country-select': { default: Empty },
'@/components/investment-territory': { TerritoryFields: Empty },
'@/components/desk-internal-operations': {
DocumentReview: Empty,
RequestOperations: Empty,
},
'@/components/investor-guidance': { InvestorGuidance: Empty },
'@/components/desk-case-state-form': { default: Empty },
'@/components/desk-document-picker': { default: Empty },
...extra,
};
mocks['@/lib/crm-record-edit'] = load(
'../../resources/js/lib/crm-record-edit.ts',
mocks,
);
mocks['@/components/crm-edit-comparison'] = { default: Empty };
mocks['@/components/crm-task-creation'] = { default: Empty };
const exports = load(file, mocks, suffix);
const Component =
exports.TaskRow ?? exports.default ?? exports.InvestorGuidance;
function render(patch = {}) {
props = { ...props, ...patch };
cursor = 0;
formCursor = 0;
tree = Component(props);
}
render();
return {
forms,
sent,
render,
all: () => nodes(tree),
text: () => text(tree),
find: (type, label) =>
nodes(tree).find(
(n) => n.type === type && (!label || text(n).includes(label)),
),
};
}
const request = {
id: 'request-one',
version: 1,
status: 'draft',
payload: { title: 'SYNTHETIC saved title' },
protocol: null,
editable_fields: [],
analyst_id: null,
due_at: null,
};
const props = {
request,
case: { id: 'case-one', title: 'SYNTHETIC dossier' },
definition: {
title: 'SYNTHETIC service',
fields: {
title: { label: 'Project title', type: 'text', required: true },
},
result: 'SYNTHETIC',
},
owner: true,
manager: false,
analyst: false,
fieldsEditable: true,
operationsEditable: true,
states: {
draft: 'Rascunho',
submitted: 'Submetido',
reviewing: 'Em análise',
},
transitions: [],
findings: [],
messages: [],
analysts: [],
territoryPaths: {},
events: [],
documents: [],
payments: [],
caseVersion: 1,
countries: [],
toolPrefill: null,
};
const page = (patch) =>
harness('../../resources/js/pages/desk/request.tsx', {
...props,
...patch,
});
test('owner cancellation uses the existing transition and restores saved server payload only after success', () => {
const h = page();
h.forms[0].setData('payload', { title: 'SYNTHETIC unsaved edit' });
h.forms[1].setData('reason', 'SYNTHETIC deliberate cancellation');
h.find('form', 'Cancelar este pedido').props.onSubmit({
preventDefault() {},
});
assert.equal(h.sent[0].data.status, 'cancelled');
assert.equal(h.sent[0].data.version, 1);
assert.equal(h.sent[0].data.reason, 'SYNTHETIC deliberate cancellation');
assert.equal(h.forms[0].data.payload.title, 'SYNTHETIC unsaved edit');
h.sent[0].options.onSuccess({
props: { request: { ...request, status: 'cancelled', version: 2 } },
});
h.render({
request: { ...request, status: 'cancelled', version: 2 },
fieldsEditable: false,
operationsEditable: false,
});
assert.equal(h.forms[0].data.payload.title, 'SYNTHETIC saved title');
assert.equal(h.forms[0].data.consent, false);
assert.equal(h.find('form', 'Cancelar este pedido'), undefined);
});
test('cancellation waits for recovery work and is unavailable to submitted requests, reviewers and closed dossiers', () => {
const Recovery = () => null;
const h = harness('../../resources/js/pages/desk/request.tsx', props, {
'@/components/desk-draft-recovery': { default: Recovery },
});
h.all()
.find((n) => n.type === Recovery)
.props.onBusy(true);
h.render();
assert.equal(
h.find('button', 'Confirmar cancelamento').props.disabled,
true,
);
h.find('form', 'Cancelar este pedido').props.onSubmit({
preventDefault() {},
});
assert.equal(h.sent.length, 0);
for (const patch of [
{ request: { ...request, status: 'submitted' } },
{ owner: false, manager: true },
{ operationsEditable: false },
])
assert.equal(
page(patch).find('form', 'Cancelar este pedido'),
undefined,
);
});
test('closed dossier request hides response, assignment and message actions while keeping previous messages visible', () => {
const open = {
request: { ...request, status: 'submitted' },
owner: false,
manager: true,
fieldsEditable: false,
transitions: ['reviewing'],
analysts: [{ id: 2, name: 'SYNTHETIC analyst' }],
messages: [
{
id: 'message-one',
body: 'SYNTHETIC historical message',
name: 'SYNTHETIC author',
kind: 'responsible_response',
created_at: '2026-11-03',
},
],
};
const h = page({ ...open, operationsEditable: false });
assert.match(h.text(), /SYNTHETIC historical message/);
assert.equal(h.find('button', 'Atribuir acompanhamento'), undefined);
assert.equal(
h.find('form', 'Resposta / análise do responsável'),
undefined,
);
assert.equal(h.find('form', 'Mensagem privada'), undefined);
assert.ok(page(open).find('form', 'Resposta / análise do responsável'));
assert.ok(page(open).find('form', 'Mensagem privada'));
});
test('correction explanation applies to returned requests and does not ask draft authors an inapplicable question', () => {
assert.doesNotMatch(page().text(), /Justificação de correcção/);
assert.match(
page({
request: {
...request,
status: 'returned',
editable_fields: ['title'],
},
}).text(),
/Justificação de correcção/,
);
});
test('closed case keeps workflow and documentary context without stage or review operations', () => {
const Review = () => null;
const base = {
case: {
id: 'case-one',
title: 'SYNTHETIC dossier',
version: 1,
closed_at: '2026-11-03',
workflow: {},
},
canWrite: false,
manager: true,
requests: [],
services: [],
documents: [],
workflow: {},
stepStates: {},
states: {},
guidance: null,
events: [],
links: [],
legacyRequest: null,
};
const h = harness('../../resources/js/pages/desk/case.tsx', base, {
'@/components/desk-internal-operations': { DocumentReview: Review },
});
assert.match(h.text(), /Dossier encerrado/);
assert.equal(h.find('button', 'Registar etapa'), undefined);
assert.equal(
h.all().some((n) => n.type === Review),
false,
);
h.render({ case: { ...base.case, closed_at: null } });
assert.ok(h.find('button', 'Registar etapa'));
assert.equal(
h.all().some((n) => n.type === Review),
true,
);
});
test('CRM linked task preserves its title, status and deadline without an update action when dossier is closed', () => {
const task = {
id: 'task-one',
title: 'SYNTHETIC linked task',
status: 'open',
due_at: '2026-11-05T12:00:00Z',
assignee_user_id: 1,
version_no: 1,
can_update: false,
};
const h = harness(
'../../resources/js/pages/crm/lead.tsx',
{ task, leadId: 'lead-one' },
{
'../management/shared': {
Management: 'main',
Field: 'label',
button: '',
control: '',
},
'./types': {
taskStates: { open: 'Aberta', done: 'Concluída' },
leadStates: {},
},
},
'\nexport { TaskRow };',
);
assert.match(h.text(), /SYNTHETIC linked task/);
assert.equal(h.find('select').props.disabled, true);
assert.equal(h.find('button', 'Actualizar tarefa'), undefined);
h.find('form').props.onSubmit({ preventDefault() {} });
assert.equal(h.sent.length, 0);
h.render({ task: { ...task, can_update: true } });
assert.ok(h.find('button', 'Actualizar tarefa'));
});
test('guidance hides task changes on a closed dossier but keeps task history and consulted sources', () => {
const guidance = {
version: 1,
fingerprint: 'x'.repeat(64),
rules_version: 'SYNTHETIC',
computed_at: '2026-11-03',
can_write: false,
can_update_tasks: false,
facts: {},
financial_draft: null,
consulted: [],
unavailable: [],
catalogue_checks: [],
phases: [
{
id: 'one',
objective: 'SYNTHETIC phase',
known: [],
questions: [],
evidence: [],
recommendations: [],
risks: [],
},
],
tasks: [
{
id: 'task-one',
title: 'SYNTHETIC saved task',
status: 'open',
due_at: '2026-11-05',
assignee_user_id: 1,
assignee_name: 'SYNTHETIC actor',
version_no: 1,
accepted_at: '2026-11-03',
desk_request_id: null,
},
],
calculator_versions: {},
crm: { reason: 'SYNTHETIC internal context' },
};
const h = harness(
'../../resources/js/components/investor-guidance.tsx',
{
caseId: 'case-one',
guidance,
manager: true,
actorId: 1,
requests: [],
},
{
'@/lib/investor-guidance-finance': {
guidanceFinance: () => ({
reason: 'SYNTHETIC no financial draft',
result: null,
}),
guidanceFinancialTools: () => null,
},
},
);
assert.match(h.text(), /SYNTHETIC saved task/);
assert.doesNotMatch(h.text(), /Resultado ou motivo/);
h.render({ guidance: { ...guidance, can_update_tasks: true } });
assert.match(h.text(), /Resultado ou motivo/);
});