@@ -25,7 +25,30 @@ function parsePlaywrightReport(reportPath) {
2525 try {
2626 const html = fs . readFileSync ( indexPath , 'utf8' ) ;
2727
28- // Method 1: Try to extract from the embedded app data
28+ // Method 1: Try to extract from window.playwrightReport
29+ const reportRegex = / w i n d o w \. p l a y w r i g h t R e p o r t \s * = \s * ( { [ \s \S ] * ?} ) ; / ;
30+ const reportMatch = html . match ( reportRegex ) ;
31+
32+ if ( reportMatch ) {
33+ try {
34+ console . log ( 'Found window.playwrightReport, parsing...' ) ;
35+ // Parse the JavaScript object
36+ const reportStr = reportMatch [ 1 ] ;
37+ // Use Function constructor to safely evaluate
38+ const reportData = new Function ( 'return ' + reportStr ) ( ) ;
39+
40+ if ( reportData && reportData . files ) {
41+ console . log ( `Found ${ reportData . files . length } test files` ) ;
42+ processReportData ( reportData , reportPath , screenshots ) ;
43+ console . log ( `📋 Extracted ${ screenshots . size } screenshots from window.playwrightReport` ) ;
44+ return screenshots ;
45+ }
46+ } catch ( e ) {
47+ console . log ( 'Failed to parse window.playwrightReport:' , e . message ) ;
48+ }
49+ }
50+
51+ // Method 2: Try to extract from the embedded app data
2952 const appDataRegex = / w i n d o w \. p l a y w r i g h t R e p o r t B a s e 6 4 \s * = \s * " ( [ ^ " ] + ) " / ;
3053 const appDataMatch = html . match ( appDataRegex ) ;
3154
@@ -81,37 +104,55 @@ function parsePlaywrightReport(reportPath) {
81104 }
82105 }
83106
84- // Method 3: Parse the bundled script content
85- const scriptRegex = / < s c r i p t [ ^ > ] * > [ \s \S ] * ?c o n s t \s + (?: d a t a | t e s t D a t a | r e p o r t ) \s * = \s * ( { [ \s \S ] * ?} ) ; [ \s \S ] * ?< \/ s c r i p t > / ;
86- const scriptMatch = html . match ( scriptRegex ) ;
107+ // Method 3: Look for JSON data in script tags
108+ const scriptTags = html . match ( / < s c r i p t [ ^ > ] * > ( [ \s \S ] * ?) < \/ s c r i p t > / gi) || [ ] ;
109+ for ( const scriptTag of scriptTags ) {
110+ const scriptContent = scriptTag . replace ( / < \/ ? s c r i p t [ ^ > ] * > / gi, '' ) ;
111+
112+ // Look for report data assignment
113+ const dataAssignmentRegex = / (?: w i n d o w \. ) ? (?: p l a y w r i g h t R e p o r t | _ _ p l a y w r i g h t _ r e p o r t _ _ | r e p o r t ) \s * = \s * ( { [ \s \S ] * ?} ) ; / ;
114+ const dataMatch = scriptContent . match ( dataAssignmentRegex ) ;
115+
116+ if ( dataMatch ) {
117+ try {
118+ console . log ( 'Found report data in script tag, attempting to parse...' ) ;
119+ // Clean up the JavaScript object string
120+ let jsonStr = dataMatch [ 1 ] ;
121+
122+ // Handle JavaScript object notation to JSON
123+ jsonStr = jsonStr
124+ . replace ( / ( \w + ) : / g, '"$1":' ) // Add quotes to keys
125+ . replace ( / ' / g, '"' ) // Replace single quotes with double quotes
126+ . replace ( / , \s * } / g, '}' ) // Remove trailing commas
127+ . replace ( / , \s * ] / g, ']' ) // Remove trailing commas in arrays
128+ . replace ( / u n d e f i n e d / g, 'null' ) ; // Replace undefined with null
129+
130+ const reportData = JSON . parse ( jsonStr ) ;
131+ if ( reportData ) {
132+ processReportData ( reportData , reportPath , screenshots ) ;
133+ console . log ( `📋 Extracted ${ screenshots . size } screenshots from script data` ) ;
134+ if ( screenshots . size > 0 ) {
135+ return screenshots ;
136+ }
137+ }
138+ } catch ( e ) {
139+ console . log ( 'Failed to parse script data:' , e . message ) ;
140+ }
141+ }
142+ }
143+
144+ // Method 4: Parse Playwright's app bundle
145+ const appBundleRegex = / \b c o n s t \s + (?: j s o n R e p o r t | r e p o r t D a t a | d a t a ) \s * = \s * ( { [ \s \S ] * ?} ) \s * ; / ;
146+ const bundleMatch = html . match ( appBundleRegex ) ;
87147
88- if ( scriptMatch ) {
148+ if ( bundleMatch ) {
89149 try {
90- const cleanedData = scriptMatch [ 1 ]
91- . replace ( / ( \w + ) : / g, '"$1":' )
92- . replace ( / ' / g, '"' )
93- . replace ( / , \s * } / g, '}' )
94- . replace ( / , \s * ] / g, ']' ) ;
95-
96- const reportData = JSON . parse ( cleanedData ) ;
150+ const reportData = new Function ( 'return ' + bundleMatch [ 1 ] ) ( ) ;
97151 processReportData ( reportData , reportPath , screenshots ) ;
98- console . log ( `📋 Extracted ${ screenshots . size } screenshots from script data ` ) ;
152+ console . log ( `📋 Extracted ${ screenshots . size } screenshots from app bundle ` ) ;
99153 return screenshots ;
100154 } catch ( e ) {
101- console . log ( 'Failed to parse script data' ) ;
102- }
103- }
104-
105- // Method 4: Look for app.js file
106- const appJsPath = path . join ( reportPath , 'app.js' ) ;
107- if ( fs . existsSync ( appJsPath ) ) {
108- const appJs = fs . readFileSync ( appJsPath , 'utf8' ) ;
109- const testDataRegex = / t e s t s : \s * \[ ( [ \s \S ] * ?) \] / ;
110- const testMatch = appJs . match ( testDataRegex ) ;
111-
112- if ( testMatch ) {
113- // Extract test data from app.js
114- console . log ( 'Found test data in app.js' ) ;
155+ console . log ( 'Failed to parse app bundle data' ) ;
115156 }
116157 }
117158
@@ -127,22 +168,70 @@ function parsePlaywrightReport(reportPath) {
127168 imageRefs . set ( filename , true ) ;
128169 }
129170
130- // Try to extract test information from the HTML structure
131- const testBlockRegex = / < d i v [ ^ > ] * c l a s s = " [ ^ " ] * t e s t [ ^ " ] * " [ ^ > ] * > [ \s \S ] * ?< \/ d i v > / gi;
132- const testBlocks = html . match ( testBlockRegex ) || [ ] ;
171+ // Enhanced: Try to find test names by looking for test result blocks
172+ const testResultRegex = / < d i v [ ^ > ] * c l a s s = " [ ^ " ] * t e s t - r e s u l t [ ^ " ] * " [ ^ > ] * > ( [ \s \S ] * ?) < \/ d i v > / gi;
173+ const testTitleRegex = / < s p a n [ ^ > ] * c l a s s = " [ ^ " ] * t e s t - t i t l e [ ^ " ] * " [ ^ > ] * > ( [ ^ < ] + ) < \/ s p a n > / gi;
174+
175+ // Map to store test name associations
176+ const testNameMap = new Map ( ) ;
177+
178+ // Look for test blocks that contain both title and image references
179+ const testBlockRegex = / < d i v [ ^ > ] * d a t a - t e s t i d = " t e s t - c a s e - t i t l e " [ ^ > ] * > ( [ ^ < ] + ) < \/ d i v > [ \s \S ] * ?< i m g [ ^ > ] * s r c = " ( [ ^ " ] + ) " / gi;
180+ let testBlockMatch ;
181+ while ( ( testBlockMatch = testBlockRegex . exec ( html ) ) !== null ) {
182+ const testName = testBlockMatch [ 1 ] . trim ( ) ;
183+ const imagePath = testBlockMatch [ 2 ] ;
184+ const imageFilename = path . basename ( imagePath ) ;
185+ if ( imageFilename && testName ) {
186+ testNameMap . set ( imageFilename , testName ) ;
187+ console . log ( ` Mapped ${ imageFilename } to test: "${ testName } "` ) ;
188+ }
189+ }
190+
191+ // Alternative: Look for attachment links with nearby test titles
192+ const attachmentRegex = / < a [ ^ > ] * h r e f = " ( [ ^ " ] * \. p n g ) " [ ^ > ] * > [ \s \S ] * ?< \/ a > / gi;
193+ let attachmentMatch ;
194+ let lastTestName = 'Unknown Test' ;
195+
196+ // First pass: collect all test names
197+ const allTestNames = [ ] ;
198+ let titleMatch ;
199+ while ( ( titleMatch = testTitleRegex . exec ( html ) ) !== null ) {
200+ allTestNames . push ( titleMatch [ 1 ] . trim ( ) ) ;
201+ }
133202
134203 // For each image reference, try to find associated test name
135204 for ( const [ filename , _ ] of imageRefs ) {
136205 const fullPath = path . join ( reportPath , 'data' , filename ) ;
137206
138207 if ( fs . existsSync ( fullPath ) ) {
139- let testName = 'Unknown Test' ;
208+ let testName = testNameMap . get ( filename ) || 'Unknown Test' ;
140209
141- // Try to find test name in nearby HTML
142- const fileRegex = new RegExp ( `${ filename } [^>]*>([^<]+)<` , 'i' ) ;
143- const nameMatch = html . match ( fileRegex ) ;
144- if ( nameMatch && nameMatch [ 1 ] ) {
145- testName = nameMatch [ 1 ] . trim ( ) ;
210+ // If we don't have a mapped name, try to find it in the HTML context
211+ if ( testName === 'Unknown Test' ) {
212+ // Look for the filename in the HTML and find the nearest test title before it
213+ const fileIndex = html . indexOf ( filename ) ;
214+ if ( fileIndex !== - 1 ) {
215+ // Find the last test title that appears before this image
216+ let nearestTestName = 'Unknown Test' ;
217+ let nearestDistance = Infinity ;
218+
219+ for ( const title of allTestNames ) {
220+ const titleIndex = html . lastIndexOf ( title , fileIndex ) ;
221+ if ( titleIndex !== - 1 && titleIndex < fileIndex ) {
222+ const distance = fileIndex - titleIndex ;
223+ if ( distance < nearestDistance && distance < 5000 ) { // Within reasonable distance
224+ nearestDistance = distance ;
225+ nearestTestName = title ;
226+ }
227+ }
228+ }
229+
230+ if ( nearestTestName !== 'Unknown Test' ) {
231+ testName = nearestTestName ;
232+ console . log ( ` Associated ${ filename } with nearby test: "${ testName } "` ) ;
233+ }
234+ }
146235 }
147236
148237 screenshots . set ( filename , {
@@ -183,14 +272,31 @@ function parsePlaywrightReport(reportPath) {
183272 * Process report data structure
184273 * ────────────────────────────────────────────────────────── */
185274function processReportData ( data , reportPath , screenshots ) {
186- // Handle files array
275+ // Handle files array (most common structure)
187276 if ( data . files && Array . isArray ( data . files ) ) {
188- data . files . forEach ( file => {
277+ console . log ( `Processing ${ data . files . length } files from report data` ) ;
278+ data . files . forEach ( ( file , fileIdx ) => {
279+ const fileName = file . fileName || file . file || `file-${ fileIdx } ` ;
280+ console . log ( ` Processing file: ${ fileName } ` ) ;
281+
189282 if ( file . tests && Array . isArray ( file . tests ) ) {
190- file . tests . forEach ( test => {
191- processTest ( test , file . fileName || '' , reportPath , screenshots ) ;
283+ file . tests . forEach ( ( test , testIdx ) => {
284+ // Process each test with proper title extraction
285+ processTestWithContext ( test , fileName , reportPath , screenshots ) ;
192286 } ) ;
193287 }
288+
289+ // Also check for specs at file level
290+ if ( file . specs && Array . isArray ( file . specs ) ) {
291+ file . specs . forEach ( spec => {
292+ processTestWithContext ( spec , fileName , reportPath , screenshots ) ;
293+ } ) ;
294+ }
295+
296+ // Check for suites at file level
297+ if ( file . suites && Array . isArray ( file . suites ) ) {
298+ processSuites ( file . suites , reportPath , screenshots , fileName ) ;
299+ }
194300 } ) ;
195301 }
196302
@@ -216,6 +322,103 @@ function processReportData(data, reportPath, screenshots) {
216322 }
217323}
218324
325+ /* ────────────────────────────────────────────────────────── *
326+ * Process test with context to extract proper title
327+ * ────────────────────────────────────────────────────────── */
328+ function processTestWithContext ( test , fileName , reportPath , screenshots ) {
329+ // Extract all title parts from the test path
330+ const titleParts = [ ] ;
331+
332+ // Get file name without extension
333+ const fileBaseName = path . basename ( fileName ) . replace ( / \. ( s p e c | t e s t ) \. ( j s | t s | j s x | t s x ) $ / , '' ) ;
334+
335+ // Add parent titles if available
336+ if ( test . parent ) {
337+ let current = test . parent ;
338+ const parentTitles = [ ] ;
339+ while ( current && current . title ) {
340+ parentTitles . unshift ( current . title ) ;
341+ current = current . parent ;
342+ }
343+ titleParts . push ( ...parentTitles ) ;
344+ }
345+
346+ // Add test title
347+ const testTitle = test . title || test . name || test . fullTitle || 'Unknown Test' ;
348+ titleParts . push ( testTitle ) ;
349+
350+ // Join all parts for display
351+ const displayTitle = titleParts . length > 1 ? titleParts . join ( ' › ' ) : testTitle ;
352+
353+ console . log ( ` Processing test: "${ displayTitle } "` ) ;
354+
355+ // Process test results
356+ const results = test . results || test . runs || [ ] ;
357+
358+ if ( Array . isArray ( results ) ) {
359+ results . forEach ( ( result , resultIdx ) => {
360+ const attachments = result . attachments || [ ] ;
361+
362+ if ( Array . isArray ( attachments ) ) {
363+ attachments . forEach ( attachment => {
364+ if ( attachment . contentType && attachment . contentType . startsWith ( 'image/' ) ) {
365+ const attachmentPath = attachment . path || attachment . name || '' ;
366+ const filename = path . basename ( attachmentPath ) ;
367+
368+ if ( filename && filename . match ( / \. ( p n g | j p e ? g ) $ / i) ) {
369+ const fullPath = path . join ( reportPath , attachmentPath ) ;
370+
371+ // Determine screenshot type
372+ let type = 'actual' ;
373+ const attachmentName = ( attachment . name || '' ) . toLowerCase ( ) ;
374+ if ( attachmentName . includes ( 'expected' ) || filename . includes ( '-expected' ) ) {
375+ type = 'expected' ;
376+ } else if ( attachmentName . includes ( 'diff' ) || filename . includes ( '-diff' ) ) {
377+ type = 'diff' ;
378+ } else if ( attachmentName . includes ( 'actual' ) || filename . includes ( '-actual' ) ) {
379+ type = 'actual' ;
380+ }
381+
382+ screenshots . set ( filename , {
383+ filename,
384+ path : fs . existsSync ( fullPath ) ? fullPath : path . join ( reportPath , 'data' , filename ) ,
385+ testName : testTitle , // Use the clean test title
386+ displayTitle : displayTitle , // Full display title with context
387+ fullTestName : `${ fileBaseName } > ${ displayTitle } ` ,
388+ testLocation : test . location ?. file || fileName || '' ,
389+ status : result . status || 'unknown' ,
390+ type,
391+ attachmentName : attachment . name
392+ } ) ;
393+
394+ console . log ( ` Found ${ type } screenshot: ${ filename } for test: "${ testTitle } "` ) ;
395+ }
396+ }
397+ } ) ;
398+ }
399+ } ) ;
400+ }
401+
402+ // Also check if test has direct attachments
403+ if ( test . attachments && Array . isArray ( test . attachments ) ) {
404+ test . attachments . forEach ( attachment => {
405+ if ( attachment . contentType && attachment . contentType . startsWith ( 'image/' ) ) {
406+ const filename = path . basename ( attachment . path || attachment . name || '' ) ;
407+ if ( filename ) {
408+ screenshots . set ( filename , {
409+ filename,
410+ path : path . join ( reportPath , attachment . path || `data/${ filename } ` ) ,
411+ testName : testTitle ,
412+ displayTitle : displayTitle ,
413+ fullTestName : `${ fileBaseName } > ${ displayTitle } ` ,
414+ type : attachment . name || 'screenshot'
415+ } ) ;
416+ }
417+ }
418+ } ) ;
419+ }
420+ }
421+
219422/* ────────────────────────────────────────────────────────── *
220423 * Process test suites recursively
221424 * ────────────────────────────────────────────────────────── */
@@ -515,15 +718,16 @@ async function matchAndCompareScreenshots(prScreenshots, mainScreenshots) {
515718 for ( const match of matches ) {
516719 // Use the test name from PR screenshot (should be the actual test title now)
517720 const testName = match . pr . testName || match . main . testName || 'Unknown Test' ;
721+ const displayTitle = match . pr . displayTitle || match . pr . testName || match . main . displayTitle || match . main . testName || 'Unknown Test' ;
518722 const diffPath = path . join ( diffDir , `diff-${ path . basename ( match . pr . filename ) } ` ) ;
519723
520- console . log ( ` Comparing: ${ testName } ` ) ;
724+ console . log ( ` Comparing: ${ displayTitle } ` ) ;
521725 const result = await compareImages ( match . main . path , match . pr . path , diffPath ) ;
522726
523727 if ( result ) {
524728 const diffPercent = result . diffPercent || 0 ;
525729 comparisons . push ( {
526- testName, // This should now be the actual test name like "should allow me to add todo items"
730+ testName : displayTitle , // Use the full display title for better context
527731 filename : match . pr . filename ,
528732 prImage : match . pr . path ,
529733 mainImage : match . main . path ,
@@ -1154,10 +1358,43 @@ function debugScreenshots() {
11541358 . filter ( f => f . endsWith ( '.png' ) ) ;
11551359 console . log ( ` Found ${ files . length } PNG files` ) ;
11561360 console . log ( ` Sample:` , files . slice ( 0 , 3 ) ) ;
1157- } else {
1361+ } else {
11581362 console . log ( ' No data directory found' ) ;
11591363 }
1160-
1364+
1365+ // Try to inspect the HTML report structure
1366+ console . log ( '\n📄 Inspecting HTML report structure:' ) ;
1367+ const indexPath = path . join ( prReportPath , 'index.html' ) ;
1368+ if ( fs . existsSync ( indexPath ) ) {
1369+ const html = fs . readFileSync ( indexPath , 'utf8' ) ;
1370+ console . log ( ` HTML file size: ${ ( html . length / 1024 ) . toFixed ( 1 ) } KB` ) ;
1371+
1372+ // Check for various report data patterns
1373+ console . log ( ' Checking for report data patterns:' ) ;
1374+ console . log ( ` - window.playwrightReport: ${ html . includes ( 'window.playwrightReport' ) ? '✓' : '✗' } ` ) ;
1375+ console . log ( ` - window.playwrightReportBase64: ${ html . includes ( 'window.playwrightReportBase64' ) ? '✓' : '✗' } ` ) ;
1376+ console . log ( ` - __playwright_report__: ${ html . includes ( '__playwright_report__' ) ? '✓' : '✗' } ` ) ;
1377+ console . log ( ` - data-testid="test-case-title": ${ html . includes ( 'data-testid="test-case-title"' ) ? '✓' : '✗' } ` ) ;
1378+
1379+ // Try to find test titles in HTML
1380+ const testTitleRegex = / d a t a - t e s t i d = " t e s t - c a s e - t i t l e " [ ^ > ] * > ( [ ^ < ] + ) < / gi;
1381+ const titles = [ ] ;
1382+ let titleMatch ;
1383+ while ( ( titleMatch = testTitleRegex . exec ( html ) ) !== null && titles . length < 5 ) {
1384+ titles . push ( titleMatch [ 1 ] . trim ( ) ) ;
1385+ }
1386+
1387+ if ( titles . length > 0 ) {
1388+ console . log ( ` Found test titles in HTML:` ) ;
1389+ titles . forEach ( title => console . log ( ` - "${ title } "` ) ) ;
1390+ }
1391+
1392+ // Save a snippet of the HTML for manual inspection
1393+ const snippet = html . substring ( 0 , 2000 ) ;
1394+ fs . writeFileSync ( path . join ( ART , 'html-snippet.txt' ) , snippet ) ;
1395+ console . log ( ` Saved HTML snippet to artifacts/html-snippet.txt for inspection` ) ;
1396+ }
1397+
11611398 console . log ( '\n' ) ;
11621399}
11631400
0 commit comments