diff --git a/client/package.json b/client/package.json
index 888495c..06cc72f 100644
--- a/client/package.json
+++ b/client/package.json
@@ -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"
}
}
diff --git a/client/src/components/ListPane.test.tsx b/client/src/components/ListPane.test.tsx
new file mode 100644
index 0000000..553eef8
--- /dev/null
+++ b/client/src/components/ListPane.test.tsx
@@ -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 }) => {item.name},
+ title: 'Test List',
+ buttonText: 'List Items'
+ }
+
+ it('renders title correctly', () => {
+ render()
+ expect(screen.getByText('Test List')).toBeInTheDocument()
+ })
+
+ it('renders list items using renderItem prop', () => {
+ render()
+ expect(screen.getByText('Item 1')).toBeInTheDocument()
+ expect(screen.getByText('Item 2')).toBeInTheDocument()
+ })
+
+ it('calls listItems when List Items button is clicked', () => {
+ render()
+ fireEvent.click(screen.getByText('List Items'))
+ expect(defaultProps.listItems).toHaveBeenCalledTimes(1)
+ })
+
+ it('calls clearItems when Clear button is clicked', () => {
+ render()
+ fireEvent.click(screen.getByText('Clear'))
+ expect(defaultProps.clearItems).toHaveBeenCalledTimes(1)
+ })
+
+ it('calls setSelectedItem when an item is clicked', () => {
+ render()
+ fireEvent.click(screen.getByText('Item 1'))
+ expect(defaultProps.setSelectedItem).toHaveBeenCalledWith(defaultProps.items[0])
+ })
+
+ it('disables Clear button when items array is empty', () => {
+ render()
+ expect(screen.getByText('Clear')).toBeDisabled()
+ })
+
+ it('respects isButtonDisabled prop for List Items button', () => {
+ render()
+ expect(screen.getByText('List Items')).toBeDisabled()
+ })
+})
diff --git a/client/src/components/ui/Button.test.tsx b/client/src/components/ui/Button.test.tsx
new file mode 100644
index 0000000..78ba200
--- /dev/null
+++ b/client/src/components/ui/Button.test.tsx
@@ -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()
+ expect(screen.getByText('Click me')).toBeInTheDocument()
+ })
+
+ it('handles click events', () => {
+ const handleClick = vi.fn()
+ render()
+ fireEvent.click(screen.getByText('Click me'))
+ expect(handleClick).toHaveBeenCalledTimes(1)
+ })
+
+ it('applies different variants correctly', () => {
+ const { rerender } = render()
+ expect(screen.getByText('Default')).toHaveClass('bg-primary')
+
+ rerender()
+ expect(screen.getByText('Outline')).toHaveClass('border-input')
+
+ rerender()
+ expect(screen.getByText('Secondary')).toHaveClass('bg-secondary')
+ })
+
+ it('applies different sizes correctly', () => {
+ const { rerender } = render()
+ expect(screen.getByText('Default')).toHaveClass('h-9')
+
+ rerender()
+ expect(screen.getByText('Small')).toHaveClass('h-8')
+
+ rerender()
+ expect(screen.getByText('Large')).toHaveClass('h-10')
+ })
+
+ it('forwards ref correctly', () => {
+ const ref = createRef()
+ render()
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement)
+ })
+
+ it('renders as a different element when asChild is true', () => {
+ render(
+
+ )
+ expect(screen.getByText('Link Button').tagName).toBe('A')
+ })
+})
diff --git a/client/src/test.d.ts b/client/src/test.d.ts
new file mode 100644
index 0000000..9d8447e
--- /dev/null
+++ b/client/src/test.d.ts
@@ -0,0 +1,12 @@
+///
+///
+
+import '@testing-library/jest-dom'
+
+declare global {
+ namespace Vi {
+ interface JestAssertion extends jest.Matchers {}
+ }
+}
+
+export {}
diff --git a/client/test/setupTests.ts b/client/test/setupTests.ts
new file mode 100644
index 0000000..e27bcc2
--- /dev/null
+++ b/client/test/setupTests.ts
@@ -0,0 +1,6 @@
+///
+///
+import '@testing-library/jest-dom/vitest'
+
+// Add any additional test setup, custom matchers, or global mocks here
+// This file runs before each test file
diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json
index 980c215..88a0752 100644
--- a/client/tsconfig.app.json
+++ b/client/tsconfig.app.json
@@ -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"]
}
diff --git a/client/tsconfig.json b/client/tsconfig.json
index fec8c8e..c31cb25 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -2,7 +2,8 @@
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
- { "path": "./tsconfig.node.json" }
+ { "path": "./tsconfig.node.json" },
+ { "path": "./tsconfig.test.json" }
],
"compilerOptions": {
"baseUrl": ".",
diff --git a/client/tsconfig.test.json b/client/tsconfig.test.json
new file mode 100644
index 0000000..e234462
--- /dev/null
+++ b/client/tsconfig.test.json
@@ -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"]
+}
diff --git a/client/vitest.config.ts b/client/vitest.config.ts
new file mode 100644
index 0000000..1de2a22
--- /dev/null
+++ b/client/vitest.config.ts
@@ -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'),
+ },
+ },
+})