// @vitest-environment happy-dom import type { ReactElement } from 'react' import type { Root } from 'react-dom/client' import type { ShownPeek } from './definitionPeek' import type { DefinitionPeek } from './types' import { act, useState } from 'react' import { createRoot } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { DefinitionPeeks, linkPeekSource } from './definitionPeek' import { DiffText } from './DiffText' import { MarkdownKindContext, TextPreview } from './TextPreview' let container: HTMLDivElement let root: Root let peeks: DefinitionPeeks | undefined beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) container = document.createElement('div') document.body.append(container) root = createRoot(container) }) afterEach(async () => { peeks?.dispose() peeks = undefined await act(async () => root.unmount()) container.remove() vi.useRealTimers() vi.unstubAllGlobals() }) async function render(element: ReactElement): Promise { await act(async () => root.render(element)) } function anchor(): HTMLAnchorElement { const link = container.querySelector('a') if (!link) throw new Error('没有渲染符号链接') return link } describe.each(['TextPreview', 'DiffText'] as const)('%s 的 Markdown 页面元素', (component) => { function preview(markdown: string): ReactElement { return component === 'TextPreview' ? : } it('父级无关状态更新、传入等值新对象时,保留原链接且仍在页面中', async () => { function Parent(): ReactElement { const [count, setCount] = useState(0) return ( <> {preview('[AsyncOpenAI]')} ) } await render() const original = anchor() for (let count = 1; count <= 3; count++) { await act(async () => container.querySelector('button')!.click()) expect(container.querySelector('button')!.textContent).toBe(String(count)) expect(anchor() === original).toBe(true) expect(original.isConnected).toBe(true) } expect(container.querySelector('.cr-markdown.custom-preview')).not.toBeNull() }) it('正文变化时更新链接和内容', async () => { await render(preview('[AsyncOpenAI]')) const original = anchor() await render(preview('[Client] 新说明')) expect(anchor().textContent).toBe('Client') expect(anchor().getAttribute('href')).toBe('symbol:Client') expect(container.textContent).toContain('新说明') expect(original.isConnected).toBe(false) }) it('空 Markdown 与着色代码片段可以来回切换', async () => { const empty = (): ReactElement => component === 'TextPreview' ? : await render(preview('[Client]')) await render(empty()) expect(container.querySelector('.cr-markdown')?.innerHTML).toBe('') expect(container.querySelector('a')).toBeNull() await render(component === 'TextPreview' ? : ) expect(container.querySelector('.cr-code .cr-role-variable')?.textContent).toBe('client') expect(container.querySelector('.cr-markdown')).toBeNull() if (component === 'DiffText') expect(container.querySelector('[data-change="add"]')?.textContent).toBe('client') await render(empty()) expect(container.querySelector('.cr-markdown')?.innerHTML).toBe('') expect(container.querySelector('.cr-code')).toBeNull() await render(preview('[Client]')) expect(anchor().textContent).toBe('Client') }) it('显示定义预览引起父级更新后,继续指向同一链接不重复请求也不关闭', async () => { vi.useFakeTimers() const post = vi.fn() function Parent(): ReactElement { const [shown, setShown] = useState>() const [controller] = useState(() => new DefinitionPeeks(post, setShown)) peeks = controller return ( <> {preview('[AsyncOpenAI]')} {shown ? '预览已显示' : '预览未显示'} ) } await render() const controller = peeks! controller.setDelays(300, 500) const point = (): void => { const link = anchor() const source = linkPeekSource(link.getAttribute('href')!) if (!source) throw new Error('符号链接不能预览') controller.point({ anchor: link, source }) } point() await act(async () => vi.advanceTimersByTime(300)) expect(post).toHaveBeenCalledTimes(1) expect(post).toHaveBeenLastCalledWith({ type: 'peekLink', id: 1, symbol: 'AsyncOpenAI' }) const peek: DefinitionPeek = { path: 'client.py', line: 1, block: { id: '1', anchor: '1', kind: 'class', startLine: 1, endLine: 2, code: '', codeTokens: [], attributes: { kind: 'class' }, concepts: [], children: [], translation: [] }, names: [], } await act(async () => controller.answer(1, peek)) expect(container.querySelector('output')?.textContent).toBe('预览已显示') point() await act(async () => vi.advanceTimersByTime(1000)) expect(post).toHaveBeenCalledTimes(1) expect(container.querySelector('output')?.textContent).toBe('预览已显示') }) }) it('组件 TextPreview 随注释与定义预览模式切换更新相同正文的链接', async () => { const preview = (kind: 'comment' | 'peek'): ReactElement => ( ) await render(preview('comment')) const original = anchor() await render(preview('peek')) expect(container.querySelector('a')).toBeNull() expect(container.textContent).toBe('[Client]\n') expect(original.isConnected).toBe(false) await render(preview('comment')) expect(anchor().getAttribute('href')).toBe('symbol:Client') }) it('组件 DiffText 更新正文时保留 Markdown 排版和增删标记', async () => { await render() expect(anchor().textContent).toBe('Client') expect(container.querySelector('del')?.textContent).toBe('旧说明') expect(container.querySelector('ins')?.textContent).toBe('新说明') await render() expect(container.querySelector('del')).toBeNull() expect(container.querySelector('ins strong')?.textContent).toBe('更新') })