Skip to content

Commit ce7f65b

Browse files
Add frontend unit tests
- Set up Vitest with React Testing Library - Add comprehensive tests for Button and ListPane components - Configure TypeScript for test environment - Add test type declarations Co-Authored-By: [email protected] <[email protected]>
1 parent 98e6f0e commit ce7f65b

File tree

9 files changed

+165
-4
lines changed

9 files changed

+165
-4
lines changed

client/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"dev": "vite",
1919
"build": "tsc -b && vite build",
2020
"lint": "eslint .",
21-
"preview": "vite preview"
21+
"preview": "vite preview",
22+
"test": "vitest run"
2223
},
2324
"dependencies": {
2425
"@modelcontextprotocol/sdk": "^1.0.3",
@@ -40,6 +41,8 @@
4041
},
4142
"devDependencies": {
4243
"@eslint/js": "^9.11.1",
44+
"@testing-library/jest-dom": "^6.6.3",
45+
"@testing-library/react": "^16.2.0",
4346
"@types/node": "^22.7.5",
4447
"@types/react": "^18.3.10",
4548
"@types/react-dom": "^18.3.0",
@@ -50,10 +53,12 @@
5053
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
5154
"eslint-plugin-react-refresh": "^0.4.12",
5255
"globals": "^15.9.0",
56+
"jsdom": "^26.0.0",
5357
"postcss": "^8.4.47",
5458
"tailwindcss": "^3.4.13",
5559
"typescript": "^5.5.3",
5660
"typescript-eslint": "^8.7.0",
57-
"vite": "^5.4.8"
61+
"vite": "^5.4.8",
62+
"vitest": "^3.0.0"
5863
}
5964
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { render, screen, fireEvent } from '@testing-library/react'
2+
import ListPane from './ListPane'
3+
import { describe, it, expect, vi } from 'vitest'
4+
5+
describe('ListPane', () => {
6+
const defaultProps = {
7+
items: [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }],
8+
listItems: vi.fn(),
9+
clearItems: vi.fn(),
10+
setSelectedItem: vi.fn(),
11+
renderItem: (item: { name: string }) => <span>{item.name}</span>,
12+
title: 'Test List',
13+
buttonText: 'List Items'
14+
}
15+
16+
it('renders title correctly', () => {
17+
render(<ListPane {...defaultProps} />)
18+
expect(screen.getByText('Test List')).toBeInTheDocument()
19+
})
20+
21+
it('renders list items using renderItem prop', () => {
22+
render(<ListPane {...defaultProps} />)
23+
expect(screen.getByText('Item 1')).toBeInTheDocument()
24+
expect(screen.getByText('Item 2')).toBeInTheDocument()
25+
})
26+
27+
it('calls listItems when List Items button is clicked', () => {
28+
render(<ListPane {...defaultProps} />)
29+
fireEvent.click(screen.getByText('List Items'))
30+
expect(defaultProps.listItems).toHaveBeenCalledTimes(1)
31+
})
32+
33+
it('calls clearItems when Clear button is clicked', () => {
34+
render(<ListPane {...defaultProps} />)
35+
fireEvent.click(screen.getByText('Clear'))
36+
expect(defaultProps.clearItems).toHaveBeenCalledTimes(1)
37+
})
38+
39+
it('calls setSelectedItem when an item is clicked', () => {
40+
render(<ListPane {...defaultProps} />)
41+
fireEvent.click(screen.getByText('Item 1'))
42+
expect(defaultProps.setSelectedItem).toHaveBeenCalledWith(defaultProps.items[0])
43+
})
44+
45+
it('disables Clear button when items array is empty', () => {
46+
render(<ListPane {...defaultProps} items={[]} />)
47+
expect(screen.getByText('Clear')).toBeDisabled()
48+
})
49+
50+
it('respects isButtonDisabled prop for List Items button', () => {
51+
render(<ListPane {...defaultProps} isButtonDisabled={true} />)
52+
expect(screen.getByText('List Items')).toBeDisabled()
53+
})
54+
})
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { render, screen, fireEvent } from '@testing-library/react'
2+
import { Button } from './button'
3+
import { describe, it, expect, vi } from 'vitest'
4+
import { createRef } from 'react'
5+
6+
describe('Button', () => {
7+
it('renders children correctly', () => {
8+
render(<Button>Click me</Button>)
9+
expect(screen.getByText('Click me')).toBeInTheDocument()
10+
})
11+
12+
it('handles click events', () => {
13+
const handleClick = vi.fn()
14+
render(<Button onClick={handleClick}>Click me</Button>)
15+
fireEvent.click(screen.getByText('Click me'))
16+
expect(handleClick).toHaveBeenCalledTimes(1)
17+
})
18+
19+
it('applies different variants correctly', () => {
20+
const { rerender } = render(<Button variant="default">Default</Button>)
21+
expect(screen.getByText('Default')).toHaveClass('bg-primary')
22+
23+
rerender(<Button variant="outline">Outline</Button>)
24+
expect(screen.getByText('Outline')).toHaveClass('border-input')
25+
26+
rerender(<Button variant="secondary">Secondary</Button>)
27+
expect(screen.getByText('Secondary')).toHaveClass('bg-secondary')
28+
})
29+
30+
it('applies different sizes correctly', () => {
31+
const { rerender } = render(<Button size="default">Default</Button>)
32+
expect(screen.getByText('Default')).toHaveClass('h-9')
33+
34+
rerender(<Button size="sm">Small</Button>)
35+
expect(screen.getByText('Small')).toHaveClass('h-8')
36+
37+
rerender(<Button size="lg">Large</Button>)
38+
expect(screen.getByText('Large')).toHaveClass('h-10')
39+
})
40+
41+
it('forwards ref correctly', () => {
42+
const ref = createRef<HTMLButtonElement>()
43+
render(<Button ref={ref}>Button with ref</Button>)
44+
expect(ref.current).toBeInstanceOf(HTMLButtonElement)
45+
})
46+
47+
it('renders as a different element when asChild is true', () => {
48+
render(
49+
<Button asChild>
50+
<a href="#">Link Button</a>
51+
</Button>
52+
)
53+
expect(screen.getByText('Link Button').tagName).toBe('A')
54+
})
55+
})

