diff --git a/client/src/__tests__/App.test.tsx b/client/src/__tests__/App.test.tsx
new file mode 100644
index 0000000..f215280
--- /dev/null
+++ b/client/src/__tests__/App.test.tsx
@@ -0,0 +1,213 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, fireEvent, act } from '@testing-library/react'
+import App from '../App'
+import { useConnection } from '../lib/hooks/useConnection'
+import { useDraggablePane } from '../lib/hooks/useDraggablePane'
+
+// Mock URL params
+const mockURLSearchParams = vi.fn()
+vi.stubGlobal('URLSearchParams', mockURLSearchParams)
+
+// Mock the hooks
+vi.mock('../lib/hooks/useConnection', () => ({
+ useConnection: vi.fn()
+}))
+
+vi.mock('../lib/hooks/useDraggablePane', () => ({
+ useDraggablePane: vi.fn()
+}))
+
+// Mock fetch for config
+const mockFetch = vi.fn()
+global.fetch = mockFetch
+
+describe('App', () => {
+ beforeEach(() => {
+ // Reset all mocks
+ vi.clearAllMocks()
+
+ // Mock URL params
+ mockURLSearchParams.mockReturnValue({
+ get: () => '3000'
+ })
+
+ // Mock fetch response
+ mockFetch.mockResolvedValue({
+ json: () => Promise.resolve({
+ defaultEnvironment: {},
+ defaultCommand: 'test-command',
+ defaultArgs: '--test'
+ })
+ })
+
+ // Mock useConnection hook
+ const mockUseConnection = useConnection as jest.Mock
+ mockUseConnection.mockReturnValue({
+ connectionStatus: 'disconnected',
+ serverCapabilities: null,
+ mcpClient: null,
+ requestHistory: [],
+ makeRequest: vi.fn(),
+ sendNotification: vi.fn(),
+ connect: vi.fn()
+ })
+
+ // Mock useDraggablePane hook
+ const mockUseDraggablePane = useDraggablePane as jest.Mock
+ mockUseDraggablePane.mockReturnValue({
+ height: 300,
+ handleDragStart: vi.fn()
+ })
+ })
+
+ it('renders initial disconnected state', async () => {
+ await act(async () => {
+ render()
+ })
+ expect(screen.getByText('Connect to an MCP server to start inspecting')).toBeInTheDocument()
+ })
+
+ it('loads config on mount', async () => {
+ await act(async () => {
+ render()
+ })
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/config')
+ })
+
+ it('shows connected interface when mcpClient is available', async () => {
+ const mockUseConnection = useConnection as jest.Mock
+ mockUseConnection.mockReturnValue({
+ connectionStatus: 'connected',
+ serverCapabilities: {
+ resources: true,
+ prompts: true,
+ tools: true
+ },
+ mcpClient: {},
+ requestHistory: [],
+ makeRequest: vi.fn(),
+ sendNotification: vi.fn(),
+ connect: vi.fn()
+ })
+
+ await act(async () => {
+ render()
+ })
+
+ // Use more specific selectors
+ const resourcesTab = screen.getByRole('tab', { name: /resources/i })
+ const promptsTab = screen.getByRole('tab', { name: /prompts/i })
+ const toolsTab = screen.getByRole('tab', { name: /tools/i })
+
+ expect(resourcesTab).toBeInTheDocument()
+ expect(promptsTab).toBeInTheDocument()
+ expect(toolsTab).toBeInTheDocument()
+ })
+
+ it('disables tabs based on server capabilities', async () => {
+ const mockUseConnection = useConnection as jest.Mock
+ mockUseConnection.mockReturnValue({
+ connectionStatus: 'connected',
+ serverCapabilities: {
+ resources: false,
+ prompts: true,
+ tools: false
+ },
+ mcpClient: {},
+ requestHistory: [],
+ makeRequest: vi.fn(),
+ sendNotification: vi.fn(),
+ connect: vi.fn()
+ })
+
+ await act(async () => {
+ render()
+ })
+
+ // Resources tab should be disabled
+ const resourcesTab = screen.getByRole('tab', { name: /resources/i })
+ expect(resourcesTab).toHaveAttribute('disabled')
+
+ // Prompts tab should be enabled
+ const promptsTab = screen.getByRole('tab', { name: /prompts/i })
+ expect(promptsTab).not.toHaveAttribute('disabled')
+
+ // Tools tab should be disabled
+ const toolsTab = screen.getByRole('tab', { name: /tools/i })
+ expect(toolsTab).toHaveAttribute('disabled')
+ })
+
+ it('shows notification count in sampling tab', async () => {
+ const mockUseConnection = useConnection as jest.Mock
+ mockUseConnection.mockReturnValue({
+ connectionStatus: 'connected',
+ serverCapabilities: { sampling: true },
+ mcpClient: {},
+ requestHistory: [],
+ makeRequest: vi.fn(),
+ sendNotification: vi.fn(),
+ connect: vi.fn(),
+ onPendingRequest: (request, resolve, reject) => {
+ // Simulate a pending request
+ setPendingSampleRequests(prev => [
+ ...prev,
+ { id: 1, request, resolve, reject }
+ ])
+ }
+ })
+
+ await act(async () => {
+ render()
+ })
+
+ // Initially no notification count
+ const samplingTab = screen.getByRole('tab', { name: /sampling/i })
+ expect(samplingTab.querySelector('.bg-red-500')).not.toBeInTheDocument()
+
+ // Simulate a pending request
+ await act(async () => {
+ mockUseConnection.mock.calls[0][0].onPendingRequest(
+ { method: 'test', params: {} },
+ () => {},
+ () => {}
+ )
+ })
+
+ // Should show notification count
+ expect(samplingTab.querySelector('.bg-red-500')).toBeInTheDocument()
+ })
+
+ it('persists command and args to localStorage', async () => {
+ const setItemSpy = vi.spyOn(Storage.prototype, 'setItem')
+
+ await act(async () => {
+ render()
+ })
+
+ // Simulate command change
+ await act(async () => {
+ const commandInput = screen.getByPlaceholderText(/command/i)
+ fireEvent.change(commandInput, { target: { value: 'new-command' } })
+ })
+
+ expect(setItemSpy).toHaveBeenCalledWith('lastCommand', 'new-command')
+ })
+
+ it('shows error message when server has no capabilities', async () => {
+ const mockUseConnection = useConnection as jest.Mock
+ mockUseConnection.mockReturnValue({
+ connectionStatus: 'connected',
+ serverCapabilities: {},
+ mcpClient: {},
+ requestHistory: [],
+ makeRequest: vi.fn(),
+ sendNotification: vi.fn(),
+ connect: vi.fn()
+ })
+
+ await act(async () => {
+ render()
+ })
+ expect(screen.getByText('The connected server does not support any MCP capabilities')).toBeInTheDocument()
+ })
+})
\ No newline at end of file
diff --git a/client/src/__tests__/setup/setup.ts b/client/src/__tests__/setup/setup.ts
index ff52061..dcba7c2 100644
--- a/client/src/__tests__/setup/setup.ts
+++ b/client/src/__tests__/setup/setup.ts
@@ -1,11 +1,34 @@
import '@testing-library/jest-dom'
-import { expect, afterEach } from 'vitest'
+import { expect, afterEach, vi } from 'vitest'
import { cleanup } from '@testing-library/react'
import * as matchers from '@testing-library/jest-dom/matchers'
// @ts-ignore
expect.extend(matchers)
+// Mock window.matchMedia
+Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation(query => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(), // Deprecated
+ removeListener: vi.fn(), // Deprecated
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+})
+
+// Mock window.location.hash
+Object.defineProperty(window, 'location', {
+ writable: true,
+ value: { hash: '' }
+})
+
afterEach(() => {
cleanup()
+ vi.clearAllMocks()
+ window.location.hash = ''
})
\ No newline at end of file