Skip to content

Commit f38e151

Browse files
committed
add tests
1 parent a91b4f9 commit f38e151

File tree

3 files changed

+190
-64
lines changed

3 files changed

+190
-64
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import {describe, expect, it} from 'vitest';
2+
3+
import {cleanCodeSnippet} from './index';
4+
5+
describe('cleanCodeSnippet', () => {
6+
describe('consecutive newlines', () => {
7+
it('should reduce two consecutive newlines to a single newline', () => {
8+
const input = 'line1\n\nline2\n\n\n line3';
9+
const result = cleanCodeSnippet(input, {});
10+
expect(result).toBe('line1\nline2\n\n line3');
11+
});
12+
13+
it('should handle input with single newlines', () => {
14+
const input = 'line1\nline2\nline3';
15+
const result = cleanCodeSnippet(input, {});
16+
expect(result).toBe('line1\nline2\nline3');
17+
});
18+
});
19+
20+
describe('diff markers', () => {
21+
it('should remove diff markers (+/-) from the beginning of lines by default', () => {
22+
const input = '+added line\n- removed line\n normal line';
23+
const result = cleanCodeSnippet(input);
24+
expect(result).toBe('added line\nremoved line\n normal line');
25+
});
26+
27+
it('should preserve diff markers when cleanDiffMarkers is set to false', () => {
28+
const input = '+ added line\n- removed line';
29+
const result = cleanCodeSnippet(input, {cleanDiffMarkers: false});
30+
expect(result).toBe('+ added line\n- removed line');
31+
});
32+
33+
it('should remove diff markers based on real-life code example', () => {
34+
const input =
35+
'-Sentry.init({\n' +
36+
"- dsn: '\n" +
37+
'\n' +
38+
"https://[email protected]/0',\n" +
39+
'- tracesSampleRate: 1.0,\n' +
40+
'-\n' +
41+
'- // uncomment the line below to enable Spotlight (https://spotlightjs.com)\n' +
42+
'- // spotlight: import.meta.env.DEV,\n' +
43+
'-});\n' +
44+
'-\n' +
45+
'-export const handle = sentryHandle();\n' +
46+
'+export const handle = sequence(\n' +
47+
'+ initCloudflareSentryHandle({\n' +
48+
"+ dsn: '\n" +
49+
'\n' +
50+
"https://[email protected]/0',\n" +
51+
'+ tracesSampleRate: 1.0,\n' +
52+
'+ }),\n' +
53+
'+ sentryHandle()\n' +
54+
'+);';
55+
56+
const result = cleanCodeSnippet(input);
57+
expect(result).toBe(
58+
'Sentry.init({\n' +
59+
" dsn: '\n" +
60+
"https://[email protected]/0',\n" +
61+
' tracesSampleRate: 1.0,\n' +
62+
' // uncomment the line below to enable Spotlight (https://spotlightjs.com)\n' +
63+
' // spotlight: import.meta.env.DEV,\n' +
64+
'});\n' +
65+
'export const handle = sentryHandle();\n' +
66+
'export const handle = sequence(\n' +
67+
' initCloudflareSentryHandle({\n' +
68+
" dsn: '\n" +
69+
"https://[email protected]/0',\n" +
70+
' tracesSampleRate: 1.0,\n' +
71+
' }),\n' +
72+
' sentryHandle()\n' +
73+
');'
74+
);
75+
});
76+
});
77+
78+
describe('bash prompt', () => {
79+
it('should remove bash prompt in bash/shell language', () => {
80+
const input = '$ ls -la\nsome output';
81+
const result = cleanCodeSnippet(input, {language: 'bash'});
82+
expect(result).toBe('ls -la\nsome output');
83+
});
84+
85+
it('should remove bash prompt in shell language', () => {
86+
const input = '$ git status\nsome output';
87+
const result = cleanCodeSnippet(input, {language: 'shell'});
88+
expect(result).toBe('git status\nsome output');
89+
});
90+
91+
it('should not remove bash prompt for non-bash/shell languages', () => {
92+
const input = '$ some text';
93+
const result = cleanCodeSnippet(input, {language: 'python'});
94+
expect(result).toBe('$ some text');
95+
});
96+
97+
it('should handle bash prompt with multiple spaces', () => {
98+
const input = '$ ls -la\nsome output';
99+
const result = cleanCodeSnippet(input, {language: 'bash'});
100+
expect(result).toBe('ls -la\nsome output');
101+
});
102+
});
103+
104+
describe('combination of options', () => {
105+
it('should handle multiple cleaning operations together', () => {
106+
const input = '+ $ ls -la\n\n- $ git status';
107+
const result = cleanCodeSnippet(input, {language: 'bash'});
108+
expect(result).toBe('ls -la\ngit status');
109+
});
110+
});
111+
112+
describe('edge cases', () => {
113+
it('should handle empty input', () => {
114+
const input = '';
115+
const result = cleanCodeSnippet(input, {});
116+
expect(result).toBe('');
117+
});
118+
119+
it('should handle input with only newlines', () => {
120+
const input = '\n\n\n';
121+
const result = cleanCodeSnippet(input, {});
122+
expect(result).toBe('\n\n');
123+
});
124+
125+
it('should preserve leading whitespace not associated with diff markers', () => {
126+
const input = ' normal line\n+ added line';
127+
const result = cleanCodeSnippet(input, {});
128+
expect(result).toBe(' normal line\nadded line');
129+
});
130+
});
131+
});

