test: move component tests to __tests__ directory

This commit is contained in:
Ashwin Bhat
2024-12-20 15:20:54 -08:00
parent 1797fbfba8
commit 1ab1aba528
12 changed files with 1654 additions and 3 deletions

View File

@@ -18,7 +18,9 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.3",
@@ -40,6 +42,8 @@
},
"devDependencies": {
"@eslint/js": "^9.11.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@types/node": "^22.7.5",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
@@ -50,10 +54,12 @@
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.12",
"globals": "^15.9.0",
"jsdom": "^25.0.1",
"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": "^2.1.8"
}
}

View File

@@ -0,0 +1,40 @@
import { describe, it, expect } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import HistoryAndNotifications from '../../components/History'
describe('HistoryAndNotifications', () => {
const mockHistory = [
{ request: JSON.stringify({ method: 'test1' }), response: JSON.stringify({ result: 'output1' }) },
{ request: JSON.stringify({ method: 'test2' }), response: JSON.stringify({ result: 'output2' }) }
]
it('renders history items', () => {
render(<HistoryAndNotifications requestHistory={mockHistory} serverNotifications={[]} />)
const items = screen.getAllByText(/test[12]/, { exact: false })
expect(items).toHaveLength(2)
})
it('expands history item when clicked', () => {
render(<HistoryAndNotifications requestHistory={mockHistory} serverNotifications={[]} />)
const firstItem = screen.getByText(/test1/, { exact: false })
fireEvent.click(firstItem)
expect(screen.getByText('Request:')).toBeInTheDocument()
expect(screen.getByText(/output1/, { exact: false })).toBeInTheDocument()
})
it('renders and expands server notifications', () => {
const notifications = [
{ method: 'notify1', params: { data: 'test data 1' } },
{ method: 'notify2', params: { data: 'test data 2' } }
]
render(<HistoryAndNotifications requestHistory={[]} serverNotifications={notifications} />)
const items = screen.getAllByText(/notify[12]/, { exact: false })
expect(items).toHaveLength(2)
fireEvent.click(items[0])
expect(screen.getByText('Details:')).toBeInTheDocument()
expect(screen.getByText(/test data/, { exact: false })).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,84 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import ListPane from '../../components/ListPane'
describe('ListPane', () => {
type TestItem = {
id: number;
name: string;
}
const mockItems: TestItem[] = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
]
const defaultProps = {
items: mockItems,
listItems: vi.fn(),
clearItems: vi.fn(),
setSelectedItem: vi.fn(),
renderItem: (item: TestItem) => (
<>
<span className="flex-1">{item.name}</span>
<span className="text-sm text-gray-500">ID: {item.id}</span>
</>
),
title: 'Test Items',
buttonText: 'List Items'
}
it('renders title and buttons', () => {
render(<ListPane {...defaultProps} />)
expect(screen.getByText('Test Items')).toBeInTheDocument()
expect(screen.getByText('List Items')).toBeInTheDocument()
expect(screen.getByText('Clear')).toBeInTheDocument()
})
it('renders list of items using renderItem prop', () => {
render(<ListPane {...defaultProps} />)
expect(screen.getByText('Item 1')).toBeInTheDocument()
expect(screen.getByText('Item 2')).toBeInTheDocument()
expect(screen.getByText('ID: 1')).toBeInTheDocument()
expect(screen.getByText('ID: 2')).toBeInTheDocument()
})
it('calls listItems when List Items button is clicked', () => {
const listItems = vi.fn()
render(<ListPane {...defaultProps} listItems={listItems} />)
fireEvent.click(screen.getByText('List Items'))
expect(listItems).toHaveBeenCalled()
})
it('calls clearItems when Clear button is clicked', () => {
const clearItems = vi.fn()
render(<ListPane {...defaultProps} clearItems={clearItems} />)
fireEvent.click(screen.getByText('Clear'))
expect(clearItems).toHaveBeenCalled()
})
it('calls setSelectedItem when an item is clicked', () => {
const setSelectedItem = vi.fn()
render(<ListPane {...defaultProps} setSelectedItem={setSelectedItem} />)
fireEvent.click(screen.getByText('Item 1'))
expect(setSelectedItem).toHaveBeenCalledWith(mockItems[0])
})
it('disables Clear button when items array is empty', () => {
render(<ListPane {...defaultProps} items={[]} />)
expect(screen.getByText('Clear')).toBeDisabled()
})
it('disables List Items button when isButtonDisabled is true', () => {
render(<ListPane {...defaultProps} isButtonDisabled={true} />)
expect(screen.getByText('List Items')).toBeDisabled()
})
it('enables List Items button when isButtonDisabled is false', () => {
render(<ListPane {...defaultProps} isButtonDisabled={false} />)
expect(screen.getByText('List Items')).not.toBeDisabled()
})
})

View File

@@ -0,0 +1,47 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import PingTab from '../../components/PingTab'
import { Tabs } from '@/components/ui/tabs'
describe('PingTab', () => {
const renderWithTabs = (component: React.ReactElement) => {
return render(
<Tabs defaultValue="ping">
{component}
</Tabs>
)
}
it('renders the MEGA PING button', () => {
renderWithTabs(<PingTab onPingClick={() => {}} />)
const button = screen.getByRole('button', { name: /mega ping/i })
expect(button).toBeInTheDocument()
expect(button).toHaveClass('bg-gradient-to-r', 'from-purple-500', 'to-pink-500')
})
it('includes rocket and explosion emojis', () => {
renderWithTabs(<PingTab onPingClick={() => {}} />)
expect(screen.getByText('🚀')).toBeInTheDocument()
expect(screen.getByText('💥')).toBeInTheDocument()
})
it('calls onPingClick when button is clicked', () => {
const onPingClick = vi.fn()
renderWithTabs(<PingTab onPingClick={onPingClick} />)
fireEvent.click(screen.getByRole('button', { name: /mega ping/i }))
expect(onPingClick).toHaveBeenCalledTimes(1)
})
it('has animation classes for visual feedback', () => {
renderWithTabs(<PingTab onPingClick={() => {}} />)
const button = screen.getByRole('button', { name: /mega ping/i })
expect(button).toHaveClass('animate-pulse', 'hover:scale-110', 'transition')
})
it('has focus styles for accessibility', () => {
renderWithTabs(<PingTab onPingClick={() => {}} />)
const button = screen.getByRole('button', { name: /mega ping/i })
expect(button).toHaveClass('focus:outline-none', 'focus:ring-4')
})
})

View File

@@ -0,0 +1,96 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import PromptsTab from '../../components/PromptsTab'
import type { Prompt } from '../../components/PromptsTab'
import { Tabs } from '@/components/ui/tabs'
describe('PromptsTab', () => {
const mockPrompts: Prompt[] = [
{
name: 'test-prompt-1',
description: 'Test prompt 1 description',
arguments: [
{ name: 'arg1', description: 'Argument 1', required: true },
{ name: 'arg2', description: 'Argument 2' }
]
},
{
name: 'test-prompt-2',
description: 'Test prompt 2 description'
}
]
const defaultProps = {
prompts: mockPrompts,
listPrompts: vi.fn(),
clearPrompts: vi.fn(),
getPrompt: vi.fn(),
selectedPrompt: null,
setSelectedPrompt: vi.fn(),
promptContent: '',
nextCursor: null,
error: null
}
const renderWithTabs = (component: React.ReactElement) => {
return render(
<Tabs defaultValue="prompts">
{component}
</Tabs>
)
}
it('renders list of prompts', () => {
renderWithTabs(<PromptsTab {...defaultProps} />)
expect(screen.getByText('test-prompt-1')).toBeInTheDocument()
expect(screen.getByText('test-prompt-2')).toBeInTheDocument()
})
it('shows prompt details when selected', () => {
const props = {
...defaultProps,
selectedPrompt: mockPrompts[0]
}
renderWithTabs(<PromptsTab {...props} />)
expect(screen.getByText('Test prompt 1 description', { selector: 'p.text-sm.text-gray-600' })).toBeInTheDocument()
expect(screen.getByText('arg1')).toBeInTheDocument()
expect(screen.getByText('arg2')).toBeInTheDocument()
})
it('handles argument input', () => {
const getPrompt = vi.fn()
const props = {
...defaultProps,
selectedPrompt: mockPrompts[0],
getPrompt
}
renderWithTabs(<PromptsTab {...props} />)
const arg1Input = screen.getByPlaceholderText('Enter arg1')
fireEvent.change(arg1Input, { target: { value: 'test value' } })
const getPromptButton = screen.getByText('Get Prompt')
fireEvent.click(getPromptButton)
expect(getPrompt).toHaveBeenCalledWith('test-prompt-1', { arg1: 'test value' })
})
it('shows error message when error prop is provided', () => {
const props = {
...defaultProps,
error: 'Test error message'
}
renderWithTabs(<PromptsTab {...props} />)
expect(screen.getByText('Test error message')).toBeInTheDocument()
})
it('shows prompt content when provided', () => {
const props = {
...defaultProps,
selectedPrompt: mockPrompts[0],
promptContent: 'Test prompt content'
}
renderWithTabs(<PromptsTab {...props} />)
expect(screen.getByText('Test prompt content')).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,132 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import ResourcesTab from '../../components/ResourcesTab'
import { Tabs } from '@/components/ui/tabs'
import type { Resource, ResourceTemplate } from '@modelcontextprotocol/sdk/types.js'
describe('ResourcesTab', () => {
const mockResources: Resource[] = [
{ uri: 'file:///test1.txt', name: 'Test 1' },
{ uri: 'file:///test2.txt', name: 'Test 2' }
]
const mockTemplates: ResourceTemplate[] = [
{
name: 'Template 1',
description: 'Test template 1',
uriTemplate: 'file:///test/{param1}/{param2}.txt'
},
{
name: 'Template 2',
description: 'Test template 2',
uriTemplate: 'file:///other/{name}.txt'
}
]
const defaultProps = {
resources: mockResources,
resourceTemplates: mockTemplates,
listResources: vi.fn(),
clearResources: vi.fn(),
listResourceTemplates: vi.fn(),
clearResourceTemplates: vi.fn(),
readResource: vi.fn(),
selectedResource: null,
setSelectedResource: vi.fn(),
resourceContent: '',
nextCursor: null,
nextTemplateCursor: null,
error: null
}
const renderWithTabs = (component: React.ReactElement) => {
return render(
<Tabs defaultValue="resources">
{component}
</Tabs>
)
}
it('renders resources list', () => {
renderWithTabs(<ResourcesTab {...defaultProps} />)
expect(screen.getByText('Test 1')).toBeInTheDocument()
expect(screen.getByText('Test 2')).toBeInTheDocument()
})
it('renders templates list', () => {
renderWithTabs(<ResourcesTab {...defaultProps} />)
expect(screen.getByText('Template 1')).toBeInTheDocument()
expect(screen.getByText('Template 2')).toBeInTheDocument()
})
it('shows resource content when resource is selected', () => {
const props = {
...defaultProps,
selectedResource: mockResources[0],
resourceContent: 'Test content'
}
renderWithTabs(<ResourcesTab {...props} />)
expect(screen.getByText('Test content')).toBeInTheDocument()
})
it('shows template form when template is selected', () => {
renderWithTabs(<ResourcesTab {...defaultProps} />)
fireEvent.click(screen.getByText('Template 1'))
expect(screen.getByText('Test template 1')).toBeInTheDocument()
expect(screen.getByLabelText('param1')).toBeInTheDocument()
expect(screen.getByLabelText('param2')).toBeInTheDocument()
})
it('fills template and reads resource', () => {
const readResource = vi.fn()
const setSelectedResource = vi.fn()
renderWithTabs(
<ResourcesTab
{...defaultProps}
readResource={readResource}
setSelectedResource={setSelectedResource}
/>
)
// Select template
fireEvent.click(screen.getByText('Template 1'))
// Fill in template parameters
fireEvent.change(screen.getByLabelText('param1'), { target: { value: 'value1' } })
fireEvent.change(screen.getByLabelText('param2'), { target: { value: 'value2' } })
// Submit form
fireEvent.click(screen.getByText('Read Resource'))
expect(readResource).toHaveBeenCalledWith('file:///test/value1/value2.txt')
expect(setSelectedResource).toHaveBeenCalledWith(
expect.objectContaining({
uri: 'file:///test/value1/value2.txt',
name: 'file:///test/value1/value2.txt'
})
)
})
it('shows error message when error prop is provided', () => {
const props = {
...defaultProps,
error: 'Test error message'
}
renderWithTabs(<ResourcesTab {...props} />)
expect(screen.getByText('Test error message')).toBeInTheDocument()
})
it('refreshes resource content when refresh button is clicked', () => {
const readResource = vi.fn()
const props = {
...defaultProps,
selectedResource: mockResources[0],
readResource
}
renderWithTabs(<ResourcesTab {...props} />)
fireEvent.click(screen.getByText('Refresh'))
expect(readResource).toHaveBeenCalledWith(mockResources[0].uri)
})
})

View File

@@ -0,0 +1,80 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import RootsTab from '../../components/RootsTab'
import { Tabs } from '@/components/ui/tabs'
import type { Root } from '@modelcontextprotocol/sdk/types.js'
describe('RootsTab', () => {
const mockRoots: Root[] = [
{ uri: 'file:///test/path1', name: 'test1' },
{ uri: 'file:///test/path2', name: 'test2' }
]
const defaultProps = {
roots: mockRoots,
setRoots: vi.fn(),
onRootsChange: vi.fn()
}
const renderWithTabs = (component: React.ReactElement) => {
return render(
<Tabs defaultValue="roots">
{component}
</Tabs>
)
}
it('renders list of roots', () => {
renderWithTabs(<RootsTab {...defaultProps} />)
expect(screen.getByDisplayValue('file:///test/path1')).toBeInTheDocument()
expect(screen.getByDisplayValue('file:///test/path2')).toBeInTheDocument()
})
it('adds a new root when Add Root button is clicked', () => {
const setRoots = vi.fn()
renderWithTabs(<RootsTab {...defaultProps} setRoots={setRoots} />)
fireEvent.click(screen.getByText('Add Root'))
expect(setRoots).toHaveBeenCalled()
const updateFn = setRoots.mock.calls[0][0]
const result = updateFn(mockRoots)
expect(result).toEqual([...mockRoots, { uri: 'file://', name: '' }])
})
it('removes a root when remove button is clicked', () => {
const setRoots = vi.fn()
renderWithTabs(<RootsTab {...defaultProps} setRoots={setRoots} />)
const removeButtons = screen.getAllByRole('button', { name: /remove root/i })
fireEvent.click(removeButtons[0])
expect(setRoots).toHaveBeenCalled()
const updateFn = setRoots.mock.calls[0][0]
const result = updateFn(mockRoots)
expect(result).toEqual([mockRoots[1]])
})
it('updates root URI when input changes', () => {
const setRoots = vi.fn()
renderWithTabs(<RootsTab {...defaultProps} setRoots={setRoots} />)
const firstInput = screen.getByDisplayValue('file:///test/path1')
fireEvent.change(firstInput, { target: { value: 'file:///new/path' } })
expect(setRoots).toHaveBeenCalled()
const updateFn = setRoots.mock.calls[0][0]
const result = updateFn(mockRoots)
expect(result[0].uri).toBe('file:///new/path')
expect(result[1]).toEqual(mockRoots[1])
})
it('calls onRootsChange when Save Changes is clicked', () => {
const onRootsChange = vi.fn()
renderWithTabs(<RootsTab {...defaultProps} onRootsChange={onRootsChange} />)
fireEvent.click(screen.getByText('Save Changes'))
expect(onRootsChange).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,85 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import SamplingTab from '../../components/SamplingTab'
import { Tabs } from '@/components/ui/tabs'
import type { CreateMessageRequest } from '@modelcontextprotocol/sdk/types.js'
describe('SamplingTab', () => {
const mockRequest: CreateMessageRequest = {
model: 'test-model',
role: 'user',
content: {
type: 'text',
text: 'Test message'
}
}
const mockPendingRequests = [
{ id: 1, request: mockRequest },
{ id: 2, request: { ...mockRequest, content: { type: 'text', text: 'Another test' } } }
]
const defaultProps = {
pendingRequests: mockPendingRequests,
onApprove: vi.fn(),
onReject: vi.fn()
}
const renderWithTabs = (component: React.ReactElement) => {
return render(
<Tabs defaultValue="sampling">
{component}
</Tabs>
)
}
it('renders empty state when no requests', () => {
renderWithTabs(<SamplingTab {...defaultProps} pendingRequests={[]} />)
expect(screen.getByText('No pending requests')).toBeInTheDocument()
})
it('renders list of pending requests', () => {
renderWithTabs(<SamplingTab {...defaultProps} />)
expect(screen.getByText(/Test message/)).toBeInTheDocument()
expect(screen.getByText(/Another test/)).toBeInTheDocument()
})
it('shows request details in JSON format', () => {
renderWithTabs(<SamplingTab {...defaultProps} />)
const requestJson = screen.getAllByText((content) => content.includes('"model": "test-model"'))
expect(requestJson).toHaveLength(2)
})
it('calls onApprove with stub response when Approve is clicked', () => {
const onApprove = vi.fn()
renderWithTabs(<SamplingTab {...defaultProps} onApprove={onApprove} />)
const approveButtons = screen.getAllByText('Approve')
fireEvent.click(approveButtons[0])
expect(onApprove).toHaveBeenCalledWith(1, {
model: 'stub-model',
stopReason: 'endTurn',
role: 'assistant',
content: {
type: 'text',
text: 'This is a stub response.'
}
})
})
it('calls onReject when Reject is clicked', () => {
const onReject = vi.fn()
renderWithTabs(<SamplingTab {...defaultProps} onReject={onReject} />)
const rejectButtons = screen.getAllByText('Reject')
fireEvent.click(rejectButtons[0])
expect(onReject).toHaveBeenCalledWith(1)
})
it('shows informational alert about sampling requests', () => {
renderWithTabs(<SamplingTab {...defaultProps} />)
expect(screen.getByText(/When the server requests LLM sampling/)).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,11 @@
import '@testing-library/jest-dom'
import { expect, afterEach } from 'vitest'
import { cleanup } from '@testing-library/react'
import * as matchers from '@testing-library/jest-dom/matchers'
// @ts-ignore
expect.extend(matchers)
afterEach(() => {
cleanup()
})

View File

@@ -54,6 +54,7 @@ const RootsTab = ({
variant="destructive"
size="sm"
onClick={() => removeRoot(index)}
aria-label="Remove root"
>
<Minus className="h-4 w-4" />
</Button>

19
client/vitest.config.ts Normal file
View File

@@ -0,0 +1,19 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/__tests__/setup/setup.ts'],
include: ['src/__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
})

1052
package-lock.json generated

File diff suppressed because it is too large Load Diff