Skip to content

Commit 4cfcbd9

Browse files
HusneShabbirHusneShabbir
andauthored
test(lightspeed): MCP servers e2e — mocked API, scenarios, en-only suite (#2724)
* test(lightspeed): e2e MCP settings across display modes Add Chatbot MCP settings describe with Overlay, Dock, and Fullscreen coverage. Introduce POM helpers (open/close panel, verifyMcpSettingsPanel) and extend display-mode menu snapshot for MCP settings. Scope MCP table locators to avoid strict-mode clashes with the catalog; avoid form-only assertions in fullscreen. Made-with: Cursor * test(lightspeed): MCP servers e2e mocks, scenarios, and en-only suite - Mock GET /api/lightspeed/mcp-servers in devMode with replaceable scenarios - Add mcpServerMocks fixtures, presets, and status-detail helper for assertions - Extend verifyMcpSettingsPanel and heading helpers; scope MCP table actions - Add two-server and empty-list tests; runMcpPanelScenario helper - skipUnlessLocales: run Chatbot MCP settings e2e only on en project Made-with: Cursor * test(lightspeed): MCP e2e — PATCH mock, toggle, Name sort, helpers - mockMcpServers: handle PATCH for enable/disable; broaden route glob - LightspeedPage: mcpServersTableBodyRows, click Name/Status columns, gridcell toggle - tests: toggle row Disabled/14 tools; Name column sort order (allHealthy) Made-with: Cursor * test(lightspeed): MCP e2e — crisper titles, status helper cleanup - Rename Chatbot MCP settings tests; Sort / Toggle 'works as expected' - Simplify getExpectedMcpStatusDetailForMock (early returns) Made-with: Cursor --------- Co-authored-by: HusneShabbir <husneshabbir447@gmail.com>
1 parent 315239c commit 4cfcbd9

6 files changed

Lines changed: 468 additions & 2 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* GET /api/lightspeed/mcp-servers body shape (see McpServersSettings McpServerResponse).
19+
* Use {@link mcpServer} for defaults; override fields per scenario.
20+
*/
21+
export type McpServerMockEntry = {
22+
name: string;
23+
/** Optional — some backends send it; not shown in the MCP table name/status cells. */
24+
url?: string;
25+
enabled: boolean;
26+
status: 'connected' | 'error' | 'unknown';
27+
toolCount: number;
28+
hasToken: boolean;
29+
hasUserToken: boolean;
30+
};
31+
32+
export type McpServersListMock = {
33+
servers: McpServerMockEntry[];
34+
};
35+
36+
/** Build one entry; defaults match a healthy connected server. */
37+
export function mcpServer(
38+
name: string,
39+
overrides: Partial<Omit<McpServerMockEntry, 'name'>> = {},
40+
): McpServerMockEntry {
41+
return {
42+
name,
43+
enabled: true,
44+
status: 'connected',
45+
toolCount: 0,
46+
hasToken: true,
47+
hasUserToken: false,
48+
...overrides,
49+
};
50+
}
51+
52+
/**
53+
* Expected Status column text for a mock row — mirrors McpServersSettings getDisplayStatus +
54+
* getDisplayDetail.
55+
*/
56+
export function getExpectedMcpStatusDetailForMock(
57+
server: McpServerMockEntry,
58+
): string {
59+
// Same branch order as McpServersSettings getDisplayStatus + getDisplayDetail.
60+
if (!server.hasToken) return 'Token required';
61+
if (!server.enabled) return 'Disabled';
62+
if (server.status === 'error') return 'Failed';
63+
if (server.status === 'unknown') return 'Unknown';
64+
const suffix = server.toolCount === 1 ? 'tool' : 'tools';
65+
return `${server.toolCount} ${suffix}`;
66+
}
67+
68+
/** Named presets for Playwright `mockMcpServers(page, scenario)` and panel assertions. */
69+
export const mcpServerScenarios = {
70+
/** Default e2e: connected + tools + second row token-required. */
71+
default: {
72+
servers: [
73+
{
74+
name: 'mcp-integration-tools',
75+
url: 'http://localhost:7008/api/mcp-actions/v1',
76+
enabled: true,
77+
status: 'connected' as const,
78+
toolCount: 14,
79+
hasToken: true,
80+
hasUserToken: false,
81+
},
82+
{
83+
name: 'test-mcp-server',
84+
url: 'http://localhost:8888/mcp',
85+
enabled: true,
86+
status: 'unknown' as const,
87+
toolCount: 0,
88+
hasToken: false,
89+
hasUserToken: false,
90+
},
91+
],
92+
} satisfies McpServersListMock,
93+
94+
empty: { servers: [] } satisfies McpServersListMock,
95+
96+
allHealthy: {
97+
servers: [
98+
mcpServer('alpha-mcp', { toolCount: 3 }),
99+
mcpServer('beta-mcp', { toolCount: 27 }),
100+
],
101+
} satisfies McpServersListMock,
102+
103+
/** Single row — exercises "1 tool" (singular) copy. */
104+
singularTool: {
105+
servers: [mcpServer('single-tool-server', { toolCount: 1 })],
106+
} satisfies McpServersListMock,
107+
108+
/** One failed row + one ok row. */
109+
errorAndOk: {
110+
servers: [
111+
mcpServer('failing-mcp', { status: 'error', toolCount: 0 }),
112+
mcpServer('ok-mcp', { toolCount: 5 }),
113+
],
114+
} satisfies McpServersListMock,
115+
116+
/** Disabled row still lists toolCount in model but UI shows "Disabled". */
117+
disabledAndOk: {
118+
servers: [
119+
mcpServer('disabled-mcp', { enabled: false, toolCount: 12 }),
120+
mcpServer('active-mcp', { toolCount: 2 }),
121+
],
122+
} satisfies McpServersListMock,
123+
124+
/** Both rows require a token — header shows "0 of 2 selected". */
125+
twoTokenRequired: {
126+
servers: [
127+
mcpServer('needs-token-a', {
128+
hasToken: false,
129+
toolCount: 0,
130+
status: 'unknown',
131+
}),
132+
mcpServer('needs-token-b', {
133+
hasToken: false,
134+
toolCount: 0,
135+
status: 'unknown',
136+
}),
137+
],
138+
} satisfies McpServersListMock,
139+
140+
/** Connected + token + unknown API status → "Unknown" in Status column. */
141+
unknownStatus: {
142+
servers: [mcpServer('ambiguous-mcp', { status: 'unknown', toolCount: 99 })],
143+
} satisfies McpServersListMock,
144+
145+
onlyTokenRequired: {
146+
servers: [
147+
mcpServer('needs-token', {
148+
hasToken: false,
149+
toolCount: 0,
150+
status: 'unknown',
151+
}),
152+
],
153+
} satisfies McpServersListMock,
154+
};
155+
156+
export const mockedMcpServersResponse: McpServersListMock =
157+
mcpServerScenarios.default;

workspaces/lightspeed/packages/app-legacy/e2e-tests/fixtures/responses.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ export const mockedShields = [
9292
provider_resource_id: 'test-shield-id',
9393
},
9494
];
95+
96+
export {
97+
getExpectedMcpStatusDetailForMock,
98+
mcpServer,
99+
mcpServerScenarios,
100+
mockedMcpServersResponse,
101+
type McpServerMockEntry,
102+
type McpServersListMock,
103+
} from './mcpServerMocks';
95104
const repeatedSentence =
96105
'OpenShift deployment is a way to manage applications on the OpenShift platform.';
97106
const openshiftLongParagraph = `${repeatedSentence} `.repeat(30);