src/components/codeBlock/index.tsx

Lines changed: 52 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,38 @@ export function CodeBlock({filename, language, children}: CodeBlockProps) {
1818
const [showCopied, setShowCopied] = useState(false);
1919
const codeRef = useRef<HTMLDivElement>(null);
2020

21-
const {copyCodeOnClick} = useCopyCodeCleaner(codeRef, {
22-
cleanDiffMarkers: true,
23-
language,
24-
});
25-
2621
// Show the copy button after js has loaded
2722
// otherwise the copy button will not work
2823
const [showCopyButton, setShowCopyButton] = useState(false);
2924
useEffect(() => {
3025
setShowCopyButton(true);
3126
}, []);
3227

33-
const handleCopyOnClick = async () => {
34-
const success = await copyCodeOnClick();
28+
useCleanSnippetInClipboard(codeRef, {language});
29+
30+
async function copyCodeOnClick() {
31+
if (codeRef.current === null) {
32+
return;
33+
}
34+
35+
const code = cleanCodeSnippet(codeRef.current.innerText, {language});
3536

36-
if (success) {
37+
try {
38+
await navigator.clipboard.writeText(code);
3739
setShowCopied(true);
3840
setTimeout(() => setShowCopied(false), 1200);
41+
} catch (error) {
42+
// eslint-disable-next-line no-console
43+
console.error('Failed to copy:', error);
3944
}
40-
};
45+
}
4146

4247
return (
4348
<div className={styles['code-block']}>
4449
<div className={styles['code-actions']}>
4550
<code className={styles.filename}>{filename}</code>
4651
{showCopyButton && (
47-
<button className={styles.copy} onClick={handleCopyOnClick}>
52+
<button className={styles.copy} onClick={copyCodeOnClick}>
4853
<Clipboard size={16} />
4954
</button>
5055
)}
@@ -70,40 +75,58 @@ const REGEX = {
7075
};
7176

7277
/**
73-
* A custom hook that handles cleaning text when copying from code blocks
78+
* Cleans a code snippet by removing diff markers (+ or -) and bash prompts.
79+
*
80+
* @internal Only exported for testing
81+
*/
82+
export function cleanCodeSnippet(rawCodeSnippet: string, options?: CleanCopyOptions) {
83+
const language = options?.language;
84+
const cleanDiffMarkers = options?.cleanDiffMarkers ?? true;
85+
const cleanBashPrompt = options?.cleanBashPrompt ?? true;
86+
87+
let cleanedSnippet = rawCodeSnippet.replace(REGEX.CONSECUTIVE_NEWLINES, '\n');
88+
89+
if (cleanDiffMarkers) {
90+
cleanedSnippet = cleanedSnippet.replace(REGEX.DIFF_MARKERS, '');
91+
}
92+
93+
if (cleanBashPrompt && (language === 'bash' || language === 'shell')) {
94+
// Split into lines, clean each line, then rejoin
95+
cleanedSnippet = cleanedSnippet
96+
.split('\n')
97+
.map(line => {
98+
const match = line.match(REGEX.BASH_PROMPT);
99+
return match ? line.substring(match[0].length) : line;
100+
})
101+
.filter(line => line.trim() !== '') // Remove empty lines
102+
.join('\n');
103+
}
104+
105+
return cleanedSnippet;
106+
}
107+
108+
/**
109+
* A custom hook that handles cleaning text when manually copying code to clipboard
110+
*
74111
* @param codeRef - Reference to the code element
75112
* @param options - Configuration options for cleaning
76113
*/
77-
export function useCopyCodeCleaner(
114+
export function useCleanSnippetInClipboard(
78115
codeRef: RefObject<HTMLElement>,
79116
options: CleanCopyOptions = {}
80117
) {
81118
const {cleanDiffMarkers = true, cleanBashPrompt = true, language = ''} = options;
82119

83-
/**
84-
* Effect, which cleans the snippet when the user manually copies it to their clipboard
85-
*/
86120
useEffect(() => {
87121
const handleUserCopyEvent = (event: ClipboardEvent) => {
88122
if (!codeRef.current || !event.clipboardData) return;
89123

90124
const selection = window.getSelection()?.toString() || '';
91125

92126
if (selection) {
93-
let cleanedText = selection;
94-
95-
if (cleanDiffMarkers) {
96-
cleanedText = cleanedText.replace(REGEX.DIFF_MARKERS, '');
97-
}
98-
99-
if (cleanBashPrompt && (language === 'bash' || language === 'shell')) {
100-
const match = cleanedText.match(REGEX.BASH_PROMPT);
101-
if (match) {
102-
cleanedText = cleanedText.substring(match[0].length);
103-
}
104-
}
127+
const cleanedSnippet = cleanCodeSnippet(selection, options);
105128

106-
event.clipboardData.setData('text/plain', cleanedText);
129+
event.clipboardData.setData('text/plain', cleanedSnippet);
107130
event.preventDefault();
108131
}
109132
};
@@ -118,40 +141,5 @@ export function useCopyCodeCleaner(
118141
codeElement.removeEventListener('copy', handleUserCopyEvent as EventListener);
119142
}
120143
};
121-
}, [codeRef, cleanDiffMarkers, language, cleanBashPrompt]);
122-
123-
/**
124-
* Function for copying code when clicking on "copy code".
125-
*
126-
* @returns Whether code was copied successfully
127-
*/
128-
const copyCodeOnClick = async (): Promise<boolean> => {
129-
if (codeRef.current === null) {
130-
return false;
131-
}
132-
133-
let code = codeRef.current.innerText.replace(REGEX.CONSECUTIVE_NEWLINES, '\n');
134-
135-
if (cleanBashPrompt && (language === 'bash' || language === 'shell')) {
136-
const match = code.match(REGEX.BASH_PROMPT);
137-
if (match) {
138-
code = code.substring(match[0].length);
139-
}
140-
}
141-
142-
if (cleanDiffMarkers) {
143-
code = code.replace(REGEX.DIFF_MARKERS, '');
144-
}
145-
146-
try {
147-
await navigator.clipboard.writeText(code);
148-
return true;
149-
} catch (error) {
150-
// eslint-disable-next-line no-console
151-
console.error('Failed to copy:', error);
152-
return false;
153-
}
154-
};
155-
156-
return {copyCodeOnClick};
144+
}, [codeRef, cleanDiffMarkers, language, cleanBashPrompt, options]);
157145
}

vitest.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// <reference types="vitest" />
2+
import tsconfigPaths from 'vite-tsconfig-paths';
3+
import {defineConfig} from 'vitest/config';
4+
5+
export default defineConfig({
6+
plugins: [tsconfigPaths()],
7+
});

0 commit comments

Comments
 (0)