import { render, screen, fireEvent } from '@testing-library/react'
import { DiffViewer, parseDiffFiles, diffLineClass } from '../diff/DiffViewer'
const SAMPLE_DIFF = `diff --git a/src/foo.ts b/src/foo.ts
index 1234567..89abcde 100644
--- a/src/foo.ts
+++ b/src/foo.ts
@@ -1,3 +1,3 @@
const a = 1
-const b = 2
+const b = 3
diff --git a/src/bar.ts b/src/bar.ts
new file mode 100644
index 0000000..1111111
--- /dev/null
+++ b/src/bar.ts
@@ -0,0 +1,2 @@
+export const bar = 'baz'
+export default bar
`
describe('parseDiffFiles', () => {
it('splits a unified diff into one block per file with the b/ path', () => {
const files = parseDiffFiles(SAMPLE_DIFF)
expect(files).toHaveLength(2)
expect(files[0].filePath).toBe('src/foo.ts')
expect(files[1].filePath).toBe('src/bar.ts')
// the block carries its own lines (including the hunk header)
expect(files[0].lines.some((l) => l.startsWith('@@'))).toBe(true)
})
it('returns an empty array for empty / whitespace input', () => {
expect(parseDiffFiles('')).toEqual([])
expect(parseDiffFiles(' \n ')).toEqual([])
})
})
describe('diffLineClass', () => {
it('colourises added / removed / hunk / context lines', () => {
expect(diffLineClass('+added')).toContain('emerald')
expect(diffLineClass('-removed')).toContain('red')
expect(diffLineClass('@@ -1 +1 @@')).toContain('cyan')
expect(diffLineClass(' context')).toContain('muted-foreground')
})
})
describe('DiffViewer', () => {
it('renders per-file blocks and colourised +/-/@@ lines', () => {
render()
// both file headers render
expect(screen.getByText('src/foo.ts')).toBeInTheDocument()
expect(screen.getByText('src/bar.ts')).toBeInTheDocument()
// an added line is present and coloured; metadata lines are hidden
expect(screen.getByText('+const b = 3')).toHaveClass('bg-emerald-500/10')
expect(screen.getByText('-const b = 2')).toHaveClass('bg-red-500/10')
expect(screen.queryByText(/^index 1234567/)).not.toBeInTheDocument()
// base ref shown in header
expect(screen.getByText('main')).toBeInTheDocument()
})
it('renders a summary line with staged/unstaged counts', () => {
render()
const summary = screen.getByTestId('diff-viewer-summary')
expect(summary).toHaveTextContent('2 files changed')
expect(summary).toHaveTextContent('1 staged')
expect(summary).toHaveTextContent('1 unstaged')
})
it('shows the empty state (with custom label) when the diff is empty', () => {
render()
expect(screen.getByTestId('diff-viewer-empty')).toBeInTheDocument()
expect(screen.getByText('Nothing to review')).toBeInTheDocument()
})
it('shows a loading state', () => {
render()
expect(screen.getByTestId('diff-viewer-loading')).toBeInTheDocument()
})
it('shows an error state and fires onRefresh from Try again', () => {
const onRefresh = jest.fn()
render()
expect(screen.getByTestId('diff-viewer-error')).toBeInTheDocument()
expect(screen.getByText('boom')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /try again/i }))
expect(onRefresh).toHaveBeenCalledTimes(1)
})
it('fires onRefresh from the header refresh button', () => {
const onRefresh = jest.fn()
render()
fireEvent.click(screen.getByRole('button', { name: /refresh diff/i }))
expect(onRefresh).toHaveBeenCalledTimes(1)
})
it('renders a collapsible working-tree status block', () => {
render()
expect(screen.getByText('Working tree status')).toBeInTheDocument()
expect(screen.getByText('?? untracked.ts')).toBeInTheDocument()
})
})