1+ import { describe , it , expect , vi , beforeEach , afterEach } from 'vitest' ;
2+ import { render , screen , fireEvent , waitFor } from '@testing-library/react' ;
3+ import '@testing-library/jest-dom' ;
4+ import BlockquoteCopyButton from './BlockquoteCopyButton' ;
5+
6+ // Mock clipboard API
7+ const mockWriteText = vi . fn ( ) ;
8+ Object . assign ( navigator , {
9+ clipboard : {
10+ writeText : mockWriteText ,
11+ } ,
12+ } ) ;
13+
14+ // Mock console methods
15+ const consoleSpy = vi . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
16+
17+ describe ( 'BlockquoteCopyButton' , ( ) => {
18+ let mockBlockquoteElement : HTMLElement ;
19+
20+ beforeEach ( ( ) => {
21+ // Create a mock blockquote element
22+ mockBlockquoteElement = document . createElement ( 'blockquote' ) ;
23+ mockBlockquoteElement . innerHTML = '<p>Test content</p>' ;
24+ document . body . appendChild ( mockBlockquoteElement ) ;
25+
26+ // Reset mocks
27+ mockWriteText . mockClear ( ) ;
28+ consoleSpy . mockClear ( ) ;
29+ } ) ;
30+
31+ afterEach ( ( ) => {
32+ // Clean up
33+ document . body . removeChild ( mockBlockquoteElement ) ;
34+ vi . clearAllTimers ( ) ;
35+ } ) ;
36+
37+ it ( 'renders with default "Copy" text' , ( ) => {
38+ render (
39+ < BlockquoteCopyButton blockquoteElement = { mockBlockquoteElement } />
40+ ) ;
41+
42+ expect ( screen . getByRole ( 'button' ) ) . toHaveTextContent ( 'Copy' ) ;
43+ expect ( screen . getByRole ( 'button' ) ) . toHaveAttribute ( 'aria-label' , 'Copy blockquote content' ) ;
44+ } ) ;
45+
46+ it ( 'copies text content when clicked' , async ( ) => {
47+ mockWriteText . mockResolvedValueOnce ( undefined ) ;
48+
49+ render (
50+ < BlockquoteCopyButton blockquoteElement = { mockBlockquoteElement } />
51+ ) ;
52+
53+ fireEvent . click ( screen . getByRole ( 'button' ) ) ;
54+
55+ await waitFor ( ( ) => {
56+ expect ( mockWriteText ) . toHaveBeenCalledWith ( 'Test content' ) ;
57+ } ) ;
58+ } ) ;
59+
60+ it ( 'copies markdown content when contentType is text/markdown' , async ( ) => {
61+ mockWriteText . mockResolvedValueOnce ( undefined ) ;
62+ mockBlockquoteElement . innerHTML = '<p><strong>Bold text</strong> and <em>italic text</em></p>' ;
63+
64+ render (
65+ < BlockquoteCopyButton
66+ blockquoteElement = { mockBlockquoteElement }
67+ contentType = "text/markdown"
68+ />
69+ ) ;
70+
71+ fireEvent . click ( screen . getByRole ( 'button' ) ) ;
72+
73+ await waitFor ( ( ) => {
74+ expect ( mockWriteText ) . toHaveBeenCalledWith ( '**Bold text** and *italic text*' ) ;
75+ } ) ;
76+ } ) ;
77+
78+ it ( 'shows "Copied!" feedback after successful copy' , async ( ) => {
79+ mockWriteText . mockResolvedValueOnce ( undefined ) ;
80+
81+ render (
82+ < BlockquoteCopyButton blockquoteElement = { mockBlockquoteElement } />
83+ ) ;
84+
85+ const button = screen . getByRole ( 'button' ) ;
86+ fireEvent . click ( button ) ;
87+
88+ // Wait for the state to change to "Copied!"
89+ await waitFor ( ( ) => {
90+ expect ( button ) . toHaveTextContent ( 'Copied!' ) ;
91+ expect ( button ) . toHaveClass ( 'copied' ) ;
92+ } , { timeout : 1000 } ) ;
93+
94+ expect ( mockWriteText ) . toHaveBeenCalledWith ( 'Test content' ) ;
95+ } ) ;
96+
97+ it ( 'shows "Error" feedback when copy fails' , async ( ) => {
98+ const error = new Error ( 'Clipboard not available' ) ;
99+ mockWriteText . mockRejectedValueOnce ( error ) ;
100+
101+ render (
102+ < BlockquoteCopyButton blockquoteElement = { mockBlockquoteElement } />
103+ ) ;
104+
105+ const button = screen . getByRole ( 'button' ) ;
106+ fireEvent . click ( button ) ;
107+
108+ // Wait for the state to change to "Error"
109+ await waitFor ( ( ) => {
110+ expect ( button ) . toHaveTextContent ( 'Error' ) ;
111+ expect ( button ) . toHaveClass ( 'error' ) ;
112+ } , { timeout : 1000 } ) ;
113+
114+ expect ( consoleSpy ) . toHaveBeenCalledWith ( 'Failed to copy text: ' , error ) ;
115+ expect ( mockWriteText ) . toHaveBeenCalledWith ( 'Test content' ) ;
116+ } ) ;
117+
118+ it ( 'removes existing copy buttons from cloned content' , async ( ) => {
119+ mockWriteText . mockResolvedValueOnce ( undefined ) ;
120+
121+ // Add a copy button to the mock blockquote
122+ const existingButton = document . createElement ( 'button' ) ;
123+ existingButton . className = 'blockquote-copy-button' ;
124+ existingButton . textContent = 'Copy' ;
125+ mockBlockquoteElement . appendChild ( existingButton ) ;
126+
127+ render (
128+ < BlockquoteCopyButton blockquoteElement = { mockBlockquoteElement } />
129+ ) ;
130+
131+ // Get all buttons and click the React-rendered one (should be the one with aria-label)
132+ const reactButton = screen . getByLabelText ( 'Copy blockquote content' ) ;
133+ fireEvent . click ( reactButton ) ;
134+
135+ await waitFor ( ( ) => {
136+ // Should copy only the text content, not including the button text
137+ expect ( mockWriteText ) . toHaveBeenCalledWith ( 'Test content' ) ;
138+ } , { timeout : 1000 } ) ;
139+ } ) ;
140+
141+ it ( 'prevents event propagation and default behavior' , ( ) => {
142+ const mockEvent = {
143+ preventDefault : vi . fn ( ) ,
144+ stopPropagation : vi . fn ( ) ,
145+ target : document . createElement ( 'button' ) ,
146+ } ;
147+
148+ render (
149+ < BlockquoteCopyButton blockquoteElement = { mockBlockquoteElement } />
150+ ) ;
151+
152+ const button = screen . getByRole ( 'button' ) ;
153+ fireEvent . click ( button , mockEvent ) ;
154+
155+ // Note: This test verifies the click handler exists and works
156+ // The actual preventDefault/stopPropagation calls are tested indirectly
157+ expect ( button ) . toBeInTheDocument ( ) ;
158+ } ) ;
159+
160+ it ( 'handles empty blockquote content' , async ( ) => {
161+ mockWriteText . mockResolvedValueOnce ( undefined ) ;
162+ mockBlockquoteElement . innerHTML = '' ;
163+
164+ render (
165+ < BlockquoteCopyButton blockquoteElement = { mockBlockquoteElement } />
166+ ) ;
167+
168+ const button = screen . getByRole ( 'button' ) ;
169+ fireEvent . click ( button ) ;
170+
171+ await vi . waitFor ( ( ) => expect ( mockWriteText ) . toHaveBeenCalled ( ) ) ;
172+
173+ expect ( mockWriteText ) . toHaveBeenCalledWith ( '' ) ;
174+ } ) ;
175+
176+ it ( 'trims whitespace from copied content' , async ( ) => {
177+ mockWriteText . mockResolvedValueOnce ( undefined ) ;
178+ mockBlockquoteElement . innerHTML = ' <p> Test content </p> ' ;
179+
180+ render (
181+ < BlockquoteCopyButton blockquoteElement = { mockBlockquoteElement } />
182+ ) ;
183+
184+ const button = screen . getByRole ( 'button' ) ;
185+ fireEvent . click ( button ) ;
186+
187+ await vi . waitFor ( ( ) => expect ( mockWriteText ) . toHaveBeenCalled ( ) ) ;
188+
189+ expect ( mockWriteText ) . toHaveBeenCalledWith ( 'Test content' ) ;
190+ } ) ;
191+
192+ it ( 'has correct accessibility attributes' , ( ) => {
193+ render (
194+ < BlockquoteCopyButton blockquoteElement = { mockBlockquoteElement } />
195+ ) ;
196+
197+ const button = screen . getByRole ( 'button' ) ;
198+ expect ( button ) . toHaveAttribute ( 'type' , 'button' ) ;
199+ expect ( button ) . toHaveAttribute ( 'aria-label' , 'Copy blockquote content' ) ;
200+ } ) ;
201+ } ) ;
0 commit comments