Skip to content

Commit efe91e5

Browse files
committed
add EXAMPLE=folder-name support for tests & basic-host
1 parent 8c3b1da commit efe91e5

6 files changed

Lines changed: 114 additions & 38 deletions

File tree

examples/run-all.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
* bun examples/run-all.ts start - Build and start all examples
77
* bun examples/run-all.ts dev - Run all examples in dev/watch mode
88
* bun examples/run-all.ts build - Build all examples
9+
*
10+
* Environment:
11+
* EXAMPLE=<folder> - Run only a single example (e.g., EXAMPLE=say-server)
912
*/
1013

1114
import { readdirSync, statSync, existsSync } from "fs";
@@ -14,21 +17,36 @@ import concurrently from "concurrently";
1417
const BASE_PORT = 3101;
1518
const BASIC_HOST = "basic-host";
1619

20+
// Optional: filter to a single example via EXAMPLE env var (folder name)
21+
const EXAMPLE_FILTER = process.env.EXAMPLE;
22+
1723
// Find all example directories except basic-host that have a package.json,
1824
// assign ports, and build URL list
19-
const servers = readdirSync("examples")
25+
const allServers = readdirSync("examples")
2026
.filter(
2127
(d) =>
2228
d !== BASIC_HOST &&
2329
statSync(`examples/${d}`).isDirectory() &&
2430
existsSync(`examples/${d}/package.json`),
2531
)
26-
.sort() // Sort for consistent port assignment
27-
.map((dir, i) => ({
28-
dir,
29-
port: BASE_PORT + i,
30-
url: `http://localhost:${BASE_PORT + i}/mcp`,
31-
}));
32+
.sort(); // Sort for consistent port assignment
33+
34+
// Filter servers if EXAMPLE is specified
35+
const filteredDirs = EXAMPLE_FILTER
36+
? allServers.filter((d) => d === EXAMPLE_FILTER)
37+
: allServers;
38+
39+
if (EXAMPLE_FILTER && filteredDirs.length === 0) {
40+
console.error(`Error: No example found matching EXAMPLE=${EXAMPLE_FILTER}`);
41+
console.error(`Available examples: ${allServers.join(", ")}`);
42+
process.exit(1);
43+
}
44+
45+
const servers = filteredDirs.map((dir, i) => ({
46+
dir,
47+
port: BASE_PORT + i,
48+
url: `http://localhost:${BASE_PORT + i}/mcp`,
49+
}));
3250

3351
const COMMANDS = ["start", "dev", "build"];
3452

