test: move component tests to __tests__ directory
This commit is contained in:
@@ -18,7 +18,9 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:coverage": "vitest run --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.0.3",
|
"@modelcontextprotocol/sdk": "^1.0.3",
|
||||||
@@ -40,6 +42,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.11.1",
|
"@eslint/js": "^9.11.1",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.1.0",
|
||||||
"@types/node": "^22.7.5",
|
"@types/node": "^22.7.5",
|
||||||
"@types/react": "^18.3.10",
|
"@types/react": "^18.3.10",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
@@ -50,10 +54,12 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.12",
|
"eslint-plugin-react-refresh": "^0.4.12",
|
||||||
"globals": "^15.9.0",
|
"globals": "^15.9.0",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"typescript-eslint": "^8.7.0",
|
"typescript-eslint": "^8.7.0",
|
||||||
"vite": "^5.4.8"
|
"vite": "^5.4.8",
|
||||||
|
"vitest": "^2.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
client/src/__tests__/components/History.test.tsx
Normal file
40
client/src/__tests__/components/History.test.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
84
client/src/__tests__/components/ListPane.test.tsx
Normal file
84
client/src/__tests__/components/ListPane.test.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
47
client/src/__tests__/components/PingTab.test.tsx
Normal file
47
client/src/__tests__/components/PingTab.test.tsx
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
96
client/src/__tests__/components/PromptsTab.test.tsx
Normal file
96
client/src/__tests__/components/PromptsTab.test.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
132
client/src/__tests__/components/ResourcesTab.test.tsx
Normal file
132
client/src/__tests__/components/ResourcesTab.test.tsx
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
80
client/src/__tests__/components/RootsTab.test.tsx
Normal file
80
client/src/__tests__/components/RootsTab.test.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
85
client/src/__tests__/components/SamplingTab.test.tsx
Normal file
85
client/src/__tests__/components/SamplingTab.test.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
11
client/src/__tests__/setup/setup.ts
Normal file
11
client/src/__tests__/setup/setup.ts
Normal 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()
|
||||||
|
})
|
||||||
@@ -54,6 +54,7 @@ const RootsTab = ({
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => removeRoot(index)}
|
onClick={() => removeRoot(index)}
|
||||||
|
aria-label="Remove root"
|
||||||
>
|
>
|
||||||
<Minus className="h-4 w-4" />
|
<Minus className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
19
client/vitest.config.ts
Normal file
19
client/vitest.config.ts
Normal 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
1052
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user