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, ); } });