import { compileFunction } from 'node:vm'; import assert from 'node:assert/strict'; import test from 'node:test'; import fs from 'node:fs'; import { createRequire } from 'node:module'; import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import ts from 'typescript'; import * as flow from '../../resources/js/lib/opportunity-fill-flow.ts'; import { fiscalPreparationGuide } from '../../resources/js/lib/fiscal-preparation-guide.ts'; const require = createRequire(import.meta.url); function load(path, mocks = {}, exportName = 'default', extra = '') { const source = fs.readFileSync(new URL(path, import.meta.url), 'utf8'); const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX, }, }).outputText; const module = { exports: {} }; compileFunction(output + extra, ['require', 'module', 'exports'])( (name) => mocks[name] ?? (name.startsWith('@/') ? { default: () => null } : require(name)), module, module.exports, ); return module.exports[exportName]; } function stateHooks() { const memory = []; let cursor = 0; return { hooks: { ...React, useState(initial) { const i = cursor++; if (!(i in memory)) memory[i] = typeof initial === 'function' ? initial() : initial; return [ memory[i], (next) => (memory[i] = typeof next === 'function' ? next(memory[i]) : next), ]; }, useRef: () => ({ current: null }), useEffect: () => {}, }, reset: () => (cursor = 0), }; } const flat = (node) => !node || typeof node !== 'object' ? [] : [node, ...React.Children.toArray(node.props?.children).flatMap(flat)]; void test('free navigation keeps every field mounted and reports resource work in the current group', () => { const state = stateHooks(); const Component = load( '../../resources/js/components/opportunity-fill-flow.tsx', { react: state.hooks, '@/lib/opportunity-fill-flow': flow }, ); const groups = flow.opportunityFillGroups([ 'official_title', 'locations', 'documents', ]); const saved = { official_title: 'Deliberate user title', locations: ['saved-site'], }; const opened = []; let tree; const render = () => { state.reset(); tree = Component({ groups, errors: {}, renderField: (field) => React.createElement('input', { name: field, value: JSON.stringify(saved[field] ?? ''), readOnly: true, }), onOpenResources: (group) => opened.push(group), }); }; const priorWindow = globalThis.window, priorFrame = globalThis.requestAnimationFrame; globalThis.window = { history: { state: { existing: 'inertia-state' }, replaceState(state, _title, url) { assert.deepEqual(state, { existing: 'inertia-state' }); this.url = url; }, }, }; globalThis.requestAnimationFrame = (callback) => callback(); try { render(); assert.equal( flat(tree).filter((node) => node.type === 'input').length, 3, ); flat(tree) .find( (node) => node.type === 'a' && node.props.href === '#fill-territory', ) .props.onClick({ preventDefault() {} }); render(); const sections = flat(tree).filter((node) => node.type === 'section'); assert.equal( sections.find((node) => node.props.id === 'fill-territory').props .hidden, false, ); assert.equal( sections.find((node) => node.props.id === 'fill-idea').props.hidden, true, ); assert.equal( flat(tree).filter((node) => node.type === 'input').length, 3, ); flat(tree) .find((node) => node.type === 'button') .props.onClick(); assert.deepEqual(opened, ['territory']); assert.equal(saved.official_title, 'Deliberate user title'); assert.deepEqual(saved.locations, ['saved-site']); assert.equal(globalThis.window.history.url, '#fill-territory'); } finally { globalThis.window = priorWindow; globalThis.requestAnimationFrame = priorFrame; } }); void test('profile request renders readable labels without dumping every available document into the form', () => { const Picker = load( '../../resources/js/components/profile-proof-picker.tsx', ); const Page = load('../../resources/js/pages/profiles/request.tsx', { '@/lib/operation-key': { createOperationKey: () => 'synthetic-key' }, '@inertiajs/react': { Head: () => null, Link: ({ href, children }) => React.createElement('a', { href }, children), router: {}, }, '@/components/profile-navigation': { default: () => null }, '@/lib/profile-json': {}, '@/pages/management/shared': { button: 'button', control: 'control' }, '@/components/opportunity-search-filters': { modalityLabels: { investment_project: 'Projecto de investimento', land_rights: 'Direitos fundiários', }, }, '@/components/profile-proof-picker': { default: Picker }, }); const html = renderToStaticMarkup( React.createElement(Page, { requests: [], types: ['investment_project', 'land_rights'], organizations: [], documents: Array.from({ length: 80 }, (_, i) => ({ id: `doc-${i}`, file_name: `UNSELECTED-DOCUMENT-${i}.txt`, })), }), ); assert.match(html, /Em nome próprio/); assert.match(html, /Entidade pública/); assert.match(html, /Projecto de investimento/); assert.match(html, /Direitos fundiários/); assert.doesNotMatch(html, /UNSELECTED-DOCUMENT/); assert.match(html, /Pesquisar nos meus ficheiros/); const visible = html.replace(/<[^>]+>/g, ''); assert.doesNotMatch( visible, /investment_project|land_rights|public_entity/, ); }); void test('proof selection is explicit and retained when the filename search changes', () => { const state = stateHooks(); const Component = load( '../../resources/js/components/profile-proof-picker.tsx', { react: state.hooks }, ); const documents = [ { id: 'identity', file_name: 'Documento próprio' }, { id: 'law', file_name: 'Lei.pdf' }, ]; const values = []; let tree; const render = () => { state.reset(); tree = Component({ documents, value: values, onChange: (ids) => { values.splice(0, values.length, ...ids); }, }); }; render(); assert.equal( flat(tree).filter( (node) => node.type === 'input' && node.props.type === 'checkbox', ).length, 0, ); flat(tree) .find((node) => node.type === 'input') .props.onChange({ target: { value: 'próprio' } }); render(); flat(tree) .find((node) => node.type === 'input' && node.props.type === 'checkbox') .props.onChange(); render(); assert.deepEqual(values, ['identity']); flat(tree) .find((node) => node.type === 'input' && node.props.type === 'search') .props.onChange({ target: { value: 'Lei' } }); render(); assert.deepEqual(values, ['identity']); assert.equal( flat(tree).filter( (node) => node.type === 'input' && node.props.type === 'checkbox', )[0].props.checked, false, ); }); void test('document entry leads to existing fillable preparation and keeps sources optional', () => { const context = '10000000-0000-4000-8000-000000000001'; const inertia = { Link: ({ href, children }) => React.createElement('a', { href }, children), usePage: () => ({ url: `/documentos?context=${context}` }), }; const Hub = load( '../../resources/js/components/journey-document-resources.tsx', { '@inertiajs/react': inertia, '@/lib/fiscal-preparation-guide': { fiscalPreparationGuide }, }, 'JourneyDocumentResources', ); const hub = renderToStaticMarkup(React.createElement(Hub, {})); assert.match( hub, new RegExp( `/investimentos/${context}/contexto/editar#preparation-questions`, ), ); assert.match(hub, /href="\/terra-duat"/); assert.match(hub, /Preparação digital e exemplar oficial/); assert.doesNotMatch(hub, /]*\bopen\b/); const Editor = load( '../../resources/js/components/private-investment-preparation.tsx', { '@inertiajs/react': inertia, '@/pages/investment/shared': { card: 'card', input: 'input' }, }, 'PreparationEditor', ); const html = renderToStaticMarkup( React.createElement(Editor, { data: { questions: [ { key: 'investment_preparation', label: 'Preparar pedido?', options: { yes: 'Sim', no: 'Não' }, source: 'SYNTHETIC QA', }, { key: 'declared_amount', label: 'Valor do investimento', type: 'number', source: 'SYNTHETIC QA', }, ], requirements: [], enabled: true, mode: 'authorization', missing_fields: ['declared_amount'], }, fields: { investment_preparation: 'yes' }, changes: {}, documents: [], links: {}, onChange() {}, onLink() {}, disabled: false, }), ); assert.match(html, / { const useForm = (data) => ({ data, errors: {}, processing: false, setData() {}, transform() { return this; }, post() {}, }); const Stage = load( '../../resources/js/pages/investor-path/show.tsx', { '@inertiajs/react': { useForm, Link: ({ href, children }) => React.createElement('a', { href }, children), }, '@/lib/investor-path': { pathStates: { not_started: 'Por iniciar' }, }, }, 'Stage', '\nmodule.exports.Stage=Stage;', ); const step = { code: 'S01', title: 'Sistematizar a ideia', state: 'not_started', detail: { scope: 'promoter_preparation' }, preparation_allowed: true, action_gaps: {}, source_paragraph: 22, applicability: 'applicable', }; const html = renderToStaticMarkup( React.createElement(Stage, { step, path: { case: { id: 'synthetic', version: 1 }, next: { code: 'S01' }, canWrite: true, canReview: false, steps: [step], documents: [], }, }), ); assert.match(html, /]*id="S01"[^>]*open/); assert.match(html, /]*>Sistematizar a ideia/); assert.doesNotMatch(html, /]*>S01/); assert.match(html, /Fonte e referência desta etapa/); }); function preparationFixture() { const requirement = (key, state, required = true) => ({ key, label: key, state, required, document_id: state === 'associated' ? 'saved-document' : null, document: null, reason: 'Synthetic QA', source: { url: '/source', reference: 'Synthetic QA' }, }); return { version: 'private-investment-preparation-2026-v1', enabled: true, mode: 'registration', period_covered: true, scope_covered: true, missing_fields: [], questions: [ { key: 'investment_preparation', label: 'Preparar?', source: 'Synthetic QA', }, ], next: 'Preparar documentos', reason: [], authority_source: { url: '/source' }, source: { url: '/source' }, requirements: [ requirement('MISSING', 'missing'), requirement('ASSOCIATED', 'associated'), requirement('UNDETERMINED', 'indeterminate', false), requirement('HISTORICAL', 'not_applicable', false), ], }; } void test('impact exposes document replacement even when its state and filename stay unchanged', () => { const Impact = load( '../../resources/js/components/private-investment-preparation.tsx', preparationMocks, 'PreparationImpact', ); const before = preparationFixture(); before.requirements = [before.requirements[1]]; before.requirements[0].document = { file_name: 'ORIGINAL.pdf' }; const after = structuredClone(before); after.requirements[0].document_id = 'replacement-document'; const original = JSON.stringify({ before, after }); const html = renderToStaticMarkup( React.createElement(Impact, { before, after }), ); const impactList = html.slice(html.indexOf('')); assert.match(impactList, /ASSOCIATED/); assert.match( impactList, /Substituição de documento: ORIGINAL.pdf → ORIGINAL.pdf/, ); assert.doesNotMatch(impactList, /Vínculo documental preservado/); assert.doesNotMatch(html, /Sem alterações às exigências/); assert.equal(JSON.stringify({ before, after }), original); }); void test('impact distinguishes detachment, unavailable names, preserved history and unchanged associations', () => { const Impact = load( '../../resources/js/components/private-investment-preparation.tsx', preparationMocks, 'PreparationImpact', ); const before = preparationFixture(); before.requirements = [before.requirements[1]]; before.requirements[0].document = { file_name: 'ORIGINAL.pdf' }; const render = (after) => renderToStaticMarkup(React.createElement(Impact, { before, after })); const detached = structuredClone(before); detached.requirements[0] = { ...detached.requirements[0], document_id: null, document: null, state: 'missing', }; assert.match( render(detached), /Documento: ORIGINAL.pdf → Sem documento associado/, ); const replaced = structuredClone(before); replaced.requirements[0] = { ...replaced.requirements[0], document_id: 'new', document: null, }; assert.match( render(replaced), /Substituição de documento: ORIGINAL.pdf → Documento associado — nome indisponível/, ); const historical = structuredClone(before); historical.requirements[0].state = 'not_applicable'; const historicalList = render(historical).split('')[0]; assert.match(historicalList, /Vínculo documental preservado/); assert.doesNotMatch(historicalList, /Substituição de documento/); assert.match( render(structuredClone(before)), /Sem alterações às exigências ou associações documentais/, ); const associated = structuredClone(detached); const added = renderToStaticMarkup( React.createElement(Impact, { before: associated, after: before }), ); assert.match(added, /Documento: Sem documento associado → ORIGINAL.pdf/); assert.equal(before.requirements[0].document_id, 'saved-document'); }); const preparationMocks = { '@inertiajs/react': { Link: ({ href, children }) => React.createElement('a', { href }, children), }, '@/pages/investment/shared': { card: 'card', input: 'input' }, }; function contextPreviewHarness() { const memory = []; const effects = []; let cursor = 0; const hooks = { ...React, useState(initial) { const index = cursor++; if (!(index in memory)) memory[index] = typeof initial === 'function' ? initial() : initial; return [ memory[index], (next) => { memory[index] = typeof next === 'function' ? next(memory[index]) : next; }, ]; }, useRef(initial) { const index = cursor++; if (!(index in memory)) memory[index] = { current: initial }; return memory[index]; }, useEffect(callback, dependencies) { effects.push({ callback, dependencies }); }, }; const pending = []; const Editor = load('../../resources/js/pages/investor-path/context.tsx', { react: hooks, '@inertiajs/react': { Link: () => null, router: {} }, '@/components/navigation-context': { useNavigationContext: () => ({ refresh() {} }), }, '@/lib/operation-key': { createOperationKey: () => 'synthetic-operation', }, '@/lib/investor-context': { contextFieldNames: { project_title: 'Título', project_summary: 'Resumo', }, contextApi: (url, body) => new Promise((resolve, reject) => pending.push({ url, body, resolve, reject }), ), }, '@/pages/investment/shared': { Frame: () => null, card: 'card', input: 'input', button: 'button', }, '@/components/investment-complements': { default: () => null, SelectedComplements: () => null, complementInput: (value) => value, }, }); const props = { initial: { case: { id: 'synthetic', version: 1, title: 'Synthetic' }, context: { selection: {}, fields: { currency: 'MZN' }, revision: 1, }, can_write: true, results: [], requests: [], }, options: { opportunities: [], sectors: [], parks: [] }, source: null, }; return { pending, effects, savedView: { ...props.initial, case: { ...props.initial.case, version: 2 }, context: { ...props.initial.context, fields: { currency: 'USD' } }, }, render() { cursor = 0; effects.length = 0; return Editor(props); }, result: { preview_hash: 'synthetic-preview', selection: {}, maintained: [], updated: [], dependent_results: [], review_steps: [], requests: [], documents_preserved: 0, }, }; } const buttonNamed = (tree, label) => flat(tree).find( (node) => node.type === 'button' && node.props.children === label, ); const settlePreview = () => new Promise((resolve) => setImmediate(resolve)); async function withDraftTimers(callback) { const previousSetTimeout = globalThis.setTimeout; const previousClearTimeout = globalThis.clearTimeout; const timers = []; globalThis.setTimeout = (callback) => { timers.push(callback); return timers.length; }; globalThis.clearTimeout = () => {}; try { await callback(timers); } finally { globalThis.setTimeout = previousSetTimeout; globalThis.clearTimeout = previousClearTimeout; } } async function readyDraftHarness() { const harness = contextPreviewHarness(); harness.render(); harness.effects .find((effect) => effect.dependencies?.length === 3) .callback(); harness.pending[0].resolve({ corridors: [], parks: [] }); await settlePreview(); harness.render(); return harness; } const runAutosaveEffect = (harness) => harness.effects .find((effect) => effect.dependencies?.length === 5) .callback(); const changeDraftCurrency = (harness, value) => { const tree = harness.render(); flat(tree) .find((node) => node.type === 'input' && node.props.maxLength === 3) .props.onChange({ target: { value } }); return harness.render(); }; void test('stale autosave timer never sends an old body with a newer draft sequence', async () => { await withDraftTimers(async (timers) => { const harness = await readyDraftHarness(); runAutosaveEffect(harness); changeDraftCurrency(harness, 'USD'); timers[0](); await settlePreview(); assert.equal(harness.pending.length, 1); runAutosaveEffect(harness); timers[1](); await settlePreview(); assert.equal(harness.pending.length, 2); assert.equal(harness.pending[1].body.declarations.currency, 'USD'); assert.ok(harness.pending[1].body.draft_sequence > 0); harness.pending[1].resolve({ saved: true }); await settlePreview(); assert.ok( flat(harness.render()).some( (node) => node.props.children === 'Rascunho guardado.', ), ); }); }); void test('queued obsolete draft and late completion cannot acknowledge a later revision with identical values', async () => { await withDraftTimers(async (timers) => { const harness = await readyDraftHarness(); changeDraftCurrency(harness, 'EUR'); runAutosaveEffect(harness); timers[0](); await settlePreview(); const firstSequence = harness.pending[1].body.draft_sequence; changeDraftCurrency(harness, 'USD'); runAutosaveEffect(harness); timers[1](); await settlePreview(); changeDraftCurrency(harness, 'EUR'); harness.pending[1].resolve({ saved: true }); await settlePreview(); assert.equal(harness.pending.length, 2); assert.equal( flat(harness.render()).some( (node) => node.props.children === 'Rascunho guardado.', ), false, ); runAutosaveEffect(harness); timers[2](); await settlePreview(); assert.equal(harness.pending[2].body.declarations.currency, 'EUR'); assert.ok(harness.pending[2].body.draft_sequence > firstSequence); harness.pending[2].resolve({ saved: true }); await settlePreview(); assert.ok( flat(harness.render()).some( (node) => node.props.children === 'Rascunho guardado.', ), ); }); }); void test('large multi-opportunity draft is offered to the transport on exit without truncation', () => { const previousWindow = globalThis.window; let flush; globalThis.window = { addEventListener(_name, callback) { flush = callback; }, removeEventListener() {}, }; let cleanup; try { const harness = contextPreviewHarness(); const initial = harness.render(); cleanup = harness.effects .find( (effect) => effect.dependencies?.length === 1 && effect.dependencies[0] === 'synthetic', ) .callback(); const choices = Array.from({ length: 100 }, (_, index) => ({ opportunity: { key: `opportunity:${index + 1}`, title: `Synthetic ${index + 1}`, }, territory: null, location_reason: 'Descrição '.repeat(80), })); flat(initial) .find( (node) => Array.isArray(node.props.choices) && node.props.onChange, ) .props.onChange(choices); harness.render(); flush(); assert.equal(harness.pending.length, 1); assert.match(harness.pending[0].url, /\/rascunho$/); assert.equal(harness.pending[0].body.opportunity_choices.length, 100); assert.equal( harness.pending[0].body.opportunity_choices[99].location_reason, choices[99].location_reason, ); assert.ok(JSON.stringify(harness.pending[0].body).length > 60000); } finally { cleanup?.(); globalThis.window = previousWindow; } }); void test('successful save followed by failed recovery offers read-only retry and never restores a stale draft', async () => { const previousWindow = globalThis.window; const listeners = new Map(); globalThis.window = { history: { state: {}, replaceState(_state, _title, url) { this.url = url; }, }, location: { hash: '#preparation-documents' }, addEventListener: (name, fn) => listeners.set(name, fn), removeEventListener: (name) => listeners.delete(name), }; let cleanup; try { const harness = contextPreviewHarness(); const initial = harness.render(); cleanup = harness.effects .find( (effect) => effect.dependencies?.length === 1 && effect.dependencies[0] === 'synthetic', ) .callback(); buttonNamed(initial, 'Rever dados antes de guardar').props.onClick(); harness.render(); harness.pending[0].resolve(harness.result); await settlePreview(); buttonNamed( harness.render(), 'Guardar dados do investimento', ).props.onClick(); await settlePreview(); assert.match(harness.pending[1].url, /\/confirmar$/); harness.pending[1].resolve({}); await settlePreview(); assert.equal( globalThis.window.history.url, '/investimentos/synthetic/contexto/editar#preparation-documents', ); assert.equal(harness.pending[2].body, undefined); harness.pending[2].reject(new Error('Synthetic recovery failure')); await settlePreview(); const failed = harness.render(); assert.equal( buttonNamed(failed, 'Guardar dados do investimento'), undefined, ); assert.equal( buttonNamed(failed, 'Rever dados antes de guardar').props.disabled, true, ); assert.ok( flat(failed).some( (node) => node.type === 'p' && String(node.props.children).includes( 'Os dados foram guardados, mas', ), ), ); listeners.get('pagehide')(); assert.equal(harness.pending.length, 3); buttonNamed(failed, 'Recuperar dados guardados').props.onClick(); assert.equal(harness.pending[3].body, undefined); harness.pending[3].resolve(harness.savedView); await settlePreview(); const recovered = harness.render(); assert.equal( buttonNamed(recovered, 'Rever dados antes de guardar').props .disabled, false, ); assert.equal( flat(recovered).find( (node) => node.type === 'input' && node.props.maxLength === 3, ).props.value, 'USD', ); assert.equal( harness.pending.filter((request) => request.url.endsWith('/confirmar'), ).length, 1, ); buttonNamed(recovered, 'Rever dados antes de guardar').props.onClick(); assert.equal(harness.pending[4].body.version, 2); } finally { cleanup?.(); globalThis.window = previousWindow; } }); void test('failed save retains edits and permits retry without claiming that the context was saved', async () => { const harness = contextPreviewHarness(); buttonNamed( harness.render(), 'Rever dados antes de guardar', ).props.onClick(); harness.render(); harness.pending[0].resolve(harness.result); await settlePreview(); buttonNamed( harness.render(), 'Guardar dados do investimento', ).props.onClick(); await settlePreview(); harness.pending[1].reject(new Error('Synthetic save rejected')); await settlePreview(); const failed = harness.render(); assert.equal(buttonNamed(failed, 'Recuperar dados guardados'), undefined); assert.equal( buttonNamed(failed, 'Guardar dados do investimento').props.disabled, false, ); assert.equal(harness.pending.length, 2); assert.equal( flat(failed).find( (node) => node.type === 'input' && node.props.maxLength === 3, ).props.value, 'MZN', ); }); void test('context comparison locks declaration and replacement controls until its matching preview is ready', async () => { const harness = contextPreviewHarness(); buttonNamed( harness.render(), 'Rever dados antes de guardar', ).props.onClick(); const busy = harness.render(); assert.equal( flat(busy) .filter((node) => node.type === 'select') .every((node) => node.props.disabled), true, ); assert.equal( flat(busy) .filter( (node) => node.type === 'input' && (node.props.maxLength || node.props.type === 'checkbox'), ) .every((node) => node.props.disabled), true, ); assert.equal(harness.pending[0].body.version, 1); harness.pending[0].resolve(harness.result); await settlePreview(); const ready = harness.render(); assert.equal( buttonNamed(ready, 'Guardar dados do investimento').props.disabled, false, ); assert.equal( flat(ready).find( (node) => node.type === 'input' && node.props.maxLength === 3, ).props.disabled, false, ); }); void test('context comparison rejects a response for a draft changed while the request was pending', async () => { const harness = contextPreviewHarness(); buttonNamed( harness.render(), 'Rever dados antes de guardar', ).props.onClick(); const busy = harness.render(); // Simulate a queued state change, independently of native disabled controls. flat(busy) .find((node) => node.type === 'input' && node.props.maxLength === 3) .props.onChange({ target: { value: 'USD' } }); harness.render(); harness.pending[0].resolve(harness.result); await settlePreview(); const stale = harness.render(); assert.equal( buttonNamed(stale, 'Guardar dados do investimento'), undefined, ); assert.ok( flat(stale).some( (node) => node.type === 'p' && node.props.children === 'Os dados mudaram durante a comparação. Reveja novamente antes de guardar.', ), ); assert.equal( flat(stale).find( (node) => node.type === 'input' && node.props.maxLength === 3, ).props.value, 'USD', ); }); void test('a failed replacement comparison cannot leave the earlier preview confirmable', async () => { const harness = contextPreviewHarness(); buttonNamed( harness.render(), 'Rever dados antes de guardar', ).props.onClick(); harness.render(); harness.pending[0].resolve(harness.result); await settlePreview(); assert.ok(buttonNamed(harness.render(), 'Guardar dados do investimento')); buttonNamed( harness.render(), 'Rever dados antes de guardar', ).props.onClick(); assert.equal( buttonNamed(harness.render(), 'Guardar dados do investimento'), undefined, ); harness.pending[1].reject(new Error('Synthetic request failed')); await settlePreview(); assert.equal( buttonNamed(harness.render(), 'Guardar dados do investimento'), undefined, ); }); void test('preparation directs complete context to missing documents without treating association or uncertainty as approval', () => { const Summary = load( '../../resources/js/components/private-investment-preparation.tsx', preparationMocks, 'PreparationSummary', ); const data = preparationFixture(); const before = JSON.stringify(data); const render = (overrides = {}) => renderToStaticMarkup( React.createElement(Summary, { data: { ...data, ...overrides }, caseId: 'synthetic', }), ); const visible = render().split('
')[0]; assert.match(visible, /Documentos por associar: 1/); assert.match(visible, /Documentos associados: 1 — conteúdo por conferir/); assert.match(visible, /Exigências documentais por determinar: 1/); assert.match(visible, /#preparation-documents/); assert.match(visible, /Associar documentos do dossier/); for (const overrides of [ { missing_fields: ['currency'] }, { scope_covered: false }, { period_covered: false }, { enabled: false }, { requirements: data.requirements.filter( (r) => r.state !== 'missing', ), }, ]) { assert.match( render(overrides).split('
')[0], /#preparation-questions/, ); } assert.equal(JSON.stringify(data), before); }); void test('associated document controls remain editable in details and deliberate detachment survives rerender', () => { const Editor = load( '../../resources/js/components/private-investment-preparation.tsx', preparationMocks, 'PreparationEditor', ); const data = preparationFixture(); const links = {}; const props = { data, fields: { investment_preparation: 'yes' }, changes: {}, documents: [ { document_id: 'saved-document', file_name: 'ORIGINAL.pdf', status: 'active', }, ], links, onChange() {}, onLink(key, value) { links[key] = value; }, disabled: false, }; const tree = Editor(props); const details = flat(tree).find( (node) => node.type === 'details' && flat(node).some( (child) => child.type === 'summary' && child.props.children === 'Documentos já associados — consultar ou alterar', ), ); assert.ok(details); assert.ok(!details.props.open); const existingSelect = flat(details).find((node) => node.type === 'select'); assert.equal(existingSelect.props.value, 'saved-document'); assert.equal(flat(tree).filter((node) => node.type === 'select').length, 2); existingSelect.props.onChange({ target: { value: '' } }); assert.deepEqual(links, { ASSOCIATED: null }); const rerender = Editor(props); assert.equal( flat(rerender) .filter((node) => node.type === 'select') .every((node) => node.props.value === ''), true, ); assert.equal(data.requirements[1].document_id, 'saved-document'); const disabled = Editor({ ...props, disabled: true }); assert.equal( flat(disabled) .filter((node) => node.type === 'select') .every((node) => node.props.disabled), true, ); });