@@ -43,6 +61,9 @@ if (!command || !COMMANDS.includes(command)) {
4361
const serversEnv = JSON.stringify(servers.map((s) => s.url));
4462

4563
console.log(`Running command: ${command}`);
64+
if (EXAMPLE_FILTER) {
65+
console.log(`Filtering to single example: ${EXAMPLE_FILTER}`);
66+
}
4667
console.log(
4768
`Server examples: ${servers.map((s) => `${s.dir}:${s.port}`).join(", ")}`,
4869
);

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@
5454
"test:e2e": "playwright test",
5555
"test:e2e:update": "playwright test --update-snapshots",
5656
"test:e2e:ui": "playwright test --ui",
57-
"test:e2e:docker": "docker run --rm -v $(pwd):/work -w /work -it mcr.microsoft.com/playwright:v1.57.0-noble sh -c 'apt-get update -qq && apt-get install -qq -y python3-venv curl > /dev/null && curl -LsSf https://astral.sh/uv/install.sh | sh && export PATH=\"$HOME/.local/bin:$PATH\" && npm i -g bun && npm ci && npx playwright test'",
58-
"test:e2e:docker:update": "npm run build:all && docker run --rm -v $(pwd):/work -w /work -it mcr.microsoft.com/playwright:v1.57.0-noble sh -c 'apt-get update -qq && apt-get install -qq -y python3-venv curl > /dev/null && curl -LsSf https://astral.sh/uv/install.sh | sh && export PATH=\"$HOME/.local/bin:$PATH\" && npm i -g bun && npm ci && npx playwright test --update-snapshots'",
57+
"test:e2e:docker": "docker run --rm -e EXAMPLE -v $(pwd):/work -w /work -it mcr.microsoft.com/playwright:v1.57.0-noble sh -c 'apt-get update -qq && apt-get install -qq -y python3-venv curl > /dev/null && curl -LsSf https://astral.sh/uv/install.sh | sh && export PATH=\"$HOME/.local/bin:$PATH\" && npm i -g bun && npm ci && npx playwright test'",
58+
"test:e2e:docker:update": "npm run build:all && docker run --rm -e EXAMPLE -v $(pwd):/work -w /work -it mcr.microsoft.com/playwright:v1.57.0-noble sh -c 'apt-get update -qq && apt-get install -qq -y python3-venv curl > /dev/null && curl -LsSf https://astral.sh/uv/install.sh | sh && export PATH=\"$HOME/.local/bin:$PATH\" && npm i -g bun && npm ci && npx playwright test --update-snapshots'",
5959
"preexamples:build": "npm run build",
6060
"examples:build": "bun examples/run-all.ts build",
6161
"examples:start": "NODE_ENV=development npm run build && bun examples/run-all.ts start",
@@ -64,7 +64,7 @@
6464
"prepare": "node scripts/setup-bun.mjs && npm run build && husky",
6565
"docs": "typedoc",
6666
"docs:watch": "typedoc --watch",
67-
"generate:screenshots": "npm run build:all && docker run --rm -v $(pwd):/work -w /work mcr.microsoft.com/playwright:v1.57.0-noble sh -c 'apt-get update -qq && apt-get install -qq -y python3-venv curl > /dev/null && curl -LsSf https://astral.sh/uv/install.sh | sh && export PATH=\"$HOME/.local/bin:$PATH\" && npm i -g bun && npm ci && npx playwright test tests/e2e/generate-grid-screenshots.spec.ts'",
67+
"generate:screenshots": "npm run build:all && docker run --rm -e EXAMPLE -v $(pwd):/work -w /work mcr.microsoft.com/playwright:v1.57.0-noble sh -c 'apt-get update -qq && apt-get install -qq -y python3-venv curl > /dev/null && curl -LsSf https://astral.sh/uv/install.sh | sh && export PATH=\"$HOME/.local/bin:$PATH\" && npm i -g bun && npm ci && npx playwright test tests/e2e/generate-grid-screenshots.spec.ts'",
6868
"prettier": "prettier -u \"**/*.{js,jsx,ts,tsx,mjs,json,md,yml,yaml}\" --check",
6969
"prettier:fix": "prettier -u \"**/*.{js,jsx,ts,tsx,mjs,json,md,yml,yaml}\" --write",
7070
"check:versions": "node scripts/check-versions.mjs"

playwright.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,19 @@ export default defineConfig({
2727
},
2828
],
2929
// Run examples server before tests
30+
// Supports EXAMPLE=<folder> env var to run a single example (e.g., EXAMPLE=say-server npm run test:e2e)
3031
webServer: {
3132
command: "npm run examples:start",
3233
url: "http://localhost:8080",
3334
// Always start fresh servers to avoid stale state issues
3435
reuseExistingServer: false,
3536
// 3 minutes to allow uv to download Python dependencies on first run
3637
timeout: 180000,
38+
// Pass through EXAMPLE env var to filter to a single server
39+
env: {
40+
...process.env,
41+
EXAMPLE: process.env.EXAMPLE ?? "",
42+
},
3743
},
3844
// Snapshot configuration
3945
expect: {

tests/e2e/generate-grid-screenshots.spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ const SKIP_SERVERS = new Set([
3030
"video-resource", // Uses custom screenshot from PR comment
3131
]);
3232

33+
// Optional: filter to a single example via EXAMPLE env var (folder name)
34+
const EXAMPLE_FILTER = process.env.EXAMPLE;
35+
3336
// Server configurations (excludes integration-server which is for E2E testing)
34-
const SERVERS = [
37+
const ALL_SERVERS = [
3538
{
3639
key: "basic-react",
3740
name: "Basic MCP App Server (React)",
@@ -54,6 +57,7 @@ const SERVERS = [
5457
},
5558
{ key: "map-server", name: "Map Server", dir: "map-server" },
5659
{ key: "pdf-server", name: "PDF Server", dir: "pdf-server" },
60+
{ key: "qr-server", name: "QR Code Server", dir: "qr-server" },
5761
{
5862
key: "scenario-modeler",
5963
name: "SaaS Scenario Modeler",
@@ -80,6 +84,11 @@ const SERVERS = [
8084
{ key: "wiki-explorer", name: "Wiki Explorer", dir: "wiki-explorer-server" },
8185
];
8286

87+
// Filter servers if EXAMPLE is specified
88+
const SERVERS = EXAMPLE_FILTER
89+
? ALL_SERVERS.filter((s) => s.dir === EXAMPLE_FILTER)
90+
: ALL_SERVERS;
91+
8392
/**
8493
* Wait for the MCP App to load inside nested iframes.
8594
*/

tests/e2e/security.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,20 @@
99
* Note: True cross-origin attack testing would require a multi-origin test
1010
* setup. These tests verify the security infrastructure is in place and
1111
* functioning correctly for valid communication paths.
12+
*
13+
* Note: These tests require the integration-server. When using EXAMPLE filter,
14+
* set EXAMPLE=integration-server to run these tests.
1215
*/
1316
import { test, expect, type Page, type ConsoleMessage } from "@playwright/test";
1417

18+
// Optional: filter to a single example via EXAMPLE env var (folder name)
19+
// Security tests require integration-server, skip if filtering to a different example
20+
const EXAMPLE_FILTER = process.env.EXAMPLE;
21+
const SKIP_SECURITY_TESTS = EXAMPLE_FILTER && EXAMPLE_FILTER !== "integration-server";
22+
23+
// Skip all security tests if filtering to a non-integration example
24+
test.skip(() => !!SKIP_SECURITY_TESTS, "Skipped: security tests require integration-server");
25+
1526
/**
1627
* Capture console messages matching a pattern
1728
*/

tests/e2e/servers.spec.ts

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -35,30 +35,44 @@ const SLOW_SERVERS: Record<string, number> = {
3535
threejs: 2000, // Three.js WebGL initialization
3636
};
3737

38-
// Server configurations (key is used for screenshot filenames, name is the MCP server name)
39-
const SERVERS = [
40-
{ key: "integration", name: "Integration Test Server" },
41-
{ key: "basic-preact", name: "Basic MCP App Server (Preact)" },
42-
{ key: "basic-react", name: "Basic MCP App Server (React)" },
43-
{ key: "basic-solid", name: "Basic MCP App Server (Solid)" },
44-
{ key: "basic-svelte", name: "Basic MCP App Server (Svelte)" },
45-
{ key: "basic-vanillajs", name: "Basic MCP App Server (Vanilla JS)" },
46-
{ key: "basic-vue", name: "Basic MCP App Server (Vue)" },
47-
{ key: "budget-allocator", name: "Budget Allocator Server" },
48-
{ key: "cohort-heatmap", name: "Cohort Heatmap Server" },
49-
{ key: "customer-segmentation", name: "Customer Segmentation Server" },
50-
{ key: "map-server", name: "Map Server" },
51-
{ key: "pdf-server", name: "PDF Server" },
52-
{ key: "qr-server", name: "QR Code Server" },
53-
{ key: "scenario-modeler", name: "SaaS Scenario Modeler" },
54-
{ key: "shadertoy", name: "ShaderToy Server" },
55-
{ key: "sheet-music", name: "Sheet Music Server" },
56-
{ key: "system-monitor", name: "System Monitor Server" },
57-
{ key: "threejs", name: "Three.js Server" },
58-
{ key: "transcript", name: "Transcript Server" },
59-
{ key: "wiki-explorer", name: "Wiki Explorer" },
38+
// Servers to skip in CI (require special resources like GPU, large ML models)
39+
const SKIP_SERVERS = new Set<string>([
40+
// None currently - say-server widget works without TTS model for screenshots
41+
]);
42+
43+
// Optional: filter to a single example via EXAMPLE env var (folder name)
44+
const EXAMPLE_FILTER = process.env.EXAMPLE;
45+
46+
// Server configurations (key is used for screenshot filenames, name is the MCP server name, dir is the folder name)
47+
const ALL_SERVERS = [
48+
{ key: "integration", name: "Integration Test Server", dir: "integration-server" },
49+
{ key: "basic-preact", name: "Basic MCP App Server (Preact)", dir: "basic-server-preact" },
50+
{ key: "basic-react", name: "Basic MCP App Server (React)", dir: "basic-server-react" },
51+
{ key: "basic-solid", name: "Basic MCP App Server (Solid)", dir: "basic-server-solid" },
52+
{ key: "basic-svelte", name: "Basic MCP App Server (Svelte)", dir: "basic-server-svelte" },
53+
{ key: "basic-vanillajs", name: "Basic MCP App Server (Vanilla JS)", dir: "basic-server-vanillajs" },
54+
{ key: "basic-vue", name: "Basic MCP App Server (Vue)", dir: "basic-server-vue" },
55+
{ key: "budget-allocator", name: "Budget Allocator Server", dir: "budget-allocator-server" },
56+
{ key: "cohort-heatmap", name: "Cohort Heatmap Server", dir: "cohort-heatmap-server" },
57+
{ key: "customer-segmentation", name: "Customer Segmentation Server", dir: "customer-segmentation-server" },
58+
{ key: "map-server", name: "Map Server", dir: "map-server" },
59+
{ key: "pdf-server", name: "PDF Server", dir: "pdf-server" },
60+
{ key: "qr-server", name: "QR Code Server", dir: "qr-server" },
61+
{ key: "say-server", name: "Say Demo", dir: "say-server" },
62+
{ key: "scenario-modeler", name: "SaaS Scenario Modeler", dir: "scenario-modeler-server" },
63+
{ key: "shadertoy", name: "ShaderToy Server", dir: "shadertoy-server" },
64+
{ key: "sheet-music", name: "Sheet Music Server", dir: "sheet-music-server" },
65+
{ key: "system-monitor", name: "System Monitor Server", dir: "system-monitor-server" },
66+
{ key: "threejs", name: "Three.js Server", dir: "threejs-server" },
67+
{ key: "transcript", name: "Transcript Server", dir: "transcript-server" },
68+
{ key: "wiki-explorer", name: "Wiki Explorer", dir: "wiki-explorer-server" },
6069
];
6170

71+
// Filter servers if EXAMPLE is specified
72+
const SERVERS = EXAMPLE_FILTER
73+
? ALL_SERVERS.filter((s) => s.dir === EXAMPLE_FILTER)
74+
: ALL_SERVERS;
75+
6276
/**
6377
* Helper to get the app frame locator (nested: sandbox > app)
6478
*/
@@ -129,12 +143,23 @@ test.describe("Host UI", () => {
129143

130144
// Define tests for each server using forEach to avoid for-loop issues
131145
SERVERS.forEach((server) => {
146+
// Skip servers that require special resources (GPU, large ML models)
147+
const shouldSkip = SKIP_SERVERS.has(server.key);
148+
132149
test.describe(server.name, () => {
133150
test("loads app UI", async ({ page }) => {
151+
if (shouldSkip) {
152+
test.skip();
153+
return;
154+
}
134155
await loadServer(page, server.name);
135156
});
136157

137158
test("screenshot matches golden", async ({ page }) => {
159+
if (shouldSkip) {
160+
test.skip();
161+
return;
162+
}
138163
await loadServer(page, server.name);
139164

140165
// Some servers (WebGL, tile-based) need extra stabilization time
@@ -153,12 +178,16 @@ SERVERS.forEach((server) => {
153178
});
154179

155180
// Interaction tests for integration server (tests all SDK communication APIs)
156-
const integrationServer = SERVERS.find((s) => s.key === "integration")!;
181+
// Only run if integration-server is included (either no filter or EXAMPLE=integration-server)
182+
const integrationServer = SERVERS.find((s) => s.key === "integration");
183+
const integrationServerName = integrationServer?.name ?? "Integration Test Server";
184+
185+
test.describe(`Integration Test Server - Interactions`, () => {
186+
test.skip(() => !integrationServer, "Skipped: integration-server not in EXAMPLE filter");
157187

158-
test.describe(`${integrationServer.name} - Interactions`, () => {
159188
test("Send Message button triggers host callback", async ({ page }) => {
160189
const logs = captureHostLogs(page);
161-
await loadServer(page, integrationServer.name);
190+
await loadServer(page, integrationServerName);
162191

163192
const appFrame = getAppFrame(page);
164193
await appFrame.locator('button:has-text("Send Message")').click();
@@ -171,7 +200,7 @@ test.describe(`${integrationServer.name} - Interactions`, () => {
171200

172201
test("Send Log button triggers host callback", async ({ page }) => {
173202
const logs = captureHostLogs(page);
174-
await loadServer(page, integrationServer.name);
203+
await loadServer(page, integrationServerName);
175204

176205
const appFrame = getAppFrame(page);
177206
await appFrame.locator('button:has-text("Send Log")').click();
@@ -185,7 +214,7 @@ test.describe(`${integrationServer.name} - Interactions`, () => {
185214

186215
test("Open Link button triggers host callback", async ({ page }) => {
187216
const logs = captureHostLogs(page);
188-
await loadServer(page, integrationServer.name);
217+
await loadServer(page, integrationServerName);
189218

190219
const appFrame = getAppFrame(page);
191220
await appFrame.locator('button:has-text("Open Link")').click();

0 commit comments

Comments
 (0)