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); export const leadId = '10000000-0000-4000-8000-000000000001'; export function props(patch = {}) { return { lead: { id: leadId, version_no: 1, title: 'SYNTHETIC lead', status: 'new', owner_user_id: 1, tasks: [], }, contacts: [], owners: [ { id: 1, name: 'SYNTHETIC manager' }, { id: 2, name: 'SYNTHETIC colleague' }, ], nextAction: { future_tasks: 0 }, taskCreation: { lead_id: leadId, actor_id: 1, commands: [] }, ...patch, }; } export function nodes(tree) { return Array.isArray(tree) ? tree.flatMap(nodes) : tree && typeof tree === 'object' ? [tree, ...nodes(tree.props?.children)] : []; } export function text(tree) { return Array.isArray(tree) ? tree.map(text).join('') : tree?.props ? text(tree.props.children) : typeof tree === 'string' || typeof tree === 'number' ? String(tree) : ''; } const clone = (value) => Array.isArray(value) ? value.map(clone) : value && Object.getPrototypeOf(value) === Object.prototype ? Object.fromEntries( Object.entries(value).map(([k, v]) => [k, clone(v)]), ) : value; export function harness(initial = props()) { const frames = new Map(), sent = [], reloads = [], before = new Set(), unload = new Set(); let active, cursor = 0, changed = false, effects = [], current = initial, tree, sequence = 0, confirmCalls = 0; const windowMock = { confirm() { confirmCalls++; return false; }, addEventListener(name, callback) { if (name === 'beforeunload') unload.add(callback); }, removeEventListener(name, callback) { if (name === 'beforeunload') unload.delete(callback); }, }; const state = (initialValue) => { const frame = active, i = cursor++; if (!(i in frame.cells)) frame.cells[i] = typeof initialValue === 'function' ? initialValue() : initialValue; return [ frame.cells[i], (value) => { const next = typeof value === 'function' ? value(frame.cells[i]) : value; if (!Object.is(next, frame.cells[i])) { frame.cells[i] = next; changed = true; } }, ]; }; const hooks = { useState: state, useRef(value) { const i = cursor++; active.cells[i] ??= { current: value }; return active.cells[i]; }, useEffect(callback, deps) { const frame = active, i = cursor++, prior = frame.cells[i]; if (!prior || !deps.every((value, j) => value === prior.deps[j])) { frame.cells[i] = { deps }; effects.push(() => { prior?.cleanup?.(); frame.cells[i].cleanup = callback(); }); } }, useCallback(callback, deps) { const i = cursor++, prior = active.cells[i]; if (!prior || !deps.every((value, j) => value === prior.deps[j])) active.cells[i] = { deps, callback }; return active.cells[i].callback; }, }; const mocks = { react: hooks, '@inertiajs/react': { usePage: () => ({ props: { auth: { user: { id: current.actorId ?? 1 } } }, }), Link: 'a', router: { reload(options) { reloads.push(options); }, on(name, callback) { if (name === 'before') before.add(callback); return () => before.delete(callback); }, post(url, data) { sent.push({ method: 'post', url, data, options: {} }); }, }, useForm(initialData) { const i = cursor++; if (active.cells[i]) return active.cells[i]; const form = { data: clone(initialData), defaults: clone(initialData), errors: {}, processing: false, setData(key, value) { this.data = typeof key === 'function' ? key(this.data) : typeof key === 'object' ? key : { ...this.data, [key]: value }; changed = true; }, setDefaults(data) { this.defaults = clone(data); }, clearErrors() { this.errors = {}; changed = true; }, reset(...keys) { for (const key of keys) this.data[key] = this.defaults[key]; changed = true; }, transform(callback) { this.transformer = callback; }, }; for (const method of ['post', 'put', 'patch']) form[method] = (url, options) => { form.processing = true; changed = true; sent.push({ method, url, options, data: clone( form.transformer ? form.transformer(form.data) : form.data, ), form, }); }; active.cells[i] = form; return form; }, }, '../management/shared': { Management: 'main', Field: 'label', button: '', control: '', }, '@/pages/management/shared': { Management: 'main', Field: 'label', button: '', control: '', }, '@/components/crm-edit-comparison': { default: () => null }, '@/lib/crm-record-edit': { useCrmRecordEdit: ({ values }) => ({ form: { processing: false }, values, error: () => undefined, blocked: false, }), }, './types': { leadStates: { new: 'Novo', qualified: 'Qualificado' }, taskStates: { open: 'Aberta', done: 'Conclu?da' }, }, }; const operation = { createOperationKey: () => `00000000-0000-4000-8000-${String(++sequence).padStart(12, '0')}`, }; mocks['./operation-key'] = operation; mocks['@/lib/operation-key'] = operation; function load(path) { const code = ts.transpileModule( fs.readFileSync( new URL('../../../' + path, import.meta.url), 'utf8', ), { 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; } mocks['./profile-json'] = load('resources/js/lib/profile-json.ts'); mocks['./crm-task-preparation'] = load( 'resources/js/lib/crm-task-preparation.ts', ); for (const [name, path] of [ ['@/lib/crm-task-creation', 'resources/js/lib/crm-task-creation.ts'], [ '@/components/crm-task-creation', 'resources/js/components/crm-task-creation.tsx', ], ]) if (fs.existsSync(new URL('../../../' + path, import.meta.url))) mocks[name] = load(path); const Page = load('resources/js/pages/crm/lead.tsx').default; function expand(value, path, seen) { if (Array.isArray(value)) return value.map((child, i) => expand(child, path + '/' + (child?.key ?? i), seen), ); if (!value || typeof value !== 'object' || !value.props) return value; if (typeof value.type === 'function') { const id = path + '/' + value.type.name + ':' + (value.key ?? ''); seen.add(id); const frame = frames.get(id) ?? { cells: [] }; frames.set(id, frame); const previous = active, priorCursor = cursor; active = frame; cursor = 0; const rendered = value.type(value.props); active = previous; cursor = priorCursor; return { type: 'component', props: { name: value.type.name, children: expand(rendered, id, seen), }, }; } return { ...value, props: { ...value.props, children: expand( value.props.children, path + '/children', seen, ), }, }; } function render(next = current) { current = next; const previousWindow = globalThis.window; globalThis.window = windowMock; try { let loops = 0; do { changed = false; const seen = new Set(); tree = expand( { type: Page, key: 'page', props: current }, 'root', seen, ); for (const [id, frame] of frames) if (!seen.has(id)) { frame.cells.forEach((cell) => cell?.cleanup?.()); frames.delete(id); } const queue = effects; effects = []; queue.forEach((effect) => effect()); if (++loops > 12) throw Error('Component state did not settle'); } while (changed); return tree; } finally { globalThis.window = previousWindow; } } render(); return { sent, reloads, render, get props() { return current; }, get tree() { return tree; }, all: () => nodes(tree), form(name) { return nodes(tree).find((node) => node.props?.id === name); }, button(label, scope = tree) { return nodes(scope).find( (node) => node.type === 'button' && text(node) === label, ); }, field(name, label) { const root = this.form(name); const field = nodes(root).find( (node) => node.type === 'label' && node.props.label === label, ); return nodes(field).find((node) => ['input', 'select', 'textarea'].includes(node.type), ); }, change(name, label, value, rerender = true) { const field = this.field(name, label); if (!field) throw Error('Missing field ' + name + '/' + label); field.props.onChange({ target: { value, checked: value, files: value ? [value] : [] }, }); if (rerender) render(); }, submit(name) { const form = this.form(name); form.props.onSubmit({ preventDefault() {} }); render(); return sent.at(-1); }, click(label, scope = tree) { const button = this.button(label, scope); if (!button) throw Error('Missing button ' + label); button.props.onClick(); render(); return sent.at(-1); }, success(next, request = sent.at(-1)) { render(next); request.options.onSuccess?.({ props: next }); request.form.processing = false; render(next); }, validation(errors, request = sent.at(-1)) { request.form.errors = errors; request.options.onError?.(errors); request.form.processing = false; render(); }, fail(request = sent.at(-1), status = null) { if (status === null) request.options.onNetworkError(); else request.options.onHttpException({ status }); request.form.processing = false; render(); }, refreshed(next, request = reloads.at(-1)) { render(next); request.onSuccess?.({ props: next }); request.onFinish?.(); render(next); }, leave(preserveState = false) { let prevented = false; const previousWindow = globalThis.window; globalThis.window = windowMock; try { for (const fn of before) fn({ detail: { visit: { method: 'get', preserveState } }, preventDefault() { prevented = true; }, }); } finally { globalThis.window = previousWindow; } return prevented; }, unload() { let prevented = false; for (const fn of unload) fn({ preventDefault() { prevented = true; }, returnValue: null, }); return prevented; }, get confirmCalls() { return confirmCalls; }, }; }