Barazo default frontend barazo.forum
at main 225 lines 8.9 kB view raw
1/** 2 * Tests for MarkdownEditor component. 3 */ 4 5import { describe, it, expect, vi } from 'vitest' 6import { render, screen } from '@testing-library/react' 7import userEvent from '@testing-library/user-event' 8import { axe } from 'vitest-axe' 9import { MarkdownEditor } from './markdown-editor' 10 11describe('MarkdownEditor', () => { 12 it('renders a labeled textarea', () => { 13 render(<MarkdownEditor value="" onChange={vi.fn()} id="content" label="Content" />) 14 expect(screen.getByRole('textbox', { name: 'Content' })).toBeInTheDocument() 15 }) 16 17 it('calls onChange when typing', async () => { 18 const user = userEvent.setup() 19 const onChange = vi.fn() 20 render(<MarkdownEditor value="" onChange={onChange} id="content" label="Content" />) 21 await user.type(screen.getByRole('textbox', { name: 'Content' }), 'Hello') 22 expect(onChange).toHaveBeenCalled() 23 }) 24 25 it('renders toolbar with formatting buttons', () => { 26 render(<MarkdownEditor value="" onChange={vi.fn()} id="content" label="Content" />) 27 const toolbar = screen.getByRole('toolbar', { name: 'Formatting' }) 28 expect(toolbar).toBeInTheDocument() 29 expect(screen.getByRole('button', { name: 'Bold' })).toBeInTheDocument() 30 expect(screen.getByRole('button', { name: 'Italic' })).toBeInTheDocument() 31 expect(screen.getByRole('button', { name: 'Link' })).toBeInTheDocument() 32 expect(screen.getByRole('button', { name: 'Code' })).toBeInTheDocument() 33 expect(screen.getByRole('button', { name: 'Quote' })).toBeInTheDocument() 34 expect(screen.getByRole('button', { name: 'List' })).toBeInTheDocument() 35 }) 36 37 it('wraps selected text with bold markers', async () => { 38 const user = userEvent.setup() 39 const onChange = vi.fn() 40 render(<MarkdownEditor value="hello world" onChange={onChange} id="content" label="Content" />) 41 const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 42 43 // Select "world" 44 textarea.setSelectionRange(6, 11) 45 await user.click(screen.getByRole('button', { name: 'Bold' })) 46 expect(onChange).toHaveBeenCalledWith('hello **world**') 47 }) 48 49 it('wraps selected text with italic markers', async () => { 50 const user = userEvent.setup() 51 const onChange = vi.fn() 52 render(<MarkdownEditor value="hello world" onChange={onChange} id="content" label="Content" />) 53 const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 54 55 textarea.setSelectionRange(6, 11) 56 await user.click(screen.getByRole('button', { name: 'Italic' })) 57 expect(onChange).toHaveBeenCalledWith('hello *world*') 58 }) 59 60 it('inserts link template when no selection', async () => { 61 const user = userEvent.setup() 62 const onChange = vi.fn() 63 render(<MarkdownEditor value="hello " onChange={onChange} id="content" label="Content" />) 64 const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 65 66 textarea.setSelectionRange(6, 6) 67 await user.click(screen.getByRole('button', { name: 'Link' })) 68 expect(onChange).toHaveBeenCalledWith('hello [text](url)') 69 }) 70 71 it('wraps selected text with code markers', async () => { 72 const user = userEvent.setup() 73 const onChange = vi.fn() 74 render(<MarkdownEditor value="hello code" onChange={onChange} id="content" label="Content" />) 75 const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 76 77 textarea.setSelectionRange(6, 10) 78 await user.click(screen.getByRole('button', { name: 'Code' })) 79 expect(onChange).toHaveBeenCalledWith('hello `code`') 80 }) 81 82 it('prefixes line with quote marker', async () => { 83 const user = userEvent.setup() 84 const onChange = vi.fn() 85 render(<MarkdownEditor value="hello" onChange={onChange} id="content" label="Content" />) 86 const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 87 88 textarea.setSelectionRange(0, 5) 89 await user.click(screen.getByRole('button', { name: 'Quote' })) 90 expect(onChange).toHaveBeenCalledWith('> hello') 91 }) 92 93 it('prefixes line with list marker', async () => { 94 const user = userEvent.setup() 95 const onChange = vi.fn() 96 render(<MarkdownEditor value="hello" onChange={onChange} id="content" label="Content" />) 97 const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 98 99 textarea.setSelectionRange(0, 5) 100 await user.click(screen.getByRole('button', { name: 'List' })) 101 expect(onChange).toHaveBeenCalledWith('- hello') 102 }) 103 104 it('supports roving tabindex on toolbar buttons', async () => { 105 const user = userEvent.setup() 106 render(<MarkdownEditor value="" onChange={vi.fn()} id="content" label="Content" />) 107 108 const boldBtn = screen.getByRole('button', { name: 'Bold' }) 109 const italicBtn = screen.getByRole('button', { name: 'Italic' }) 110 111 // First button is tabbable, others have tabindex -1 112 expect(boldBtn).toHaveAttribute('tabindex', '0') 113 expect(italicBtn).toHaveAttribute('tabindex', '-1') 114 115 // Focus first button, arrow right to move focus 116 boldBtn.focus() 117 await user.keyboard('{ArrowRight}') 118 expect(italicBtn).toHaveFocus() 119 }) 120 121 it('shows error message when provided', () => { 122 render( 123 <MarkdownEditor 124 value="" 125 onChange={vi.fn()} 126 id="content" 127 label="Content" 128 error="Content is required" 129 /> 130 ) 131 expect(screen.getByText('Content is required')).toBeInTheDocument() 132 expect(screen.getByRole('textbox', { name: 'Content' })).toHaveAttribute('aria-invalid', 'true') 133 }) 134 135 describe('smart link paste', () => { 136 function pasteUrl(textarea: HTMLTextAreaElement, url: string) { 137 const event = new Event('paste', { bubbles: true, cancelable: true }) 138 Object.defineProperty(event, 'clipboardData', { 139 value: { getData: (type: string) => (type === 'text/plain' ? url : '') }, 140 }) 141 textarea.dispatchEvent(event) 142 return event 143 } 144 145 it('wraps selected text as markdown link when pasting a URL', () => { 146 const onChange = vi.fn() 147 render( 148 <MarkdownEditor value="check this out" onChange={onChange} id="content" label="Content" /> 149 ) 150 const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 151 152 // Select "this" 153 textarea.setSelectionRange(6, 10) 154 pasteUrl(textarea, 'https://example.com') 155 156 expect(onChange).toHaveBeenCalledWith('check [this](https://example.com) out') 157 }) 158 159 it('does not intercept paste when no text is selected', () => { 160 const onChange = vi.fn() 161 render(<MarkdownEditor value="hello " onChange={onChange} id="content" label="Content" />) 162 const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 163 164 textarea.setSelectionRange(6, 6) 165 const event = pasteUrl(textarea, 'https://example.com') 166 167 // Event should not be prevented — default paste behavior 168 expect(event.defaultPrevented).toBe(false) 169 expect(onChange).not.toHaveBeenCalled() 170 }) 171 172 it('does not intercept paste when pasted text is not a URL', () => { 173 const onChange = vi.fn() 174 render( 175 <MarkdownEditor value="hello world" onChange={onChange} id="content" label="Content" /> 176 ) 177 const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 178 179 textarea.setSelectionRange(6, 11) 180 const event = pasteUrl(textarea, 'not a url') 181 182 expect(event.defaultPrevented).toBe(false) 183 expect(onChange).not.toHaveBeenCalled() 184 }) 185 186 it('handles http:// URLs', () => { 187 const onChange = vi.fn() 188 render( 189 <MarkdownEditor 190 value="click here please" 191 onChange={onChange} 192 id="content" 193 label="Content" 194 /> 195 ) 196 const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 197 198 textarea.setSelectionRange(6, 10) 199 pasteUrl(textarea, 'http://example.com') 200 201 expect(onChange).toHaveBeenCalledWith('click [here](http://example.com) please') 202 }) 203 204 it('places cursor after the inserted link', () => { 205 const onChange = vi.fn() 206 render( 207 <MarkdownEditor value="see docs now" onChange={onChange} id="content" label="Content" /> 208 ) 209 const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 210 211 textarea.setSelectionRange(4, 8) 212 pasteUrl(textarea, 'https://docs.example.com') 213 214 expect(onChange).toHaveBeenCalledWith('see [docs](https://docs.example.com) now') 215 }) 216 }) 217 218 it('passes axe accessibility check', async () => { 219 const { container } = render( 220 <MarkdownEditor value="Some content" onChange={vi.fn()} id="content" label="Content" /> 221 ) 222 const results = await axe(container) 223 expect(results).toHaveNoViolations() 224 }) 225})