279 lines
8.9 KiB
JavaScript
279 lines
8.9 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) {
|
|
const code = ts.transpileModule(
|
|
fs.readFileSync(new URL(file, import.meta.url), 'utf8'),
|
|
{
|
|
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.default;
|
|
}
|
|
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)
|
|
: '';
|
|
function harness(file, initial, extra = {}) {
|
|
const forms = [],
|
|
cells = [],
|
|
calls = [];
|
|
let formCursor = 0,
|
|
cursor = 0,
|
|
serial = 0,
|
|
tree,
|
|
props = initial;
|
|
const Empty = () => null;
|
|
const Component = load(file, {
|
|
react: {
|
|
useState(initial) {
|
|
const i = cursor++;
|
|
if (!(i in cells)) cells[i] = initial;
|
|
return [
|
|
cells[i],
|
|
(value) => {
|
|
cells[i] = value;
|
|
},
|
|
];
|
|
},
|
|
useRef(initial) {
|
|
const i = cursor++;
|
|
cells[i] ??= { current: initial };
|
|
return cells[i];
|
|
},
|
|
},
|
|
'@inertiajs/react': {
|
|
Link: 'a',
|
|
router: { reload() {} },
|
|
usePage: () => ({
|
|
url: '/synthetic',
|
|
props: { auth: { user: { id: 1 } } },
|
|
}),
|
|
useForm(initial) {
|
|
const i = formCursor++;
|
|
forms[i] ??= {
|
|
data: { ...initial },
|
|
errors: {},
|
|
processing: false,
|
|
setData(key, value) {
|
|
this.data[key] = value;
|
|
},
|
|
reset(...keys) {
|
|
for (const key of keys) this.data[key] = initial[key];
|
|
},
|
|
put(url, options) {
|
|
calls.push({ url, options, data: { ...this.data } });
|
|
},
|
|
};
|
|
return forms[i];
|
|
},
|
|
},
|
|
'@/lib/operation-key': {
|
|
createOperationKey: () => `SYNTHETIC-${++serial}`,
|
|
},
|
|
'@/pages/investment/shared': {
|
|
Frame: 'main',
|
|
card: '',
|
|
input: '',
|
|
button: '',
|
|
},
|
|
'@/components/investor-guidance': { InvestorGuidance: Empty },
|
|
'@/components/desk-internal-operations': { DocumentReview: Empty },
|
|
'@/components/desk-document-picker': { default: Empty },
|
|
...extra,
|
|
});
|
|
function render(patch = {}) {
|
|
props = { ...props, ...patch };
|
|
cursor = 0;
|
|
formCursor = 0;
|
|
tree = Component(props);
|
|
}
|
|
render();
|
|
return {
|
|
forms,
|
|
calls,
|
|
render,
|
|
all: () => nodes(tree),
|
|
text: () => text(tree),
|
|
find: (type, label) =>
|
|
nodes(tree).find(
|
|
(n) => n.type === type && (!label || text(n).includes(label)),
|
|
),
|
|
submit() {
|
|
nodes(tree)
|
|
.find((n) => n.type === 'form')
|
|
.props.onSubmit({ preventDefault() {} });
|
|
},
|
|
};
|
|
}
|
|
const dossier = { id: 'case-one', version: 1, closed_at: null };
|
|
const activity = { open_requests: 2, open_tasks: 1, pending_payments: 3 };
|
|
const state = (extra) =>
|
|
harness('../../resources/js/components/desk-case-state-form.tsx', {
|
|
dossier,
|
|
activity,
|
|
...extra,
|
|
});
|
|
|
|
test('owner can explicitly close with recorded pending items and sends only state decision data', () => {
|
|
const h = state();
|
|
h.find('textarea').props.onChange({
|
|
target: { value: 'SYNTHETIC owner reason' },
|
|
});
|
|
h.render();
|
|
h.submit();
|
|
assert.match(h.calls[0].url, /dossiers\/case-one\/estado$/);
|
|
assert.deepEqual(
|
|
Object.keys(h.calls[0].data).sort(),
|
|
['version', 'state', 'reason', 'client_key'].sort(),
|
|
);
|
|
assert.equal(h.calls[0].data.state, 'closed');
|
|
assert.equal(h.calls[0].data.version, 1);
|
|
assert.match(
|
|
h.text(),
|
|
/Pedidos em curso: 2.*tarefas em curso: 1.*comprovativos por conferir: 3/,
|
|
);
|
|
});
|
|
|
|
test('success adopts actual server state and version for reopening and calls parent cleanup only for the same dossier', () => {
|
|
let changed = 0;
|
|
const h = state({
|
|
onChanged() {
|
|
changed++;
|
|
},
|
|
});
|
|
h.find('textarea').props.onChange({
|
|
target: { value: 'SYNTHETIC close reason' },
|
|
});
|
|
h.render();
|
|
h.submit();
|
|
const key = h.forms[0].data.client_key;
|
|
h.calls[0].options.onSuccess({
|
|
props: { case: { ...dossier, version: 2, closed_at: '2026-11-03' } },
|
|
});
|
|
h.render({ dossier: { ...dossier, version: 2, closed_at: '2026-11-03' } });
|
|
assert.equal(h.forms[0].data.state, 'open');
|
|
assert.equal(h.forms[0].data.version, 2);
|
|
assert.equal(h.forms[0].data.reason, '');
|
|
assert.notEqual(h.forms[0].data.client_key, key);
|
|
assert.equal(changed, 1);
|
|
h.calls[0].options.onSuccess({
|
|
props: { case: { id: 'other-case', version: 9 } },
|
|
});
|
|
assert.equal(changed, 1);
|
|
assert.ok(h.find('button', 'Confirmar reabertura'));
|
|
});
|
|
|
|
test('failed retry keeps operation identity and reason while editing or explicitly adopting new state starts a new intent', () => {
|
|
const h = state();
|
|
h.find('textarea').props.onChange({
|
|
target: { value: 'SYNTHETIC deliberate reason' },
|
|
});
|
|
h.render();
|
|
h.submit();
|
|
h.submit();
|
|
assert.deepEqual(h.calls[0].data, h.calls[1].data);
|
|
const key = h.forms[0].data.client_key;
|
|
h.render({ dossier: { ...dossier, version: 3, closed_at: '2026-11-03' } });
|
|
assert.equal(h.find('button', 'Confirmar reabertura').props.disabled, true);
|
|
h.submit();
|
|
assert.equal(h.calls.length, 2);
|
|
h.find('button', 'Usar estado actual').props.onClick();
|
|
h.render();
|
|
assert.equal(h.forms[0].data.version, 3);
|
|
assert.equal(h.forms[0].data.state, 'open');
|
|
assert.equal(h.forms[0].data.reason, 'SYNTHETIC deliberate reason');
|
|
assert.notEqual(h.forms[0].data.client_key, key);
|
|
});
|
|
|
|
test('unknown optional counts remain unknown and in-flight business work disables the decision', () => {
|
|
const h = state({
|
|
activity: {
|
|
open_requests: 0,
|
|
open_tasks: null,
|
|
pending_payments: null,
|
|
},
|
|
disabled: true,
|
|
});
|
|
assert.match(
|
|
h.text(),
|
|
/Pedidos em curso: 0.*tarefas em curso: por confirmar.*comprovativos por conferir: por confirmar/,
|
|
);
|
|
assert.equal(
|
|
h.find('button', 'Confirmar encerramento').props.disabled,
|
|
true,
|
|
);
|
|
h.submit();
|
|
assert.equal(h.calls.length, 0);
|
|
});
|
|
|
|
test('case exposes owner control separately from ordinary writing and clears unassociated file consent after success', () => {
|
|
const State = () => null;
|
|
const base = {
|
|
case: {
|
|
...dossier,
|
|
closed_at: '2026-11-03',
|
|
title: 'SYNTHETIC owner dossier',
|
|
workflow: {},
|
|
},
|
|
canWrite: false,
|
|
canChangeState: true,
|
|
activity,
|
|
manager: false,
|
|
requests: [],
|
|
services: [],
|
|
documents: [],
|
|
workflow: {},
|
|
stepStates: {},
|
|
states: {},
|
|
guidance: null,
|
|
events: [],
|
|
links: [],
|
|
legacyRequest: null,
|
|
};
|
|
const h = harness('../../resources/js/pages/desk/case.tsx', base, {
|
|
'@/components/desk-case-state-form': { default: State },
|
|
});
|
|
let widget = h.all().find((n) => n.type === State);
|
|
assert.ok(widget);
|
|
h.forms[2].setData('file', { name: 'SYNTHETIC unsent file' });
|
|
h.forms[2].setData('consent', true);
|
|
widget.props.onChanged();
|
|
assert.equal(h.forms[2].data.file, null);
|
|
assert.equal(h.forms[2].data.consent, false);
|
|
h.forms[2].processing = true;
|
|
h.render();
|
|
widget = h.all().find((n) => n.type === State);
|
|
assert.equal(widget.props.disabled, true);
|
|
h.render({ canChangeState: false, canWrite: true, manager: true });
|
|
assert.equal(
|
|
h.all().some((n) => n.type === State),
|
|
false,
|
|
);
|
|
});
|