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: ashwin@anthropic.com <ashwin@anthropic.com>
This commit is contained in:
@@ -18,7 +18,8 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.3",
|
||||
@@ -40,6 +41,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.11.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/node": "^22.7.5",
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
@@ -50,10 +53,12 @@
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.12",
|
||||
"globals": "^15.9.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.7.0",
|
||||
"vite": "^5.4.8"
|
||||
"vite": "^5.4.8",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
54
client/src/components/ListPane.test.tsx
Normal file
54
client/src/components/ListPane.test.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import ListPane from './ListPane'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
|
||||
describe('ListPane', () => {
|
||||
const defaultProps = {
|
||||
items: [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }],
|
||||
listItems: vi.fn(),
|
||||
clearItems: vi.fn(),
|
||||
setSelectedItem: vi.fn(),
|
||||
renderItem: (item: { name: string }) => <span>{item.name}</span>,
|
||||
title: 'Test List',
|
||||
buttonText: 'List Items'
|
||||
}
|
||||
|
||||
it('renders title correctly', () => {
|
||||
render(<ListPane {...defaultProps} />)
|
||||
expect(screen.getByText('Test List')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders list items using renderItem prop', () => {
|
||||
render(<ListPane {...defaultProps} />)
|
||||
expect(screen.getByText('Item 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Item 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls listItems when List Items button is clicked', () => {
|
||||
render(<ListPane {...defaultProps} />)
|
||||
fireEvent.click(screen.getByText('List Items'))
|
||||
expect(defaultProps.listItems).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('calls clearItems when Clear button is clicked', () => {
|
||||
render(<ListPane {...defaultProps} />)
|
||||
fireEvent.click(screen.getByText('Clear'))
|
||||
expect(defaultProps.clearItems).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('calls setSelectedItem when an item is clicked', () => {
|
||||
render(<ListPane {...defaultProps} />)
|
||||
fireEvent.click(screen.getByText('Item 1'))
|
||||
expect(defaultProps.setSelectedItem).toHaveBeenCalledWith(defaultProps.items[0])
|
||||
})
|
||||
|
||||
it('disables Clear button when items array is empty', () => {
|
||||
render(<ListPane {...defaultProps} items={[]} />)
|
||||
expect(screen.getByText('Clear')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('respects isButtonDisabled prop for List Items button', () => {
|
||||
render(<ListPane {...defaultProps} isButtonDisabled={true} />)
|
||||
expect(screen.getByText('List Items')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
55
client/src/components/ui/Button.test.tsx
Normal file
55
client/src/components/ui/Button.test.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { Button } from './button'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { createRef } from 'react'
|
||||
|
||||
describe('Button', () => {
|
||||
it('renders children correctly', () => {
|
||||
render(<Button>Click me</Button>)
|
||||
expect(screen.getByText('Click me')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles click events', () => {
|
||||
const handleClick = vi.fn()
|
||||
render(<Button onClick={handleClick}>Click me</Button>)
|
||||
fireEvent.click(screen.getByText('Click me'))
|
||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('applies different variants correctly', () => {
|
||||
const { rerender } = render(<Button variant="default">Default</Button>)
|
||||
expect(screen.getByText('Default')).toHaveClass('bg-primary')
|
||||
|
||||
rerender(<Button variant="outline">Outline</Button>)
|
||||
expect(screen.getByText('Outline')).toHaveClass('border-input')
|
||||
|
||||
rerender(<Button variant="secondary">Secondary</Button>)
|
||||
expect(screen.getByText('Secondary')).toHaveClass('bg-secondary')
|
||||
})
|
||||
|
||||
it('applies different sizes correctly', () => {
|
||||
const { rerender } = render(<Button size="default">Default</Button>)
|
||||
expect(screen.getByText('Default')).toHaveClass('h-9')
|
||||
|
||||
rerender(<Button size="sm">Small</Button>)
|
||||
expect(screen.getByText('Small')).toHaveClass('h-8')
|
||||
|
||||
rerender(<Button size="lg">Large</Button>)
|
||||
expect(screen.getByText('Large')).toHaveClass('h-10')
|
||||
})
|
||||
|
||||
it('forwards ref correctly', () => {
|
||||
const ref = createRef<HTMLButtonElement>()
|
||||
render(<Button ref={ref}>Button with ref</Button>)
|
||||
expect(ref.current).toBeInstanceOf(HTMLButtonElement)
|
||||
})
|
||||
|
||||
it('renders as a different element when asChild is true', () => {
|
||||
render(
|
||||
<Button asChild>
|
||||
<a href="#">Link Button</a>
|
||||
</Button>
|
||||
)
|
||||
expect(screen.getByText('Link Button').tagName).toBe('A')
|
||||
})
|
||||
})
|
||||
12
client/src/test.d.ts
vendored
Normal file
12
client/src/test.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/// <reference types="vitest/globals" />
|
||||
/// <reference types="@testing-library/jest-dom" />
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
declare global {
|
||||
namespace Vi {
|
||||
interface JestAssertion<T = any> extends jest.Matchers<void, T> {}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
6
client/test/setupTests.ts
Normal file
6
client/test/setupTests.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="vitest/globals" />
|
||||
/// <reference types="@testing-library/jest-dom" />
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
|
||||
// Add any additional test setup, custom matchers, or global mocks here
|
||||
// This file runs before each test file
|
||||
@@ -4,6 +4,7 @@
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
||||
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
@@ -26,5 +27,5 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./tsconfig.test.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
|
||||
7
client/tsconfig.test.json
Normal file
7
client/tsconfig.test.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": ["src/**/*.test.tsx", "src/**/*.test.ts", "test/**/*.ts"]
|
||||
}
|
||||
20
client/vitest.config.ts
Normal file
20
client/vitest.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./test/setupTests.ts'],
|
||||
typecheck: {
|
||||
tsconfig: './tsconfig.test.json'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user