import { describe, expect, mock, test } from 'bun:test'; import { localPathFromFileUrl, parseFileReference, type ParsedFileReference } from './fileReferenceParser'; const parse = (value: string): ParsedFileReference | null => parseFileReference(value); type FakeElement = { childNodes: FakeElement[]; children: FakeElement[]; parentNode: FakeElement | null; attributes: Map; style: { display: string; setProperty: () => void }; innerHTML: string; setAttribute: (name: string, value: string) => void; getAttribute: (name: string) => string | null; appendChild: (child: FakeElement) => FakeElement; replaceWith: (replacement: FakeElement) => void; remove: () => void; querySelector: (selector: string) => FakeElement | null; querySelectorAll: (selector: string) => T[]; addEventListener: () => void; removeEventListener: () => void; contains: (child: FakeElement) => boolean; isEqualNode: () => boolean; }; type FakeDocument = { createElement: () => FakeElement }; type FakeJsxProps = { ref?: { current: FakeElement | null }; children?: FakeElement | FakeElement[]; className?: string; 'data-markdown-content'?: boolean; }; let syncRenderCalls = 0; let morphCalls = 0; let decorateCalls = 0; let mermaidRegistryCreates = 0; let mermaidRegistryCleanups = 0; let cachedRendererBlocks: Array<{ id: string; html: string }> | null = null; let renderedRendererBlocks: Array<{ id: string; html: string }> = []; let currentContextVersion = 0; const layoutEffects: Array<() => void> = []; const passiveEffects: Array<() => void | (() => void)> = []; let hookCursor = 0; let hookStates: Array<{ current: null } | undefined> = []; let activeFakeDocument: FakeDocument | null = null; const makeFakeElement = (ownerDocument: { createElement: () => FakeElement }): FakeElement => { let html = ''; const element: FakeElement = { childNodes: [], children: [], parentNode: null, attributes: new Map(), style: { display: '', setProperty: () => undefined }, get innerHTML() { return html; }, set innerHTML(value: string) { html = value; }, setAttribute(name, value) { this.attributes.set(name, value); }, getAttribute(name) { return this.attributes.get(name) ?? null; }, appendChild(child) { child.parentNode = this; this.childNodes.push(child); this.children.push(child); return child; }, replaceWith(replacement) { if (!this.parentNode) return; const parent = this.parentNode; const index = parent.children.indexOf(this); if (index < 0) return; replacement.parentNode = parent; parent.children[index] = replacement; parent.childNodes[index] = replacement; this.parentNode = null; }, remove() { if (!this.parentNode) return; const parent = this.parentNode; parent.children = parent.children.filter((child) => child !== this); parent.childNodes = parent.childNodes.filter((child) => child !== this); this.parentNode = null; }, querySelector(selector) { if (selector === '[data-markdown-content]') { return this.children.find((child) => child.getAttribute('data-markdown-content') === '') ?? null; } if (selector === '[data-markdown="mermaid-block"]' && html.includes('data-markdown="mermaid-block"')) { return this; } for (const child of this.children) { const match = child.querySelector(selector); if (match) return match; } return null; }, querySelectorAll: () => [], addEventListener: () => undefined, removeEventListener: () => undefined, contains(child) { return child === this || this.children.some((candidate) => candidate.contains(child)); }, isEqualNode: () => false, }; return element; }; const installRendererDom = () => { const previousDocument = Object.getOwnPropertyDescriptor(globalThis, 'document'); const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window'); const previousMutationObserver = Object.getOwnPropertyDescriptor(globalThis, 'MutationObserver'); const documentStub: FakeDocument = { createElement: () => makeFakeElement(documentStub) }; activeFakeDocument = documentStub; Object.defineProperty(globalThis, 'document', { configurable: true, value: documentStub }); Object.defineProperty(globalThis, 'window', { configurable: true, value: { matchMedia: () => ({ matches: false }), setTimeout, clearTimeout, requestAnimationFrame: (callback: FrameRequestCallback) => setTimeout(() => callback(Date.now()), 0), }, }); Object.defineProperty(globalThis, 'MutationObserver', { configurable: true, value: class { observe() {} disconnect() {} }, }); return () => { if (previousDocument) Object.defineProperty(globalThis, 'document', previousDocument); else Reflect.deleteProperty(globalThis, 'document'); if (previousWindow) Object.defineProperty(globalThis, 'window', previousWindow); else Reflect.deleteProperty(globalThis, 'window'); if (previousMutationObserver) Object.defineProperty(globalThis, 'MutationObserver', previousMutationObserver); else Reflect.deleteProperty(globalThis, 'MutationObserver'); activeFakeDocument = null; }; }; const rendererThemes = [{ metadata: { id: 'renderer-test' }, colors: { surface: { elevated: '#fff', foreground: '#000', mutedForeground: '#666', muted: '#eee' }, interactive: { border: '#ccc' }, primary: { base: '#00f' }, }, }, { metadata: { id: 'renderer-test-next' }, colors: { surface: { elevated: '#eee', foreground: '#111', mutedForeground: '#555', muted: '#ddd' }, interactive: { border: '#bbb' }, primary: { base: '#f00' }, }, }]; let rendererThemeIndex = 0; const rendererTheme = () => rendererThemes[rendererThemeIndex] ?? rendererThemes[0]; const rendererUiState = { codeBlockLineWrap: false, mermaidRenderingMode: 'svg', setCodeBlockLineWrap: () => undefined, openContextPreview: () => undefined, }; const fakeReact = { useCallback: (callback: T): T => { hookCursor += 1; return callback; }, useEffect: (effect: () => void | (() => void)) => { passiveEffects.push(effect); }, useLayoutEffect: (effect: () => void) => { layoutEffects.push(effect); }, useMemo: (factory: () => T): T => { hookCursor += 1; return factory(); }, useRef: (current: T) => { const index = hookCursor; hookCursor += 1; if (!hookStates[index]) hookStates[index] = { current: null }; // SAFETY: this test hook preserves one mutable ref slot per hook index. return hookStates[index] as { current: T }; }, memo: (component: T): T => component, }; const fakeJsx = (_type: string, props: FakeJsxProps | null, ...children: FakeElement[]): FakeElement => { const ref = props?.ref; // SAFETY: the renderer test installs the typed fake document before JSX is // evaluated; this branch only supplies its fake element factory. const fakeDocument = activeFakeDocument; if (!fakeDocument) throw new Error('Renderer fake document is not installed'); const element = ref?.current ?? makeFakeElement(fakeDocument); if (!ref?.current) { element.childNodes.length = 0; element.children.length = 0; } if (props) { if (ref) ref.current = element; if (props.className) element.setAttribute('class', props.className); if (props['data-markdown-content']) element.setAttribute('data-markdown-content', ''); } const jsxChildren = props?.children; const allChildren = jsxChildren === undefined ? children : Array.isArray(jsxChildren) ? jsxChildren : [jsxChildren]; for (const child of allChildren) { if (child) element.appendChild(child); } return element; }; mock.module('react', () => ({ default: fakeReact })); mock.module('react/jsx-runtime', () => ({ jsx: fakeJsx, jsxs: fakeJsx, Fragment: 'fragment' })); mock.module('react/jsx-dev-runtime', () => ({ jsxDEV: fakeJsx, Fragment: 'fragment' })); mock.module('beautiful-mermaid', () => ({ renderMermaidASCII: () => '', renderMermaidSVG: (_source: string, colors: { bg: string }) => colors.bg, })); mock.module('@/lib/utils', () => ({ cn: (...values: string[]) => values.filter(Boolean).join(' ') })); mock.module('@/lib/i18n', () => ({ useI18n: () => ({ t: (key: string) => `${key}:${currentContextVersion}` }) })); mock.module('@/lib/runtime-fetch', () => ({ runtimeFetch: async () => ({ ok: false }) })); mock.module('@/lib/url', () => ({ getUrlScheme: () => null, isAppLinkUrl: () => false, isExternalHttpUrl: () => false, openConfirmedAppLinkUrl: async () => false, openExternalUrl: async () => undefined, })); mock.module('@/contexts/useThemeSystem', () => ({ useOptionalThemeSystem: () => ({ currentTheme: rendererTheme() }) })); mock.module('@/lib/theme/themes', () => ({ getDefaultTheme: () => rendererTheme() })); mock.module('./message/FadeInOnReveal', () => ({ FadeInOnReveal: ({ children }: { children: FakeElement | FakeElement[] }) => children })); type RendererUiSelectorResult = boolean | string | (() => void); const fakeUseUIStore = Object.assign( (selector: (state: typeof rendererUiState) => RendererUiSelectorResult) => selector(rendererUiState), { getState: () => rendererUiState }, ); mock.module('@/stores/useUIStore', () => ({ useUIStore: fakeUseUIStore })); mock.module('@/hooks/useEffectiveDirectory', () => ({ useEffectiveDirectory: () => null })); mock.module('@/hooks/useRuntimeAPIs', () => ({ useRuntimeAPIs: () => ({ editor: undefined, runtime: { isVSCode: false } }) })); mock.module('@/lib/desktop', () => ({ isDesktopLocalOriginActive: () => false, isDesktopShell: () => false, isVSCodeRuntime: () => false })); mock.module('@/lib/runtimeSurface', () => ({ isMobileSurfaceRuntime: () => false })); mock.module('@/lib/outsideFileGrants', () => ({ ensureOutsideFileGrantForDesktop: async () => undefined })); mock.module('@/lib/path-utils', () => ({ getDirectoryForFilePath: () => '', isFilePathWithinDirectory: () => true, toAbsoluteFilePath: () => '' })); mock.module('./markdown/markdownCore', () => ({ getCachedMarkdownBlocks: () => cachedRendererBlocks, renderMarkdownBlocks: async () => renderedRendererBlocks, renderMarkdownSync: () => { syncRenderCalls += 1; return '

cold

'; }, })); mock.module('./markdown/markdownTheme', () => ({ ensureMarkdownShikiTheme: () => undefined })); mock.module('./markdown/markdownSyntaxVars', () => ({ getMarkdownSyntaxVars: () => ({}) })); type TestDecorateContext = { labels: { copy: string }; codeBlockLineWrap: boolean; renderMermaid: (source: string) => { svg?: string }; }; mock.module('./markdown/decorate', () => ({ attachMarkdownInteractions: () => () => undefined, applyMarkdownCodeBlockWrapState: () => undefined, decorateMarkdown: (root: FakeElement, ctx: TestDecorateContext) => { decorateCalls += 1; if (root.getAttribute('data-test-decoration-marker') === 'true') return; root.setAttribute('data-test-decoration-marker', 'true'); root.setAttribute( 'data-test-decoration', `${ctx.labels.copy}|${ctx.codeBlockLineWrap}|${ctx.renderMermaid('test').svg ?? ''}`, ); }, getMarkdownCodeText: () => '', })); mock.module('./markdown/textPosition', () => ({ findTextPosition: () => null })); mock.module('./markdown/mermaidViewer', () => ({ createMermaidViewerRegistry: () => { mermaidRegistryCreates += 1; return { refresh: () => undefined, cleanup: () => { mermaidRegistryCleanups += 1; }, }; }, MERMAID_BLOCK_SELECTOR: '[data-markdown="mermaid-block"]', shouldRefreshMermaidViewers: (container: Pick) => container.querySelector('[data-markdown="mermaid-block"]') !== null, })); mock.module('@/stores/utils/streamDebug', () => ({ streamPerfCount: () => undefined, streamPerfObserve: () => undefined })); mock.module('morphdom', () => ({ default: () => { morphCalls += 1; } })); const { MarkdownRenderer } = await import('./MarkdownRendererImpl'); const resetRendererTestState = () => { cachedRendererBlocks = null; renderedRendererBlocks = []; syncRenderCalls = 0; morphCalls = 0; decorateCalls = 0; mermaidRegistryCreates = 0; mermaidRegistryCleanups = 0; hookCursor = 0; hookStates = []; layoutEffects.length = 0; passiveEffects.length = 0; currentContextVersion = 0; rendererThemeIndex = 0; rendererUiState.codeBlockLineWrap = false; }; const beginRendererRender = () => { hookCursor = 0; return renderMarkdownForTest(); }; const rendererRoot = (value: ReturnType): FakeElement => { if (!(value instanceof Object) || !('childNodes' in value) || !('getAttribute' in value)) { throw new Error('Renderer test did not return its fake JSX root'); } // SAFETY: the structural check confirms this ReactNode is the object // returned by the mocked JSX runtime. const candidate = value as object; // SAFETY: the mocked JSX runtime creates the complete FakeElement shape. return candidate as FakeElement; }; const runRendererLayoutEffects = () => { const pending = layoutEffects.splice(0); for (const effect of pending) effect(); }; const runRendererPassiveEffects = () => passiveEffects.splice(0).map((effect) => effect()); const findBlock = (root: FakeElement, id: string): FakeElement | null => { if (root.getAttribute('data-md-id') === id) return root; for (const child of root.children) { const match = findBlock(child, id); if (match) return match; } return null; }; const renderMarkdownForTest = () => MarkdownRenderer({ content: 'cached markdown', messageId: 'message-1', isAnimated: false, isStreaming: false, }); const withRendererDom = async (run: () => void | Promise): Promise => { const restoreDom = installRendererDom(); const previousThemeIndex = rendererThemeIndex; try { await run(); } finally { rendererThemeIndex = previousThemeIndex; restoreDom(); } }; describe('parseFileReference', () => { test('returns null for empty or whitespace input', () => { expect(parse('')).toBeNull(); expect(parse(' ')).toBeNull(); }); test('parses bare path', () => { expect(parse('src/foo.ts')).toEqual({ path: 'src/foo.ts' }); }); test('parses path with single line', () => { expect(parse('src/foo.ts:42')).toEqual({ path: 'src/foo.ts', line: 42 }); }); test('parses path with line and column', () => { expect(parse('src/foo.ts:42:8')).toEqual({ path: 'src/foo.ts', line: 42, column: 8 }); }); test('parses path with line range', () => { expect(parse('src/foo.ts:42-58')).toEqual({ path: 'src/foo.ts', line: 42, endLine: 58, }); }); test('parses path with single-line range (start equals end)', () => { expect(parse('src/foo.ts:10-10')).toEqual({ path: 'src/foo.ts', line: 10, endLine: 10, }); }); test('rejects range with end before start', () => { expect(parse('src/foo.ts:20-10')).toBeNull(); }); test('falls back to path-only when range endpoint is non-numeric', () => { // `src/foo.ts:10-abc` and `src/foo.ts:abc-20` are malformed; the // line info is discarded and only the path is returned (the trailing // `:`-suffix is stripped). expect(parse('src/foo.ts:10-abc')).toEqual({ path: 'src/foo.ts' }); expect(parse('src/foo.ts:abc-20')).toEqual({ path: 'src/foo.ts' }); }); test('strips backtick and quote wrapping from range forms', () => { expect(parse('`src/foo.ts:10-20`')).toEqual({ path: 'src/foo.ts', line: 10, endLine: 20, }); expect(parse('"src/foo.ts:1-3"')).toEqual({ path: 'src/foo.ts', line: 1, endLine: 3, }); }); test('parses absolute Windows path with line range', () => { expect(parse('C:/repo/src/foo.ts:5-9')).toEqual({ path: 'C:/repo/src/foo.ts', line: 5, endLine: 9, }); }); test('preserves line:col form (does not interpret as range)', () => { expect(parse('src/foo.ts:42:8')).toEqual({ path: 'src/foo.ts', line: 42, column: 8 }); }); test('preserves hash form', () => { expect(parse('src/foo.ts#L42C8')).toEqual({ path: 'src/foo.ts', line: 42, column: 8, }); expect(parse('src/foo.ts#L42')).toEqual({ path: 'src/foo.ts', line: 42, }); }); test('range form takes precedence over line-only when suffix matches digits-dash-digits', () => { const result = parse('src/foo.ts:42-58'); expect(result).toEqual({ path: 'src/foo.ts', line: 42, endLine: 58 }); }); }); describe('localPathFromFileUrl', () => { test('converts local file URLs to absolute paths', () => { expect(localPathFromFileUrl('file:///private/tmp/report%20viewer.html')).toBe('/private/tmp/report viewer.html'); expect(localPathFromFileUrl('file://localhost/private/tmp/REPORT.md')).toBe('/private/tmp/REPORT.md'); expect(localPathFromFileUrl('file:///C:/Users/test/report.html')).toBe('C:/Users/test/report.html'); }); test('rejects non-file URLs and remote file hosts', () => { expect(localPathFromFileUrl('https://example.com/report.html')).toBeNull(); expect(localPathFromFileUrl('file://remote-host/share/report.html')).toBeNull(); expect(localPathFromFileUrl('file:///tmp/bad%ZZpath')).toBeNull(); }); }); describe('MarkdownRenderer warm settled path', () => { test('installs cached blocks without sync fallback and skips same-ID morph', async () => { await withRendererDom(async () => { resetRendererTestState(); cachedRendererBlocks = [{ id: 'full:cached', html: '

cached

' }]; renderedRendererBlocks = cachedRendererBlocks; syncRenderCalls = 0; morphCalls = 0; decorateCalls = 0; // SAFETY: the test JSX adapter returns the fake element assigned to the // renderer container ref and exposes the DOM members used below. const root = rendererRoot(beginRendererRender()); runRendererLayoutEffects(); expect(syncRenderCalls).toBe(0); const block = findBlock(root, 'full:cached'); expect(block).not.toBeNull(); expect(block?.innerHTML).toBe('

cached

'); expect(block?.getAttribute('data-md-block')).toBe(''); expect(block?.getAttribute('data-md-id')).toBe('full:cached'); expect(block?.style.display).toBe('contents'); expect(decorateCalls).toBe(1); runRendererPassiveEffects(); await Promise.resolve(); expect(morphCalls).toBe(0); }); }); test('recreates the Mermaid registry after StrictMode-like cleanup without remounting blocks', () => { return withRendererDom(() => { resetRendererTestState(); const mermaidHtml = '
'; cachedRendererBlocks = [{ id: 'full:mermaid', html: mermaidHtml }]; renderedRendererBlocks = cachedRendererBlocks; mermaidRegistryCreates = 0; mermaidRegistryCleanups = 0; morphCalls = 0; const root = rendererRoot(beginRendererRender()); runRendererLayoutEffects(); expect(mermaidRegistryCreates).toBe(1); const cleanups = runRendererPassiveEffects(); for (const cleanup of cleanups) cleanup?.(); expect(mermaidRegistryCleanups).toBe(1); beginRendererRender(); runRendererLayoutEffects(); expect(mermaidRegistryCreates).toBe(2); expect(findBlock(root, 'full:mermaid')).not.toBeNull(); expect(morphCalls).toBe(0); }); }); test('redecorates a same-ID block when decoration context changes before async completion', async () => { await withRendererDom(async () => { resetRendererTestState(); cachedRendererBlocks = [{ id: 'full:context', html: '

cached

', }]; renderedRendererBlocks = cachedRendererBlocks; const root = rendererRoot(beginRendererRender()); runRendererLayoutEffects(); const block = findBlock(root, 'full:context'); const firstDecorationId = block?.getAttribute('data-md-decoration-id'); expect(firstDecorationId).not.toBeNull(); const firstDecorateCalls = decorateCalls; rendererThemeIndex = 1; currentContextVersion = 1; rendererUiState.codeBlockLineWrap = true; beginRendererRender(); runRendererLayoutEffects(); runRendererPassiveEffects(); await Promise.resolve(); expect(decorateCalls).toBeGreaterThan(firstDecorateCalls); expect(syncRenderCalls).toBe(0); expect(morphCalls).toBe(0); const updatedBlock = findBlock(root, 'full:context'); expect(updatedBlock?.getAttribute('data-md-decoration-id')).not.toBe(firstDecorationId); expect(updatedBlock?.getAttribute('data-test-decoration')).toContain(':1|true|#eee'); expect(updatedBlock?.getAttribute('data-test-decoration-marker')).toBe('true'); expect(mermaidRegistryCleanups).toBeGreaterThan(0); expect(mermaidRegistryCreates).toBeGreaterThan(1); }); }); });