Files
Investidor-V2/tests/frontend/crm-record-versions.test.mjs
T

464 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 harness(initial = {}, component = null) {
const cells = [],
sent = [],
reloads = [];
let cursor,
result,
props = {
id: 'one',
kind: 'investor',
version: 1,
values: { name: 'original', status: 'prospect' },
...initial,
};
const form = {
errors: {},
processing: false,
setData(key, value) {
this.data =
typeof key === 'function'
? key(this.data)
: typeof key === 'object'
? key
: { ...this.data, [key]: value };
},
setDefaults(value) {
this.defaults = value;
},
clearErrors(key) {
if (key) delete this.errors[key];
else this.errors = {};
},
transform(callback) {
this.transformer = callback;
},
patch(url, options) {
sent.push({
url,
options,
data: structuredClone(this.transformer(this.data)),
});
},
};
const mocks = {
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;
},
];
},
},
'@inertiajs/react': {
Link: 'a',
router: {
reload(options) {
reloads.push(options);
},
},
useForm(initial) {
form.data ??= structuredClone(initial);
return form;
},
},
'../management/shared': {
Management: 'main',
Field: 'label',
control: '',
button: '',
},
'./types': {
leadStates: { new: 'Novo', qualified: 'Qualificado' },
taskStates: { open: 'Aberta', in_progress: 'Em curso' },
},
};
const hook = load('../../resources/js/lib/crm-record-edit.ts', mocks);
mocks['@/lib/crm-record-edit'] = hook;
mocks['@/components/crm-edit-comparison'] = { default: () => null };
mocks['@/components/crm-task-creation'] = { default: () => null };
const renderComponent = component
? load(component.file, mocks, component.suffix ?? '')[
component.name ?? 'default'
]
: hook.useCrmRecordEdit;
function render(patch = {}) {
props = { ...props, ...patch };
cursor = 0;
result = renderComponent(props);
return result;
}
render();
return {
form,
sent,
reloads,
render,
get edit() {
return result;
},
};
}
function nodes(tree) {
return Array.isArray(tree)
? tree.flatMap(nodes)
: tree && typeof tree === 'object'
? [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)
: '';
test('different fields merge only after explicit adoption, retaining batched deliberate edits', () => {
const h = harness();
h.edit.change('name', 'my name');
h.edit.change('extra', 'ignored');
h.render({ version: 2, values: { name: 'original', status: 'active' } });
assert.deepEqual(h.edit.changed, ['status']);
assert.deepEqual(h.edit.unresolved, []);
h.edit.submit('/synthetic');
assert.equal(h.sent.length, 0);
h.edit.adopt();
h.render();
assert.deepEqual(h.edit.values, { name: 'my name', status: 'active' });
h.edit.submit('/synthetic');
assert.deepEqual(h.sent[0].data, {
name: 'my name',
status: 'active',
version: 2,
});
});
test('same-field conflicts require a choice and deliberate return to the initial value is retained', () => {
const h = harness();
h.edit.change('name', 'temporary');
h.edit.change('name', 'original');
h.render({
version: 2,
values: { name: 'other operator', status: 'prospect' },
});
assert.deepEqual(h.edit.unresolved, ['name']);
h.edit.adopt();
h.render();
assert.equal(h.edit.baseVersion, 1);
h.edit.choose('name', 'mine');
h.render();
h.edit.adopt();
h.render();
assert.equal(h.edit.values.name, 'original');
assert.equal(h.edit.baseVersion, 2);
});
test('choosing the current value changes only that field and another update requires a fresh choice', () => {
const h = harness();
h.edit.change('name', 'mine');
h.edit.change('status', 'inactive');
h.render({ version: 2, values: { name: 'remote', status: 'prospect' } });
h.edit.choose('name', 'current');
h.render();
h.edit.adopt();
h.render();
assert.deepEqual(h.edit.values, { name: 'remote', status: 'inactive' });
h.render({
version: 3,
values: { name: 'new remote', status: 'prospect' },
});
assert.deepEqual(h.edit.unresolved, ['name']);
});
test('refresh keeps the draft and version error until the current revision is compared', () => {
const h = harness();
h.edit.change('name', 'draft');
h.form.errors.version = 'Stale';
h.render();
h.edit.submit('/synthetic');
assert.equal(h.sent.length, 0);
h.edit.refresh();
h.render();
assert.deepEqual(h.reloads[0].only, ['investor', 'contacts']);
assert.equal(h.edit.values.name, 'draft');
assert.equal(h.edit.blocked, true);
h.reloads[0].onFinish();
h.render({ version: 2, values: { name: 'original', status: 'active' } });
h.edit.adopt();
h.render();
assert.equal(h.form.errors.version, undefined);
assert.equal(h.edit.values.name, 'draft');
});
test('success uses the actual returned record and keeps edits made while saving', () => {
const h = harness({
kind: 'contact',
values: { email: 'UPPER@EXAMPLE.INVALID', phone: '111' },
});
h.edit.submit('/synthetic');
h.edit.change('email', 'temporary');
h.edit.change('email', 'UPPER@EXAMPLE.INVALID');
h.edit.change('phone', '222');
assert.equal(h.sent[0].data.phone, '111');
h.sent[0].options.onSuccess({
props: {
contacts: [
{ id: 'different', version_no: 900 },
{
id: 'one',
version_no: 2,
email: 'upper@example.invalid',
phone: '111',
},
],
},
});
h.render({
version: 2,
values: { email: 'upper@example.invalid', phone: '111' },
});
assert.deepEqual(h.edit.values, {
email: 'UPPER@EXAMPLE.INVALID',
phone: '222',
});
assert.deepEqual(h.form.defaults, {
version: 2,
values: { email: 'upper@example.invalid', phone: '111' },
});
h.edit.submit('/synthetic');
assert.equal(h.sent[1].data.version, 2);
});
test('lead success clears the previous event reason while keeping a newly typed reason', () => {
for (const duringSave of [false, true]) {
const h = harness({
kind: 'lead',
values: { status: 'new', qualification_reason: '' },
blankAfterSave: ['qualification_reason'],
});
h.edit.change('qualification_reason', 'first event');
h.render();
h.edit.submit('/synthetic');
if (duringSave) h.edit.change('qualification_reason', 'new event');
h.sent[0].options.onSuccess({
props: {
taskCreation: { lead_id: 'one', actor_id: 1, commands: [] },
lead: {
id: 'one',
version_no: 2,
status: 'qualified',
qualification_reason: 'first event',
},
},
});
h.render({
version: 2,
values: { status: 'qualified', qualification_reason: '' },
});
assert.equal(
h.edit.values.qualification_reason,
duringSave ? 'new event' : '',
);
}
});
test('task success resolves the exact task; invalid responses and another record cannot reset a draft', () => {
const h = harness({ kind: 'task', values: { status: 'open' } });
h.edit.change('status', 'in_progress');
h.render();
h.edit.submit('/synthetic');
for (const props of [
{ lead: { tasks: [{ id: 'other', version_no: 2, status: 'done' }] } },
{ lead: { tasks: [{ id: 'one', version_no: '2', status: 'done' }] } },
]) {
h.sent[0].options.onSuccess({ props });
assert.equal(h.form.data.values.status, 'in_progress');
}
h.sent[0].options.onSuccess({
props: {
lead: {
tasks: [{ id: 'one', version_no: 2, status: 'in_progress' }],
},
},
});
h.render({ version: 2 });
assert.equal(h.edit.baseVersion, 2);
h.edit.submit('/synthetic');
h.render({ id: 'two' });
h.sent[1].options.onSuccess({
props: {
lead: { tasks: [{ id: 'one', version_no: 3, status: 'done' }] },
},
});
assert.equal(h.form.data.values.status, 'in_progress');
assert.equal(h.edit.blocked, true);
});
test('invalid version blocks a command and transformed field errors remain accessible', () => {
const h = harness({ version: 0 });
h.edit.submit('/synthetic');
assert.equal(h.sent.length, 0);
h.form.errors.email = 'Invalid email';
assert.equal(h.edit.error('email'), 'Invalid email');
});
test('comparison offers explicit non-submit choices and blocks adoption while conflicts remain', () => {
const h = harness();
h.edit.change('name', 'mine');
h.render({ version: 2, values: { name: 'remote', status: 'prospect' } });
const Panel = load(
'../../resources/js/components/crm-edit-comparison.tsx',
).default;
const tree = Panel({ editor: h.edit, labels: { name: 'Nome' } });
const buttons = nodes(tree).filter((node) => node.type === 'button');
assert.ok(buttons.every((button) => button.props.type === 'button'));
assert.match(text(tree), /Actualmente guardado/);
assert.match(text(tree), /mine/);
assert.match(text(tree), /remote/);
assert.equal(buttons.at(-1).props.disabled, true);
buttons
.find((button) => text(button).includes('Manter minha'))
.props.onClick();
h.render();
assert.equal(h.edit.unresolved.length, 0);
});
test('all four CRM forms send their real revisions without asking the user to enter one', () => {
const cases = [
{
file: '../../resources/js/pages/crm/investor.tsx',
props: {
investor: {
id: 'one',
version_no: 6,
legal_name: 'Synthetic investor',
investor_type: 'domestic',
status: 'prospect',
},
contacts: [],
leads: [],
},
expected: {
legal_name: 'Synthetic investor',
investor_type: 'domestic',
status: 'prospect',
version: 6,
},
},
{
file: '../../resources/js/pages/crm/investor.tsx',
suffix: '\nexport { ContactForm };',
name: 'ContactForm',
props: {
investorId: 'i',
contact: {
id: 'one',
version_no: 7,
full_name: 'Synthetic',
email: 'a@example.invalid',
phone: '',
},
},
expected: {
full_name: 'Synthetic',
email: 'a@example.invalid',
phone: '',
version: 7,
},
},
{
file: '../../resources/js/pages/crm/lead.tsx',
props: {
taskCreation: { lead_id: 'one', actor_id: 1, commands: [] },
lead: {
id: 'one',
version_no: 9,
title: 'Synthetic lead',
status: 'new',
owner_user_id: 1,
tasks: [],
},
contacts: [],
owners: [{ id: 1, name: 'Synthetic owner' }],
},
expected: {
status: 'new',
owner_user_id: '1',
qualification_reason: '',
version: 9,
},
},
{
file: '../../resources/js/pages/crm/lead.tsx',
suffix: '\nexport { TaskRow };',
name: 'TaskRow',
props: {
leadId: 'l',
task: {
id: 'one',
version_no: 8,
title: 'Synthetic',
status: 'open',
due_at: null,
assignee_user_id: 1,
can_update: true,
},
},
expected: { status: 'open', version: 8 },
},
];
for (const item of cases) {
const h = harness(item.props, item);
nodes(h.edit)
.find((node) => node.type === 'form')
.props.onSubmit({ preventDefault() {} });
assert.deepEqual(h.sent[0].data, item.expected);
assert.equal(
nodes(h.edit).filter(
(node) =>
node.type === 'input' && node.props.name === 'version',
).length,
0,
);
}
});