Clean up examples (#182)
* Centralize example server transport logic in shared utils Move stdio/HTTP transport handling into `server-utils.ts` so example servers only need to call `startServer(createServer)`. Switch HTTP transport to stateless mode and remove legacy SSE endpoints. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Make example servers self-contained with local server-utils Copy `server-utils.ts` into each example's `src/` directory so examples can be used standalone without the shared directory. Updates imports from `"../shared/server-utils.js"` to `"./src/server-utils.js"`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add `integration-server` example for E2E testing Add dedicated `integration-server` example (duplicated from `basic-server-react`) to preserve E2E testing capabilities while allowing `basic-server-react` to be simplified as a minimal example. Changes: - Add `examples/integration-server` exercising SDK communication APIs - Update E2E tests to use `integration-server` instead of `basic-server-react` and `basic-server-vanillajs` - Use server name labels instead of indices for more robust test selection - Remove `basic-react.png` and `basic-vanillajs.png` golden snapshots 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Simplify `basic-server-*` examples - Clarify comments - Return plain text from `get-time` tool instead of JSON - Remove simulated cleanup delay from `onteardown` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3222c87c56
commit
9e9f5f0210
2 changed files with 1748 additions and 475 deletions
|
|
@ -4,7 +4,8 @@ import { test, expect, type Page, type ConsoleMessage } from "@playwright/test";
|
|||
// Note: CSS modules generate unique class names, so we use attribute selectors
|
||||
// with partial matches (e.g., [class*="heatmapWrapper"]) for those components
|
||||
const DYNAMIC_MASKS: Record<string, string[]> = {
|
||||
"basic-react": ["code"], // Server time display
|
||||
integration: ["#server-time"], // Server time display
|
||||
"basic-react": ["#server-time"], // Server time display
|
||||
"basic-vanillajs": ["#server-time"], // Server time display
|
||||
"cohort-heatmap": ['[class*="heatmapWrapper"]'], // Heatmap grid (random data)
|
||||
"customer-segmentation": [".chart-container"], // Scatter plot (random data)
|
||||
|
|
@ -16,29 +17,22 @@ const DYNAMIC_MASKS: Record<string, string[]> = {
|
|||
"#memory-bar-fill", // Memory bar fill level
|
||||
"#info-uptime", // System uptime
|
||||
],
|
||||
"wiki-explorer": ["#graph"], // Force-directed graph (dynamic layout)
|
||||
threejs: ["canvas"], // 3D render canvas (dynamic animation)
|
||||
"wiki-explorer": ["#graph"], // Force-directed graph (dynamic layout)
|
||||
};
|
||||
|
||||
// Server configurations
|
||||
// Server configurations (key is used for screenshot filenames, name is the MCP server name)
|
||||
const SERVERS = [
|
||||
{ key: "basic-react", index: 0, name: "Basic MCP App Server (React-based)" },
|
||||
{
|
||||
key: "basic-vanillajs",
|
||||
index: 1,
|
||||
name: "Basic MCP App Server (Vanilla JS)",
|
||||
},
|
||||
{ key: "budget-allocator", index: 2, name: "Budget Allocator Server" },
|
||||
{ key: "cohort-heatmap", index: 3, name: "Cohort Heatmap Server" },
|
||||
{
|
||||
key: "customer-segmentation",
|
||||
index: 4,
|
||||
name: "Customer Segmentation Server",
|
||||
},
|
||||
{ key: "scenario-modeler", index: 5, name: "SaaS Scenario Modeler" },
|
||||
{ key: "system-monitor", index: 6, name: "System Monitor Server" },
|
||||
{ key: "threejs", index: 7, name: "Three.js Server" },
|
||||
{ key: "wiki-explorer", index: 8, name: "Wiki Explorer" },
|
||||
{ key: "integration", name: "Integration Test Server" },
|
||||
{ key: "basic-react", name: "Basic MCP App Server (React)" },
|
||||
{ key: "basic-vanillajs", name: "Basic MCP App Server (Vanilla JS)" },
|
||||
{ key: "budget-allocator", name: "Budget Allocator Server" },
|
||||
{ key: "cohort-heatmap", name: "Cohort Heatmap Server" },
|
||||
{ key: "customer-segmentation", name: "Customer Segmentation Server" },
|
||||
{ key: "scenario-modeler", name: "SaaS Scenario Modeler" },
|
||||
{ key: "system-monitor", name: "System Monitor Server" },
|
||||
{ key: "threejs", name: "Three.js Server" },
|
||||
{ key: "wiki-explorer", name: "Wiki Explorer" },
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -72,13 +66,13 @@ async function waitForAppLoad(page: Page) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Load a server by selecting it and clicking Call Tool
|
||||
* Load a server by selecting it by name and clicking Call Tool
|
||||
*/
|
||||
async function loadServer(page: Page, serverIndex: number) {
|
||||
async function loadServer(page: Page, serverName: string) {
|
||||
await page.goto("/");
|
||||
// Wait for servers to connect (select becomes enabled when servers are ready)
|
||||
await expect(page.locator("select").first()).toBeEnabled({ timeout: 30000 });
|
||||
await page.locator("select").first().selectOption({ index: serverIndex });
|
||||
await page.locator("select").first().selectOption({ label: serverName });
|
||||
await page.click('button:has-text("Call Tool")');
|
||||
await waitForAppLoad(page);
|
||||
}
|
||||
|
|
@ -113,11 +107,11 @@ test.describe("Host UI", () => {
|
|||
SERVERS.forEach((server) => {
|
||||
test.describe(server.name, () => {
|
||||
test("loads app UI", async ({ page }) => {
|
||||
await loadServer(page, server.index);
|
||||
await loadServer(page, server.name);
|
||||
});
|
||||
|
||||
test("screenshot matches golden", async ({ page }) => {
|
||||
await loadServer(page, server.index);
|
||||
await loadServer(page, server.name);
|
||||
await page.waitForTimeout(500); // Brief stabilization
|
||||
|
||||
// Get mask locators for dynamic content (timestamps, charts, etc.)
|
||||
|
|
@ -131,52 +125,46 @@ SERVERS.forEach((server) => {
|
|||
});
|
||||
});
|
||||
|
||||
// Interaction tests for basic servers (React and Vanilla JS have identical UIs)
|
||||
const BASIC_SERVERS = SERVERS.filter(
|
||||
(s) => s.key === "basic-react" || s.key === "basic-vanillajs",
|
||||
);
|
||||
// Interaction tests for integration server (tests all SDK communication APIs)
|
||||
const integrationServer = SERVERS.find((s) => s.key === "integration")!;
|
||||
|
||||
BASIC_SERVERS.forEach((server) => {
|
||||
test.describe(`${server.name} - Interactions`, () => {
|
||||
test("Send Message button triggers host callback", async ({ page }) => {
|
||||
const logs = captureHostLogs(page);
|
||||
await loadServer(page, server.index);
|
||||
test.describe(`${integrationServer.name} - Interactions`, () => {
|
||||
test("Send Message button triggers host callback", async ({ page }) => {
|
||||
const logs = captureHostLogs(page);
|
||||
await loadServer(page, integrationServer.name);
|
||||
|
||||
const appFrame = getAppFrame(page);
|
||||
await appFrame.locator('button:has-text("Send Message")').click();
|
||||
const appFrame = getAppFrame(page);
|
||||
await appFrame.locator('button:has-text("Send Message")').click();
|
||||
|
||||
// Wait for the async message to be processed
|
||||
await page.waitForTimeout(500);
|
||||
// Wait for the async message to be processed
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
expect(logs.some((log) => log.includes("Message from MCP App"))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
expect(logs.some((log) => log.includes("Message from MCP App"))).toBe(true);
|
||||
});
|
||||
|
||||
test("Send Log button triggers host callback", async ({ page }) => {
|
||||
const logs = captureHostLogs(page);
|
||||
await loadServer(page, server.index);
|
||||
test("Send Log button triggers host callback", async ({ page }) => {
|
||||
const logs = captureHostLogs(page);
|
||||
await loadServer(page, integrationServer.name);
|
||||
|
||||
const appFrame = getAppFrame(page);
|
||||
await appFrame.locator('button:has-text("Send Log")').click();
|
||||
const appFrame = getAppFrame(page);
|
||||
await appFrame.locator('button:has-text("Send Log")').click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
expect(logs.some((log) => log.includes("Log message from MCP App"))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
expect(logs.some((log) => log.includes("Log message from MCP App"))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("Open Link button triggers host callback", async ({ page }) => {
|
||||
const logs = captureHostLogs(page);
|
||||
await loadServer(page, server.index);
|
||||
test("Open Link button triggers host callback", async ({ page }) => {
|
||||
const logs = captureHostLogs(page);
|
||||
await loadServer(page, integrationServer.name);
|
||||
|
||||
const appFrame = getAppFrame(page);
|
||||
await appFrame.locator('button:has-text("Open Link")').click();
|
||||
const appFrame = getAppFrame(page);
|
||||
await appFrame.locator('button:has-text("Open Link")').click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
expect(logs.some((log) => log.includes("Open link request"))).toBe(true);
|
||||
});
|
||||
expect(logs.some((log) => log.includes("Open link request"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
BIN
tests/e2e/servers.spec.ts-snapshots/integration.png
Normal file
BIN
tests/e2e/servers.spec.ts-snapshots/integration.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Loading…
Reference in a new issue