Skip to content
961 changes: 0 additions & 961 deletions app/src/sections/spec-detail/SpecTabs.tsx

This file was deleted.

73 changes: 73 additions & 0 deletions app/src/sections/spec-detail/SpecTabs/CodeTab.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it, vi } from 'vitest';

import { CodeTab } from 'src/sections/spec-detail/SpecTabs/CodeTab';
import { render, screen, userEvent, waitFor } from 'src/test-utils';

// Mock the lazy-loaded CodeHighlighter
vi.mock('src/components/CodeHighlighter', () => ({
default: ({ code }: { code: string }) => <pre data-testid="code-highlighter">{code}</pre>,
}));

const baseProps = {
code: 'print("hello")',
specId: 'scatter-basic',
libraryId: 'matplotlib',
};

describe('CodeTab', () => {
it('renders the code through CodeHighlighter', async () => {
render(<CodeTab {...baseProps} />);

await waitFor(() => {
expect(screen.getByTestId('code-highlighter')).toBeInTheDocument();
});
expect(screen.getByTestId('code-highlighter')).toHaveTextContent('print("hello")');
});

it('renders no highlighter when code is null', () => {
render(<CodeTab {...baseProps} code={null} />);
expect(screen.queryByTestId('code-highlighter')).not.toBeInTheDocument();
});

it('copies code, fires the tracking event, and shows the copied state', async () => {
const user = userEvent.setup();
const writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, 'clipboard', {
value: { writeText },
writable: true,
configurable: true,
});

const onTrackEvent = vi.fn();
render(<CodeTab {...baseProps} onTrackEvent={onTrackEvent} />);

await user.click(screen.getByRole('button', { name: /copy code/i }));

expect(writeText).toHaveBeenCalledWith('print("hello")');
expect(onTrackEvent).toHaveBeenCalledWith('copy_code', {
spec: 'scatter-basic',
library: 'matplotlib',
method: 'tab',
page: 'spec_detail',
});
expect(screen.getByTestId('CheckIcon')).toBeInTheDocument();
});

it('does not copy or track when code is null', async () => {
const user = userEvent.setup();
const writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, 'clipboard', {
value: { writeText },
writable: true,
configurable: true,
});

const onTrackEvent = vi.fn();
render(<CodeTab {...baseProps} code={null} onTrackEvent={onTrackEvent} />);

await user.click(screen.getByRole('button', { name: /copy code/i }));

expect(writeText).not.toHaveBeenCalled();
expect(onTrackEvent).not.toHaveBeenCalled();
});
});
88 changes: 88 additions & 0 deletions app/src/sections/spec-detail/SpecTabs/CodeTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { lazy, Suspense, useCallback, useState } from 'react';

import CheckIcon from '@mui/icons-material/Check';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';

import type { TrackEventFn } from 'src/sections/spec-detail/SpecTabs/utils';
import { semanticColors, typography } from 'src/theme';

const CodeHighlighter = lazy(() => import('src/components/CodeHighlighter'));

interface CodeTabProps {
code: string | null;
specId: string;
libraryId: string;
language?: string;
onTrackEvent?: TrackEventFn;
}

export function CodeTab({ code, specId, libraryId, language, onTrackEvent }: CodeTabProps) {
const [copied, setCopied] = useState(false);

const handleCopy = useCallback(async () => {
if (!code) return;
try {
await navigator.clipboard.writeText(code);
setCopied(true);
onTrackEvent?.('copy_code', {
spec: specId,
library: libraryId,
method: 'tab',
page: 'spec_detail',
});
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Copy failed:', err);
}
}, [code, specId, libraryId, onTrackEvent]);

// Lazy-loaded syntax highlighter - only loads when Code tab is opened
const highlightedCode = code ? (
<Suspense
fallback={
<Box
sx={{
fontFamily: typography.fontFamily,
fontSize: '0.85rem',
whiteSpace: 'pre-wrap',
overflowWrap: 'anywhere',
overflowX: 'auto',
minWidth: 0,
color: semanticColors.labelText,
}}
>
{code}
</Box>
}
>
<CodeHighlighter code={code} language={language} library={libraryId} />
</Suspense>
) : null;

return (
<Box sx={{ position: 'relative', minWidth: 0 }}>
<Tooltip title={copied ? '.copied' : '.copy()'}>
<IconButton
onClick={handleCopy}
aria-label="Copy code"
sx={{
position: 'absolute',
top: 12,
right: 12,
bgcolor: 'var(--bg-elevated)',
border: '1px solid var(--code-border)',
zIndex: 1,
'&:hover': { bgcolor: 'var(--bg-surface)' },
}}
size="small"
>
{copied ? <CheckIcon color="success" /> : <ContentCopyIcon fontSize="small" />}
</IconButton>
</Tooltip>
{highlightedCode}
</Box>
);
}
60 changes: 60 additions & 0 deletions app/src/sections/spec-detail/SpecTabs/ImplTab.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest';

import { ImplTab } from 'src/sections/spec-detail/SpecTabs/ImplTab';
import { render, screen } from 'src/test-utils';

