// Smoke test for the Web client: loads client.js against a tiny stub React + primitives and walks // the rendered tree, so the row/card code paths run without a browser or a React install. import test from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' const SRC = readFileSync(fileURLToPath(new URL('../client.js', import.meta.url)), 'utf8') function makeReact() { let slots = []; let i = 0 const React = { Fragment: Symbol('Fragment'), createElement: (type, props, ...children) => ({ type, props: { ...(props || {}), children: props?.children ?? children.flat() } }), useState: (init) => { const k = i++; if (!(k in slots)) slots[k] = typeof init === 'function' ? init() : init; return [slots[k], (v) => { slots[k] = typeof v === 'function' ? v(slots[k]) : v }] }, useMemo: (f) => f(), useCallback: (f) => f, reset() { slots = []; i = 0 }, rewind() { i = 0 }, } return React } const stub = (name) => Object.assign((p) => ({ type: name, props: p }), { displayName: name }) const P = { IconEditOutlineRegular: stub('IconEdit'), IconWrapFillRegular: stub('IconWrap'), IconNowrapFillRegular: stub('IconNowrap'), IconCopyOutlineRegular: stub('IconCopy'), IconCheckOutlineRegular: stub('IconCheck'), Tooltip: ({ children }) => children, TextShimmer: ({ children, className }) => ({ type: 'span', props: { className, children } }), languageForPath: (p) => (p.endsWith('.js') ? 'javascript' : undefined), writeClipboard: async () => true, DisclosureRow: (p) => ({ type: 'div', props: { 'data-disclosure': true, children: [p.icon, p.title, p.collapsedContent, p.open ? p.children : null] } }), } function load() { const React = makeReact() let mod globalThis.window = { __ModuleLoader__: { load: ({ factory }) => { mod = factory((id) => (id === 'react' ? React : P)) } }, setTimeout } new Function(SRC)() let view mod.apply({ slots: { inject: (_n, f) => f(), register: (_m, C) => { view = C } } }) return { React, view } } /** Expand function components into host nodes; collect text and class names. */ function render(node, out = { text: [], classes: [] }) { if (node == null || node === false || node === true) return out if (Array.isArray(node)) { node.forEach((n) => render(n, out)); return out } if (typeof node !== 'object') { out.text.push(String(node)); return out } if (typeof node.type === 'function') return render(node.type(node.props), out) if (node.props?.className) out.classes.push(...String(node.props.className).split(' ')) return render(node.props?.children, out) } const t = (k, p) => ({ 'tool.title.edit': 'Edit', 'row.output': 'Output' }[k] ?? k) const diffs = [{ path: '/ws/a.js', oldText: 'x\n', newText: 'y\n', oldStart: 5, newStart: 5, rows: [[' ', 'keep'], ['-', 'x'], ['+', 'y']] }] const result = (extra = {}) => ({ kind: 'tool-result', callId: 'c1', isError: false, content: [{ type: 'text', text: 'ok' }], meta: { diffs }, call: { argsRaw: JSON.stringify({ input: '[a.js#ABCD]\nPUT 6.=6:\n+y' }) }, ...extra }) test('settled edit renders DSH row chrome; card only when expanded', () => { const { React, view } = load() let expanded = false const props = { t, toolName: 'edit', cwd: '/ws', phase: 'result', block: result(), openFile: () => {}, useDisclosure: () => ({ expanded, setExpanded() {}, toggle() { expanded = !expanded } }) } React.reset() let out = render(view(props)) assert.ok(out.text.includes('Edit')) assert.ok(out.text.includes('a.js'), 'workspace-relative file summary') assert.ok(out.classes.includes('ompe-file'), 'dotted open-file link') assert.ok(out.text.includes('+1') && out.text.includes('-1'), 'diff stat') assert.ok(!out.classes.includes('ompe-card'), 'collapsed by default like stock rows') expanded = true React.reset() out = render(view(props)) assert.ok(out.classes.includes('ompe-card')) assert.ok(out.text.includes('javascript'), 'language header') for (const n of ['5', '6', 'keep', 'x', 'y']) assert.ok(out.text.includes(n), `row/gutter text ${n}`) assert.ok(out.classes.includes('ompe-add') && out.classes.includes('ompe-del') && out.classes.includes('ompe-ln')) }) test('long diffs fold to head/tail with an expand control', () => { const { React, view } = load() const rows = Array.from({ length: 30 }, (_, i) => ['+', `line${i}`]) const block = result({ meta: { diffs: [{ path: 'b.txt', oldText: null, newText: '', oldStart: 1, newStart: 1, rows }] } }) React.reset() const out = render(view({ t, toolName: 'edit', phase: 'result', block, useDisclosure: () => ({ expanded: true, toggle() {} }) })) assert.ok(out.classes.includes('ompe-expand')) assert.ok(out.text.includes('line0') && out.text.includes('line29') && !out.text.includes('line15')) }) test('failed edit shows red first-line summary and Output card, no stat', () => { const { React, view } = load() const block = result({ isError: true, meta: {}, content: [{ type: 'text', text: 'Error: stale tag\nmore' }] }) React.reset() const out = render(view({ t, toolName: 'edit', phase: 'result', block, useDisclosure: () => ({ expanded: true, toggle() {} }) })) assert.ok(out.text.includes('Error: stale tag')) assert.ok(out.classes.includes('ompe-error-summary')) assert.ok(out.classes.includes('ompe-io-card') && out.text.includes('Output')) assert.ok(!out.text.some((s) => /^\+\d/.test(s))) }) test('preparing and running phases render without a body', () => { const { React, view } = load() React.reset() let out = render(view({ t, toolName: 'edit', phase: 'preparing', block: { phase: 'preparing', callId: 'c' }, useToolCallArgumentsPartial: () => 'x'.repeat(3000) })) assert.ok(out.text.includes('Edit')) React.reset() out = render(view({ t, toolName: 'edit', phase: 'start', block: { phase: 'start', callId: 'c', argsRaw: JSON.stringify({ input: '[x/y.md#1A2B]\nPUT 1.=1:\n+a' }) } })) assert.ok(out.text.includes('x/y.md')) })