Barazo default frontend
barazo.forum
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})