diff --git a/examples/run-all.ts b/examples/run-all.ts index bff568a..876655e 100644 --- a/examples/run-all.ts +++ b/examples/run-all.ts @@ -6,6 +6,9 @@ * bun examples/run-all.ts start - Build and start all examples * bun examples/run-all.ts dev - Run all examples in dev/watch mode * bun examples/run-all.ts build - Build all examples + * + * Environment: + * EXAMPLE= - Run only a single example (e.g., EXAMPLE=say-server) */ import { readdirSync, statSync, existsSync } from "fs"; @@ -14,21 +17,36 @@ import concurrently from "concurrently"; const BASE_PORT = 3101; const BASIC_HOST = "basic-host"; +// Optional: filter to a single example via EXAMPLE env var (folder name) +const EXAMPLE_FILTER = process.env.EXAMPLE; + // Find all example directories except basic-host that have a package.json, // assign ports, and build URL list -const servers = readdirSync("examples") +const allServers = readdirSync("examples") .filter( (d) => d !== BASIC_HOST && statSync(`examples/${d}`).isDirectory() && existsSync(`examples/${d}/package.json`), ) - .sort() // Sort for consistent port assignment - .map((dir, i) => ({ - dir, - port: BASE_PORT + i, - url: `http://localhost:${BASE_PORT + i}/mcp`, - })); + .sort(); // Sort for consistent port assignment + +// Filter servers if EXAMPLE is specified +const filteredDirs = EXAMPLE_FILTER + ? allServers.filter((d) => d === EXAMPLE_FILTER) + : allServers; + +if (EXAMPLE_FILTER && filteredDirs.length === 0) { + console.error(`Error: No example found matching EXAMPLE=${EXAMPLE_FILTER}`); + console.error(`Available examples: ${allServers.join(", ")}`); + process.exit(1); +} + +const servers = filteredDirs.map((dir, i) => ({ + dir, + port: BASE_PORT + i, + url: `http://localhost:${BASE_PORT + i}/mcp`, +})); const COMMANDS = ["start", "dev", "build"]; @@ -43,6 +61,9 @@ if (!command || !COMMANDS.includes(command)) { const serversEnv = JSON.stringify(servers.map((s) => s.url)); console.log(`Running command: ${command}`); +if (EXAMPLE_FILTER) { + console.log(`Filtering to single example: ${EXAMPLE_FILTER}`); +} console.log( `Server examples: ${servers.map((s) => `${s.dir}:${s.port}`).join(", ")}`, ); diff --git a/examples/threejs-server/README.md b/examples/threejs-server/README.md index 298fc91..a0d6efd 100644 --- a/examples/threejs-server/README.md +++ b/examples/threejs-server/README.md @@ -127,7 +127,11 @@ This is transparent to user code - just use `requestAnimationFrame` normally. Supports alpha transparency for seamless host UI integration: ```javascript -const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); +const renderer = new THREE.WebGLRenderer({ + canvas, + antialias: true, + alpha: true, +}); renderer.setClearColor(0x000000, 0); // Fully transparent ``` diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx index 01f7dc8..649e7e6 100644 --- a/examples/threejs-server/src/threejs-app.tsx +++ b/examples/threejs-server/src/threejs-app.tsx @@ -207,7 +207,9 @@ export default function ThreeJSApp({ const [error, setError] = useState(null); const canvasRef = useRef(null); const containerRef = useRef(null); - const animControllerRef = useRef | null>(null); + const animControllerRef = useRef | null>(null); const height = toolInputs?.height ?? toolInputsPartial?.height ?? 400; const code = toolInputs?.code || DEFAULT_THREEJS_CODE; diff --git a/package.json b/package.json index 5cd7bc6..fe42503 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,8 @@ "test:e2e": "playwright test", "test:e2e:update": "playwright test --update-snapshots", "test:e2e:ui": "playwright test --ui", - "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'", - "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'", + "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'", + "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'", "preexamples:build": "npm run build", "examples:build": "bun examples/run-all.ts build", "examples:start": "NODE_ENV=development npm run build && bun examples/run-all.ts start", @@ -64,7 +64,7 @@ "prepare": "node scripts/setup-bun.mjs && npm run build && husky", "docs": "typedoc", "docs:watch": "typedoc --watch", - "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'", + "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'", "prettier": "prettier -u \"**/*.{js,jsx,ts,tsx,mjs,json,md,yml,yaml}\" --check", "prettier:fix": "prettier -u \"**/*.{js,jsx,ts,tsx,mjs,json,md,yml,yaml}\" --write", "check:versions": "node scripts/check-versions.mjs" diff --git a/playwright.config.ts b/playwright.config.ts index 0b450b8..677fbfc 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -27,6 +27,7 @@ export default defineConfig({ }, ], // Run examples server before tests + // Supports EXAMPLE= env var to run a single example (e.g., EXAMPLE=say-server npm run test:e2e) webServer: { command: "npm run examples:start", url: "http://localhost:8080", @@ -34,6 +35,11 @@ export default defineConfig({ reuseExistingServer: false, // 3 minutes to allow uv to download Python dependencies on first run timeout: 180000, + // Pass through EXAMPLE env var to filter to a single server + env: { + ...process.env, + EXAMPLE: process.env.EXAMPLE ?? "", + }, }, // Snapshot configuration expect: { diff --git a/tests/e2e/generate-grid-screenshots.spec.ts b/tests/e2e/generate-grid-screenshots.spec.ts index b44ccc8..80e3c0a 100644 --- a/tests/e2e/generate-grid-screenshots.spec.ts +++ b/tests/e2e/generate-grid-screenshots.spec.ts @@ -30,8 +30,11 @@ const SKIP_SERVERS = new Set([ "video-resource", // Uses custom screenshot from PR comment ]); +// Optional: filter to a single example via EXAMPLE env var (folder name) +const EXAMPLE_FILTER = process.env.EXAMPLE; + // Server configurations (excludes integration-server which is for E2E testing) -const SERVERS = [ +const ALL_SERVERS = [ { key: "basic-react", name: "Basic MCP App Server (React)", @@ -55,7 +58,6 @@ const SERVERS = [ { key: "map-server", name: "Map Server", dir: "map-server" }, { key: "pdf-server", name: "PDF Server", dir: "pdf-server" }, { key: "qr-server", name: "QR Code Server", dir: "qr-server" }, - { key: "say-server", name: "Say Demo", dir: "say-server" }, { key: "scenario-modeler", name: "SaaS Scenario Modeler", @@ -82,6 +84,11 @@ const SERVERS = [ { key: "wiki-explorer", name: "Wiki Explorer", dir: "wiki-explorer-server" }, ]; +// Filter servers if EXAMPLE is specified +const SERVERS = EXAMPLE_FILTER + ? ALL_SERVERS.filter((s) => s.dir === EXAMPLE_FILTER) + : ALL_SERVERS; + /** * Wait for the MCP App to load inside nested iframes. */ diff --git a/tests/e2e/security.spec.ts b/tests/e2e/security.spec.ts index f98c0b5..273e8cc 100644 --- a/tests/e2e/security.spec.ts +++ b/tests/e2e/security.spec.ts @@ -9,9 +9,24 @@ * Note: True cross-origin attack testing would require a multi-origin test * setup. These tests verify the security infrastructure is in place and * functioning correctly for valid communication paths. + * + * Note: These tests require the integration-server. When using EXAMPLE filter, + * set EXAMPLE=integration-server to run these tests. */ import { test, expect, type Page, type ConsoleMessage } from "@playwright/test"; +// Optional: filter to a single example via EXAMPLE env var (folder name) +// Security tests require integration-server, skip if filtering to a different example +const EXAMPLE_FILTER = process.env.EXAMPLE; +const SKIP_SECURITY_TESTS = + EXAMPLE_FILTER && EXAMPLE_FILTER !== "integration-server"; + +// Skip all security tests if filtering to a non-integration example +test.skip( + () => !!SKIP_SECURITY_TESTS, + "Skipped: security tests require integration-server", +); + /** * Capture console messages matching a pattern */ diff --git a/tests/e2e/servers.spec.ts b/tests/e2e/servers.spec.ts index 29b251e..a3df89f 100644 --- a/tests/e2e/servers.spec.ts +++ b/tests/e2e/servers.spec.ts @@ -35,30 +35,92 @@ const SLOW_SERVERS: Record = { threejs: 2000, // Three.js WebGL initialization }; -// Server configurations (key is used for screenshot filenames, name is the MCP server name) -const SERVERS = [ - { key: "integration", name: "Integration Test Server" }, - { key: "basic-preact", name: "Basic MCP App Server (Preact)" }, - { key: "basic-react", name: "Basic MCP App Server (React)" }, - { key: "basic-solid", name: "Basic MCP App Server (Solid)" }, - { key: "basic-svelte", name: "Basic MCP App Server (Svelte)" }, - { key: "basic-vanillajs", name: "Basic MCP App Server (Vanilla JS)" }, - { key: "basic-vue", name: "Basic MCP App Server (Vue)" }, - { key: "budget-allocator", name: "Budget Allocator Server" }, - { key: "cohort-heatmap", name: "Cohort Heatmap Server" }, - { key: "customer-segmentation", name: "Customer Segmentation Server" }, - { key: "map-server", name: "Map Server" }, - { key: "pdf-server", name: "PDF Server" }, - { key: "qr-server", name: "QR Code Server" }, - { key: "scenario-modeler", name: "SaaS Scenario Modeler" }, - { key: "shadertoy", name: "ShaderToy Server" }, - { key: "sheet-music", name: "Sheet Music Server" }, - { key: "system-monitor", name: "System Monitor Server" }, - { key: "threejs", name: "Three.js Server" }, - { key: "transcript", name: "Transcript Server" }, - { key: "wiki-explorer", name: "Wiki Explorer" }, +// Servers to skip in CI (require special resources like GPU, large ML models) +const SKIP_SERVERS = new Set([ + // None currently - say-server widget works without TTS model for screenshots +]); + +// Optional: filter to a single example via EXAMPLE env var (folder name) +const EXAMPLE_FILTER = process.env.EXAMPLE; + +// Server configurations (key is used for screenshot filenames, name is the MCP server name, dir is the folder name) +const ALL_SERVERS = [ + { + key: "integration", + name: "Integration Test Server", + dir: "integration-server", + }, + { + key: "basic-preact", + name: "Basic MCP App Server (Preact)", + dir: "basic-server-preact", + }, + { + key: "basic-react", + name: "Basic MCP App Server (React)", + dir: "basic-server-react", + }, + { + key: "basic-solid", + name: "Basic MCP App Server (Solid)", + dir: "basic-server-solid", + }, + { + key: "basic-svelte", + name: "Basic MCP App Server (Svelte)", + dir: "basic-server-svelte", + }, + { + key: "basic-vanillajs", + name: "Basic MCP App Server (Vanilla JS)", + dir: "basic-server-vanillajs", + }, + { + key: "basic-vue", + name: "Basic MCP App Server (Vue)", + dir: "basic-server-vue", + }, + { + key: "budget-allocator", + name: "Budget Allocator Server", + dir: "budget-allocator-server", + }, + { + key: "cohort-heatmap", + name: "Cohort Heatmap Server", + dir: "cohort-heatmap-server", + }, + { + key: "customer-segmentation", + name: "Customer Segmentation Server", + dir: "customer-segmentation-server", + }, + { key: "map-server", name: "Map Server", dir: "map-server" }, + { key: "pdf-server", name: "PDF Server", dir: "pdf-server" }, + { key: "qr-server", name: "QR Code Server", dir: "qr-server" }, + { key: "say-server", name: "Say Demo", dir: "say-server" }, + { + key: "scenario-modeler", + name: "SaaS Scenario Modeler", + dir: "scenario-modeler-server", + }, + { key: "shadertoy", name: "ShaderToy Server", dir: "shadertoy-server" }, + { key: "sheet-music", name: "Sheet Music Server", dir: "sheet-music-server" }, + { + key: "system-monitor", + name: "System Monitor Server", + dir: "system-monitor-server", + }, + { key: "threejs", name: "Three.js Server", dir: "threejs-server" }, + { key: "transcript", name: "Transcript Server", dir: "transcript-server" }, + { key: "wiki-explorer", name: "Wiki Explorer", dir: "wiki-explorer-server" }, ]; +// Filter servers if EXAMPLE is specified +const SERVERS = EXAMPLE_FILTER + ? ALL_SERVERS.filter((s) => s.dir === EXAMPLE_FILTER) + : ALL_SERVERS; + /** * Helper to get the app frame locator (nested: sandbox > app) */ @@ -129,12 +191,23 @@ test.describe("Host UI", () => { // Define tests for each server using forEach to avoid for-loop issues SERVERS.forEach((server) => { + // Skip servers that require special resources (GPU, large ML models) + const shouldSkip = SKIP_SERVERS.has(server.key); + test.describe(server.name, () => { test("loads app UI", async ({ page }) => { + if (shouldSkip) { + test.skip(); + return; + } await loadServer(page, server.name); }); test("screenshot matches golden", async ({ page }) => { + if (shouldSkip) { + test.skip(); + return; + } await loadServer(page, server.name); // Some servers (WebGL, tile-based) need extra stabilization time @@ -153,12 +226,20 @@ SERVERS.forEach((server) => { }); // Interaction tests for integration server (tests all SDK communication APIs) -const integrationServer = SERVERS.find((s) => s.key === "integration")!; +// Only run if integration-server is included (either no filter or EXAMPLE=integration-server) +const integrationServer = SERVERS.find((s) => s.key === "integration"); +const integrationServerName = + integrationServer?.name ?? "Integration Test Server"; + +test.describe(`Integration Test Server - Interactions`, () => { + test.skip( + () => !integrationServer, + "Skipped: integration-server not in EXAMPLE filter", + ); -test.describe(`${integrationServer.name} - Interactions`, () => { test("Send Message button triggers host callback", async ({ page }) => { const logs = captureHostLogs(page); - await loadServer(page, integrationServer.name); + await loadServer(page, integrationServerName); const appFrame = getAppFrame(page); await appFrame.locator('button:has-text("Send Message")').click(); @@ -171,7 +252,7 @@ test.describe(`${integrationServer.name} - Interactions`, () => { test("Send Log button triggers host callback", async ({ page }) => { const logs = captureHostLogs(page); - await loadServer(page, integrationServer.name); + await loadServer(page, integrationServerName); const appFrame = getAppFrame(page); await appFrame.locator('button:has-text("Send Log")').click(); @@ -185,7 +266,7 @@ test.describe(`${integrationServer.name} - Interactions`, () => { test("Open Link button triggers host callback", async ({ page }) => { const logs = captureHostLogs(page); - await loadServer(page, integrationServer.name); + await loadServer(page, integrationServerName); const appFrame = getAppFrame(page); await appFrame.locator('button:has-text("Open Link")').click();