client/src/test.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/// <reference types="vitest/globals" />
2+
/// <reference types="@testing-library/jest-dom" />
3+
4+
import '@testing-library/jest-dom'
5+
6+
declare global {
7+
namespace Vi {
8+
interface JestAssertion<T = any> extends jest.Matchers<void, T> {}
9+
}
10+
}
11+
12+
export {}

client/test/setupTests.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// <reference types="vitest/globals" />
2+
/// <reference types="@testing-library/jest-dom" />
3+
import '@testing-library/jest-dom/vitest'
4+
5+
// Add any additional test setup, custom matchers, or global mocks here
6+
// This file runs before each test file

client/tsconfig.app.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"paths": {
55
"@/*": ["./src/*"]
66
},
7+
"types": ["vitest/globals", "@testing-library/jest-dom"],
78

89
"target": "ES2020",
910
"useDefineForClassFields": true,
@@ -26,5 +27,5 @@
2627
"noFallthroughCasesInSwitch": true,
2728
"resolveJsonModule": true
2829
},
29-
"include": ["src"]
30+
"include": ["src", "test"]
3031
}

client/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"files": [],
33
"references": [
44
{ "path": "./tsconfig.app.json" },
5-
{ "path": "./tsconfig.node.json" }
5+
{ "path": "./tsconfig.node.json" },
6+
{ "path": "./tsconfig.test.json" }
67
],
78
"compilerOptions": {
89
"baseUrl": ".",

client/tsconfig.test.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "./tsconfig.app.json",
3+
"compilerOptions": {
4+
"types": ["vitest/globals", "@testing-library/jest-dom"]
5+
},
6+
"include": ["src/**/*.test.tsx", "src/**/*.test.ts", "test/**/*.ts"]
7+
}

client/vitest.config.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { defineConfig } from 'vitest/config'
2+
import react from '@vitejs/plugin-react'
3+
import path from 'path'
4+
5+
export default defineConfig({
6+
plugins: [react()],
7+
test: {
8+
environment: 'jsdom',
9+
globals: true,
10+
setupFiles: ['./test/setupTests.ts'],
11+
typecheck: {
12+
tsconfig: './tsconfig.test.json'
13+
}
14+
},
15+
resolve: {
16+
alias: {
17+
'@': path.resolve(__dirname, './src'),
18+
},
19+
},
20+
})

0 commit comments

Comments
 (0)