diff --git a/src/components/CodeBlock/CodeBlock.test.tsx b/src/components/CodeBlock/CodeBlock.test.tsx
new file mode 100644
index 000000000..117b9525f
--- /dev/null
+++ b/src/components/CodeBlock/CodeBlock.test.tsx
@@ -0,0 +1,54 @@
+import { renderCUI } from '@/utils/test-utils';
+import userEvent from '@testing-library/user-event';
+import { CodeBlock } from '@/components/CodeBlock';
+
+describe('CodeBlock', () => {
+ const renderCodeBlock = (children: React.ReactNode, language = 'bash') =>
+ renderCUI({children});
+
+ it('renders a plain string', () => {
+ const { container } = renderCodeBlock('echo hello');
+ expect(container.textContent).toContain('echo hello');
+ });
+
+ it('renders $VARIABLE syntax as literal text', () => {
+ const { container } = renderCodeBlock('export FOO="$BAR"');
+ expect(container.textContent).toContain('$BAR');
+ });
+
+ it('renders angle-bracket placeholders as literal text', () => {
+ const { container } = renderCodeBlock('export PROJECT_ID=""');
+ expect(container.textContent).toContain('');
+ expect(container.textContent).not.toContain('[object Object]');
+ });
+
+ it('copies the plain-text string to clipboard', async () => {
+ const writeText = vi.fn().mockResolvedValue(undefined);
+ Object.assign(navigator, { clipboard: { writeText } });
+
+ const code = 'echo hello';
+ const { getByRole } = renderCodeBlock(code);
+
+ await userEvent.click(getByRole('button'));
+ expect(writeText).toHaveBeenCalledWith(code);
+ });
+
+ it('calls onCopy with the plain-text string', async () => {
+ const writeText = vi.fn().mockResolvedValue(undefined);
+ Object.assign(navigator, { clipboard: { writeText } });
+ const onCopy = vi.fn();
+
+ const code = 'echo hello';
+ const { getByRole } = renderCUI(
+
+ {code}
+
+ );
+
+ await userEvent.click(getByRole('button'));
+ expect(onCopy).toHaveBeenCalledWith(code);
+ });
+});
diff --git a/src/components/CodeBlock/CodeBlock.tsx b/src/components/CodeBlock/CodeBlock.tsx
index d38edfa59..e353e425e 100644
--- a/src/components/CodeBlock/CodeBlock.tsx
+++ b/src/components/CodeBlock/CodeBlock.tsx
@@ -1,9 +1,7 @@
-import React, { HTMLAttributes, useState } from 'react';
+import React, { HTMLAttributes, ReactNode, useState } from 'react';
import { Light as SyntaxHighlighter, createElement } from 'react-syntax-highlighter';
-
import { EmptyButton } from '@/components/EmptyButton';
import { IconButton } from '@/components/IconButton';
-
import { styled } from 'styled-components';
import useColorStyle from './useColorStyle';
import { CodeBlockProps, CodeThemeType } from './CodeBlock.types';
@@ -21,6 +19,23 @@ import tsx from 'react-syntax-highlighter/dist/cjs/languages/hljs/typescript.js'
import plaintext from 'react-syntax-highlighter/dist/cjs/languages/hljs/plaintext.js';
/* eslint-enable import/extensions */
+const nodeToString = (node: ReactNode): string => {
+ if (node === null || node === undefined || typeof node === 'boolean') { return ''; }
+ if (typeof node === 'string') { return node; }
+ if (typeof node === 'number') { return String(node); }
+ if (Array.isArray(node)) { return node.map(nodeToString).join(''); }
+ if (React.isValidElement(node)) {
+ const element = node as React.ReactElement<{ children?: ReactNode }>;
+ const tagName = typeof element.type === 'string' ? element.type : '';
+ const children = element.props.children;
+ if (children !== undefined && children !== null) {
+ return `<${tagName}>${nodeToString(children)}${tagName}>`;
+ }
+ return `<${tagName}>`;
+ }
+ return String(node);
+};
+
SyntaxHighlighter.registerLanguage('sql', sql.default || sql);
SyntaxHighlighter.registerLanguage('bash', bash.default || bash);
SyntaxHighlighter.registerLanguage('json', json.default || json);
@@ -109,12 +124,13 @@ export const CodeBlock = ({
const [errorCopy, setErrorCopy] = useState(false);
const [wrap, setWrap] = useState(wrapLines);
const customStyle = useColorStyle(theme);
+ const codeString = nodeToString(children);
const copyCodeToClipboard = async () => {
try {
- await navigator.clipboard.writeText(children);
+ await navigator.clipboard.writeText(codeString);
if (typeof onCopy == 'function') {
- onCopy(children);
+ onCopy(codeString);
}
setCopied(true);
setTimeout(() => setCopied(false), 2000);
@@ -197,7 +213,7 @@ export const CodeBlock = ({
wrapLines={wrap || wrapLines}
wrapLongLines={wrap || wrapLines}
>
- {children}
+ {codeString}
);
diff --git a/src/components/CodeBlock/CodeBlock.types.ts b/src/components/CodeBlock/CodeBlock.types.ts
index 5319f710e..b3de0ed39 100644
--- a/src/components/CodeBlock/CodeBlock.types.ts
+++ b/src/components/CodeBlock/CodeBlock.types.ts
@@ -1,4 +1,4 @@
-import { HTMLAttributes } from 'react';
+import { HTMLAttributes, ReactNode } from 'react';
export type CodeThemeType = 'light' | 'dark';
@@ -7,7 +7,7 @@ export interface CodeBlockProps extends Omit<
'children' | 'onCopy'
> {
language?: string;
- children: string;
+ children: ReactNode;
theme?: CodeThemeType;
showLineNumbers?: boolean;
showWrapButton?: boolean;