Add tests for URL fragment routing behavior with order-independent window mocking
- Add tests for hash routing based on server capabilities priority - Test that no hash is set when disconnected - Add e2e test verifying startup has no hash fragment - Use Jest module mocking for useConnection hook states 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
28d3fc56c1
commit
461403b624
2 changed files with 175 additions and 0 deletions
16
client/e2e/startup-state.spec.ts
Normal file
16
client/e2e/startup-state.spec.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
// Adjust the URL if your dev server runs on a different port
|
||||||
|
const APP_URL = "http://localhost:6274/";
|
||||||
|
|
||||||
|
test.describe("Startup State", () => {
|
||||||
|
test("should not navigate to a tab when Inspector first opens", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto(APP_URL);
|
||||||
|
|
||||||
|
// Check that there is no hash fragment in the URL
|
||||||
|
const url = page.url();
|
||||||
|
expect(url).not.toContain("#");
|
||||||
|
});
|
||||||
|
});
|
||||||
159
client/src/__tests__/App.routing.test.tsx
Normal file
159
client/src/__tests__/App.routing.test.tsx
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { render, waitFor } from "@testing-library/react";
|
||||||
|
import App from "../App";
|
||||||
|
import { useConnection } from "../lib/hooks/useConnection";
|
||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
|
||||||
|
// Mock auth dependencies first
|
||||||
|
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
||||||
|
auth: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../lib/oauth-state-machine", () => ({
|
||||||
|
OAuthStateMachine: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../lib/auth", () => ({
|
||||||
|
InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
|
||||||
|
tokens: jest.fn().mockResolvedValue(null),
|
||||||
|
clear: jest.fn(),
|
||||||
|
})),
|
||||||
|
DebugInspectorOAuthClientProvider: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the config utils
|
||||||
|
jest.mock("../utils/configUtils", () => ({
|
||||||
|
...jest.requireActual("../utils/configUtils"),
|
||||||
|
getMCPProxyAddress: jest.fn(() => "http://localhost:6277"),
|
||||||
|
getMCPProxyAuthToken: jest.fn(() => ({
|
||||||
|
token: "",
|
||||||
|
header: "X-MCP-Proxy-Auth",
|
||||||
|
})),
|
||||||
|
getInitialTransportType: jest.fn(() => "stdio"),
|
||||||
|
getInitialSseUrl: jest.fn(() => "http://localhost:3001/sse"),
|
||||||
|
getInitialCommand: jest.fn(() => "mcp-server-everything"),
|
||||||
|
getInitialArgs: jest.fn(() => ""),
|
||||||
|
initializeInspectorConfig: jest.fn(() => ({})),
|
||||||
|
saveInspectorConfig: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Default connection state is disconnected
|
||||||
|
const disconnectedConnectionState = {
|
||||||
|
connectionStatus: "disconnected" as const,
|
||||||
|
serverCapabilities: null,
|
||||||
|
mcpClient: null,
|
||||||
|
requestHistory: [],
|
||||||
|
makeRequest: jest.fn(),
|
||||||
|
sendNotification: jest.fn(),
|
||||||
|
handleCompletion: jest.fn(),
|
||||||
|
completionsSupported: false,
|
||||||
|
connect: jest.fn(),
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connected state for tests that need an active connection
|
||||||
|
const connectedConnectionState = {
|
||||||
|
...disconnectedConnectionState,
|
||||||
|
connectionStatus: "connected" as const,
|
||||||
|
serverCapabilities: {},
|
||||||
|
mcpClient: {
|
||||||
|
request: jest.fn(),
|
||||||
|
notification: jest.fn(),
|
||||||
|
close: jest.fn(),
|
||||||
|
} as unknown as Client,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock required dependencies, but unrelated to routing.
|
||||||
|
jest.mock("../lib/hooks/useDraggablePane", () => ({
|
||||||
|
useDraggablePane: () => ({
|
||||||
|
height: 300,
|
||||||
|
handleDragStart: jest.fn(),
|
||||||
|
}),
|
||||||
|
useDraggableSidebar: () => ({
|
||||||
|
width: 320,
|
||||||
|
isDragging: false,
|
||||||
|
handleDragStart: jest.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/Sidebar", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div>Sidebar</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock fetch
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({ json: () => Promise.resolve({}) });
|
||||||
|
|
||||||
|
// Use an empty module mock, so that mock state can be reset between tests.
|
||||||
|
jest.mock("../lib/hooks/useConnection", () => ({
|
||||||
|
useConnection: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("App - URL Fragment Routing", () => {
|
||||||
|
const mockUseConnection = jest.mocked(useConnection);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
|
||||||
|
// Inspector starts disconnected.
|
||||||
|
mockUseConnection.mockReturnValue(disconnectedConnectionState);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not set hash when starting disconnected", async () => {
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(window.location.hash).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sets default hash based on server capabilities priority", async () => {
|
||||||
|
// Tab priority follows UI order: Resources | Prompts | Tools | Ping | Sampling | Roots | Auth
|
||||||
|
//
|
||||||
|
// Server capabilities determine the first three tabs; if none are present, falls back to Ping.
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
capabilities: { resources: { listChanged: true, subscribe: true } },
|
||||||
|
expected: "#resources",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: { prompts: { listChanged: true, subscribe: true } },
|
||||||
|
expected: "#prompts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: { tools: { listChanged: true, subscribe: true } },
|
||||||
|
expected: "#tools",
|
||||||
|
},
|
||||||
|
{ capabilities: {}, expected: "#ping" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { rerender } = render(<App />);
|
||||||
|
|
||||||
|
for (const { capabilities, expected } of testCases) {
|
||||||
|
window.location.hash = "";
|
||||||
|
mockUseConnection.mockReturnValue({
|
||||||
|
...connectedConnectionState,
|
||||||
|
serverCapabilities: capabilities,
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender(<App />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(window.location.hash).toBe(expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clears hash when disconnected", async () => {
|
||||||
|
// Start with a hash set (simulating a connection)
|
||||||
|
window.location.hash = "#resources";
|
||||||
|
|
||||||
|
// App starts disconnected (default mock)
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
// Should clear the hash when disconnected
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(window.location.hash).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue