Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions __tests__/CodeView-test.js
Original file line number Diff line number Diff line change
@@ -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 }) => (
<div data-testid="mock-markdown-renderer">{children}</div>
))
);

describe('CodeView', () => {
it('renders without crashing', () => {
render(<CodeView sourceCode="const a = 1;" />);
expect(screen.getByTestId('mock-markdown-renderer')).toBeInTheDocument();
});

it('passes copyButtonProps to MarkdownRenderer', () => {
const mockCopyButtonProps = {
'data-testid': 'custom-copy-btn',
className: 'custom-class',
};

render(
<CodeView
sourceCode="const a = 1;"
copyButtonProps={mockCopyButtonProps}
/>
);

// 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(<CodeView sourceCode={sourceCode} />);

// Check if the source code is passed down to the MarkdownRenderer
expect(MarkdownRenderer).toHaveBeenCalledWith(
expect.objectContaining({
children: sourceCode
}),
expect.anything()
);
});
});
152 changes: 152 additions & 0 deletions __tests__/MarkdownRenderer-test.js
Original file line number Diff line number Diff line change
@@ -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(<MarkdownRenderer>{'<h2>Test</h2>'}</MarkdownRenderer>);
expect(screen.getByText('Test').tagName).toBe('H2');
});

it('renders code blocks with copy button', () => {
const { container } = render(
<MarkdownRenderer>
{`<div class="rcv-code-renderer"><pre><code>const a = 1;</code></pre></div>`}
</MarkdownRenderer>
);

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(
<MarkdownRenderer copyButtonProps={customProps}>
{`<div class="rcv-code-renderer"><pre><code>const a = 1;</code></pre></div>`}
</MarkdownRenderer>
);

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(
<MarkdownRenderer>
{`<div class="rcv-code-renderer"><pre><code>const test = 'test';</code></pre></div>`}
</MarkdownRenderer>
);

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(
<MarkdownRenderer>
{`<div class="rcv-code-renderer"><pre><code>const test = 'test';</code></pre></div>`}
</MarkdownRenderer>
);

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(
<MarkdownRenderer>
{`<div class="rcv-code-renderer"><pre><code>with copy</code></pre></div>`}
</MarkdownRenderer>
);

const copyButton = container.querySelector('button[title="Copy code"]');
expect(copyButton).toBeInTheDocument();
});

it('handles multiple code blocks', () => {
const { container } = render(
<MarkdownRenderer>
{`
<div class="rcv-code-renderer"><pre><code>first block</code></pre></div>
<div class="rcv-code-renderer"><pre><code>second block</code></pre></div>
`}
</MarkdownRenderer>
);

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');
});
});
11 changes: 9 additions & 2 deletions src/CodeView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import React from 'react';
import MarkdownRenderer from './MarkdownRenderer';
import parseHTML from './utils/parseHTML';
Expand All @@ -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<HTMLButtonElement>;
}

const CodeView = React.forwardRef((props: CodeViewProps, ref: React.Ref<HTMLDivElement>) => {
Expand All @@ -21,6 +23,7 @@ const CodeView = React.forwardRef((props: CodeViewProps, ref: React.Ref<HTMLDivE
theme = 'light',
editable,
transformOptions,
copyButtonProps,
renderToolbar,
onChange,
beforeCompile,
Expand Down Expand Up @@ -57,7 +60,11 @@ const CodeView = React.forwardRef((props: CodeViewProps, ref: React.Ref<HTMLDivE
/>
);
} else if (fragment.type === 'html') {
return <MarkdownRenderer key={fragment.key}>{fragment.content}</MarkdownRenderer>;
return (
<MarkdownRenderer key={fragment.key} copyButtonProps={copyButtonProps}>
{fragment.content}
</MarkdownRenderer>
);
}
})}
</div>
Expand Down
22 changes: 17 additions & 5 deletions src/MarkdownRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@ import { iconPath as checkPath } from './icons/Check';

interface MarkdownRendererProps extends React.HTMLAttributes<HTMLDivElement> {
children?: string | null;
copyButtonProps?: React.HTMLAttributes<HTMLButtonElement>;
}

function appendCopyButton(container?: HTMLDivElement | null) {
function appendCopyButton(
container?: HTMLDivElement | null,
buttonProps?: React.HTMLAttributes<HTMLButtonElement>
) {
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;
Expand All @@ -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<HTMLDivElement>) => {
const { children, className, ...rest } = props;
const { children, className, copyButtonProps, ...rest } = props;
const mdRef = React.useRef<HTMLDivElement>(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) {
Expand Down
Loading