Skip to content

Commit 21aa917

Browse files
authored
Merge pull request #638 from richardkmichael/routing-fragment-cleanup
chore: Routing fragment only when connected
2 parents 158720c + 098a0e5 commit 21aa917

File tree

4 files changed

+193
-3
lines changed

4 files changed

+193
-3
lines changed

client/e2e/startup-state.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
// Adjust the URL if your dev server runs on a different port
4+
const APP_URL = "http://localhost:6274/";
5+
6+
test.describe("Startup State", () => {
7+
test("should not navigate to a tab when Inspector first opens", async ({
8+
page,
9+
}) => {
10+
await page.goto(APP_URL);
11+
12+
// Check that there is no hash fragment in the URL
13+
const url = page.url();
14+
expect(url).not.toContain("#");
15+
});
16+
});

client/jest.config.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ module.exports = {
3232
"/e2e/",
3333
"\\.config\\.(js|ts|cjs|mjs)$",
3434
],
35+
randomize: true,
3536
};

client/src/App.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -465,10 +465,24 @@ const App = () => {
465465
}, [roots]);
466466

467467
useEffect(() => {
468-
if (!window.location.hash) {
469-
window.location.hash = "resources";
468+
if (mcpClient && !window.location.hash) {
469+
const defaultTab = serverCapabilities?.resources
470+
? "resources"
471+
: serverCapabilities?.prompts
472+
? "prompts"
473+
: serverCapabilities?.tools
474+
? "tools"
475+
: "ping";
476+
window.location.hash = defaultTab;
477+
} else if (!mcpClient && window.location.hash) {
478+
// Clear hash when disconnected - completely remove the fragment
479+
window.history.replaceState(
480+
null,
481+
"",
482+
window.location.pathname + window.location.search,
483+
);
470484
}
471-
}, []);
485+
}, [mcpClient, serverCapabilities]);
472486

473487
useEffect(() => {
474488
const handleHashChange = () => {
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { render, waitFor } from "@testing-library/react";
2+
import App from "../App";
3+
import { useConnection } from "../lib/hooks/useConnection";
4+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5+
6+
// Mock auth dependencies first
7+
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
8+
auth: jest.fn(),
9+
}));
10+
11+
jest.mock("../lib/oauth-state-machine", () => ({
12+
OAuthStateMachine: jest.fn(),
13+
}));
14+
15+
jest.mock("../lib/auth", () => ({
16+
InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
17+
tokens: jest.fn().mockResolvedValue(null),
18+
clear: jest.fn(),
19+
})),
20+
DebugInspectorOAuthClientProvider: jest.fn(),
21+
}));
22+
23+
// Mock the config utils
24+
jest.mock("../utils/configUtils", () => ({
25+
...jest.requireActual("../utils/configUtils"),
26+
getMCPProxyAddress: jest.fn(() => "http://localhost:6277"),
27+
getMCPProxyAuthToken: jest.fn(() => ({
28+
token: "",
29+
header: "X-MCP-Proxy-Auth",
30+
})),
31+
getInitialTransportType: jest.fn(() => "stdio"),
32+
getInitialSseUrl: jest.fn(() => "http://localhost:3001/sse"),
33+
getInitialCommand: jest.fn(() => "mcp-server-everything"),
34+
getInitialArgs: jest.fn(() => ""),
35+
initializeInspectorConfig: jest.fn(() => ({})),
36+
saveInspectorConfig: jest.fn(),
37+
}));
38+
39+
// Default connection state is disconnected
40+
const disconnectedConnectionState = {
41+
connectionStatus: "disconnected" as const,
42+
serverCapabilities: null,
43+
mcpClient: null,
44+
requestHistory: [],
45+
makeRequest: jest.fn(),
46+
sendNotification: jest.fn(),
47+
handleCompletion: jest.fn(),
48+
completionsSupported: false,
49+
connect: jest.fn(),
50+
disconnect: jest.fn(),
51+
};
52+
53+
// Connected state for tests that need an active connection
54+
const connectedConnectionState = {
55+
...disconnectedConnectionState,
56+
connectionStatus: "connected" as const,
57+
serverCapabilities: {},
58+
mcpClient: {
59+
request: jest.fn(),
60+
notification: jest.fn(),
61+
close: jest.fn(),
62+
} as unknown as Client,
63+
};
64+
65+
// Mock required dependencies, but unrelated to routing.
66+
jest.mock("../lib/hooks/useDraggablePane", () => ({
67+
useDraggablePane: () => ({
68+
height: 300,
69+
handleDragStart: jest.fn(),
70+
}),
71+
useDraggableSidebar: () => ({
72+
width: 320,
73+
isDragging: false,
74+
handleDragStart: jest.fn(),
75+
}),
76+
}));
77+
78+
jest.mock("../components/Sidebar", () => ({
79+
__esModule: true,
80+
default: () => <div>Sidebar</div>,
81+
}));
82+
83+
// Mock fetch
84+
global.fetch = jest.fn().mockResolvedValue({ json: () => Promise.resolve({}) });
85+
86+
// Use an empty module mock, so that mock state can be reset between tests.
87+
jest.mock("../lib/hooks/useConnection", () => ({
88+
useConnection: jest.fn(),
89+
}));
90+
91+
describe("App - URL Fragment Routing", () => {
92+
const mockUseConnection = jest.mocked(useConnection);
93+
94+
beforeEach(() => {
95+
jest.restoreAllMocks();
96+
97+
// Inspector starts disconnected.
98+
mockUseConnection.mockReturnValue(disconnectedConnectionState);
99+
});
100+
101+
test("does not set hash when starting disconnected", async () => {
102+
render(<App />);
103+
104+
await waitFor(() => {
105+
expect(window.location.hash).toBe("");
106+
});
107+
});
108+
109+
test("sets default hash based on server capabilities priority", async () => {
110+
// Tab priority follows UI order: Resources | Prompts | Tools | Ping | Sampling | Roots | Auth
111+
//
112+
// Server capabilities determine the first three tabs; if none are present, falls back to Ping.
113+
114+
const testCases = [
115+
{
116+
capabilities: { resources: { listChanged: true, subscribe: true } },
117+
expected: "#resources",
118+
},
119+
{
120+
capabilities: { prompts: { listChanged: true, subscribe: true } },
121+
expected: "#prompts",
122+
},
123+
{
124+
capabilities: { tools: { listChanged: true, subscribe: true } },
125+
expected: "#tools",
126+
},
127+
{ capabilities: {}, expected: "#ping" },
128+
];
129+
130+
const { rerender } = render(<App />);
131+
132+
for (const { capabilities, expected } of testCases) {
133+
window.location.hash = "";
134+
mockUseConnection.mockReturnValue({
135+
...connectedConnectionState,
136+
serverCapabilities: capabilities,
137+
});
138+
139+
rerender(<App />);
140+
141+
await waitFor(() => {
142+
expect(window.location.hash).toBe(expected);
143+
});
144+
}
145+
});
146+
147+
test("clears hash when disconnected", async () => {
148+
// Start with a hash set (simulating a connection)
149+
window.location.hash = "#resources";
150+
151+
// App starts disconnected (default mock)
152+
render(<App />);
153+
154+
// Should clear the hash when disconnected
155+
await waitFor(() => {
156+
expect(window.location.hash).toBe("");
157+
});
158+
});
159+
});

0 commit comments

Comments
 (0)