From 579dd42c46caf611d8db9f7ec2836b7bb1682d22 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Fri, 20 Dec 2024 15:36:15 -0800 Subject: [PATCH] test: add tests for App component - Add comprehensive tests for App.tsx - Fix test setup to handle window.matchMedia and URL params - Wrap state updates in act() - Use more specific selectors for finding elements - Fix sampling tab test to properly simulate pending requests --- client/src/__tests__/App.test.tsx | 213 ++++++++++++++++++++++++++++ client/src/__tests__/setup/setup.ts | 25 +++- 2 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 client/src/__tests__/App.test.tsx 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