Skip to content

Commit 6cd4d1a

Browse files
ochafikclaude
andcommitted
Mask dynamic content in E2E screenshot tests
Add Playwright mask option to handle servers with dynamic/random content: - basic-react/vanillajs: mask server time display - system-monitor: mask CPU chart, memory stats, uptime - cohort-heatmap: mask heatmap grid (random data) - customer-segmentation: mask scatter chart (random data) This addresses PR feedback about handling examples with non-deterministic output. Masking replaces the previous 10% tolerance with proper exclusion of dynamic elements, allowing tighter 1% tolerance for the rest of the UI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 20ef214 commit 6cd4d1a

File tree

6 files changed

+58
-21
lines changed

6 files changed

+58
-21
lines changed

tests/e2e/servers.spec.ts

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
11
import { test, expect, type Page, type ConsoleMessage } from "@playwright/test";
22

3-
/**
4-
* Helper to get the app frame locator (nested: sandbox > app)
5-
*/
6-
function getAppFrame(page: Page) {
7-
return page.frameLocator("iframe").first().frameLocator("iframe").first();
8-
}
9-
10-
/**
11-
* Collect console messages with [HOST] prefix
12-
*/
13-
function captureHostLogs(page: Page): string[] {
14-
const logs: string[] = [];
15-
page.on("console", (msg: ConsoleMessage) => {
16-
const text = msg.text();
17-
if (text.includes("[HOST]")) {
18-
logs.push(text);
19-
}
20-
});
21-
return logs;
22-
}
3+
// Dynamic element selectors to mask for screenshot comparison
4+
// Note: CSS modules generate unique class names, so we use attribute selectors
5+
// with partial matches (e.g., [class*="heatmapWrapper"]) for those components
6+
const DYNAMIC_MASKS: Record<string, string[]> = {
7+
"basic-react": ["code"], // Server time display
8+
"basic-vanillajs": ["#server-time"], // Server time display
9+
"cohort-heatmap": ['[class*="heatmapWrapper"]'], // Heatmap grid (random data)
10+
"customer-segmentation": [".chart-container"], // Scatter plot (random data)
11+
"system-monitor": [
12+
".chart-container", // CPU chart (highly dynamic)
13+
"#status-text", // Current timestamp
14+
"#memory-percent", // Memory percentage
15+
"#memory-detail", // Memory usage details
16+
"#memory-bar-fill", // Memory bar fill level
17+
"#info-uptime", // System uptime
18+
],
19+
};
2320

2421
// Server configurations
2522
const SERVERS = [
@@ -41,6 +38,27 @@ const SERVERS = [
4138
{ key: "threejs", index: 7, name: "Three.js Server" },
4239
];
4340

41+
/**
42+
* Helper to get the app frame locator (nested: sandbox > app)
43+
*/
44+
function getAppFrame(page: Page) {
45+
return page.frameLocator("iframe").first().frameLocator("iframe").first();
46+
}
47+
48+
/**
49+
* Collect console messages with [HOST] prefix
50+
*/
51+
function captureHostLogs(page: Page): string[] {
52+
const logs: string[] = [];
53+
page.on("console", (msg: ConsoleMessage) => {
54+
const text = msg.text();
55+
if (text.includes("[HOST]")) {
56+
logs.push(text);
57+
}
58+
});
59+
return logs;
60+
}
61+
4462
/**
4563
* Wait for the MCP App to load inside nested iframes.
4664
* Structure: page > iframe (sandbox) > iframe (app)
@@ -50,13 +68,27 @@ async function waitForAppLoad(page: Page) {
5068
await expect(outerFrame.locator("iframe")).toBeVisible();
5169
}
5270

71+
/**
72+
* Load a server by selecting it and clicking Call Tool
73+
*/
5374
async function loadServer(page: Page, serverIndex: number) {
5475
await page.goto("/");
5576
await page.locator("select").first().selectOption({ index: serverIndex });
5677
await page.click('button:has-text("Call Tool")');
5778
await waitForAppLoad(page);
5879
}
5980

81+
/**
82+
* Get mask locators for dynamic elements inside the nested app iframe.
83+
*/
84+
function getMaskLocators(page: Page, serverKey: string) {
85+
const selectors = DYNAMIC_MASKS[serverKey];
86+
if (!selectors) return [];
87+
88+
const appFrame = getAppFrame(page);
89+
return selectors.map((selector) => appFrame.locator(selector));
90+
}
91+
6092
test.describe("Host UI", () => {
6193
test("initial state shows controls", async ({ page }) => {
6294
await page.goto("/");
@@ -82,8 +114,13 @@ SERVERS.forEach((server) => {
82114
test("screenshot matches golden", async ({ page }) => {
83115
await loadServer(page, server.index);
84116
await page.waitForTimeout(500); // Brief stabilization
117+
118+
// Get mask locators for dynamic content (timestamps, charts, etc.)
119+
const mask = getMaskLocators(page, server.key);
120+
85121
await expect(page).toHaveScreenshot(`${server.key}.png`, {
86-
maxDiffPixelRatio: 0.1,
122+
mask,
123+
maxDiffPixelRatio: 0.01, // 1% tolerance (tighter now that dynamic content is masked)
87124
});
88125
});
89126
});
-3.79 KB
Loading
-3.97 KB
Loading
-77 KB
Loading
-59.2 KB
Loading
-35.5 KB
Loading

0 commit comments

Comments
 (0)