| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- // @vitest-environment jsdom
- import { cleanup, fireEvent, render, screen } from '@testing-library/react'
- import { afterEach, describe, expect, it } from 'vitest'
- import { JsonBlock, MarkdownText, MessageText } from '@deepseek-ai/dsh-client-ui-primitives'
- afterEach(cleanup)
- describe('MessageText', () => {
- it('renders the text verbatim', () => {
- const { container } = render(<MessageText text={'# line1\n`line2`'} />)
- expect(container.textContent).toBe('# line1\n`line2`')
- expect(container.querySelector('h1')).toBeNull()
- })
- })
- describe('MarkdownText', () => {
- it('renders CommonMark and GFM elements as semantic DOM', () => {
- const markdown = [
- '# Heading',
- '',
- 'Paragraph with **strong**, *emphasis*, ~~deleted~~, `inline`, and [safe](https://example.com). ',
- 'Hard break.',
- '',
- '> Quote',
- '',
- '- parent',
- ' - child',
- '',
- '1. first',
- '2. second',
- '',
- '- [x] done',
- '- [ ] pending',
- '',
- '| Name | Value |',
- '| --- | --- |',
- '| alpha | beta |',
- '',
- '---',
- '',
- '```ts',
- 'const answer = 42',
- '```',
- '',
- '<https://deepseek.com>',
- ].join('\n')
- const { container } = render(<MarkdownText text={markdown} />)
- expect(screen.getByRole('heading', { level: 1, name: 'Heading' })).toBeTruthy()
- expect(container.querySelector('strong')?.textContent).toBe('strong')
- expect(container.querySelector('em')?.textContent).toBe('emphasis')
- expect(container.querySelector('del')?.textContent).toBe('deleted')
- expect(container.querySelector('blockquote')?.textContent?.trim()).toBe('Quote')
- expect(container.querySelectorAll('ul')).toHaveLength(3)
- expect(container.querySelector('ol')).not.toBeNull()
- expect(container.querySelectorAll('input[type="checkbox"]')).toHaveLength(2)
- expect(container.querySelector('table')?.textContent).toContain('alphabeta')
- expect(container.querySelector('hr')).not.toBeNull()
- expect(container.querySelector('pre code')?.textContent).toContain('const answer = 42')
- expect(container.querySelector('br')).not.toBeNull()
- expect(screen.getByRole('link', { name: 'safe' }).getAttribute('target')).toBe('_blank')
- expect(screen.getByRole('link', { name: 'https://deepseek.com' })).toBeTruthy()
- })
- it('neutralizes raw HTML, unsafe or relative links, and remote images', () => {
- const markdown = [
- '<script>globalThis.compromised = true</script>',
- '<img src="x" onerror="globalThis.compromised = true">',
- '[script](javascript:alert(1)) [relative](/settings)',
- '[mail](mailto:dev@example.com) [web](http://example.com) [upper](HTTPS://example.com)',
- '',
- ].join('\n\n')
- const { container } = render(<MarkdownText text={markdown} />)
- expect(container.querySelector('script')).toBeNull()
- expect(container.querySelector('img')).toBeNull()
- const neutralized = [...container.querySelectorAll('p')]
- .find(paragraph => paragraph.textContent === 'script relative')
- expect(neutralized?.querySelector('a')).toBeNull()
- expect(screen.getByRole('link', { name: 'mail' }).getAttribute('target')).toBeNull()
- expect(screen.getByRole('link', { name: 'web' }).getAttribute('rel')).toBe('noopener noreferrer')
- expect(screen.getByRole('link', { name: 'upper' }).getAttribute('target')).toBe('_blank')
- expect(screen.getByText('remote diagram')).toBeTruthy()
- })
- it('keeps incomplete streaming Markdown renderable', () => {
- const { container } = render(<MarkdownText text={'## Streaming\n\n- first\n- **unfinished'} />)
- expect(screen.getByRole('heading', { level: 2, name: 'Streaming' })).toBeTruthy()
- expect(container.querySelectorAll('li')).toHaveLength(2)
- expect(screen.getByText('**unfinished')).toBeTruthy()
- })
- })
- describe('JsonBlock', () => {
- it('collapsed by default; toggle reveals pretty-printed payload', () => {
- render(<JsonBlock label="args" payload={{ a: 1 }} />)
- expect(screen.queryByText(/"a": 1/)).toBeNull()
- fireEvent.click(screen.getByRole('button', { name: /args/ }))
- expect(screen.getByText(/"a": 1/)).toBeDefined()
- fireEvent.click(screen.getByRole('button', { name: /args/ }))
- expect(screen.queryByText(/"a": 1/)).toBeNull()
- })
- it('defaultOpen renders the body immediately', () => {
- const { container } = render(<JsonBlock label="args" payload={[1, 2]} defaultOpen />)
- expect(container.querySelector('pre')?.textContent).toContain('1')
- })
- it('stringifies undefined payloads via String()', () => {
- const { container } = render(<JsonBlock label="x" payload={undefined} defaultOpen />)
- expect(container.querySelector('pre')?.textContent).toBe('undefined')
- })
- it('falls back to String() for circular payloads', () => {
- const circular: { self?: unknown } = {}
- circular.self = circular
- const { container } = render(<JsonBlock label="x" payload={circular} defaultOpen />)
- expect(container.querySelector('pre')?.textContent).toBe('[object Object]')
- })
- it('truncates beyond the size cap with a suffix note', () => {
- const big = 'x'.repeat(30_000)
- const { container } = render(<JsonBlock label="x" payload={big} defaultOpen />)
- const body = container.querySelector('pre')!.textContent!
- expect(body.length).toBeLessThan(30_000)
- expect(body).toContain('截断')
- })
- })
|