Skip to content

Commit a46f967

Browse files
fix(tests): flaky Chat Client E2E tests and failing CreateServer unit test (#8887)
* Initial plan * Fix flaky and failing Chat Client E2E tests - Fix 5 flaky tests (strict mode violations) in username-display.spec.ts and message-variations.spec.ts by adding .first() to getByText() locators. The SSE mock echo response 'I received your message: ...' contains the sent text, causing Playwright to find 2 elements and fail strict mode. - Fix consistently failing test 'should disable input while waiting for response' in input-validation.spec.ts by adding a test-specific route handler for message/stream that delays the response by 3 seconds, ensuring the agent request stays in-flight long enough to observe the disabled input state (previously route.continue() failed immediately with no real backend, causing the input to re-enable too quickly). - Extract delay as named constant MESSAGE_STREAM_MOCK_DELAY_MS for clarity. Co-authored-by: ccastrotrejo <102700317+ccastrotrejo@users.noreply.github.com> * Fix unit test: CreateServer closes panel after successful submission Two related fixes: 1. libs/designer/src/lib/ui/mcp/panel/server/create.tsx: Move dispatch(closePanel()) inside the try block (after await onUpdate()) instead of after the finally block. This is semantically correct (panel should only close on success, not when onUpdate throws) and avoids subtle Promise chain timing issues in async tests with React 18 + jsdom. 2. libs/designer/src/lib/ui/mcp/panel/server/__test__/create.spec.tsx: Rewrite "closes panel after successful submission" test to use fireEvent.change instead of userEvent.type. Fluent UI's tabster focus-trapping library intercepts keyboard events fired by userEvent.type, causing only partial text to be typed (CI evidence: value="T" in HTML snapshot instead of value="TestServer"). Use waitFor to wait for the submit button to become enabled before clicking, matching the established pattern in the file (test "calls onUpdate with correct data" at line 472). Co-authored-by: ccastrotrejo <102700317+ccastrotrejo@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ccastrotrejo <102700317+ccastrotrejo@users.noreply.github.com>
1 parent 1771731 commit a46f967

File tree

5 files changed

+65
-14
lines changed

5 files changed

+65
-14
lines changed

e2e/chatClient/tests/features/authentication/username-display.spec.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,8 @@ test.describe('Username Edge Cases', { tag: '@mock' }, () => {
205205
await sendButton.click();
206206

207207
// Message should still appear and chat should function normally
208-
await expect(page.getByText('Test message without username')).toBeVisible({ timeout: 5000 });
208+
// Use .first() to avoid strict mode violation when agent echo response also contains the message text
209+
await expect(page.getByText('Test message without username').first()).toBeVisible({ timeout: 5000 });
209210
// Chat should remain functional
210211
await expect(messageInput).toBeEnabled({ timeout: 10000 });
211212
});
@@ -233,7 +234,8 @@ test.describe('Username Edge Cases', { tag: '@mock' }, () => {
233234
await sendButton.click();
234235

235236
// Chat should still work despite malformed JWT
236-
await expect(page.getByText('Test with malformed token')).toBeVisible({ timeout: 5000 });
237+
// Use .first() to avoid strict mode violation when agent echo response also contains the message text
238+
await expect(page.getByText('Test with malformed token').first()).toBeVisible({ timeout: 5000 });
237239
await expect(messageInput).toBeEnabled({ timeout: 10000 });
238240
});
239241

@@ -260,7 +262,8 @@ test.describe('Username Edge Cases', { tag: '@mock' }, () => {
260262
await sendButton.click();
261263

262264
// Chat should still function without username
263-
await expect(page.getByText('Test without access token')).toBeVisible({ timeout: 5000 });
265+
// Use .first() to avoid strict mode violation when agent echo response also contains the message text
266+
await expect(page.getByText('Test without access token').first()).toBeVisible({ timeout: 5000 });
264267
await expect(messageInput).toBeEnabled({ timeout: 10000 });
265268
});
266269
});

