diff --git a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.test.tsx b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.test.tsx new file mode 100644 index 000000000..a920df3a0 --- /dev/null +++ b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.test.tsx @@ -0,0 +1,171 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import CodeBlockMessage from './CodeBlockMessage'; + +// Mock clipboard API +Object.assign(navigator, { + clipboard: { + writeText: jest.fn() + } +}); + +describe('CodeBlockMessage', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render inline code for single-line content', () => { + render(const x = 5;); + const code = screen.getByText('const x = 5;'); + expect(code.tagName).toBe('CODE'); + expect(code).toHaveClass('pf-chatbot__message-inline-code'); + }); + + it('should render code block for multi-line content', () => { + const multilineCode = 'const x = 5;\nconst y = 10;'; + const { container } = render({multilineCode}); + const codeElement = container.querySelector('code'); + expect(codeElement?.textContent).toBe(multilineCode); + }); + + it('should display language label', () => { + const code = 'const x = 5;\nconst y = 10;'; + render({code}); + expect(screen.getByText('javascript')).toBeInTheDocument(); + }); + + it('should render copy button', () => { + const code = 'const x = 5;\nconst y = 10;'; + render({code}); + expect(screen.getByRole('button', { name: 'Copy code' })).toBeInTheDocument(); + }); + + it('should copy plain string content to clipboard', async () => { + const code = 'const x = 5;\nconst y = 10;'; + render({code}); + + const copyButton = screen.getByRole('button', { name: 'Copy code' }); + await userEvent.click(copyButton); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(code); + }); + + it('should extract text content from React elements when copying', async () => { + // Simulate what happens with syntax highlighting - children become React elements + const { container } = render( + + const x = 5;{'\n'} + const y = 10; + + ); + + const copyButton = screen.getByRole('button', { name: 'Copy code' }); + await userEvent.click(copyButton); + + // Should extract actual text content from DOM, not "[object Object]" + const codeElement = container.querySelector('code'); + const expectedText = codeElement?.textContent || ''; + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expectedText); + expect(expectedText).not.toContain('[object Object]'); + }); + + it('should show check icon after copying', async () => { + const code = 'const x = 5;\nconst y = 10;'; + render({code}); + + const copyButton = screen.getByRole('button', { name: 'Copy code' }); + await userEvent.click(copyButton); + + // Check icon should be visible (we can verify by checking if CopyIcon is not present) + const svgElement = copyButton.querySelector('svg'); + expect(svgElement).toBeInTheDocument(); + }); + + it('should render expandable section when isExpandable is true', () => { + const code = 'const x = 5;\nconst y = 10;'; + render({code}); + + expect(screen.getByRole('button', { name: 'Show more' })).toBeInTheDocument(); + }); + + it('should toggle expandable section', async () => { + const code = 'const x = 5;\nconst y = 10;'; + render({code}); + + const toggleButton = screen.getByRole('button', { name: 'Show more' }); + await userEvent.click(toggleButton); + + expect(screen.getByRole('button', { name: 'Show less' })).toBeInTheDocument(); + }); + + it('should use custom expanded/collapsed text', () => { + const code = 'const x = 5;\nconst y = 10;'; + render( + + {code} + + ); + + expect(screen.getByRole('button', { name: 'Reveal' })).toBeInTheDocument(); + }); + + it('should pass through expandableSectionProps', () => { + const code = 'const x = 5;\nconst y = 10;'; + const { container } = render( + + {code} + + ); + + const expandableSection = container.querySelector('.pf-v6-c-expandable-section.custom-expandable-class'); + expect(expandableSection).toBeInTheDocument(); + }); + + it('should render custom actions', () => { + const code = 'const x = 5;\nconst y = 10;'; + const customAction = ; + render({code}); + + expect(screen.getByRole('button', { name: 'Custom action' })).toBeInTheDocument(); + }); + + it('should apply isPrimary class to inline code', () => { + render(const x = 5;); + const code = screen.getByText('const x = 5;'); + expect(code).toHaveClass('pf-m-primary'); + }); + + it('should apply shouldRetainStyles class to code block', () => { + const code = 'const x = 5;\nconst y = 10;'; + const { container } = render({code}); + + const codeBlockDiv = container.querySelector('.pf-chatbot__message-code-block'); + expect(codeBlockDiv).toHaveClass('pf-m-markdown'); + }); + + it('should use custom aria-label for copy button', () => { + const code = 'const x = 5;\nconst y = 10;'; + render({code}); + + expect(screen.getByRole('button', { name: 'Copy this code' })).toBeInTheDocument(); + }); + + it('should prioritize data-expanded-text over expandedText prop', () => { + const code = 'const x = 5;\nconst y = 10;'; + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn()); + + render( + + {code} + + ); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Message:', + expect.stringContaining('data-expanded-text or data-collapsed-text will override') + ); + + consoleErrorSpy.mockRestore(); + }); +}); diff --git a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx index 1e051ff42..ca38e2e12 100644 --- a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx +++ b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx @@ -92,13 +92,22 @@ const CodeBlockMessage = ({ ); } - const onToggle = (isExpanded) => { + const onToggle = (isExpanded: boolean) => { setIsExpanded(isExpanded); }; // Handle clicking copy button - const handleCopy = useCallback((event, text) => { - navigator.clipboard.writeText(text.toString()); + const handleCopy = useCallback((_event: React.MouseEvent, text: React.ReactNode) => { + let textToCopy = ''; + if (typeof text === 'string') { + textToCopy = text; + } else { + if (codeBlockRef.current) { + const codeElement = codeBlockRef.current.querySelector('code'); + textToCopy = codeElement?.textContent || ''; + } + } + navigator.clipboard.writeText(textToCopy); setCopied(true); }, []);