diff --git a/client/src/hooks/MCP/__tests__/useMCPDeepLink.spec.tsx b/client/src/hooks/MCP/__tests__/useMCPDeepLink.spec.tsx new file mode 100644 index 0000000000..4815f682ee --- /dev/null +++ b/client/src/hooks/MCP/__tests__/useMCPDeepLink.spec.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { renderHook, act } from '@testing-library/react'; +import { createMemoryRouter, RouterProvider } from 'react-router-dom'; +import useMCPDeepLink from '../useMCPDeepLink'; + +if (typeof Request === 'undefined') { + global.Request = class Request { + constructor( + public url: string, + public init?: RequestInit, + ) {} + } as any; +} + +function TestWrapper({ router }: { router: ReturnType }) { + return ; +} + +function renderDeepLinkHook(state?: Record) { + let hookResult: { current: ReturnType }; + + function HookConsumer() { + hookResult = { current: useMCPDeepLink() }; + return null; + } + + const router = createMemoryRouter( + [ + { + path: 'c/:conversationId', + element: , + }, + ], + { + initialEntries: [{ pathname: '/c/new', state }], + }, + ); + + const renderResult = renderHook(() => hookResult!.current, { + wrapper: ({ children }) => ( + <> + + {children} + + ), + }); + + return { ...renderResult, router, getHook: () => hookResult!.current }; +} + +describe('useMCPDeepLink', () => { + it('should open dialog with initialValues from route state including valid transport', async () => { + const { getHook } = renderDeepLinkHook({ + mcpName: 'My Server', + mcpUrl: 'https://example.com/mcp', + mcpTransport: 'sse', + }); + + await act(async () => {}); + + const result = getHook(); + expect(result.isOpen).toBe(true); + expect(result.initialValues).toEqual({ + title: 'My Server', + url: 'https://example.com/mcp', + type: 'sse', + }); + }); + + it('should ignore an invalid mcpTransport value', async () => { + const { getHook } = renderDeepLinkHook({ + mcpName: 'Server', + mcpTransport: 'websocket', + }); + + await act(async () => {}); + + const result = getHook(); + expect(result.isOpen).toBe(true); + expect(result.initialValues).toEqual({ title: 'Server' }); + expect(result.initialValues).not.toHaveProperty('type'); + }); + + it('should not open the dialog when route state has no MCP params', async () => { + const { getHook } = renderDeepLinkHook(undefined); + + await act(async () => {}); + + const result = getHook(); + expect(result.isOpen).toBe(false); + expect(result.initialValues).toBeUndefined(); + }); + + it('should clear initialValues when dialog is closed via onOpenChange', async () => { + const { getHook } = renderDeepLinkHook({ mcpName: 'Server' }); + + await act(async () => {}); + expect(getHook().isOpen).toBe(true); + expect(getHook().initialValues).toEqual({ title: 'Server' }); + + act(() => { + getHook().onOpenChange(false); + }); + + expect(getHook().isOpen).toBe(false); + expect(getHook().initialValues).toBeUndefined(); + }); +}); diff --git a/client/src/routes/__tests__/MCPAddRedirect.spec.tsx b/client/src/routes/__tests__/MCPAddRedirect.spec.tsx new file mode 100644 index 0000000000..fbcaa00791 --- /dev/null +++ b/client/src/routes/__tests__/MCPAddRedirect.spec.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { createMemoryRouter, RouterProvider, useLocation } from 'react-router-dom'; +import MCPAddRedirect from '../MCPAddRedirect'; + +if (typeof Request === 'undefined') { + global.Request = class Request { + constructor( + public url: string, + public init?: RequestInit, + ) {} + } as any; +} + +function CaptureState() { + const location = useLocation(); + (window as any).__capturedState = location.state; + // eslint-disable-next-line i18next/no-literal-string + return
Chat
; +} + +const createTestRouter = (initialEntry: string) => + createMemoryRouter( + [ + { + path: 'mcps/add', + element: , + }, + { + path: 'c/:conversationId', + element: , + }, + ], + { initialEntries: [initialEntry] }, + ); + +describe('MCPAddRedirect', () => { + afterEach(() => { + (window as any).__capturedState = undefined; + }); + + it('should redirect to /c/new forwarding all params via route state', async () => { + const router = createTestRouter( + '/mcps/add?name=My+Server&url=https://example.com/mcp&transport=sse', + ); + render(); + + await waitFor(() => { + expect(router.state.location.pathname).toBe('/c/new'); + }); + + expect(router.state.historyAction).toBe('REPLACE'); + expect((window as any).__capturedState).toEqual({ + mcpName: 'My Server', + mcpUrl: 'https://example.com/mcp', + mcpTransport: 'sse', + }); + }); + + it('should redirect to /c/new even when no query params are provided', async () => { + const router = createTestRouter('/mcps/add'); + render(); + + await waitFor(() => { + expect(router.state.location.pathname).toBe('/c/new'); + }); + + expect((window as any).__capturedState).toEqual({ + mcpName: undefined, + mcpUrl: undefined, + mcpTransport: undefined, + }); + }); +});