const baseProps = {
specId: 'scatter-basic',
libraryId: 'matplotlib',
};

describe('ImplTab', () => {
it('renders description, strengths, and weaknesses', () => {
render(
<ImplTab
{...baseProps}
imageDescription="A colorful scatter plot"
strengths={['Clear layout']}
weaknesses={['Missing legend']}
/>
);

expect(screen.getByText('A colorful scatter plot')).toBeInTheDocument();
expect(screen.getByText('Strengths')).toBeInTheDocument();
expect(screen.getByText('Clear layout')).toBeInTheDocument();
expect(screen.getByText('Weaknesses')).toBeInTheDocument();
expect(screen.getByText('Missing legend')).toBeInTheDocument();
});

it('shows the no-data message when no review data is present', () => {
render(<ImplTab {...baseProps} />);
expect(screen.getByText('No implementation review data available.')).toBeInTheDocument();
});

it('prefers generatedAt over updated and created in the metadata line', () => {
render(
<ImplTab
{...baseProps}
generatedAt="2025-01-15T00:00:00Z"
updated="2025-02-20T00:00:00Z"
created="2025-03-25T00:00:00Z"
/>
);
expect(screen.getByText(/scatter-basic · matplotlib · Jan 15, 2025/)).toBeInTheDocument();
});

it('falls back to updated, then created, for the metadata date', () => {
const { rerender } = render(
<ImplTab {...baseProps} updated="2025-02-20T00:00:00Z" created="2025-03-25T00:00:00Z" />
);
expect(screen.getByText(/scatter-basic · matplotlib · Feb 20, 2025/)).toBeInTheDocument();

rerender(<ImplTab {...baseProps} created="2025-03-25T00:00:00Z" />);
expect(screen.getByText(/scatter-basic · matplotlib · Mar 25, 2025/)).toBeInTheDocument();
});

it('omits the date from the metadata line when no date is available', () => {
render(<ImplTab {...baseProps} />);
expect(screen.getByText('scatter-basic · matplotlib')).toBeInTheDocument();
});
});
140 changes: 140 additions & 0 deletions app/src/sections/spec-detail/SpecTabs/ImplTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';

import { MdHeading } from 'src/sections/spec-detail/SpecTabs/md';
import { formatDate } from 'src/sections/spec-detail/SpecTabs/utils';
import { colors, fontSize, semanticColors, typography } from 'src/theme';

interface ImplTabProps {
imageDescription?: string;
strengths?: string[];
weaknesses?: string[];
specId: string;
libraryId: string;
generatedAt?: string;
updated?: string;
created?: string;
}

export function ImplTab({
imageDescription,
strengths,
weaknesses,
specId,
libraryId,
generatedAt,
updated,
created,
}: ImplTabProps) {
return (
<Box
sx={{
bgcolor: 'var(--bg-page)',
p: 3,
borderRadius: 1,
fontFamily: typography.fontFamily,
}}
>
{/* Image Description */}
{imageDescription && (
<>
<MdHeading level={2}>Description</MdHeading>
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: '0.85rem',
color: semanticColors.labelText,
lineHeight: 1.7,
}}
>
{imageDescription}
</Typography>
</>
)}

{/* Strengths */}
{strengths && strengths.length > 0 && (
<>
<MdHeading level={2}>Strengths</MdHeading>
<Box component="ul" sx={{ m: 0, pl: 0, listStyle: 'disc' }}>
{strengths.map((s, i) => (
<Typography
key={i}
component="li"
sx={{
fontFamily: typography.fontFamily,
fontSize: '0.85rem',
color: semanticColors.labelText,
lineHeight: 1.7,
ml: 2,
mb: 0.25,
'&::marker': { color: colors.success },
}}
>
{s}
</Typography>
))}
</Box>
</>
)}

{/* Weaknesses */}
{weaknesses && weaknesses.length > 0 && (
<>
<MdHeading level={2}>Weaknesses</MdHeading>
<Box component="ul" sx={{ m: 0, pl: 0, listStyle: 'disc' }}>
{weaknesses.map((w, i) => (
<Typography
key={i}
component="li"
sx={{
fontFamily: typography.fontFamily,
fontSize: '0.85rem',
color: semanticColors.labelText,
lineHeight: 1.7,
ml: 2,
mb: 0.25,
'&::marker': { color: colors.error },
}}
>
{w}
</Typography>
))}
</Box>
</>
)}

{/* No data message */}
{!imageDescription &&
(!strengths || strengths.length === 0) &&
(!weaknesses || weaknesses.length === 0) && (
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: '0.85rem',
color: 'var(--ink-muted)',
}}
>
No implementation review data available.
</Typography>
)}

{/* Metadata */}
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: fontSize.sm,
color: 'var(--ink-muted)',
mt: 2,
}}
>
{specId}
{libraryId && ` · ${libraryId}`}
{(() => {
const date = generatedAt || updated || created;
return date ? ` · ${formatDate(date)}` : '';
})()}
</Typography>
</Box>
);
}
Loading
Loading