e2e/chatClient/tests/features/messaging/input-validation.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ const AGENT_CARD = {
2424

2525
const AGENT_CARD_URL = 'http://localhost:3001/api/agents/test/.well-known/agent-card.json';
2626

27+
// Delay (ms) used in the message/stream mock to keep the agent request in-flight
28+
// long enough for tests to observe the disabled input state.
29+
const MESSAGE_STREAM_MOCK_DELAY_MS = 3000;
30+
2731
test.describe('Input Validation', { tag: '@mock' }, () => {
2832
test.beforeEach(async ({ page }) => {
2933
// Mock authentication - return authenticated user
@@ -276,6 +280,48 @@ test.describe('Input State Management', { tag: '@mock' }, () => {
276280
// Input should be enabled initially
277281
await expect(messageInput).toBeEnabled();
278282

283+
// Override agent route for this test to add a delay on message/stream requests.
284+
// This ensures the request stays in-flight long enough to observe the disabled state,
285+
// instead of failing immediately (route.continue() with no real backend) and re-enabling.
286+
await page.route('**/api/agents/test', async (route: Route) => {
287+
if (route.request().method() !== 'POST') {
288+
await route.continue();
289+
return;
290+
}
291+
292+
const postData = route.request().postDataJSON();
293+
294+
if (postData?.method === 'contexts/list') {
295+
await route.fulfill({
296+
status: 200,
297+
contentType: 'application/json',
298+
body: JSON.stringify({ jsonrpc: '2.0', id: postData.id, result: [] }),
299+
});
300+
return;
301+
}
302+
303+
if (postData?.method === 'message/stream') {
304+
// Delay response so the input stays disabled long enough to be observed
305+
await new Promise<void>((resolve) => setTimeout(resolve, MESSAGE_STREAM_MOCK_DELAY_MS));
306+
await route.fulfill({
307+
status: 200,
308+
headers: {
309+
'Content-Type': 'text/event-stream',
310+
'Cache-Control': 'no-cache',
311+
Connection: 'keep-alive',
312+
},
313+
body: `data: ${JSON.stringify({
314+
jsonrpc: '2.0',
315+
id: postData.id,
316+
result: { status: { state: 'completed' }, kind: 'status-update', final: true },
317+
})}\n\n`,
318+
});
319+
return;
320+
}
321+
322+
await route.continue();
323+
});
324+
279325
// Send first message
280326
await messageInput.fill('First message');
281327
await sendButton.click();

e2e/chatClient/tests/features/messaging/message-variations.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ test.describe('Emoji and Complex Unicode', { tag: '@mock' }, () => {
277277
await messageInput.fill(symbolsMessage);
278278
await sendButton.click();
279279

280-
await expect(page.getByText(symbolsMessage)).toBeVisible({ timeout: 5000 });
280+
await expect(page.getByText(symbolsMessage).first()).toBeVisible({ timeout: 5000 });
281281
});
282282

283283
test('should handle box drawing and block characters', async ({ page }) => {
@@ -289,7 +289,8 @@ test.describe('Emoji and Complex Unicode', { tag: '@mock' }, () => {
289289
await sendButton.click();
290290

291291
// Check for a part of the box drawing
292-
await expect(page.getByText('┌─┬─┐', { exact: false })).toBeVisible({ timeout: 5000 });
292+
// Use .first() to avoid strict mode violation when agent echo response also contains the message text
293+
await expect(page.getByText('┌─┬─┐', { exact: false }).first()).toBeVisible({ timeout: 5000 });
293294
});
294295
});
295296

libs/designer/src/lib/ui/mcp/panel/server/__test__/create.spec.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -528,22 +528,23 @@ describe('CreateServer', () => {
528528
});
529529

530530
it('closes panel after successful submission', async () => {
531-
const user = userEvent.setup();
532-
533531
renderWithProviders({ onUpdate: mockOnUpdate, onClose: mockOnClose });
534532

535533
const nameInput = screen.getByTestId('textfield-name');
536534
const descriptionInput = screen.getByTestId('textarea-description');
537535
const workflow1Option = screen.getByTestId('option-workflow1');
538-
const submitButton = screen.getByTestId('footer-button-0');
539536

540-
await user.clear(nameInput);
541-
await user.type(nameInput, 'TestServer');
542-
await user.clear(descriptionInput);
543-
await user.type(descriptionInput, 'Test Description');
537+
// Use fireEvent.change for direct, synchronous control to avoid keyboard event
538+
// interference from Fluent UI's tabster focus-trapping (consistent with the
539+
// "calls onUpdate with correct data" test above)
540+
fireEvent.change(nameInput, { target: { value: 'TestServer' } });
541+
fireEvent.change(descriptionInput, { target: { value: 'Test Description' } });
544542
fireEvent.click(workflow1Option);
545543

546-
fireEvent.click(submitButton);
544+
// Wait for state updates to settle so the submit button is enabled
545+
await waitFor(() => expect(screen.getByTestId('footer-button-0')).toBeEnabled());
546+
547+
fireEvent.click(screen.getByTestId('footer-button-0'));
547548

548549
await waitFor(() => {
549550
expect(mockClosePanel).toHaveBeenCalled();

libs/designer/src/lib/ui/mcp/panel/server/create.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,12 +274,12 @@ export const CreateServer = ({
274274
description: serverDescription,
275275
tools: selectedTools.map((toolName) => ({ name: workflowOptions.find((option) => option.value === toolName)?.label ?? toolName })),
276276
});
277+
dispatch(closePanel());
277278
} catch {
278279
// Need to log the error properly here.
279280
} finally {
280281
setIsCreatingOrUpdating(false);
281282
}
282-
dispatch(closePanel());
283283
}, [dispatch, onUpdate, serverName, serverDescription, selectedTools, workflowOptions]);
284284

285285
const createOrUpdateButtonText = useMemo(() => {

0 commit comments

Comments
 (0)