import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { compileFunction } from 'node:vm'; import { createRequire } from 'node:module'; import React from 'react'; import ts from 'typescript'; import { PreparationAttempt, swotDimensions, } from '../../resources/js/lib/investment-preparation.ts'; import { createOperationKey } from '../../resources/js/lib/operation-key.ts'; const require = createRequire(import.meta.url), flush = () => new Promise((resolve) => setImmediate(resolve)); const fixture = () => ({ case: { id: 'qa-case', title: 'SYNTHETIC preparation', closed: false }, opportunity: null, version: 0, apiex: { note: 'Saved APIEX note', additional_documents: [], review: null, exports: [], external: [], }, swot: { object_key: 'investment:qa-case', scope: 'Saved SWOT scope', rows: [], source_fingerprint: 'a'.repeat(64), review: null, actions: [], }, api_state: 'preparing', api_issues: [], api_fingerprint: 'b'.repeat(64), swot_fingerprint: 'a'.repeat(64), swot_review_required: true, can_write: true, can_review: true, links: { dossier: '/investimentos/qa-case', context: '/investimentos/qa-case/contexto/editar', documents: '/balcao-virtual/dossiers/qa-case', editorial: '/acesso-gestao', }, objects: [ { key: 'investment:qa-case', label: 'SYNTHETIC preparation', kind: 'investment', }, ], sources: [], context: { revision: 1, fields: { project_title: 'SYNTHETIC' } }, assessment: { requirements: [] }, documents: [], history: [], }); const profileModule = { exports: {} }; compileFunction( ts.transpileModule( fs.readFileSync( new URL('../../resources/js/lib/profile-json.ts', import.meta.url), 'utf8', ), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, }, }, ).outputText, ['require', 'module', 'exports'], )(require, profileModule, profileModule.exports); const { ProfileValidationError } = profileModule.exports; const source = fs.readFileSync( new URL( '../../resources/js/pages/investor-path/preparation.tsx', import.meta.url, ), 'utf8', ), compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX, }, }).outputText; const text = (n) => typeof n === 'string' || typeof n === 'number' ? String(n) : React.isValidElement(n) ? React.Children.toArray(n.props.children).map(text).join('') : ''; const walk = (node, parents = []) => React.isValidElement(node) ? [ { node, parents }, ...React.Children.toArray(node.props.children).flatMap((n) => walk(n, [...parents, node]), ), ] : []; function harness(initial = fixture()) { let cursor = 0; const slots = [], calls = [], mod = { exports: {} }; const hookReact = { ...React, useEffect: () => {}, useState: (initial) => { const i = cursor++; if (!(i in slots)) slots[i] = typeof initial === 'function' ? initial() : initial; return [ slots[i], (value) => { slots[i] = typeof value === 'function' ? value(slots[i]) : value; }, ]; }, useRef: (initial) => { const i = cursor++; if (!(i in slots)) slots[i] = { current: initial }; return slots[i]; }, }; compileFunction(compiled, ['require', 'module', 'exports'])( (name) => name === 'react' ? hookReact : name === '@inertiajs/react' ? { Link: 'a' } : name === '@/pages/investment/shared' ? { Frame: 'div', card: 'card', input: 'input', button: 'button', } : name === '@/lib/profile-json' ? { ProfileValidationError, profileJson: (url, body, method) => new Promise((resolve, reject) => calls.push({ url, body: structuredClone(body), method, resolve, reject, }), ), } : name === '@/lib/operation-key' ? { createOperationKey } : name === '@/lib/investment-preparation' ? { PreparationAttempt, swotDimensions } : require(name), mod, mod.exports, ); const h = { calls, render() { cursor = 0; const page = mod.exports.default({ workspace: initial }); return page.type(page.props); }, findButton(label) { return walk(this.render()).find( ({ node }) => node.type === 'button' && text(node) === label, ); }, click(label) { const entry = this.findButton(label); assert.ok(entry, label); if ( entry.node.props.disabled || entry.parents.some( (p) => p.type === 'fieldset' && p.props.disabled, ) ) return false; entry.node.props.onClick(); return true; }, field(label) { const parent = walk(this.render()).find( ({ node }) => node.type === 'label' && text(node).startsWith(label), ); assert.ok(parent, label); const item = walk(parent.node).find(({ node }) => ['input', 'textarea', 'select'].includes(node.type), ); assert.ok(item); return item.node; }, change(label, value) { this.field(label).props.onChange({ target: { value } }); }, async reply(value, index = calls.length - 1) { calls[index].resolve(value); await flush(); }, async fail(error, index = calls.length - 1) { calls[index].reject(error); await flush(); }, }; return h; } void test('uncertain preparation retries use an immutable operation and require a safe explicit recovery', () => { const a = new PreparationAttempt(), data = { draft: { scope: 'Deliberate' } }; const first = a.start(2, 'opportunity:7', 'swot_save', data, 'QA'); data.draft.scope = 'Later edit'; assert.equal(a.current().draft.scope, 'Deliberate'); assert.deepEqual(a.current(), first); assert.throws(() => a.start(2, null, 'external', {}, 'other')); assert.equal(a.canAdopt({ version: 2, history: [] }), false); assert.equal(a.canAdopt({ version: 3, history: [] }), true); assert.equal( a.canAdopt({ version: 2, history: [{ operation_key: first.operation_key }], }), true, ); a.complete(); assert.equal(a.current(), null); }); void test('actual APIEX form preserves local edits on lost response and retries the same body once', async () => { const h = harness(); h.change('Nota para organizar a preparação', 'Deliberate local APIEX'); h.click('Guardar preparação documental'); assert.equal(h.calls.length, 1); const first = structuredClone(h.calls[0].body); assert.equal(h.click('Guardar preparação documental'), false); await h.fail(new Error('Lost response')); assert.equal( h.field('Nota para organizar a preparação').props.value, 'Deliberate local APIEX', ); h.click('Repetir a mesma tentativa'); assert.deepEqual(h.calls[1].body, first); const next = fixture(); next.version = 1; next.apiex.note = 'Deliberate local APIEX'; await h.reply(next); assert.equal( h.field('Nota para organizar a preparação').props.value, 'Deliberate local APIEX', ); }); void test('saving APIEX does not clear unsaved SWOT changes or let them be reviewed as a saved server version', async () => { const h = harness(); h.click('Análise SWOT'); h.change('Âmbito e fronteiras do objecto', 'Deliberate unsaved SWOT'); h.click('Documentos APIEX'); h.change('Nota para organizar a preparação', 'Another APIEX note'); h.click('Guardar preparação documental'); const next = fixture(); next.version = 1; next.apiex.note = 'Another APIEX note'; await h.reply(next); h.click('Análise SWOT'); assert.equal( h.field('Âmbito e fronteiras do objecto').props.value, 'Deliberate unsaved SWOT', ); assert.equal( h.findButton('Conferi objecto,quatro dimensões e fontes desta versão') .node.props.disabled, true, ); }); void test('consulting a concurrent version does not replace local conclusions; explicit keep-local adopts only the base', async () => { const h = harness(); h.click('Análise SWOT'); h.change( 'Âmbito e fronteiras do objecto', 'Keep this deliberate local scope', ); h.click('Consultar versão do servidor'); const next = fixture(); next.version = 4; next.swot.scope = 'Other server scope'; await h.reply(next); assert.equal( h.field('Âmbito e fronteiras do objecto').props.value, 'Keep this deliberate local scope', ); h.click( 'Manter os meus campos e usar esta versão como base para nova revisão', ); assert.equal( h.field('Âmbito e fronteiras do objecto').props.value, 'Keep this deliberate local scope', ); h.click('Guardar análise privada'); assert.equal(h.calls[1].body.version, 4); assert.equal( h.calls[1].body.draft.scope, 'Keep this deliberate local scope', ); }); void test('publication is never an automatic save/review effect; four dimensions remain explicit and uncertain by default', () => { const h = harness(); h.click('Análise SWOT'); for (const label of [ 'Acrescentar forças', 'Acrescentar fraquezas', 'Acrescentar oportunidades', 'Acrescentar ameaças', ]) assert.ok(h.findButton(label)); h.click('Acrescentar forças'); assert.equal(h.field('Classificação declarada').props.value, 'unconfirmed'); assert.equal(h.calls.length, 0); const links = walk(h.render()) .filter(({ node }) => node.type === 'a') .map(({ node }) => node.props.href); assert.ok(links.includes('/acesso-gestao')); }); void test('team review keeps existing owner-only inputs read-only while retaining the review action', () => { const view = fixture(); view.can_write = false; view.can_review = true; const h = harness(view); h.click('Análise SWOT'); assert.equal( h.field('Âmbito e fronteiras do objecto').props.readOnly, true, ); assert.equal( h.findButton('Guardar análise privada').node.props.disabled, true, ); assert.equal( h.findButton('Conferi objecto,quatro dimensões e fontes desta versão') .node.props.disabled, false, ); }); void test('a definite HTTP conflict permits comparison and correction without treating a rejected write as an uncertain commit',async()=>{const h=harness();h.change('Nota para organizar a preparação','Preserve after source conflict');h.click('Guardar preparação documental');const first=h.calls[0].body.operation_key;await h.fail(new ProfileValidationError('Fonte alterada',{},false,409));assert.equal(h.field('Nota para organizar a preparação').props.value,'Preserve after source conflict');h.click('Consultar versão do servidor');const next=fixture();next.api_fingerprint='c'.repeat(64);await h.reply(next);h.click('Manter os meus campos e usar esta versão como base para nova revisão');h.click('Guardar preparação documental');assert.notEqual(h.calls[2].body.operation_key,first);assert.equal(h.calls[2].body.note,'Preserve after source conflict');});