fix(e2e): improve test stability and developer experience (#337)
* fix(e2e): add masks for dynamic content and increase timeouts for slow servers - Add HOST_MASKS for Tool Input/Result panels with dynamic timestamps - Add say-server to SLOW_SERVERS (10s) for TTS model download - Add say-server to EXTRA_WAIT_MS (30s) in generate-grid-screenshots * feat: add update-lock:docker script to regenerate lockfile with public registry * pin package versions for dev compatibility * feat(basic-host): add ?theme=hide query param to hide theme toggle in screenshots - Add hideThemeToggle param to getQueryParams() - Conditionally render ThemeToggle based on ?theme=hide - Update e2e tests to use ?theme=hide for consistent screenshots * update screenshots * update screenshots * fix(husky): load Node.js environment for GUI apps like GitHub Desktop * fix(e2e): increase map-server wait time to 15s for tile loading * fix(e2e): increase waitForAppLoad timeout to 30s for nested iframe loading * ci: add workflow to update e2e snapshots * ci: trigger snapshot update on push to this branch * chore: update e2e snapshots [skip ci] * ci: trigger CI after snapshot update * style: fix prettier formatting in update-snapshots workflow * fix: pin seroval, seroval-plugins, and solid-js for artifactory compatibility --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
|
|
@ -28,6 +28,8 @@ const EXTRA_WAIT_MS: Record<string, number> = {
|
|||
// Servers to skip (screenshots maintained manually)
|
||||
const SKIP_SERVERS = new Set([
|
||||
"video-resource", // Uses custom screenshot from PR comment
|
||||
"qr-server", // Uses custom screenshot from PR comment
|
||||
"say-server", // TTS model download from HuggingFace can be slow
|
||||
]);
|
||||
|
||||
// Optional: filter to a single example via EXAMPLE env var (folder name)
|
||||
|
|
@ -103,9 +105,10 @@ async function waitForAppLoad(page: Page) {
|
|||
|
||||
/**
|
||||
* Load a server by selecting it from dropdown and clicking Call Tool.
|
||||
* Uses ?theme=hide to hide the theme toggle for consistent screenshots.
|
||||
*/
|
||||
async function loadServer(page: Page, serverName: string) {
|
||||
await page.goto("/");
|
||||
await page.goto("/?theme=hide");
|
||||
// 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({ label: serverName });
|
||||
|
|
|
|||
|
|
@ -34,13 +34,32 @@ const DYNAMIC_MASKS: Record<string, string[]> = {
|
|||
|
||||
// Servers that need extra stabilization time (e.g., for tile loading, WebGL init)
|
||||
const SLOW_SERVERS: Record<string, number> = {
|
||||
"map-server": 5000, // CesiumJS needs time for tiles to load
|
||||
"map-server": 15000, // CesiumJS needs time for tiles to load
|
||||
threejs: 2000, // Three.js WebGL initialization
|
||||
"say-server": 10000, // TTS model download from HuggingFace can be slow
|
||||
};
|
||||
|
||||
// Host-level masks (outside app iframe) - for dynamic content in Tool Input/Result panels
|
||||
// Use [class*="..."] for CSS modules which generate unique class names
|
||||
const HOST_MASKS: Record<string, string[]> = {
|
||||
// Servers with dynamic timestamps in Tool Result (get-time response)
|
||||
// Mask entire collapsible panels to avoid font rendering differences
|
||||
integration: ['[class*="collapsiblePanel"]'],
|
||||
"basic-preact": ['[class*="collapsiblePanel"]'],
|
||||
"basic-react": ['[class*="collapsiblePanel"]'],
|
||||
"basic-solid": ['[class*="collapsiblePanel"]'],
|
||||
"basic-svelte": ['[class*="collapsiblePanel"]'],
|
||||
"basic-vanillajs": ['[class*="collapsiblePanel"]'],
|
||||
"basic-vue": ['[class*="collapsiblePanel"]'],
|
||||
// System monitor has dynamic system stats in result
|
||||
"system-monitor": ['[class*="collapsiblePanel"]'],
|
||||
};
|
||||
|
||||
// Servers to skip in CI (require special resources like GPU, large ML models)
|
||||
const SKIP_SERVERS = new Set<string>([
|
||||
// None currently - say-server view works without TTS model for screenshots
|
||||
"qr-server", // TODO
|
||||
"say-server", // TTS model download from HuggingFace can be slow
|
||||
]);
|
||||
|
||||
// Optional: filter to a single example via EXAMPLE env var (folder name)
|
||||
|
|
@ -157,14 +176,15 @@ function captureHostLogs(page: Page): string[] {
|
|||
*/
|
||||
async function waitForAppLoad(page: Page) {
|
||||
const outerFrame = page.frameLocator("iframe").first();
|
||||
await expect(outerFrame.locator("iframe")).toBeVisible();
|
||||
await expect(outerFrame.locator("iframe")).toBeVisible({ timeout: 30000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a server by selecting it by name and clicking Call Tool
|
||||
* Load a server by selecting it by name and clicking Call Tool.
|
||||
* Uses ?theme=hide to hide the theme toggle for consistent screenshots.
|
||||
*/
|
||||
async function loadServer(page: Page, serverName: string) {
|
||||
await page.goto("/");
|
||||
await page.goto("/?theme=hide");
|
||||
// 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({ label: serverName });
|
||||
|
|
@ -173,26 +193,37 @@ async function loadServer(page: Page, serverName: string) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get mask locators for dynamic elements inside the nested app iframe.
|
||||
* Get mask locators for dynamic elements (both in app iframe and host).
|
||||
*/
|
||||
function getMaskLocators(page: Page, serverKey: string) {
|
||||
const selectors = DYNAMIC_MASKS[serverKey];
|
||||
if (!selectors) return [];
|
||||
const masks = [];
|
||||
|
||||
const appFrame = getAppFrame(page);
|
||||
return selectors.map((selector) => appFrame.locator(selector));
|
||||
// App-level masks (inside nested iframe)
|
||||
const appSelectors = DYNAMIC_MASKS[serverKey];
|
||||
if (appSelectors) {
|
||||
const appFrame = getAppFrame(page);
|
||||
masks.push(...appSelectors.map((selector) => appFrame.locator(selector)));
|
||||
}
|
||||
|
||||
// Host-level masks (Tool Input/Result panels with dynamic content)
|
||||
const hostSelectors = HOST_MASKS[serverKey];
|
||||
if (hostSelectors) {
|
||||
masks.push(...hostSelectors.map((selector) => page.locator(selector)));
|
||||
}
|
||||
|
||||
return masks;
|
||||
}
|
||||
|
||||
test.describe("Host UI", () => {
|
||||
test("initial state shows controls", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.goto("/?theme=hide");
|
||||
await expect(page.locator("label:has-text('Server')")).toBeVisible();
|
||||
await expect(page.locator("label:has-text('Tool')")).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Call Tool")')).toBeVisible();
|
||||
});
|
||||
|
||||
test("screenshot of initial state", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.goto("/?theme=hide");
|
||||
await expect(page.locator('button:has-text("Call Tool")')).toBeVisible();
|
||||
await expect(page).toHaveScreenshot("host-initial.png");
|
||||
});
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 666 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 32 KiB |