Skip to content

Commit 63b0c78

Browse files
test(mcp): add Playwright E2E test for search and filter
Add E2E tests for the Remote MCP servers list page that run against a production build with React Compiler active. These tests verify that the search input accepts keystrokes, filters table rows, and that the status faceted filter opens and is selectable — all of which were broken when React Compiler memoized shared DataTable component callbacks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dcae623 commit 63b0c78

File tree

1 file changed

+127
-0
lines changed

1 file changed

+127
-0
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
/**
4+
* Remote MCP Servers - Search & Filter E2E Tests
5+
*
6+
* These tests run against a production build where React Compiler is active.
7+
* They verify that the search input and status filter on the MCP servers list
8+
* page work correctly — a regression that occurred when React Compiler
9+
* incorrectly memoized DataTableFacetedFilter and Input callbacks.
10+
*/
11+
test.describe('Remote MCP Servers - Search & Filter', () => {
12+
test('search input accepts keystrokes and reflects typed value', async ({ page }) => {
13+
await page.goto('/mcp-servers');
14+
15+
const searchInput = page.getByPlaceholder('Filter servers...');
16+
await expect(searchInput).toBeVisible();
17+
18+
// Type into the search input — this was broken when React Compiler
19+
// memoized the onChange handler and froze the stale filter value.
20+
await searchInput.fill('test-server');
21+
await expect(searchInput).toHaveValue('test-server');
22+
23+
// Clear and retype
24+
await searchInput.clear();
25+
await expect(searchInput).toHaveValue('');
26+
27+
await searchInput.fill('another');
28+
await expect(searchInput).toHaveValue('another');
29+
});
30+
31+
test('search input filters table rows by name', async ({ page }) => {
32+
await page.goto('/mcp-servers');
33+
34+
const searchInput = page.getByPlaceholder('Filter servers...');
35+
await expect(searchInput).toBeVisible();
36+
37+
// Wait for table to settle (loading state to complete)
38+
await page.waitForTimeout(500);
39+
40+
// Count initial rows
41+
const table = page.locator('table');
42+
const hasTable = await table.isVisible({ timeout: 2000 }).catch(() => false);
43+
44+
if (hasTable) {
45+
const initialRowCount = await table.locator('tbody tr').count();
46+
47+
if (initialRowCount > 0) {
48+
// Get the name of the first server
49+
const firstName = await table.locator('tbody tr').first().locator('td').first().textContent();
50+
51+
// Search for a non-matching term
52+
await searchInput.fill('zzz-nonexistent-server-xyz');
53+
await page.waitForTimeout(300);
54+
55+
const filteredCount = await table.locator('tbody tr').count();
56+
// Either 0 rows or an empty state row
57+
expect(filteredCount).toBeLessThanOrEqual(1);
58+
59+
// Clear and search for the first server's name
60+
await searchInput.clear();
61+
await searchInput.fill(firstName?.trim() || '');
62+
await page.waitForTimeout(300);
63+
64+
const matchedCount = await table.locator('tbody tr').count();
65+
expect(matchedCount).toBeGreaterThanOrEqual(1);
66+
}
67+
}
68+
});
69+
70+
test('status filter dropdown opens and options are selectable', async ({ page }) => {
71+
await page.goto('/mcp-servers');
72+
73+
// Wait for page to load
74+
await page.waitForTimeout(500);
75+
76+
// Find the Status filter button — DataTableFacetedFilter renders a button with title text
77+
const statusButton = page.getByRole('button', { name: 'Status' });
78+
const hasStatusFilter = await statusButton.isVisible({ timeout: 2000 }).catch(() => false);
79+
80+
if (hasStatusFilter) {
81+
// Click to open the filter popover — this was broken when React Compiler
82+
// memoized the Popover state and prevented re-opens.
83+
await statusButton.click();
84+
85+
// Verify the popover opened with status options
86+
const runningOption = page.getByRole('option', { name: /running/i });
87+
const hasRunning = await runningOption.isVisible({ timeout: 1000 }).catch(() => false);
88+
89+
if (hasRunning) {
90+
await runningOption.click();
91+
92+
// The "Clear" button should appear when a filter is active
93+
const clearButton = page.getByRole('button', { name: 'Clear' });
94+
await expect(clearButton).toBeVisible({ timeout: 2000 });
95+
96+
// Click clear to reset
97+
await clearButton.click();
98+
}
99+
}
100+
});
101+
102+
test('search input can be combined with status filter', async ({ page }) => {
103+
await page.goto('/mcp-servers');
104+
105+
const searchInput = page.getByPlaceholder('Filter servers...');
106+
await expect(searchInput).toBeVisible();
107+
108+
// Type a search term
109+
await searchInput.fill('test');
110+
await expect(searchInput).toHaveValue('test');
111+
112+
// Open status filter while search is active
113+
const statusButton = page.getByRole('button', { name: 'Status' });
114+
const hasStatusFilter = await statusButton.isVisible({ timeout: 2000 }).catch(() => false);
115+
116+
if (hasStatusFilter) {
117+
await statusButton.click();
118+
await page.waitForTimeout(300);
119+
120+
// Close by clicking elsewhere
121+
await page.keyboard.press('Escape');
122+
}
123+
124+
// Verify search input still has the value (not cleared by filter interaction)
125+
await expect(searchInput).toHaveValue('test');
126+
});
127+
});

0 commit comments

Comments
 (0)