tpmjs/packages/ui/src/CodeBlock/CodeBlock.test.tsx
Ajax Davis 0ff50fbd9b fix(ui): correct CodeBlock test selectors to match component structure
Tests were checking classes on <code> element but variant classes are
applied to the wrapper div with data-language attribute.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 02:55:57 +10:00

348 lines
12 KiB
TypeScript

import { render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CodeBlock } from './CodeBlock';
// Mock react-syntax-highlighter to avoid ESM compatibility issues
vi.mock('react-syntax-highlighter', () => ({
Prism: ({
children,
codeTagProps,
}: {
children: React.ReactNode;
codeTagProps?: { style?: React.CSSProperties; className?: string };
[key: string]: unknown;
}) => <code {...(codeTagProps || {})}>{children}</code>,
}));
vi.mock('react-syntax-highlighter/dist/esm/styles/prism', () => ({
prism: {},
vscDarkPlus: {},
}));
// Mock clipboard API
const mockClipboard = {
writeText: vi.fn(),
};
Object.defineProperty(navigator, 'clipboard', {
value: mockClipboard,
writable: true,
configurable: true,
});
describe('CodeBlock', () => {
beforeEach(() => {
mockClipboard.writeText.mockClear();
});
describe('Rendering', () => {
it('renders a code block element', () => {
render(<CodeBlock code='console.log("hello")' data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
expect(codeblock).toBeInTheDocument();
expect(codeblock.tagName).toBe('DIV');
});
it('renders code content', () => {
render(<CodeBlock code="npm install package" />);
expect(screen.getByText('npm install package')).toBeInTheDocument();
});
it('renders code in a code element', () => {
render(<CodeBlock code="test code" data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
const code = codeblock.querySelector('code');
expect(code).toBeInTheDocument();
expect(code?.textContent).toBe('test code');
});
it('preserves whitespace in code', () => {
const multilineCode = 'line 1\n line 2\n line 3';
render(<CodeBlock code={multilineCode} data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
const code = codeblock.querySelector('code');
expect(code?.textContent).toBe(multilineCode);
});
});
describe('Language', () => {
it('sets data-language attribute', () => {
render(<CodeBlock code="const x = 5" language="javascript" data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
const codeWrapper = codeblock.querySelector('[data-language]');
expect(codeWrapper).toHaveAttribute('data-language', 'javascript');
});
it('defaults to text language', () => {
render(<CodeBlock code="plain text" data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
const codeWrapper = codeblock.querySelector('[data-language]');
expect(codeWrapper).toHaveAttribute('data-language', 'text');
});
it('supports different languages', () => {
const { rerender } = render(
<CodeBlock code="code" language="python" data-testid="codeblock" />
);
let codeblock = screen.getByTestId('codeblock');
let codeWrapper = codeblock.querySelector('[data-language]');
expect(codeWrapper).toHaveAttribute('data-language', 'python');
rerender(<CodeBlock code="code" language="bash" data-testid="codeblock" />);
codeblock = screen.getByTestId('codeblock');
codeWrapper = codeblock.querySelector('[data-language]');
expect(codeWrapper).toHaveAttribute('data-language', 'bash');
});
});
describe('Size Variants', () => {
it('applies small size', () => {
render(<CodeBlock code="code" size="sm" data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
const codeWrapper = codeblock.querySelector('[data-language]');
expect(codeWrapper?.className).toContain('text-xs');
expect(codeWrapper?.className).toContain('p-3');
});
it('applies medium size', () => {
render(<CodeBlock code="code" size="md" data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
const codeWrapper = codeblock.querySelector('[data-language]');
expect(codeWrapper?.className).toContain('text-sm');
expect(codeWrapper?.className).toContain('p-4');
});
it('applies large size', () => {
render(<CodeBlock code="code" size="lg" data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
const codeWrapper = codeblock.querySelector('[data-language]');
expect(codeWrapper?.className).toContain('text-base');
expect(codeWrapper?.className).toContain('p-6');
});
it('uses md size by default', () => {
render(<CodeBlock code="code" data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
const codeWrapper = codeblock.querySelector('[data-language]');
expect(codeWrapper?.className).toContain('text-sm');
expect(codeWrapper?.className).toContain('p-4');
});
});
describe('Copy Button', () => {
it('shows copy button by default', () => {
render(<CodeBlock code="code" />);
expect(screen.getByTestId('copy-button')).toBeInTheDocument();
});
it('hides copy button when showCopy is false', () => {
render(<CodeBlock code="code" showCopy={false} />);
expect(screen.queryByTestId('copy-button')).not.toBeInTheDocument();
});
it('copy button has correct aria-label', () => {
render(<CodeBlock code="code" />);
const button = screen.getByTestId('copy-button');
expect(button).toHaveAttribute('aria-label', 'Copy code');
});
it('copy button is a button element', () => {
render(<CodeBlock code="code" />);
const button = screen.getByTestId('copy-button');
expect(button.tagName).toBe('BUTTON');
expect(button).toHaveAttribute('type', 'button');
});
});
describe('Copy Functionality', () => {
it('copies code to clipboard when button clicked', async () => {
render(<CodeBlock code="test code" />);
const button = screen.getByTestId('copy-button');
button.click();
await waitFor(() => {
expect(mockClipboard.writeText).toHaveBeenCalledWith('test code');
});
});
it('shows check icon after successful copy', async () => {
render(<CodeBlock code="test code" data-testid="codeblock" />);
const button = screen.getByTestId('copy-button');
button.click();
await waitFor(() => {
const icon = button.querySelector('svg');
// Check icon has a different path than copy icon
const path = icon?.querySelector('path');
expect(path).toHaveAttribute(
'd',
'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'
);
});
});
it('changes aria-label to "Copied!" after copy', async () => {
render(<CodeBlock code="test code" />);
const button = screen.getByTestId('copy-button');
button.click();
await waitFor(() => {
expect(button).toHaveAttribute('aria-label', 'Copied!');
});
});
it('reverts to copy icon after 2 seconds', async () => {
vi.useFakeTimers();
try {
render(<CodeBlock code="test code" data-testid="codeblock" />);
const button = screen.getByTestId('copy-button');
button.click();
// Wait for copy to complete
await vi.waitFor(() => {
expect(mockClipboard.writeText).toHaveBeenCalled();
});
// Verify check icon appears
await vi.waitFor(() => {
const icon = button.querySelector('svg');
const path = icon?.querySelector('path');
expect(path).toHaveAttribute(
'd',
'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'
);
});
// Fast forward 2 seconds
vi.advanceTimersByTime(2000);
// Verify copy icon is back
await vi.waitFor(() => {
const icon = button.querySelector('svg');
const path = icon?.querySelector('path');
expect(path).toHaveAttribute(
'd',
'M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z'
);
});
} finally {
vi.useRealTimers();
}
});
it('handles clipboard API errors gracefully', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
mockClipboard.writeText.mockRejectedValueOnce(new Error('Clipboard not available'));
try {
render(<CodeBlock code="test code" />);
const button = screen.getByTestId('copy-button');
button.click();
await waitFor(
() => {
expect(consoleErrorSpy).toHaveBeenCalled();
},
{ timeout: 1000 }
);
} finally {
consoleErrorSpy.mockRestore();
}
});
});
describe('HTML Attributes', () => {
it('passes through id attribute', () => {
render(<CodeBlock code="code" id="codeblock-id" data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
expect(codeblock).toHaveAttribute('id', 'codeblock-id');
});
it('passes through data attributes', () => {
render(<CodeBlock code="code" data-custom="test" data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
expect(codeblock).toHaveAttribute('data-custom', 'test');
});
});
describe('Custom className', () => {
it('merges custom className with variant classes', () => {
render(<CodeBlock code="code" className="custom-class" data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
expect(codeblock.className).toContain('custom-class');
expect(codeblock.className).toContain('bg-background');
expect(codeblock.className).toContain('border');
});
});
describe('Ref Forwarding', () => {
it('forwards ref to container element', () => {
let ref: HTMLDivElement | null = null;
render(
<CodeBlock
code="code"
ref={(el) => {
ref = el;
}}
/>
);
expect(ref).toBeInstanceOf(HTMLDivElement);
expect(ref!.querySelector('code')).toBeInTheDocument();
});
});
describe('Base Classes', () => {
it('container includes base classes', () => {
render(<CodeBlock code="code" data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
expect(codeblock.className).toContain('relative');
expect(codeblock.className).toContain('bg-background');
expect(codeblock.className).toContain('border');
expect(codeblock.className).toContain('overflow-hidden');
});
it('code wrapper includes base classes', () => {
render(<CodeBlock code="code" data-testid="codeblock" />);
const codeblock = screen.getByTestId('codeblock');
const codeWrapper = codeblock.querySelector('[data-language]');
expect(codeWrapper?.className).toContain('block');
expect(codeWrapper?.className).toContain('font-mono');
expect(codeWrapper?.className).toContain('text-foreground-secondary');
expect(codeWrapper?.className).toContain('overflow-x-auto');
expect(codeWrapper?.className).toContain('whitespace-pre');
});
});
describe('Compound Scenarios', () => {
it('works correctly with large size and custom language', () => {
render(
<CodeBlock
code='def hello():\n print("world")'
language="python"
size="lg"
data-testid="codeblock"
/>
);
const codeblock = screen.getByTestId('codeblock');
const codeWrapper = codeblock.querySelector('[data-language]');
expect(codeWrapper?.className).toContain('text-base');
expect(codeWrapper?.className).toContain('p-6');
expect(codeWrapper).toHaveAttribute('data-language', 'python');
});
it('works correctly with no copy button and custom className', () => {
render(
<CodeBlock
code="test"
showCopy={false}
className="my-custom-class"
data-testid="codeblock"
/>
);
const codeblock = screen.getByTestId('codeblock');
expect(codeblock.className).toContain('my-custom-class');
expect(screen.queryByTestId('copy-button')).not.toBeInTheDocument();
});
});
});