Skip to content
This repository was archived by the owner on Jan 28, 2026. It is now read-only.

Commit 40f9e60

Browse files
committed
v1.3.1 release test
1 parent 0f53434 commit 40f9e60

1 file changed

Lines changed: 281 additions & 44 deletions

File tree

scripts/visual-regression.js

Lines changed: 281 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = /window\.playwrightReport\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 = /window\.playwrightReportBase64\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 = /<script[^>]*>[\s\S]*?const\s+(?:data|testData|report)\s*=\s*({[\s\S]*?});[\s\S]*?<\/script>/;
86-
const scriptMatch = html.match(scriptRegex);
107+
// Method 3: Look for JSON data in script tags
108+
const scriptTags = html.match(/<script[^>]*>([\s\S]*?)<\/script>/gi) || [];
109+
for (const scriptTag of scriptTags) {
110+
const scriptContent = scriptTag.replace(/<\/?script[^>]*>/gi, '');
111+
112+
// Look for report data assignment
113+
const dataAssignmentRegex = /(?:window\.)?(?:playwrightReport|__playwright_report__|report)\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(/undefined/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 = /\bconst\s+(?:jsonReport|reportData|data)\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 = /tests:\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 = /<div[^>]*class="[^"]*test[^"]*"[^>]*>[\s\S]*?<\/div>/gi;
132-
const testBlocks = html.match(testBlockRegex) || [];
171+
// Enhanced: Try to find test names by looking for test result blocks
172+
const testResultRegex = /<div[^>]*class="[^"]*test-result[^"]*"[^>]*>([\s\S]*?)<\/div>/gi;
173+
const testTitleRegex = /<span[^>]*class="[^"]*test-title[^"]*"[^>]*>([^<]+)<\/span>/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 = /<div[^>]*data-testid="test-case-title"[^>]*>([^<]+)<\/div>[\s\S]*?<img[^>]*src="([^"]+)"/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[^>]*href="([^"]*\.png)"[^>]*>[\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
* ────────────────────────────────────────────────────────── */
185274
function 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(/\.(spec|test)\.(js|ts|jsx|tsx)$/, '');
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(/\.(png|jpe?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 = /data-testid="test-case-title"[^>]*>([^<]+)</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

Comments
 (0)