From ce7f65b5be42f942fec651bb2e0c1bc1ee4c63f0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 16 Jan 2025 19:24:51 +0000 Subject: [PATCH] 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 --- client/package.json | 9 +++- client/src/components/ListPane.test.tsx | 54 +++++++++++++++++++++++ client/src/components/ui/Button.test.tsx | 55 ++++++++++++++++++++++++ client/src/test.d.ts | 12 ++++++ client/test/setupTests.ts | 6 +++ client/tsconfig.app.json | 3 +- client/tsconfig.json | 3 +- client/tsconfig.test.json | 7 +++ client/vitest.config.ts | 20 +++++++++ 9 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 client/src/components/ListPane.test.tsx create mode 100644 client/src/components/ui/Button.test.tsx create mode 100644 client/src/test.d.ts create mode 100644 client/test/setupTests.ts create mode 100644 client/tsconfig.test.json create mode 100644 client/vitest.config.ts 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'), + }, + }, +})