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

496 lines
16 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 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'])(
(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 = (node) =>
typeof node === 'string'
? node
: Array.isArray(node)
? node.map(text).join('')
: node?.props
? text(node.props.children)
: typeof node === 'number'
? String(node)
: '';
const document = {
id: 'source-one',
document_id: 'original-one',
file_name: 'SYNTHETIC source.txt',
version_no: 3,
source_reference: 'SYNTHETIC exact source',
category: 'project_evidence',
stage: 'E2',
valid_until: '2028-01-02',
origin_label: 'SYNTHETIC previous dossier',
status: 'active',
malware_scan_status: 'clean',
media_type: 'text/plain',
size_bytes: 100,
};
function harness(handler) {
const cells = [],
calls = [],
selections = [];
let cursor = 0,
props,
tree;
const Component = load(
'../../resources/js/components/desk-document-picker.tsx',
{
react: {
useId: () => 'synthetic-picker',
useRef(initial) {
const i = cursor++;
cells[i] ??= { current: initial };
return cells[i];
},
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;
},
];
},
},
'@/lib/profile-json': {
async profileJson(url, body, method) {
calls.push({ url, body, method });
return handler(url);
},
},
},
);
props = {
caseId: 'synthetic-case',
disabled: false,
selected: null,
onSelect: (doc) => {
selections.push(doc);
props.selected = doc;
},
};
const render = (updates) => {
props = { ...props, ...updates };
cursor = 0;
tree = Component(props);
return tree;
};
const find = (type, label) =>
nodes(tree).find(
(n) => n.type === type && (!label || text(n).includes(label)),
);
render();
return {
render,
find,
all: () => nodes(tree),
calls,
selections,
text: () => text(tree),
};
}
const settle = async () => {
await new Promise((resolve) => setImmediate(resolve));
};
test('search encodes literal input, returns names/provenance and never associates on search or selection', async () => {
const h = harness(async () => ({
documents: [document],
next_offset: null,
}));
h.find('input').props.onChange({
target: { value: '%_ source & dossier' },
});
h.render();
h.find('button', 'Pesquisar documentos').props.onClick();
await settle();
h.render();
assert.equal(h.calls.length, 1);
assert.equal(h.calls[0].method, 'GET');
const params = new URL(h.calls[0].url, 'http://localhost').searchParams;
assert.equal(params.get('q'), '%_ source & dossier');
assert.equal(params.get('offset'), '0');
assert.match(
h.text(),
/SYNTHETIC source\.txt.*versão 3.*SYNTHETIC previous dossier.*exact source.*2028-01-02/,
);
h.all()
.find((n) => n.type === 'input' && n.props.type === 'radio')
.props.onChange();
h.render();
assert.deepEqual(h.selections, [document]);
assert.equal(h.calls.length, 1);
assert.match(h.text(), /Origem seleccionada/);
h.find('button', 'Retirar origem seleccionada').props.onClick();
assert.equal(h.selections.at(-1), null);
});
test('pagination does not erase a selected source and new searches start at zero', async () => {
const h = harness(async (url) => ({
documents: [document],
next_offset: url.includes('offset=0') ? 25 : null,
}));
h.render({ selected: document });
h.find('button', 'Pesquisar documentos').props.onClick();
await settle();
h.render();
h.find('button', 'Próxima página').props.onClick();
await settle();
h.render();
assert.equal(
new URL(h.calls.at(-1).url, 'http://localhost').searchParams.get(
'offset',
),
'25',
);
assert.match(h.text(), /Origem seleccionada/);
h.find('button', 'Pesquisar documentos').props.onClick();
await settle();
h.render();
assert.equal(
new URL(h.calls.at(-1).url, 'http://localhost').searchParams.get(
'offset',
),
'0',
);
assert.equal(h.selections.length, 0);
});
test('failed or malformed responses retain selected source and do not expose partial replacements', async () => {
let failed = true;
const h = harness(async () => {
if (failed) throw Error('SYNTHETIC denied');
return { documents: {}, next_offset: null };
});
h.render({ selected: document });
h.find('button', 'Pesquisar documentos').props.onClick();
await settle();
h.render();
assert.match(h.text(), /SYNTHETIC denied/);
assert.match(h.text(), /Origem seleccionada/);
failed = false;
h.find('button', 'Pesquisar documentos').props.onClick();
await settle();
h.render();
assert.match(h.text(), /selecção anterior foi conservada/);
assert.equal(h.selections.length, 0);
});
test('editing the search removes the old page cursor while retaining a deliberate document selection', async () => {
const h = harness(async () => ({ documents: [document], next_offset: 25 }));
h.render({ selected: document });
h.find('button', 'Pesquisar documentos').props.onClick();
await settle();
h.render();
assert.ok(h.find('button', 'Próxima página'));
h.find('input').props.onChange({ target: { value: 'different source' } });
h.render();
assert.equal(h.find('button', 'Próxima página'), undefined);
assert.match(h.text(), /Pesquisa alterada/);
assert.match(h.text(), /Origem seleccionada/);
assert.equal(h.selections.length, 0);
});
test('search pending prevents duplicate requests and stale dossier responses are ignored', async () => {
let resolve;
const h = harness(
() =>
new Promise((r) => {
resolve = r;
}),
);
h.find('button', 'Pesquisar documentos').props.onClick();
h.render();
assert.equal(h.find('button', 'Pesquisar documentos').props.disabled, true);
h.find('button', 'Pesquisar documentos').props.onClick();
assert.equal(h.calls.length, 1);
h.render({ caseId: 'different-case' });
resolve({ documents: [document], next_offset: null });
await settle();
h.render();
assert.equal(
h.all().filter((n) => n.type === 'input' && n.props.type === 'radio')
.length,
0,
);
h.render({ disabled: true });
h.find('button', 'Pesquisar documentos').props.onClick();
assert.equal(h.calls.length, 1);
});
test('Enter searches without submitting the surrounding form and an unlinked upload remains selectable', async () => {
const h = harness(async () => ({
documents: [
{
...document,
id: null,
origin_label: null,
source_reference: null,
},
],
next_offset: null,
}));
let prevented = false;
h.find('input').props.onKeyDown({
key: 'Enter',
preventDefault() {
prevented = true;
},
});
await settle();
h.render();
assert.equal(prevented, true);
assert.match(h.text(), /Ficheiro enviado por si.*Referência por completar/);
h.all()
.find((n) => n.type === 'input' && n.props.type === 'radio')
.props.onChange();
assert.equal(h.selections[0].id, null);
assert.equal(h.selections[0].document_id, document.document_id);
assert.ok(
h
.all()
.filter((n) => n.type === 'button')
.every((n) => n.props.type === 'button'),
);
});
test('general and question consumers send chosen source identity without asking for UUIDs or rewriting known source', () => {
const general = fs.readFileSync(
new URL('../../resources/js/pages/desk/case.tsx', import.meta.url),
'utf8',
);
const field = fs.readFileSync(
new URL(
'../../resources/js/components/desk-attachment-field.tsx',
import.meta.url,
),
'utf8',
);
assert.match(general, /<DeskDocumentPicker/);
assert.doesNotMatch(
general,
/Ou identificador de documento|Identificador do documento de suporte/,
);
assert.match(general, /document_link_id/);
assert.match(general, /reuse_link_id: documentOrigin\?\.id/);
assert.match(
general,
/source_reference:\s*documentOrigin\?\.source_reference\?\.trim\(\)/,
);
assert.match(field, /<DeskDocumentPicker/);
assert.match(
field,
/if \(origin.id\) data.set\('reuse_link_id', origin.id\)/,
);
});
function generalHarness() {
const cells = [],
forms = [],
posts = [];
let cursor = 0,
formCursor = 0,
tree;
const Picker = () => null;
const Component = load('../../resources/js/pages/desk/case.tsx', {
react: {
useRef(initial) {
const i = cursor++;
cells[i] ??= { current: initial };
return cells[i];
},
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;
},
];
},
},
'@/lib/operation-key': { createOperationKey: () => 'synthetic-key' },
'@inertiajs/react': {
Link: 'a',
router: { reload() {} },
usePage: () => ({
props: { auth: { user: { id: 1 } } },
url: '/balcao-virtual/dossiers/synthetic-case',
}),
useForm(initial) {
const i = formCursor++;
forms[i] ??= {
data: { ...initial },
errors: {},
processing: false,
setData(key, value) {
this.data[key] = value;
},
transform(callback) {
this.transformer = callback;
},
post(url, options) {
posts.push({
url,
data: this.transformer
? this.transformer(this.data)
: this.data,
options,
});
},
reset(...keys) {
for (const key of keys) this.data[key] = initial[key];
},
};
return forms[i];
},
},
'@/pages/investment/shared': {
Frame: 'main',
card: '',
input: '',
button: '',
},
'@/components/investor-guidance': { InvestorGuidance: () => null },
'@/components/desk-case-state-form': { default: () => null },
'@/components/desk-internal-operations': { DocumentReview: () => null },
'@/components/desk-document-picker': { default: Picker },
});
const props = {
case: {
id: 'synthetic-case',
title: 'SYNTHETIC dossier',
version: 7,
workflow: {},
payload: {},
},
canWrite: true,
manager: false,
requests: [],
services: [],
documents: [],
workflow: {},
stepStates: {},
states: {},
guidance: null,
events: [],
links: [],
legacyRequest: null,
};
function render() {
cursor = 0;
formCursor = 0;
tree = Component(props);
}
render();
return {
render,
forms,
posts,
picker: () => nodes(tree).find((n) => n.type === Picker),
all: () => nodes(tree),
submit() {
nodes(tree)
.find(
(n) =>
n.type === 'form' &&
nodes(n).some((child) => child.type === Picker),
)
.props.onSubmit({ preventDefault() {} });
},
};
}
test('general association sends the selected link, original version context and consent, preserving selection on failure', () => {
const h = generalHarness();
h.forms[2].setData('file', { name: 'SYNTHETIC old local choice' });
h.forms[2].setData('consent', true);
h.picker().props.onSelect(document);
h.render();
assert.equal(h.forms[2].data.file, null);
assert.equal(h.forms[2].data.consent, false);
h.forms[2].setData('consent', true);
h.submit();
const posted = h.posts[0];
assert.equal(posted.data.version, 7);
assert.equal(posted.data.document_id, document.document_id);
assert.equal(posted.data.reuse_link_id, document.id);
assert.equal(posted.data.file, null);
assert.equal(posted.data.source_reference, undefined);
h.render();
assert.equal(h.picker().props.selected.id, document.id);
posted.options.onSuccess();
h.render();
assert.equal(h.picker().props.selected, null);
assert.equal(h.forms[2].data.consent, false);
});
test('general association asks only for missing provenance on unlinked uploads and preserves entered text until success', () => {
const h = generalHarness();
h.picker().props.onSelect({
...document,
id: null,
source_reference: null,
});
h.render();
const source = h.all().find((n) => n.type === 'input' && n.props.required);
assert.ok(source);
source.props.onChange({
target: { value: 'SYNTHETIC missing provenance supplied' },
});
h.render();
h.forms[2].setData('consent', true);
h.submit();
assert.equal(h.posts[0].data.document_id, document.document_id);
assert.equal(h.posts[0].data.reuse_link_id, '');
assert.equal(
h.posts[0].data.source_reference,
'SYNTHETIC missing provenance supplied',
);
h.render();
assert.equal(
h.forms[2].data.source_reference,
'SYNTHETIC missing provenance supplied',
);
h.posts[0].options.onSuccess();
assert.equal(h.forms[2].data.source_reference, '');
});