diff --git a/__tests__/CodeView-test.js b/__tests__/CodeView-test.js
new file mode 100644
index 0000000..9a0250f
--- /dev/null
+++ b/__tests__/CodeView-test.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import CodeView from '../src/CodeView';
+import MarkdownRenderer from '../src/MarkdownRenderer';
+
+// Mock the MarkdownRenderer component
+jest.mock('../src/MarkdownRenderer', () =>
+ jest.fn(({ children }) => (
+
{children}
+ ))
+);
+
+describe('CodeView', () => {
+ it('renders without crashing', () => {
+ render();
+ expect(screen.getByTestId('mock-markdown-renderer')).toBeInTheDocument();
+ });
+
+ it('passes copyButtonProps to MarkdownRenderer', () => {
+ const mockCopyButtonProps = {
+ 'data-testid': 'custom-copy-btn',
+ className: 'custom-class',
+ };
+
+ render(
+
+ );
+
+ // Check if MarkdownRenderer was called with the correct props
+ expect(MarkdownRenderer).toHaveBeenCalledWith(
+ expect.objectContaining({
+ copyButtonProps: mockCopyButtonProps
+ }),
+ expect.anything()
+ );
+ });
+
+ it('passes sourceCode to Renderer', () => {
+ const sourceCode = 'const a = 1;';
+ render();
+
+ // Check if the source code is passed down to the MarkdownRenderer
+ expect(MarkdownRenderer).toHaveBeenCalledWith(
+ expect.objectContaining({
+ children: sourceCode
+ }),
+ expect.anything()
+ );
+ });
+});
diff --git a/__tests__/MarkdownRenderer-test.js b/__tests__/MarkdownRenderer-test.js
new file mode 100644
index 0000000..0a45171
--- /dev/null
+++ b/__tests__/MarkdownRenderer-test.js
@@ -0,0 +1,152 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import copy from 'copy-to-clipboard';
+import MarkdownRenderer from '../src/MarkdownRenderer';
+
+// Mock the copy-to-clipboard library
+jest.mock('copy-to-clipboard');
+
+// Mock document.execCommand which is used as a fallback
+beforeEach(() => {
+ document.execCommand = jest.fn();
+ document.createRange = () => ({
+ setStart: () => true,
+ setEnd: () => true,
+ commonAncestorContainer: {
+ nodeName: 'BODY',
+ ownerDocument: document,
+ },
+ });
+ window.getSelection = () => ({
+ removeAllRanges: () => undefined,
+ addRange: () => undefined,
+ toString: () => '',
+ });
+
+ // Mock the clipboard API
+ Object.defineProperty(navigator, 'clipboard', {
+ value: {
+ writeText: jest.fn().mockResolvedValue(undefined),
+ },
+ writable: true,
+ configurable: true,
+ });
+
+ // Reset all mocks
+ jest.clearAllMocks();
+});
+
+afterEach(() => {
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
+});
+
+describe('MarkdownRenderer', () => {
+ it('renders without crashing', () => {
+ render({'Test
'});
+ expect(screen.getByText('Test').tagName).toBe('H2');
+ });
+
+ it('renders code blocks with copy button', () => {
+ const { container } = render(
+
+ {``}
+
+ );
+
+ const codeBlock = container.querySelector('.rcv-code-renderer');
+ expect(codeBlock).toBeInTheDocument();
+
+ // Check if copy button is rendered with the correct class
+ const copyButton = container.querySelector('button[title="Copy code"]');
+ expect(copyButton).toBeInTheDocument();
+ expect(copyButton).toHaveClass('btn-copy-code');
+ });
+
+ it('applies custom copy button props', () => {
+ const customProps = {
+ 'data-testid': 'custom-copy-button',
+ class: 'custom-button-class',
+ onClick: jest.fn(),
+ };
+
+ const { container } = render(
+
+ {``}
+
+ );
+
+ const copyButton = container.querySelector('button[title="Copy code"]');
+ expect(copyButton).toHaveAttribute('data-testid', 'custom-copy-button');
+ expect(copyButton.getAttribute('class')).toEqual('custom-button-class');
+ expect(copyButton.getAttribute('data-testid')).toEqual('custom-copy-button');
+ });
+
+ it('copies code to clipboard when copy button is clicked', () => {
+ const { container } = render(
+
+ {``}
+
+ );
+
+ const copyButton = container.querySelector('button[title="Copy code"]');
+ fireEvent.click(copyButton);
+
+ // Check if copy-to-clipboard was called with the correct code
+ expect(copy).toHaveBeenCalledWith('const test = \'test\';');
+ });
+
+ it('copies code using copy-to-clipboard', () => {
+ const { container } = render(
+
+ {``}
+
+ );
+
+ const copyButton = container.querySelector('button[title="Copy code"]');
+ fireEvent.click(copyButton);
+
+ // Verify copy was called with the correct text (including the semicolon)
+ expect(copy).toHaveBeenCalledWith('const test = \'test\';');
+
+ // Verify the copy button exists and has the correct title
+ expect(copyButton).toBeInTheDocument();
+ expect(copyButton).toHaveAttribute('title', 'Copy code');
+
+ // Verify the copy function was called with the correct text
+ expect(copy).toHaveBeenCalledWith('const test = \'test\';');
+ });
+
+ it('adds copy button by default', () => {
+ const { container } = render(
+
+ {``}
+
+ );
+
+ const copyButton = container.querySelector('button[title="Copy code"]');
+ expect(copyButton).toBeInTheDocument();
+ });
+
+ it('handles multiple code blocks', () => {
+ const { container } = render(
+
+ {`
+
+
+ `}
+
+ );
+
+ const copyButtons = container.querySelectorAll('button[title="Copy code"]');
+ expect(copyButtons).toHaveLength(2);
+
+ // Test first button
+ fireEvent.click(copyButtons[0]);
+ expect(copy).toHaveBeenCalledWith('first block');
+
+ // Test second button
+ fireEvent.click(copyButtons[1]);
+ expect(copy).toHaveBeenCalledWith('second block');
+ });
+});
diff --git a/src/CodeView.tsx b/src/CodeView.tsx
index fd26b8f..588fec3 100644
--- a/src/CodeView.tsx
+++ b/src/CodeView.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/no-var-requires */
import React from 'react';
import MarkdownRenderer from './MarkdownRenderer';
import parseHTML from './utils/parseHTML';
@@ -10,6 +9,9 @@ export interface CodeViewProps extends RendererProps {
/** The code to be rendered is executed */
sourceCode?: string;
+
+ /** The properties of the copy button */
+ copyButtonProps?: React.HTMLAttributes;
}
const CodeView = React.forwardRef((props: CodeViewProps, ref: React.Ref) => {
@@ -21,6 +23,7 @@ const CodeView = React.forwardRef((props: CodeViewProps, ref: React.Ref
);
} else if (fragment.type === 'html') {
- return {fragment.content};
+ return (
+
+ {fragment.content}
+
+ );
}
})}
diff --git a/src/MarkdownRenderer.tsx b/src/MarkdownRenderer.tsx
index c05f4f9..3c4e6a0 100644
--- a/src/MarkdownRenderer.tsx
+++ b/src/MarkdownRenderer.tsx
@@ -7,18 +7,22 @@ import { iconPath as checkPath } from './icons/Check';
interface MarkdownRendererProps extends React.HTMLAttributes {
children?: string | null;
+ copyButtonProps?: React.HTMLAttributes;
}
-function appendCopyButton(container?: HTMLDivElement | null) {
+function appendCopyButton(
+ container?: HTMLDivElement | null,
+ buttonProps?: React.HTMLAttributes
+) {
if (!container) {
return;
}
const button = document.createElement('button');
- button.className =
- 'copy-code-button rs-btn-icon rs-btn-icon-circle rs-btn rs-btn-subtle rs-btn-xs';
+ button.className = 'btn-copy-code';
button.title = 'Copy code';
button.innerHTML = svgTpl(copyPath);
+
button.onclick = e => {
e.preventDefault();
const code = container?.querySelector('code')?.textContent;
@@ -33,18 +37,26 @@ function appendCopyButton(container?: HTMLDivElement | null) {
icon?.setAttribute('d', copyPath);
}, 2000);
};
+
+ if (buttonProps) {
+ Object.entries(buttonProps || {}).forEach(([key, value]) => {
+ button.setAttribute(key, value);
+ });
+ }
+
container?.appendChild(button);
}
const MarkdownRenderer = React.forwardRef(
(props: MarkdownRendererProps, ref: React.Ref) => {
- const { children, className, ...rest } = props;
+ const { children, className, copyButtonProps, ...rest } = props;
const mdRef = React.useRef(null);
useEffect(() => {
mdRef.current?.querySelectorAll('.rcv-code-renderer').forEach((el: any) => {
- appendCopyButton(el);
+ appendCopyButton(el, copyButtonProps);
});
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!children) {