Skip to content

Commit 2268522

Browse files
authored
Merge pull request #56 from jrjohnson/diffier-diffs
Various Report Improvements
2 parents 57239d7 + 5775036 commit 2268522

11 files changed

Lines changed: 851 additions & 138 deletions

bin/visual-differ.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ program
9191
console.log('❌ Visual differences detected.');
9292
}
9393

94-
console.log(`\n📄 Report generated: ${resolvedOutput}/index.html\n`);
94+
console.log(`\n📄 Reports generated:`);
95+
console.log(` HTML: ${resolvedOutput}/index.html`);
96+
console.log(` Markdown: ${resolvedOutput}/report.md\n`);
9597

9698
process.exit(result.exitCode);
9799
} catch (error) {

lib/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,17 @@ export const CANDIDATE_SUFFIX = '-candidate.png';
1717
*/
1818
export const DIFF_SUFFIX = '-diff.png';
1919

20+
/**
21+
* Subdirectory within the output directory where diff images are stored
22+
*/
23+
export const IMAGES_DIR = 'images';
24+
2025
/**
2126
* Name of the generated HTML report file
2227
*/
2328
export const REPORT_FILENAME = 'index.html';
29+
30+
/**
31+
* Name of the generated Markdown report file
32+
*/
33+
export const MARKDOWN_REPORT_FILENAME = 'report.md';

lib/markdown-report-generator.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { writeFileSync } from 'fs';
2+
import { join } from 'path';
3+
import { MARKDOWN_REPORT_FILENAME } from './constants.js';
4+
import type { ComparisonResult } from './image-comparer.js';
5+
import type { ScannedFile } from './file-scanner.js';
6+
7+
// Generates a markdown report and writes it to the output directory
8+
export function generateMarkdownReport(
9+
comparisonResults: ComparisonResult[],
10+
baselineOnly: ScannedFile[],
11+
candidateOnly: ScannedFile[],
12+
outputDir: string,
13+
): void {
14+
const markdown = generateMarkdown(comparisonResults, baselineOnly, candidateOnly);
15+
writeFileSync(join(outputDir, MARKDOWN_REPORT_FILENAME), markdown);
16+
}
17+
18+
// Builds the full markdown string from comparison data
19+
function generateMarkdown(
20+
comparisonResults: ComparisonResult[],
21+
baselineOnly: ScannedFile[],
22+
candidateOnly: ScannedFile[],
23+
): string {
24+
const withDifferences = comparisonResults.filter((r) => r.hasDifference);
25+
const withoutDifferences = comparisonResults.filter((r) => !r.hasDifference);
26+
27+
const totalImages = comparisonResults.length + baselineOnly.length + candidateOnly.length;
28+
const diffCount = withDifferences.length;
29+
const removedCount = baselineOnly.length;
30+
const addedCount = candidateOnly.length;
31+
const identicalCount = withoutDifferences.length;
32+
33+
const hasFailed = diffCount > 0 || removedCount > 0;
34+
const statusEmoji = hasFailed ? '❌' : '✅';
35+
const statusText = hasFailed ? 'FAILED' : 'PASSED';
36+
37+
const lines: string[] = [];
38+
39+
// Status header
40+
lines.push(`# ${statusEmoji} Visual Diff Report — ${statusText}`);
41+
lines.push('');
42+
43+
// Summary line
44+
const parts: string[] = [];
45+
if (diffCount > 0) parts.push(`**${diffCount}** different`);
46+
if (removedCount > 0) parts.push(`**${removedCount}** removed`);
47+
if (addedCount > 0) parts.push(`**${addedCount}** added`);
48+
if (identicalCount > 0) parts.push(`**${identicalCount}** identical`);
49+
lines.push(`**${totalImages}** images compared: ${parts.join(' · ')}`);
50+
lines.push('');
51+
52+
// Details section with all file lists
53+
const hasDetails =
54+
withDifferences.length > 0 || removedCount > 0 || addedCount > 0 || identicalCount > 0;
55+
56+
if (hasDetails) {
57+
lines.push('<details>');
58+
lines.push('<summary>Details</summary>');
59+
lines.push('');
60+
61+
if (withDifferences.length > 0) {
62+
lines.push(`### Differences (${diffCount})`);
63+
lines.push('');
64+
lines.push('| File | Diff % | Notes |');
65+
lines.push('|------|--------|-------|');
66+
for (const result of withDifferences) {
67+
const notes = result.dimensionMismatch
68+
? `⚠️ Dimension mismatch (${result.dimensionMismatch.baseline}${result.dimensionMismatch.candidate})`
69+
: '';
70+
lines.push(`| ${result.pair.name} | ${result.diffPercentage.toFixed(2)}% | ${notes} |`);
71+
}
72+
lines.push('');
73+
}
74+
75+
if (removedCount > 0) {
76+
lines.push(`### Removed Files (${removedCount})`);
77+
lines.push('');
78+
for (const file of baselineOnly) {
79+
lines.push(`- \`${file.name}\``);
80+
}
81+
lines.push('');
82+
}
83+
84+
if (addedCount > 0) {
85+
lines.push(`### Added Files (${addedCount})`);
86+
lines.push('');
87+
for (const file of candidateOnly) {
88+
lines.push(`- \`${file.name}\``);
89+
}
90+
lines.push('');
91+
}
92+
93+
if (identicalCount > 0) {
94+
lines.push(`### Identical Files (${identicalCount})`);
95+
lines.push('');
96+
for (const result of withoutDifferences) {
97+
lines.push(`- \`${result.pair.name}\``);
98+
}
99+
lines.push('');
100+
}
101+
102+
lines.push('</details>');
103+
lines.push('');
104+
}
105+
106+
return lines.join('\n');
107+
}

lib/report-generator.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from 'fs';
22
import { join, basename, dirname } from 'path';
33
import { fileURLToPath } from 'url';
44
import Handlebars from 'handlebars';
5-
import { REPORT_FILENAME } from './constants.js';
5+
import { IMAGES_DIR, REPORT_FILENAME } from './constants.js';
66
import type { ComparisonResult } from './image-comparer.js';
77
import type { ScannedFile } from './file-scanner.js';
88

@@ -67,9 +67,9 @@ function generateHTML(
6767
name: result.pair.name,
6868
dimensionMismatch: result.dimensionMismatch,
6969
diffPercentage: result.diffPercentage.toFixed(2),
70-
baselineImage: basename(result.pair.baselinePath),
71-
diffImage: basename(result.pair.diffPath),
72-
candidateImage: basename(result.pair.candidatePath),
70+
baselineImage: `${IMAGES_DIR}/${basename(result.pair.baselinePath)}`,
71+
diffImage: `${IMAGES_DIR}/${basename(result.pair.diffPath)}`,
72+
candidateImage: `${IMAGES_DIR}/${basename(result.pair.candidatePath)}`,
7373
})),
7474
withoutDifferences: withoutDifferences.map((result) => ({
7575
name: result.pair.name,

lib/visual-differ.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { writeFileSync } from 'fs';
1+
import { mkdirSync, writeFileSync } from 'fs';
2+
import { join } from 'path';
23
import { PNG } from 'pngjs';
34
import { scanAndMatchFiles } from './file-scanner.js';
45
import { PngFilePair } from './png-file-pair.js';
56
import { compareImages } from './image-comparer.js';
67
import { calculateExitCode } from './exit-code-calculator.js';
78
import { generateReport } from './report-generator.js';
9+
import { generateMarkdownReport } from './markdown-report-generator.js';
10+
import { IMAGES_DIR } from './constants.js';
811
import type { ComparisonResult } from './image-comparer.js';
912

1013
/**
@@ -44,13 +47,17 @@ export function compareDirectories(
4447
// Scan and match files
4548
const fileMatches = scanAndMatchFiles(baselineDir, candidateDir);
4649

50+
// Create images subdirectory for diff output
51+
const imagesDir = join(outputDir, IMAGES_DIR);
52+
mkdirSync(imagesDir, { recursive: true });
53+
4754
// Load and compare matched PNG pairs
4855
const comparisonResults: ComparisonResult[] = fileMatches.matched.map((matched) => {
4956
const pngPair = new PngFilePair(
5057
matched.name,
5158
{ name: matched.name, path: matched.baselinePath },
5259
{ name: matched.name, path: matched.candidatePath },
53-
outputDir,
60+
imagesDir,
5461
);
5562

5663
// Handle dimension mismatch - write PNGs and treat as 100% different
@@ -76,8 +83,14 @@ export function compareDirectories(
7683
// Calculate exit code
7784
const exitCode = calculateExitCode(comparisonResults, fileMatches.baselineOnly);
7885

79-
// Generate report
86+
// Generate reports
8087
generateReport(comparisonResults, fileMatches.baselineOnly, fileMatches.candidateOnly, outputDir);
88+
generateMarkdownReport(
89+
comparisonResults,
90+
fileMatches.baselineOnly,
91+
fileMatches.candidateOnly,
92+
outputDir,
93+
);
8194

8295
// Return summary
8396
const withDifferences = comparisonResults.filter((r) => r.hasDifference).length;

0 commit comments

Comments
 (0)