Skip to content

Commit 9df0726

Browse files
authored
Update MarkdownRenderer and add test files (#56)
1 parent bdaa85d commit 9df0726

File tree

4 files changed

+231
-7
lines changed

4 files changed

+231
-7
lines changed

__tests__/CodeView-test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import CodeView from '../src/CodeView';
4+
import MarkdownRenderer from '../src/MarkdownRenderer';
5+
6+
// Mock the MarkdownRenderer component
7+
jest.mock('../src/MarkdownRenderer', () =>
8+
jest.fn(({ children }) => (
9+
<div data-testid="mock-markdown-renderer">{children}</div>
10+
))
11+
);
12+
13+
describe('CodeView', () => {
14+
it('renders without crashing', () => {
15+
render(<CodeView sourceCode="const a = 1;" />);
16+
expect(screen.getByTestId('mock-markdown-renderer')).toBeInTheDocument();
17+
});
18+
19+
it('passes copyButtonProps to MarkdownRenderer', () => {
20+
const mockCopyButtonProps = {
21+
'data-testid': 'custom-copy-btn',
22+
className: 'custom-class',
23+
};
24+
25+
render(
26+
<CodeView
27+
sourceCode="const a = 1;"
28+
copyButtonProps={mockCopyButtonProps}
29+
/>
30+
);
31+
32+
// Check if MarkdownRenderer was called with the correct props
33+
expect(MarkdownRenderer).toHaveBeenCalledWith(
34+
expect.objectContaining({
35+
copyButtonProps: mockCopyButtonProps
36+
}),
37+
expect.anything()
38+
);
39+
});
40+
41+
it('passes sourceCode to Renderer', () => {
42+
const sourceCode = 'const a = 1;';
43+
render(<CodeView sourceCode={sourceCode} />);
44+
45+
// Check if the source code is passed down to the MarkdownRenderer
46+
expect(MarkdownRenderer).toHaveBeenCalledWith(
47+
expect.objectContaining({
48+
children: sourceCode
49+
}),
50+
expect.anything()
51+
);
52+
});
53+
});

__tests__/MarkdownRenderer-test.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import copy from 'copy-to-clipboard';
4+
import MarkdownRenderer from '../src/MarkdownRenderer';
5+
6+
// Mock the copy-to-clipboard library
7+
jest.mock('copy-to-clipboard');
8+
9+
// Mock document.execCommand which is used as a fallback
10+
beforeEach(() => {
11+
document.execCommand = jest.fn();
12+
document.createRange = () => ({
13+
setStart: () => true,
14+
setEnd: () => true,
15+
commonAncestorContainer: {
16+
nodeName: 'BODY',
17+
ownerDocument: document,
18+
},
19+
});
20+
window.getSelection = () => ({
21+
removeAllRanges: () => undefined,
22+
addRange: () => undefined,
23+
toString: () => '',
24+
});
25+
26+
// Mock the clipboard API
27+
Object.defineProperty(navigator, 'clipboard', {
28+
value: {
29+
writeText: jest.fn().mockResolvedValue(undefined),
30+
},
31+
writable: true,
32+
configurable: true,
33+
});
34+
35+
// Reset all mocks
36+
jest.clearAllMocks();
37+
});
38+
39+
afterEach(() => {
40+
jest.clearAllMocks();
41+
jest.restoreAllMocks();
42+
});
43+
44+
describe('MarkdownRenderer', () => {
45+
it('renders without crashing', () => {
46+
render(<MarkdownRenderer>{'<h2>Test</h2>'}</MarkdownRenderer>);
47+
expect(screen.getByText('Test').tagName).toBe('H2');
48+
});
49+
50+
it('renders code blocks with copy button', () => {
51+
const { container } = render(
52+
<MarkdownRenderer>
53+
{`<div class="rcv-code-renderer"><pre><code>const a = 1;</code></pre></div>`}
54+
</MarkdownRenderer>
55+
);
56+
57+
const codeBlock = container.querySelector('.rcv-code-renderer');
58+
expect(codeBlock).toBeInTheDocument();
59+
60+
// Check if copy button is rendered with the correct class
61+
const copyButton = container.querySelector('button[title="Copy code"]');
62+
expect(copyButton).toBeInTheDocument();
63+
expect(copyButton).toHaveClass('btn-copy-code');
64+
});
65+
66+
it('applies custom copy button props', () => {
67+
const customProps = {
68+
'data-testid': 'custom-copy-button',
69+
class: 'custom-button-class',
70+
onClick: jest.fn(),
71+
};
72+
73+
const { container } = render(
74+
<MarkdownRenderer copyButtonProps={customProps}>
75+
{`<div class="rcv-code-renderer"><pre><code>const a = 1;</code></pre></div>`}
76+
</MarkdownRenderer>
77+
);
78+
79+
const copyButton = container.querySelector('button[title="Copy code"]');
80+
expect(copyButton).toHaveAttribute('data-testid', 'custom-copy-button');
81+
expect(copyButton.getAttribute('class')).toEqual('custom-button-class');
82+
expect(copyButton.getAttribute('data-testid')).toEqual('custom-copy-button');
83+
});
84+
85+
it('copies code to clipboard when copy button is clicked', () => {
86+
const { container } = render(
87+
<MarkdownRenderer>
88+
{`<div class="rcv-code-renderer"><pre><code>const test = 'test';</code></pre></div>`}
89+
</MarkdownRenderer>
90+
);
91+
92+
const copyButton = container.querySelector('button[title="Copy code"]');
93+
fireEvent.click(copyButton);
94+
95+
// Check if copy-to-clipboard was called with the correct code
96+
expect(copy).toHaveBeenCalledWith('const test = \'test\';');
97+
});
98+
99+
it('copies code using copy-to-clipboard', () => {
100+
const { container } = render(
101+
<MarkdownRenderer>
102+
{`<div class="rcv-code-renderer"><pre><code>const test = 'test';</code></pre></div>`}
103+
</MarkdownRenderer>
104+
);
105+
106+
const copyButton = container.querySelector('button[title="Copy code"]');
107+
fireEvent.click(copyButton);
108+
109+
// Verify copy was called with the correct text (including the semicolon)
110+
expect(copy).toHaveBeenCalledWith('const test = \'test\';');
111+
112+
// Verify the copy button exists and has the correct title
113+
expect(copyButton).toBeInTheDocument();
114+
expect(copyButton).toHaveAttribute('title', 'Copy code');
115+
116+
// Verify the copy function was called with the correct text
117+
expect(copy).toHaveBeenCalledWith('const test = \'test\';');
118+
});
119+
120+
it('adds copy button by default', () => {
121+
const { container } = render(
122+
<MarkdownRenderer>
123+
{`<div class="rcv-code-renderer"><pre><code>with copy</code></pre></div>`}
124+
</MarkdownRenderer>
125+
);
126+
127+
const copyButton = container.querySelector('button[title="Copy code"]');
128+
expect(copyButton).toBeInTheDocument();
129+
});
130+
131+
it('handles multiple code blocks', () => {
132+
const { container } = render(
133+
<MarkdownRenderer>
134+
{`
135+
<div class="rcv-code-renderer"><pre><code>first block</code></pre></div>
136+
<div class="rcv-code-renderer"><pre><code>second block</code></pre></div>
137+
`}
138+
</MarkdownRenderer>
139+
);
140+
141+
const copyButtons = container.querySelectorAll('button[title="Copy code"]');
142+
expect(copyButtons).toHaveLength(2);
143+
144+
// Test first button
145+
fireEvent.click(copyButtons[0]);
146+
expect(copy).toHaveBeenCalledWith('first block');
147+
148+
// Test second button
149+
fireEvent.click(copyButtons[1]);
150+
expect(copy).toHaveBeenCalledWith('second block');
151+
});
152+
});

src/CodeView.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable @typescript-eslint/no-var-requires */
21
import React from 'react';
32
import MarkdownRenderer from './MarkdownRenderer';
43
import parseHTML from './utils/parseHTML';
@@ -10,6 +9,9 @@ export interface CodeViewProps extends RendererProps {
109

1110
/** The code to be rendered is executed */
1211
sourceCode?: string;
12+
13+
/** The properties of the copy button */
14+
copyButtonProps?: React.HTMLAttributes<HTMLButtonElement>;
1315
}
1416

1517
const CodeView = React.forwardRef((props: CodeViewProps, ref: React.Ref<HTMLDivElement>) => {
@@ -21,6 +23,7 @@ const CodeView = React.forwardRef((props: CodeViewProps, ref: React.Ref<HTMLDivE
2123
theme = 'light',
2224
editable,
2325
transformOptions,
26+
copyButtonProps,
2427
renderToolbar,
2528
onChange,
2629
beforeCompile,
@@ -57,7 +60,11 @@ const CodeView = React.forwardRef((props: CodeViewProps, ref: React.Ref<HTMLDivE
5760
/>
5861
);
5962
} else if (fragment.type === 'html') {
60-
return <MarkdownRenderer key={fragment.key}>{fragment.content}</MarkdownRenderer>;
63+
return (
64+
<MarkdownRenderer key={fragment.key} copyButtonProps={copyButtonProps}>
65+
{fragment.content}
66+
</MarkdownRenderer>
67+
);
6168
}
6269
})}
6370
</div>

src/MarkdownRenderer.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@ import { iconPath as checkPath } from './icons/Check';
77

88
interface MarkdownRendererProps extends React.HTMLAttributes<HTMLDivElement> {
99
children?: string | null;
10+
copyButtonProps?: React.HTMLAttributes<HTMLButtonElement>;
1011
}
1112

12-
function appendCopyButton(container?: HTMLDivElement | null) {
13+
function appendCopyButton(
14+
container?: HTMLDivElement | null,
15+
buttonProps?: React.HTMLAttributes<HTMLButtonElement>
16+
) {
1317
if (!container) {
1418
return;
1519
}
1620

1721
const button = document.createElement('button');
18-
button.className =
19-
'copy-code-button rs-btn-icon rs-btn-icon-circle rs-btn rs-btn-subtle rs-btn-xs';
22+
button.className = 'btn-copy-code';
2023
button.title = 'Copy code';
2124
button.innerHTML = svgTpl(copyPath);
25+
2226
button.onclick = e => {
2327
e.preventDefault();
2428
const code = container?.querySelector('code')?.textContent;
@@ -33,18 +37,26 @@ function appendCopyButton(container?: HTMLDivElement | null) {
3337
icon?.setAttribute('d', copyPath);
3438
}, 2000);
3539
};
40+
41+
if (buttonProps) {
42+
Object.entries(buttonProps || {}).forEach(([key, value]) => {
43+
button.setAttribute(key, value);
44+
});
45+
}
46+
3647
container?.appendChild(button);
3748
}
3849

3950
const MarkdownRenderer = React.forwardRef(
4051
(props: MarkdownRendererProps, ref: React.Ref<HTMLDivElement>) => {
41-
const { children, className, ...rest } = props;
52+
const { children, className, copyButtonProps, ...rest } = props;
4253
const mdRef = React.useRef<HTMLDivElement>(null);
4354

4455
useEffect(() => {
4556
mdRef.current?.querySelectorAll('.rcv-code-renderer').forEach((el: any) => {
46-
appendCopyButton(el);
57+
appendCopyButton(el, copyButtonProps);
4758
});
59+
// eslint-disable-next-line react-hooks/exhaustive-deps
4860
}, []);
4961

5062
if (!children) {

0 commit comments

Comments
 (0)