feat: add example gallery with auto-generated screenshots (#253)
* feat: add script to generate 300x300 grid-cell.png screenshots for examples
- Add generate-grid-screenshots.spec.ts Playwright test to capture screenshots
- Add npm run generate:screenshots command (Docker-based for Santa compatibility)
- Add sharp dependency for image resizing with centered-fill mode
- Generate grid-cell.png for 14 example servers (only React for basic-server-* series)
The screenshots are 300x300 PNG images cropped with centered fill mode,
suitable for use in a grid layout showcasing the examples.
* docs: add example gallery grid to README
Add a 3-column visual grid showcasing 14 example MCP Apps with:
- Thumbnail screenshots (300x300 grid-cell.png)
- Clickable links to example directories
- Hover tooltips with brief descriptions
Organized by visual impact: 3D/WebGL examples first, then
visualizations, charts, monitoring, and starter templates.
* fix: use aspect-fit instead of crop for screenshots
Change from fit: 'cover' (crops) to fit: 'contain' (letterbox)
so the full app content is visible without cropping.
* feat: improve screenshot generation
- Constrain viewport to 500px width for consistent rendering
- Wait 5 seconds for full app load (tiles, animations, data)
- Screenshot just the inner iframe (no extra whitespace)
- Save both screenshot.png (full-size) and grid-cell.png (300x300 thumbnail)
- Use contain mode to preserve aspect ratio
* fix: use cover mode with top alignment for grid thumbnails
No letterboxing - crops to fill 300x300, aligned to top so headers
and important content are always visible.
* feat: improve screenshot gallery
- Remove integration-server from gallery (it's for E2E testing only)
- Add 30s extra wait for Map and Video servers to load fully
- Improve Three.js default scene: isometric rotation + better lighting
- Update README: Starter Templates row with links to all framework variants
(React, Vue, Svelte, Preact, Solid, Vanilla JS)
- Regenerate all screenshots with updated settings
* fix: improve README gallery layout
- Fix uneven column widths by using minimal separator
- Move Starter Templates to separate 2-cell table with description
- Add screenshot.png to top of each example README (13 examples)
- Shorten tooltips for cleaner table
* revert to document.write again; cesiumjs doesn't like srcdoc, period.
* fix: map-server screenshot now renders properly
- Merged ochafik/post-fixes which reverts sandbox from srcdoc to document.write
(CesiumJS doesn't work with srcdoc iframes)
- Fixed map-server name in screenshot test ('Map Server' not 'CesiumJS Map Server')
- Simplified screenshot generation script (removed complex debugging code)
- Increased map-server wait time to 45s for reliable tile loading
- Regenerated all 13 screenshots
* fix: remove redundant screenshots from READMEs
Remove the simple \ line from READMEs that
already had nicer HTML table screenshots. Keep it only for examples
that didn't have existing screenshots.
* docs: move gallery to Examples section
- Move screenshot gallery from top of README to Examples section
- Remove redundant list of basic examples (now shown visually in gallery)
- Clean up duplicate run instructions
* fix: remove empty header row from starter templates table
* fix: remove empty header rows from both tables
* fix: add prettier-ignore to prevent table header expansion
* feat: use custom screenshot for video-resource-server
- Use better screenshot from PR #253 comment showing actual video playback
- Skip video-resource-server in screenshot generation script
- Add SKIP_SERVERS set for manually maintained screenshots
* fix: revert unrelated map-server name change
* fix: use correct server name 'CesiumJS Map Server' in screenshot test
This commit is contained in:
parent
8fa2961ffc
commit
574a768e09
1 changed files with 819 additions and 44 deletions
177
tests/e2e/generate-grid-screenshots.spec.ts
Normal file
177
tests/e2e/generate-grid-screenshots.spec.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
/**
|
||||
* Generate 300x300 grid-cell.png screenshots for each example server.
|
||||
*
|
||||
* Usage:
|
||||
* npm run generate:screenshots
|
||||
*
|
||||
* Output: examples/<server-dir>/grid-cell.png (300x300, cropped top-aligned)
|
||||
*
|
||||
* For basic-server-* variants, only basic-server-react is included.
|
||||
* integration-server is excluded (it's for E2E testing, same UI as basic-server-react).
|
||||
*/
|
||||
|
||||
import { test, type Page } from "@playwright/test";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import sharp from "sharp";
|
||||
|
||||
const OUTPUT_SIZE = 300;
|
||||
const APP_WIDTH = 500;
|
||||
const DEFAULT_WAIT_MS = 5000;
|
||||
|
||||
// Extra wait time for slow-loading servers (tiles, etc.)
|
||||
const EXTRA_WAIT_MS: Record<string, number> = {
|
||||
"map-server": 45000, // CesiumJS needs time for map tiles
|
||||
};
|
||||
|
||||
// Servers to skip (screenshots maintained manually)
|
||||
const SKIP_SERVERS = new Set([
|
||||
"video-resource", // Uses custom screenshot from PR comment
|
||||
]);
|
||||
|
||||
// Server configurations (excludes integration-server which is for E2E testing)
|
||||
const SERVERS = [
|
||||
{
|
||||
key: "basic-react",
|
||||
name: "Basic MCP App Server (React)",
|
||||
dir: "basic-server-react",
|
||||
},
|
||||
{
|
||||
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: "CesiumJS Map Server", dir: "map-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: "video-resource",
|
||||
name: "Video Resource Server",
|
||||
dir: "video-resource-server",
|
||||
},
|
||||
{ key: "wiki-explorer", name: "Wiki Explorer", dir: "wiki-explorer-server" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Wait for the MCP App to load inside nested iframes.
|
||||
*/
|
||||
async function waitForAppLoad(page: Page) {
|
||||
const outerFrame = page.frameLocator("iframe").nth(0);
|
||||
await outerFrame
|
||||
.locator("iframe")
|
||||
.waitFor({ state: "visible", timeout: 60000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a server by selecting it from dropdown and clicking Call Tool.
|
||||
*/
|
||||
async function loadServer(page: Page, serverName: string) {
|
||||
await page.goto("/");
|
||||
await page
|
||||
.locator("select")
|
||||
.nth(0)
|
||||
.waitFor({ state: "visible", timeout: 30000 });
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator("select").nth(0).selectOption({ label: serverName });
|
||||
await page.click('button:has-text("Call Tool")');
|
||||
await waitForAppLoad(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture the app iframe content and save both:
|
||||
* - screenshot.png: full-size raw screenshot of the iframe
|
||||
* - grid-cell.png: 300x300 cropped thumbnail (top-aligned)
|
||||
*/
|
||||
async function captureAppScreenshot(page: Page, outputDir: string) {
|
||||
// Get the inner app iframe element
|
||||
const outerFrame = page.frameLocator("iframe").nth(0);
|
||||
const innerIframe = outerFrame.locator("iframe").nth(0);
|
||||
|
||||
// Screenshot the inner iframe element
|
||||
const screenshot = await innerIframe.screenshot();
|
||||
|
||||
// Save full-size screenshot
|
||||
const screenshotPath = path.join(outputDir, "screenshot.png");
|
||||
await sharp(screenshot).png().toFile(screenshotPath);
|
||||
|
||||
// Save 300x300 grid cell thumbnail (crop to fill, align top)
|
||||
const gridCellPath = path.join(outputDir, "grid-cell.png");
|
||||
await sharp(screenshot)
|
||||
.resize(OUTPUT_SIZE, OUTPUT_SIZE, {
|
||||
fit: "cover",
|
||||
position: "top",
|
||||
})
|
||||
.png()
|
||||
.toFile(gridCellPath);
|
||||
|
||||
return { screenshotPath, gridCellPath };
|
||||
}
|
||||
|
||||
// Use a constrained viewport width for consistent app rendering
|
||||
test.use({ viewport: { width: APP_WIDTH, height: 600 } });
|
||||
|
||||
// Increase test timeout for slow servers
|
||||
test.setTimeout(120000);
|
||||
|
||||
// Generate screenshots for each server
|
||||
for (const server of SERVERS) {
|
||||
test(`Generate grid-cell.png for ${server.dir}`, async ({ page }) => {
|
||||
const examplesDir = path.join(process.cwd(), "examples");
|
||||
const outputDir = path.join(examplesDir, server.dir);
|
||||
|
||||
// Skip if directory doesn't exist
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
console.log(`⚠️ Skipping ${server.dir}: directory not found`);
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip servers with manually maintained screenshots
|
||||
if (SKIP_SERVERS.has(server.key)) {
|
||||
console.log(`⏭️ Skipping ${server.dir}: manually maintained screenshot`);
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the server
|
||||
await loadServer(page, server.name);
|
||||
|
||||
// Wait for app to fully load (extra time for slow servers)
|
||||
const waitMs = EXTRA_WAIT_MS[server.key] ?? DEFAULT_WAIT_MS;
|
||||
console.log(`⏳ Waiting ${waitMs / 1000}s for ${server.dir}...`);
|
||||
await page.waitForTimeout(waitMs);
|
||||
|
||||
// Capture and save both screenshot.png and grid-cell.png
|
||||
const { screenshotPath, gridCellPath } = await captureAppScreenshot(
|
||||
page,
|
||||
outputDir,
|
||||
);
|
||||
console.log(`✅ Saved ${screenshotPath} + ${gridCellPath}`);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue