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

779 lines
25 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 = {}, globals = {}) {
const output = 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(output, [
'require',
'module',
'exports',
...Object.keys(globals),
])(
(name) => mocks[name] ?? require(name),
module,
module.exports,
...Object.values(globals),
);
return module.exports;
}
const lib = load('../../resources/js/lib/desk-checkpoints.ts');
class ValidationError extends Error {
constructor(message, fields = {}) {
super(message);
this.fields = fields;
}
}
function nodes(tree) {
if (Array.isArray(tree)) return tree.flatMap(nodes);
if (!tree || typeof tree !== 'object') return [];
return [tree, ...nodes(tree.props?.children)];
}
const request = {
id: 'synthetic-request',
version: 1,
status: 'draft',
payload: { title: 'Original', choices: ['0'], declaration: null },
editable_fields: [],
can_edit: true,
protocol: null,
analyst_id: null,
due_at: null,
};
const initialView = () => ({
installed: true,
request: structuredClone(request),
checkpoint: null,
});
function serverCheckpoint(body) {
return {
version: body.version + 1,
request_version: body.request_version,
updated_at: '2026-11-03T03:00:00Z',
payload: structuredClone(body.payload),
reason: body.reason,
base_payload: structuredClone(request.payload),
edited_keys: [...body.edited_keys],
};
}
function harness(handler) {
const cells = [],
timers = new Map(),
listeners = new Map(),
before = [];
let cursor = 0,
dirty = true,
tree,
props,
sequence = 0,
clock = 0;
const effects = [],
calls = [],
recovered = [],
blocked = [],
busy = [];
const react = {
useState(initial) {
const i = cursor++;
if (!cells[i])
cells[i] = {
value: typeof initial === 'function' ? initial() : initial,
};
return [
cells[i].value,
(value) => {
const next =
typeof value === 'function'
? value(cells[i].value)
: value;
if (!Object.is(next, cells[i].value)) {
cells[i].value = next;
dirty = true;
}
},
];
},
useRef(initial) {
const i = cursor++;
cells[i] ??= { current: initial };
return cells[i];
},
useEffect(effect, deps) {
const i = cursor++,
old = cells[i];
if (!old || deps.some((v, j) => !Object.is(v, old.deps[j]))) {
cells[i] = { deps, cleanup: old?.cleanup };
effects.push(() => {
cells[i].cleanup?.();
cells[i].cleanup = effect();
});
}
},
};
const Component = load(
'../../resources/js/components/desk-draft-recovery.tsx',
{
react,
'@inertiajs/react': {
router: {
on(name, callback) {
before.push(callback);
return () => {
before.splice(before.indexOf(callback), 1);
};
},
},
},
'@/lib/profile-json': {
ProfileValidationError: ValidationError,
async profileJson(url, body = {}, method = 'POST') {
calls.push({ url, body: structuredClone(body), method });
return handler(url, body, method);
},
},
'@/lib/operation-key': {
createOperationKey: () => `synthetic-operation-${++sequence}`,
},
'@/lib/desk-checkpoints': lib,
},
{
window: {
addEventListener: (name, fn) => listeners.set(name, fn),
removeEventListener: (name) => listeners.delete(name),
confirm: () => false,
},
setTimeout: (fn) => {
const id = ++clock;
timers.set(id, fn);
return id;
},
clearTimeout: (id) => timers.delete(id),
},
).default;
props = {
id: request.id,
version: 1,
initial: structuredClone(request.payload),
payload: structuredClone(request.payload),
reason: '',
fields: {
title: { label: 'Title' },
choices: { label: 'Choices' },
declaration: { label: 'Declaration' },
},
paused: false,
onBusy: (value) => busy.push(value),
onBlocked: (value) => blocked.push(value),
onRecover: (payload, reason, current) => {
recovered.push({ payload, reason, current });
props = { ...props, payload, reason, version: current.version };
dirty = true;
},
};
function render(patch = {}) {
props = { ...props, ...patch };
dirty = true;
flush();
return tree;
}
function flush() {
let loops = 0;
while (dirty || effects.length) {
if (++loops > 25) throw Error('Effect loop');
if (dirty) {
dirty = false;
cursor = 0;
tree = Component(props);
}
while (effects.length) effects.shift()();
}
return tree;
}
async function settle() {
for (let i = 0; i < 10; i++) {
await Promise.resolve();
flush();
}
return tree;
}
return {
render,
settle,
calls,
recovered,
blocked,
busy,
listeners,
before,
timers,
get tree() {
return tree;
},
get props() {
return props;
},
async advance() {
for (const [id, fn] of timers) {
timers.delete(id);
fn();
}
await settle();
},
button(label) {
const node = nodes(tree).find(
(n) => n.type === 'button' && n.props.children === label,
);
assert.ok(node, `Button missing: ${label}`);
return node;
},
unmount() {
for (const cell of cells) cell?.cleanup?.();
},
};
}
await test('recovery preserves typed choices, explicit zero and deliberate reverts; concurrent answers require a choice', () => {
const base = {
title: 'Original',
amount: 0,
choices: ['0'],
declaration: 'declined',
};
const saved = {
base_payload: base,
payload: { ...base, title: 'Saved', choices: ['cotton', '0'] },
edited_keys: ['title', 'choices', 'declaration'],
};
const current = { ...base, title: 'External', declaration: 'accepted' };
const result = lib.checkpointRecovery(
base,
base,
[],
current,
saved,
Object.keys(base),
);
assert.deepEqual(result.conflicts, ['title', 'declaration']);
const chosen = lib.checkpointRecovery(
base,
base,
[],
current,
saved,
Object.keys(base),
{ title: 'current', declaration: 'saved' },
);
assert.deepEqual(chosen.conflicts, []);
assert.equal(chosen.payload.title, 'External');
assert.equal(chosen.payload.declaration, 'declined');
assert.equal(chosen.payload.amount, 0);
assert.deepEqual(chosen.payload.choices, ['cotton', '0']);
const returned = lib.checkpointRecovery(
base,
{ ...base, title: 'Forbidden' },
['title'],
current,
saved,
['amount'],
);
assert.equal(returned.payload.title, 'External');
assert.equal(
lib.checkpointSignature({ choices: ['0', 'cotton'] }),
lib.checkpointSignature({ choices: ['cotton', '0'] }),
);
});
await test('opportunity and its preserved context are recovered as one choice without displaying technical snapshots', () => {
const base = {
context_opportunity: 'old',
context_opportunity_snapshot: JSON.stringify({
choice: { opportunity: { title: 'Old opportunity' } },
revision: 1,
}),
context_reuse: 'old metadata',
};
const page = {
context_opportunity: 'local',
context_opportunity_snapshot: JSON.stringify({
choice: { opportunity: { title: 'Local opportunity' } },
revision: 2,
}),
context_reuse: 'local metadata',
};
const current = {
context_opportunity: 'remote',
context_opportunity_snapshot: JSON.stringify({
choice: { opportunity: { title: 'Remote opportunity' } },
revision: 3,
}),
context_reuse: 'remote metadata',
};
const result = lib.checkpointRecovery(
base,
page,
Object.keys(page),
current,
null,
Object.keys(page),
{ context_opportunity: 'page' },
);
assert.deepEqual(result.payload, page);
assert.equal(result.rows.length, 1);
assert.equal(result.rows[0].page, 'Local opportunity · revisão 2');
assert.deepEqual(result.intent, Object.keys(page));
assert.ok(!JSON.stringify(result.rows).includes('local metadata'));
});
await test('autosave is debounced, captures edits during a pending write and never sends submission or consent flags', async () => {
let view = initialView(),
resolveFirst;
const h = harness(async (url, body, method) => {
if (method === 'GET') return structuredClone(view);
if (!resolveFirst)
return new Promise((resolve) => {
resolveFirst = () => {
view.checkpoint = serverCheckpoint(body);
resolve(structuredClone(view));
};
});
view.checkpoint = serverCheckpoint(body);
return structuredClone(view);
});
h.render();
await h.settle();
assert.equal(h.timers.size, 0);
h.render({ payload: { ...request.payload, title: 'First' } });
assert.equal(h.timers.size, 1);
await h.advance();
assert.equal(h.calls.filter((c) => c.method === 'POST').length, 1);
h.render({ payload: { ...request.payload, title: 'Newer while saving' } });
assert.equal(h.timers.size, 0);
resolveFirst();
await h.settle();
assert.equal(h.timers.size, 1);
await h.advance();
const posts = h.calls.filter((c) => c.method === 'POST');
assert.equal(posts.length, 2);
assert.equal(posts[1].body.version, 1);
assert.equal(posts[1].body.payload.title, 'Newer while saving');
assert.equal(Object.hasOwn(posts[0].body, 'submit'), false);
assert.equal(Object.hasOwn(posts[0].body, 'consent'), false);
assert.equal(Object.hasOwn(posts[0].body, 'context_reviewed'), false);
assert.equal(h.timers.size, 0);
assert.deepEqual(h.busy, [true, false, true, false]);
h.unmount();
});
await test('ambiguous network failure repeats the exact attempt before saving newer local answers', async () => {
let view = initialView(),
failed = false;
const h = harness(async (url, body, method) => {
if (method === 'GET') return structuredClone(view);
if (!failed) {
failed = true;
view.checkpoint = serverCheckpoint(body);
throw Error('SYNTHETIC connection lost after commit');
}
if (body.version === 0) return structuredClone(view);
view.checkpoint = serverCheckpoint(body);
return structuredClone(view);
});
h.render();
await h.settle();
h.render({
payload: { ...request.payload, title: 'Committed but unacknowledged' },
});
await h.advance();
h.render({ payload: { ...request.payload, title: 'Newer' } });
assert.equal(h.timers.size, 0);
h.button('Tentar novamente a mesma gravação').props.onClick();
await h.settle();
let posts = h.calls.filter((c) => c.method === 'POST');
assert.deepEqual(posts[0].body, posts[1].body);
assert.equal(h.props.payload.title, 'Newer');
await h.advance();
posts = h.calls.filter((c) => c.method === 'POST');
assert.equal(posts[2].body.version, 1);
assert.notEqual(posts[2].body.operation_key, posts[1].body.operation_key);
h.unmount();
});
await test('server normalization does not cause autosave loops and validation failures wait for an actual edit', async () => {
let view = initialView(),
invalid = true;
const h = harness(async (url, body, method) => {
if (method === 'GET') return structuredClone(view);
if (invalid)
throw new ValidationError('SYNTHETIC invalid type', {
'checkpoint.payload.title': 'Invalid',
});
view.checkpoint = serverCheckpoint(body);
view.checkpoint.payload.title = body.payload.title.trim() || null;
return structuredClone(view);
});
h.render();
await h.settle();
h.render({ payload: { ...request.payload, title: ' text ' } });
await h.advance();
await h.settle();
assert.equal(h.timers.size, 0);
invalid = false;
h.render({ payload: { ...request.payload, title: ' corrected ' } });
await h.settle();
assert.equal(h.timers.size, 1);
await h.advance();
assert.equal(h.timers.size, 0);
assert.equal(h.calls.filter((c) => c.method === 'POST').length, 2);
h.unmount();
});
await test('stored drafts are compared before adoption and a failed recovery is retried without changing its chosen answers', async () => {
let view = initialView(),
failed = false;
view.request.version = 2;
view.request.payload.title = 'External';
view.checkpoint = {
...serverCheckpoint({
version: 0,
request_version: 1,
payload: {
...request.payload,
title: 'Saved',
declaration: 'declined',
},
reason: 'Saved reason',
edited_keys: ['title', 'declaration'],
}),
base_payload: structuredClone(request.payload),
};
const h = harness(async (url, body, method) => {
if (method === 'GET') return structuredClone(view);
view.checkpoint = serverCheckpoint(body);
if (!failed) {
failed = true;
throw Error('SYNTHETIC recovery response lost');
}
return structuredClone(view);
});
h.render();
await h.settle();
assert.equal(h.recovered.length, 0);
assert.equal(
h.button('Recuperar as respostas escolhidas').props.disabled,
true,
);
const select = nodes(h.tree).find(
(n) => n.type === 'select' && n.props.value === '',
);
select.props.onChange({ target: { value: 'saved' } });
await h.settle();
assert.equal(
h.button('Recuperar as respostas escolhidas').props.disabled,
false,
);
h.button('Recuperar as respostas escolhidas').props.onClick();
await h.settle();
assert.equal(h.recovered.length, 0);
h.button('Recuperar as respostas escolhidas').props.onClick();
await h.settle();
assert.equal(h.recovered.length, 1);
assert.equal(h.recovered[0].current.version, 2);
assert.equal(h.recovered[0].payload.title, 'Saved');
assert.equal(h.recovered[0].payload.declaration, 'declined');
const posts = h.calls.filter((c) => c.method === 'POST');
assert.deepEqual(posts[0].body, posts[1].body);
assert.equal(h.blocked.at(-1), false);
h.unmount();
});
await test('manual operations pause autosave, unavailable installation leaves manual access, and unsaved navigation can be cancelled', async () => {
let view = initialView();
const h = harness(async (url, body, method) => {
if (method === 'GET') return structuredClone(view);
view.checkpoint = serverCheckpoint(body);
return structuredClone(view);
});
h.render();
await h.settle();
h.render({
payload: { ...request.payload, title: 'Unsaved' },
paused: true,
});
assert.equal(h.timers.size, 0);
let prevented = false;
h.before[0]({
detail: { visit: { method: 'get' } },
preventDefault() {
prevented = true;
},
});
assert.equal(prevented, true);
h.render({ paused: false });
await h.advance();
assert.equal(h.timers.size, 0);
h.unmount();
assert.equal(h.before.length, 0);
assert.equal(h.listeners.size, 0);
view.installed = false;
const fallback = harness(async () => structuredClone(view));
fallback.render();
await fallback.settle();
fallback.render({ payload: { title: 'Manual still available' } });
assert.equal(fallback.timers.size, 0);
assert.ok(!fallback.blocked.includes(true));
fallback.unmount();
});
await test('request recovery adopts the actual server version and returned scope while clearing submission and context approvals', () => {
const cells = [],
forms = [],
sent = [];
let stateCursor = 0,
formCursor = 0;
const Empty = () => null,
Recovery = () => null,
Attachment = () => null;
const Page = load('../../resources/js/pages/desk/request.tsx', {
react: {
useState(initial) {
const i = stateCursor++;
if (!(i in cells)) cells[i] = initial;
return [
cells[i],
(value) =>
(cells[i] =
typeof value === 'function'
? value(cells[i])
: value),
];
},
},
'@inertiajs/react': {
Link: 'a',
useForm(initial) {
const i = formCursor++;
forms[i] ??= {
data: structuredClone(initial),
errors: {},
processing: false,
setData(value, next) {
this.data =
typeof value === 'function'
? value(this.data)
: { ...this.data, [value]: next };
},
transform(fn) {
this.mapping = fn;
},
put(url) {
sent.push({
url,
data: this.mapping
? this.mapping(this.data)
: this.data,
});
},
};
return forms[i];
},
},
'@/components/desk-draft-recovery': {
__esModule: true,
default: Recovery,
},
'@/components/dossier-prefill': { DossierPrefill: Empty },
'@/components/country-select': { default: Empty },
'@/components/investment-territory': { TerritoryFields: Empty },
'@/components/desk-internal-operations': {
DocumentReview: Empty,
RequestOperations: Empty,
},
'@/components/desk-typed-choice': { default: Empty },
'@/components/desk-attachment-field': { default: Attachment },
'@/lib/operation-key': { createOperationKey: () => 'synthetic' },
'@/lib/desk-field-visibility': load(
'../../resources/js/lib/desk-field-visibility.ts',
),
'@/lib/desk-field-values': load(
'../../resources/js/lib/desk-field-values.ts',
),
'@/pages/investment/shared': {
Frame: 'main',
card: '',
input: '',
button: '',
},
}).default;
const props = {
request: { ...request, analyst_id: null, due_at: null },
case: { id: 'synthetic-case', title: 'Synthetic' },
definition: {
title: 'Synthetic',
result: 'Internal',
fields: {
title: { type: 'text', label: 'Title', required: true },
support: {
type: 'attachment',
label: 'Support',
required: false,
max_files: 2,
max_size_kb: 2,
mime_types: ['text/plain'],
},
choices: {
type: 'multiselect',
label: 'Choices',
required: false,
options: { 0: 'Zero' },
},
declaration: {
type: 'consent',
label: 'Declaration',
required: false,
statement: 'Synthetic',
},
},
},
owner: true,
manager: false,
analyst: false,
fieldsEditable: true,
states: { draft: 'Draft', returned: 'Returned' },
transitions: [],
findings: [],
messages: [],
analysts: [],
territoryPaths: {},
events: [],
documents: [],
payments: [],
caseVersion: 1,
countries: [],
toolPrefill: null,
};
const render = () => {
stateCursor = 0;
formCursor = 0;
return Page(props);
};
let tree = render();
forms[0].data.payload.title = 'Deliberate edit during upload';
nodes(tree)
.find((n) => n.type === Attachment)
.props.onBusy(true);
tree = render();
assert.equal(
nodes(tree).find((n) => n.type === Recovery).props.pendingWork,
true,
);
nodes(tree)
.find((n) => n.type === 'form')
.props.onSubmit({ preventDefault() {} });
assert.equal(sent.length, 0);
const uploaded = {
id: 'link-first',
document_id: 'binary-one',
file_name: 'Synthetic original',
status: 'quarantined',
malware_scan_status: 'pending',
};
nodes(tree)
.find((n) => n.type === Attachment)
.props.onInventory(
{ documents: [uploaded], case_version: 2, link_id: uploaded.id },
true,
);
nodes(tree)
.find((n) => n.type === Attachment)
.props.onBusy(false);
tree = render();
assert.equal(forms[0].data.payload.title, 'Deliberate edit during upload');
assert.deepEqual(forms[0].data.payload.support, ['link-first']);
const alias = { ...uploaded, id: 'link-alias' };
nodes(tree)
.find((n) => n.type === Attachment)
.props.onInventory(
{
documents: [uploaded, alias],
case_version: 2,
link_id: alias.id,
},
true,
);
tree = render();
assert.deepEqual(forms[0].data.payload.support, ['link-first']);
props.caseVersion = 2;
props.documents = [
{ ...uploaded, status: 'active', malware_scan_status: 'clean' },
];
tree = render();
assert.equal(
nodes(tree).find((n) => n.type === Attachment).props.documents[0]
.malware_scan_status,
'clean',
);
const refreshed = {
...uploaded,
malware_scan_status: 'synthetic-new-status',
};
nodes(tree)
.find((n) => n.type === Attachment)
.props.onInventory({ documents: [refreshed], case_version: 2 }, false);
tree = render();
assert.equal(
nodes(tree).find((n) => n.type === Attachment).props.documents[0]
.malware_scan_status,
'synthetic-new-status',
);
forms[0].data.consent = true;
forms[0].data.context_reviewed = true;
forms[0].data.context_revision = 8;
const restore = nodes(tree).find((n) => n.type === Recovery);
restore.props.onRecover(
{
title: 'Current official draft',
choices: ['0'],
declaration: 'declined',
},
'Recovered reason',
{
...request,
version: 3,
status: 'returned',
editable_fields: ['choices'],
},
);
tree = render();
assert.equal(forms[0].data.version, 3);
assert.equal(forms[0].data.consent, false);
assert.equal(forms[0].data.context_reviewed, false);
assert.equal(forms[0].data.context_revision, 0);
assert.equal(forms[0].data.payload.declaration, 'declined');
const form = nodes(tree).find((n) => n.type === 'form');
form.props.onSubmit({ preventDefault() {} });
assert.equal(sent[0].data.version, 3);
assert.equal(sent[0].data.submit, false);
nodes(tree)
.find((n) => n.type === Recovery)
.props.onBusy(true);
tree = render();
nodes(tree)
.find((n) => n.type === 'form')
.props.onSubmit({ preventDefault() {} });
assert.equal(sent.length, 1);
});