workspaces/lightspeed/packages/app-legacy/e2e-tests/lightspeed.test.ts

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
botResponse,
2424
moreConversations,
2525
mockedShields,
26+
mcpServerScenarios,
27+
type McpServersListMock,
2628
thinkingContent,
2729
assistantResponse,
2830
} from './fixtures/responses';
@@ -48,7 +50,15 @@ import {
4850
expectEmptyChatHistory,
4951
expectConversationArea,
5052
chatStopButton,
53+
verifyMcpSettingsPanel,
5154
waitForChatMessageLoadingHidden,
55+
openMcpSettingsPanel,
56+
closeMcpSettingsPanel,
57+
mcpServerToggle,
58+
mcpServerRow,
59+
clickMcpServersStatusColumn,
60+
clickMcpServersNameColumn,
61+
mcpServersTableBodyRows,
5262
} from './pages/LightspeedPage';
5363
import {
5464
uploadFiles,
@@ -109,6 +119,7 @@ import {
109119
mockChatHistory,
110120
mockConversations,
111121
mockFeedbackStatus,
122+
mockMcpServers,
112123
mockModels,
113124
mockQuery,
114125
mockQueryWithResponseDelay,
@@ -120,6 +131,7 @@ import {
120131
getTranslations,
121132
} from './utils/translations';
122133
import { runAccessibilityTests } from './utils/accessibility';
134+
import { skipUnlessLocales } from './utils/localeSkip';
123135

124136
test.describe('Lightspeed tests', () => {
125137
const botQuery = 'Please respond';
@@ -138,6 +150,7 @@ test.describe('Lightspeed tests', () => {
138150
await mockChatHistory(sharedPage);
139151
await mockQuery(sharedPage, botQuery, conversations);
140152
await mockShields(sharedPage, mockedShields);
153+
await mockMcpServers(sharedPage);
141154
await mockFeedbackStatus(sharedPage);
142155

143156
await sharedPage.goto('/');
@@ -192,8 +205,103 @@ test.describe('Lightspeed tests', () => {
192205
});
193206
});
194207

208+
test.describe('Chatbot MCP settings', () => {
209+
/** Applies mock + opens chatbot + runs panel assertions (single source for `mcpList` vs mock). */
210+
async function runMcpPanelScenario(mcpList: McpServersListMock) {
211+
await mockMcpServers(sharedPage, mcpList);
212+
await openChatbot(sharedPage);
213+
await verifyMcpSettingsPanel(sharedPage, translations, mcpList);
214+
}
215+
216+
test.beforeEach(async ({}, testInfo) => {
217+
skipUnlessLocales(
218+
testInfo,
219+
['en'],
220+
'Chatbot MCP settings uses English-only UI strings.',
221+
);
222+
await sharedPage.goto('/');
223+
});
224+
225+
test.afterEach(async () => {
226+
await mockMcpServers(sharedPage);
227+
});
228+
229+
test('Overlay', async () => {
230+
await expectBackstagePageVisible(sharedPage);
231+
await openChatbot(sharedPage);
232+
await verifyMcpSettingsPanel(sharedPage, translations);
233+
});
234+
235+
test('Dock to Window', async () => {
236+
await openChatbot(sharedPage);
237+
await selectDisplayMode(sharedPage, translations, 'Dock to window');
238+
await verifyMcpSettingsPanel(sharedPage, translations);
239+
});
240+
241+
test('Fullscreen', async () => {
242+
await openChatbot(sharedPage);
243+
await selectDisplayMode(sharedPage, translations, 'Fullscreen');
244+
await verifyMcpSettingsPanel(sharedPage, translations);
245+
});
246+
247+
test('Empty list', async () => {
248+
await runMcpPanelScenario(mcpServerScenarios.empty);
249+
});
250+
251+
test.describe('Two-row mocks', () => {
252+
test('All healthy', async () => {
253+
await runMcpPanelScenario(mcpServerScenarios.allHealthy);
254+
});
255+
256+
test('Failed + OK', async () => {
257+
await runMcpPanelScenario(mcpServerScenarios.errorAndOk);
258+
});
259+
260+
test('Disabled + active', async () => {
261+
await runMcpPanelScenario(mcpServerScenarios.disabledAndOk);
262+
});
263+
264+
test('Both need token', async () => {
265+
await runMcpPanelScenario(mcpServerScenarios.twoTokenRequired);
266+
});
267+
});
268+
269+
test('Sort works as expected', async () => {
270+
await mockMcpServers(sharedPage, mcpServerScenarios.allHealthy);
271+
await openChatbot(sharedPage);
272+
await openMcpSettingsPanel(sharedPage, translations);
273+
274+
const rows = mcpServersTableBodyRows(sharedPage);
275+
await expect(rows.nth(0)).toContainText('alpha-mcp');
276+
await expect(rows.nth(1)).toContainText('beta-mcp');
277+
278+
await clickMcpServersNameColumn(sharedPage);
279+
await expect(rows.nth(0)).toContainText('beta-mcp');
280+
await expect(rows.nth(1)).toContainText('alpha-mcp');
281+
282+
await closeMcpSettingsPanel(sharedPage);
283+
});
284+
285+
test('Toggle works as expected', async () => {
286+
const serverName = 'mcp-integration-tools';
287+
await openChatbot(sharedPage);
288+
await openMcpSettingsPanel(sharedPage, translations);
289+
290+
const row = mcpServerRow(sharedPage, serverName);
291+
await clickMcpServersStatusColumn(sharedPage);
292+
await mcpServerToggle(sharedPage, serverName).click();
293+
await expect(row.getByText('Disabled', { exact: true })).toBeVisible();
294+
295+
await mcpServerToggle(sharedPage, serverName).click();
296+
await expect(row.getByText('14 tools', { exact: true })).toBeVisible();
297+
298+
await closeMcpSettingsPanel(sharedPage);
299+
});
300+
});
301+
195302
test('Lightspeed is available', async ({ browser }, testInfo) => {
196-
expect(sharedPage.url()).toContain('/lightspeed');
303+
await openLightspeed(sharedPage);
304+
await expect(sharedPage).toHaveURL(/\/lightspeed/);
197305

198306
const headings = sharedPage.getByRole('heading');
199307
await expect(headings.first()).toContainText(

0 commit comments

Comments
 (0)