import type { StreamdownProps } from 'streamdown'
import type { SimplePluginInfo } from '../streamdown-wrapper'
import { render, screen } from '@testing-library/react'
import { Markdown } from '../index'
const { mockReactMarkdownWrapper } = vi.hoisted(() => ({
mockReactMarkdownWrapper: vi.fn(),
}))
vi.mock('@/next/dynamic', () => ({
default: () => {
const MockStreamdownWrapper = (props: { latexContent: string }) => {
mockReactMarkdownWrapper(props)
return
{props.latexContent}
}
MockStreamdownWrapper.displayName = 'MockStreamdownWrapper'
return MockStreamdownWrapper
},
}))
type CapturedProps = {
latexContent: string
pluginInfo?: SimplePluginInfo
customComponents?: StreamdownProps['components']
customDisallowedElements?: string[]
rehypePlugins?: StreamdownProps['rehypePlugins']
isAnimating?: StreamdownProps['isAnimating']
mode?: StreamdownProps['mode']
}
const getLastWrapperProps = (): CapturedProps => {
const calls = mockReactMarkdownWrapper.mock.calls
const lastCall = calls[calls.length - 1]
return lastCall[0] as CapturedProps
}
describe('Markdown', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render wrapper content', () => {
render()
expect(screen.getByTestId('react-markdown-wrapper')).toHaveTextContent('Hello World')
})
it('should apply default classes', () => {
const { container } = render()
const markdownDiv = container.querySelector('.markdown-body')
expect(markdownDiv).toHaveClass('markdown-body', '!text-text-primary')
})
it('should merge custom className with default classes', () => {
const { container } = render()
const markdownDiv = container.querySelector('.markdown-body')
expect(markdownDiv).toHaveClass('markdown-body', '!text-text-primary', 'custom', 'another')
})
it('should not include undefined in className', () => {
const { container } = render()
const markdownDiv = container.querySelector('.markdown-body')
expect(markdownDiv?.className).not.toContain('undefined')
})
it('should preprocess think tags', () => {
render()
const props = getLastWrapperProps()
expect(props.latexContent).toContain('')
expect(props.latexContent).toContain('Thought')
expect(props.latexContent).toContain('[ENDTHINKFLAG] ')
})
it('should preprocess latex block notation', () => {
render()
const props = getLastWrapperProps()
expect(props.latexContent).toContain('$$x^2 + y^2 = z^2$$')
})
it('should preprocess latex parentheses notation', () => {
render()
const props = getLastWrapperProps()
expect(props.latexContent).toContain('$$a + b$$')
})
it('should preserve latex inside code blocks', () => {
render()
const props = getLastWrapperProps()
expect(props.latexContent).toContain('$E = mc^2$')
})
it('should pass pluginInfo through', () => {
const pluginInfo = {
pluginUniqueIdentifier: 'plugin-unique',
pluginId: 'plugin-id',
}
render()
const props = getLastWrapperProps()
expect(props.pluginInfo).toEqual(pluginInfo)
})
it('should pass default empty customComponents when omitted', () => {
render()
const props = getLastWrapperProps()
expect(props.customComponents).toEqual({})
})
it('should pass customComponents through', () => {
const customComponents = {
h1: ({ children }: { children?: React.ReactNode }) => {children}
,
}
render()
const props = getLastWrapperProps()
expect(props.customComponents).toBe(customComponents)
})
it('should pass customDisallowedElements through', () => {
const customDisallowedElements = ['strong', 'em']
render()
const props = getLastWrapperProps()
expect(props.customDisallowedElements).toBe(customDisallowedElements)
})
it('should pass rehypePlugins through', () => {
const plugin = () => (tree: unknown) => tree
const rehypePlugins = [plugin]
render()
const props = getLastWrapperProps()
expect(props.rehypePlugins).toBe(rehypePlugins)
})
it('should pass isAnimating through', () => {
render()
const props = getLastWrapperProps()
expect(props.isAnimating).toBe(true)
})
it('should pass mode through', () => {
render()
const props = getLastWrapperProps()
expect(props.mode).toBe('streaming')
})
})