// @vitest-environment jsdom // CodeBlock + the shiki singleton: registered grammars highlight into token // spans colored by --shiki-* custom properties; unknown/absent languages take // the identical-geometry plain arm; aliases resolve; the trailing newline is // display-trimmed. MarkdownText's fence route is pinned in markdown.spec.tsx // alongside the rest of the markdown family. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import { CodeBlock } from '../src/markdown/CodeBlock.tsx' import { highlightToHtml } from '../src/markdown/highlight.ts' afterEach(cleanup) beforeEach(() => { vi.useRealTimers() }) describe('highlightToHtml', () => { it('highlights a registered grammar into css-variables token spans', () => { const html = highlightToHtml('const x: number = 1', 'typescript') expect(html).toContain('pre class="shiki css-variables"') expect(html).toContain('var(--shiki-') }) it.each([['ts'], ['js'], ['bash'], ['sh'], ['jsonc']])('resolves the %s alias', (alias) => { expect(highlightToHtml('x', alias)).toContain('shiki') }) it('returns undefined for unknown or absent languages', () => { expect(highlightToHtml('x', 'cobol')).toBeUndefined() expect(highlightToHtml('x', undefined)).toBeUndefined() }) }) describe('CodeBlock', () => { it('renders the highlighted tree for TypeScript', () => { const view = render() const pre = view.container.querySelector('pre.shiki') expect(pre).not.toBeNull() expect(pre!.textContent).toBe('const a = 1') expect(pre!.querySelectorAll('span[style]').length).toBeGreaterThan(1) }) it('renders the plain arm for an unknown language with the text verbatim', () => { const view = render() expect(view.container.querySelector('pre.shiki')).toBeNull() expect(view.getByText('IDENTIFICATION DIVISION.')).toBeTruthy() }) it('renders the plain arm when no language is given', () => { const view = render() expect(view.container.querySelector('pre.shiki')).toBeNull() expect(view.getByText('plain text')).toBeTruthy() }) it('shows the language banner and copies the pre textContent', async () => { vi.useFakeTimers() const writeText = vi.fn().mockResolvedValue(undefined) Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText }, }) render() expect(screen.getByText('ts')).toBeTruthy() fireEvent.click(screen.getByRole('button', { name: '复制' })) expect(writeText).toHaveBeenCalledWith('const a = 1') // Flush the clipboard promise under fake timers before asserting the label. await act(async () => { await Promise.resolve() }) expect(screen.getByRole('button', { name: '复制成功' })).toBeTruthy() // While the ok label is showing, further clicks are no-ops. fireEvent.click(screen.getByRole('button', { name: '复制成功' })) expect(writeText).toHaveBeenCalledTimes(1) await vi.advanceTimersByTimeAsync(1000) expect(screen.getByRole('button', { name: '复制' })).toBeTruthy() }) it('does not claim success when clipboard.writeText rejects', async () => { const writeText = vi.fn().mockRejectedValue(new Error('denied')) Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText }, }) render() fireEvent.click(screen.getByRole('button', { name: '复制' })) await act(async () => { await Promise.resolve() }) expect(screen.getByRole('button', { name: '复制' })).toBeTruthy() expect(screen.queryByRole('button', { name: '复制成功' })).toBeNull() }) it('falls back to execCommand when clipboard.writeText is unavailable', async () => { Object.defineProperty(navigator, 'clipboard', { configurable: true, value: undefined, }) const exec = vi.fn().mockReturnValue(true) Object.defineProperty(document, 'execCommand', { configurable: true, value: exec, }) render() fireEvent.click(screen.getByRole('button', { name: '复制' })) expect(exec).toHaveBeenCalledWith('copy') expect(await screen.findByRole('button', { name: '复制成功' })).toBeTruthy() }) it('does not claim success when execCommand throws or is absent', async () => { Object.defineProperty(navigator, 'clipboard', { configurable: true, value: undefined, }) Object.defineProperty(document, 'execCommand', { configurable: true, value: () => { throw new Error('denied') }, }) const denied = render() fireEvent.click(denied.getByRole('button', { name: '复制' })) await Promise.resolve() expect(denied.getByRole('button', { name: '复制' })).toBeTruthy() denied.unmount() Object.defineProperty(document, 'execCommand', { configurable: true, value: undefined, }) const absent = render() fireEvent.click(absent.getByRole('button', { name: '复制' })) await Promise.resolve() expect(absent.getByRole('button', { name: '复制' })).toBeTruthy() expect(absent.queryByRole('button', { name: '复制成功' })).toBeNull() }) })