11import { 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
2522const 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+ */
5374async 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+
6092test . 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 } ) ;
0 commit comments