Skip to content

Commit 0af8fe7

Browse files
authored
Merge pull request #783 from thatblindgeye/iss782
fix(CodeBlockMessage): resolved copy issue after markdown update
2 parents 7079612 + 56bed29 commit 0af8fe7

File tree

2 files changed

+183
-3
lines changed

2 files changed

+183
-3
lines changed
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import '@testing-library/jest-dom';
2+
import { render, screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import CodeBlockMessage from './CodeBlockMessage';
5+
6+
// Mock clipboard API
7+
Object.assign(navigator, {
8+
clipboard: {
9+
writeText: jest.fn()
10+
}
11+
});
12+
13+
describe('CodeBlockMessage', () => {
14+
beforeEach(() => {
15+
jest.clearAllMocks();
16+
});
17+
18+
it('should render inline code for single-line content', () => {
19+
render(<CodeBlockMessage className="language-javascript">const x = 5;</CodeBlockMessage>);
20+
const code = screen.getByText('const x = 5;');
21+
expect(code.tagName).toBe('CODE');
22+
expect(code).toHaveClass('pf-chatbot__message-inline-code');
23+
});
24+
25+
it('should render code block for multi-line content', () => {
26+
const multilineCode = 'const x = 5;\nconst y = 10;';
27+
const { container } = render(<CodeBlockMessage className="language-javascript">{multilineCode}</CodeBlockMessage>);
28+
const codeElement = container.querySelector('code');
29+
expect(codeElement?.textContent).toBe(multilineCode);
30+
});
31+
32+
it('should display language label', () => {
33+
const code = 'const x = 5;\nconst y = 10;';
34+
render(<CodeBlockMessage className="language-javascript">{code}</CodeBlockMessage>);
35+
expect(screen.getByText('javascript')).toBeInTheDocument();
36+
});
37+
38+
it('should render copy button', () => {
39+
const code = 'const x = 5;\nconst y = 10;';
40+
render(<CodeBlockMessage>{code}</CodeBlockMessage>);
41+
expect(screen.getByRole('button', { name: 'Copy code' })).toBeInTheDocument();
42+
});
43+
44+
it('should copy plain string content to clipboard', async () => {
45+
const code = 'const x = 5;\nconst y = 10;';
46+
render(<CodeBlockMessage>{code}</CodeBlockMessage>);
47+
48+
const copyButton = screen.getByRole('button', { name: 'Copy code' });
49+
await userEvent.click(copyButton);
50+
51+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(code);
52+
});
53+
54+
it('should extract text content from React elements when copying', async () => {
55+
// Simulate what happens with syntax highlighting - children become React elements
56+
const { container } = render(
57+
<CodeBlockMessage className="language-javascript">
58+
<span className="hljs-keyword">const</span> x = 5;{'\n'}
59+
<span className="hljs-keyword">const</span> y = 10;
60+
</CodeBlockMessage>
61+
);
62+
63+
const copyButton = screen.getByRole('button', { name: 'Copy code' });
64+
await userEvent.click(copyButton);
65+
66+
// Should extract actual text content from DOM, not "[object Object]"
67+
const codeElement = container.querySelector('code');
68+
const expectedText = codeElement?.textContent || '';
69+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expectedText);
70+
expect(expectedText).not.toContain('[object Object]');
71+
});
72+
73+
it('should show check icon after copying', async () => {
74+
const code = 'const x = 5;\nconst y = 10;';
75+
render(<CodeBlockMessage>{code}</CodeBlockMessage>);
76+
77+
const copyButton = screen.getByRole('button', { name: 'Copy code' });
78+
await userEvent.click(copyButton);
79+
80+
// Check icon should be visible (we can verify by checking if CopyIcon is not present)
81+
const svgElement = copyButton.querySelector('svg');
82+
expect(svgElement).toBeInTheDocument();
83+
});
84+
85+
it('should render expandable section when isExpandable is true', () => {
86+
const code = 'const x = 5;\nconst y = 10;';
87+
render(<CodeBlockMessage isExpandable>{code}</CodeBlockMessage>);
88+
89+
expect(screen.getByRole('button', { name: 'Show more' })).toBeInTheDocument();
90+
});
91+
92+
it('should toggle expandable section', async () => {
93+
const code = 'const x = 5;\nconst y = 10;';
94+
render(<CodeBlockMessage isExpandable>{code}</CodeBlockMessage>);
95+
96+
const toggleButton = screen.getByRole('button', { name: 'Show more' });
97+
await userEvent.click(toggleButton);
98+
99+
expect(screen.getByRole('button', { name: 'Show less' })).toBeInTheDocument();
100+
});
101+
102+
it('should use custom expanded/collapsed text', () => {
103+
const code = 'const x = 5;\nconst y = 10;';
104+
render(
105+
<CodeBlockMessage isExpandable expandedText="Hide" collapsedText="Reveal">
106+
{code}
107+
</CodeBlockMessage>
108+
);
109+
110+
expect(screen.getByRole('button', { name: 'Reveal' })).toBeInTheDocument();
111+
});
112+
113+
it('should pass through expandableSectionProps', () => {
114+
const code = 'const x = 5;\nconst y = 10;';
115+
const { container } = render(
116+
<CodeBlockMessage isExpandable expandableSectionProps={{ className: 'custom-expandable-class' }}>
117+
{code}
118+
</CodeBlockMessage>
119+
);
120+
121+
const expandableSection = container.querySelector('.pf-v6-c-expandable-section.custom-expandable-class');
122+
expect(expandableSection).toBeInTheDocument();
123+
});
124+
125+
it('should render custom actions', () => {
126+
const code = 'const x = 5;\nconst y = 10;';
127+
const customAction = <button aria-label="Custom action">Custom</button>;
128+
render(<CodeBlockMessage customActions={customAction}>{code}</CodeBlockMessage>);
129+
130+
expect(screen.getByRole('button', { name: 'Custom action' })).toBeInTheDocument();
131+
});
132+
133+
it('should apply isPrimary class to inline code', () => {
134+
render(<CodeBlockMessage isPrimary>const x = 5;</CodeBlockMessage>);
135+
const code = screen.getByText('const x = 5;');
136+
expect(code).toHaveClass('pf-m-primary');
137+
});
138+
139+
it('should apply shouldRetainStyles class to code block', () => {
140+
const code = 'const x = 5;\nconst y = 10;';
141+
const { container } = render(<CodeBlockMessage shouldRetainStyles>{code}</CodeBlockMessage>);
142+
143+
const codeBlockDiv = container.querySelector('.pf-chatbot__message-code-block');
144+
expect(codeBlockDiv).toHaveClass('pf-m-markdown');
145+
});
146+
147+
it('should use custom aria-label for copy button', () => {
148+
const code = 'const x = 5;\nconst y = 10;';
149+
render(<CodeBlockMessage aria-label="Copy this code">{code}</CodeBlockMessage>);
150+
151+
expect(screen.getByRole('button', { name: 'Copy this code' })).toBeInTheDocument();
152+
});
153+
154+
it('should prioritize data-expanded-text over expandedText prop', () => {
155+
const code = 'const x = 5;\nconst y = 10;';
156+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn());
157+
158+
render(
159+
<CodeBlockMessage isExpandable expandedText="Custom Expanded" data-expanded-text="Data Expanded">
160+
{code}
161+
</CodeBlockMessage>
162+
);
163+
164+
expect(consoleErrorSpy).toHaveBeenCalledWith(
165+
'Message:',
166+
expect.stringContaining('data-expanded-text or data-collapsed-text will override')
167+
);
168+
169+
consoleErrorSpy.mockRestore();
170+
});
171+
});

packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,22 @@ const CodeBlockMessage = ({
9292
);
9393
}
9494

95-
const onToggle = (isExpanded) => {
95+
const onToggle = (isExpanded: boolean) => {
9696
setIsExpanded(isExpanded);
9797
};
9898

9999
// Handle clicking copy button
100-
const handleCopy = useCallback((event, text) => {
101-
navigator.clipboard.writeText(text.toString());
100+
const handleCopy = useCallback((_event: React.MouseEvent, text: React.ReactNode) => {
101+
let textToCopy = '';
102+
if (typeof text === 'string') {
103+
textToCopy = text;
104+
} else {
105+
if (codeBlockRef.current) {
106+
const codeElement = codeBlockRef.current.querySelector('code');
107+
textToCopy = codeElement?.textContent || '';
108+
}
109+
}
110+
navigator.clipboard.writeText(textToCopy);
102111
setCopied(true);
103112
}, []);
104113

0 commit comments

Comments
 (0)