Skip to content

Commit c919d19

Browse files
[Feature] - Add scene comparison report. (Resolves #43) (#56)
1 parent e35c5d0 commit c919d19

File tree

7 files changed

+374
-105
lines changed

7 files changed

+374
-105
lines changed

lib/src/goldens/golden_comparisons.dart

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ GoldenCollectionMismatches compareGoldenCollections(
1111
// For every golden, look for missing and mismatching screenshots.
1212
for (final id in goldens.ids) {
1313
if (!screenshots.hasId(id)) {
14-
mismatches[id] = MissingGoldenMismatch(
15-
golden: goldens[id],
14+
mismatches[id] = MissingCandidateMismatch(
15+
golden: goldens[id]!,
1616
);
1717
continue;
1818
}
@@ -44,7 +44,7 @@ GoldenCollectionMismatches compareGoldenCollections(
4444
for (final id in screenshots.ids) {
4545
if (!goldens.hasId(id)) {
4646
mismatches[id] = MissingGoldenMismatch(
47-
screenshot: screenshots[id],
47+
screenshot: screenshots[id]!,
4848
);
4949
}
5050
}
@@ -132,22 +132,29 @@ class WrongSizeGoldenMismatch extends GoldenMismatch {
132132
Map<String, dynamic> get describeStructured => throw UnimplementedError();
133133
}
134134

135-
/// Attempted to compare a screenshot to a golden, but either the screenshot was never
136-
/// generated, or the screenshot was generated for a golden that doesn't exist.
135+
/// Attempted to compare a candidate to a golden, but the candidate was generated for a golden that doesn't exist.
137136
class MissingGoldenMismatch extends GoldenMismatch {
138137
MissingGoldenMismatch({
139-
super.golden,
140-
super.screenshot,
141-
});
138+
required GoldenImage screenshot,
139+
}) : super(screenshot: screenshot);
142140

143-
GoldenImage get _existingGolden => golden ?? screenshot!;
141+
@override
142+
String get describe =>
143+
"A new screenshot was generated with ID '${screenshot!.id}', but there's no existing golden image with that ID.";
144144

145145
@override
146-
String get describe => "A new screenshot was generated with ID '${_existingGolden.id}', $_missingMessage";
146+
Map<String, dynamic> get describeStructured => throw UnimplementedError();
147+
}
148+
149+
/// Attempted to compare a candidante to a golden, but the candidate was never generated.
150+
class MissingCandidateMismatch extends GoldenMismatch {
151+
MissingCandidateMismatch({
152+
required GoldenImage golden,
153+
}) : super(golden: golden);
147154

148-
String get _missingMessage => golden != null //
149-
? "but no screenshot was generated with that ID."
150-
: "but there's no existing golden image with that ID.";
155+
@override
156+
String get describe =>
157+
"A new screenshot was generated with ID '${golden!.id}', but no screenshot was generated with that ID.";
151158

152159
@override
153160
Map<String, dynamic> get describeStructured => throw UnimplementedError();

lib/src/goldens/golden_scenes.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ RenderRepaintBoundary? _findNearestRepaintBoundary(Finder bounds) {
128128
class GoldenSceneMetadata {
129129
static GoldenSceneMetadata fromJson(Map<String, dynamic> json) {
130130
return GoldenSceneMetadata(
131+
description: json["description"] ?? "",
131132
images: [
132133
for (final imageJson in (json["images"] as List<dynamic>)) //
133134
GoldenImageMetadata.fromJson(imageJson),
@@ -136,13 +137,16 @@ class GoldenSceneMetadata {
136137
}
137138

138139
const GoldenSceneMetadata({
140+
required this.description,
139141
required this.images,
140142
});
141143

144+
final String description;
142145
final List<GoldenImageMetadata> images;
143146

144147
Map<String, dynamic> toJson() {
145148
return {
149+
"description": description,
146150
"images": images.map((image) => image.toJson()).toList(growable: false),
147151
};
148152
}

lib/src/scenes/failure_scene.dart

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import 'dart:math';
2+
import 'dart:ui' as ui;
3+
4+
import 'package:flutter_test_goldens/src/goldens/pixel_comparisons.dart';
5+
import 'package:image/image.dart';
6+
7+
import 'package:flutter_test_goldens/flutter_test_goldens.dart';
8+
9+
/// Given a [mismatch] between a golden and a screenshot, generates an image
10+
/// that shows the golden, the screenshot, and the differences between them.
11+
Future<Image> paintGoldenMismatchImages(GoldenMismatch mismatch) async {
12+
final goldenWidth = mismatch.golden!.image.width;
13+
final goldenHeight = mismatch.golden!.image.height;
14+
15+
final screenshotWidth = mismatch.screenshot!.image.width;
16+
final screenshotHeight = mismatch.screenshot!.image.height;
17+
18+
final maxWidth = max(goldenWidth, screenshotWidth);
19+
final maxHeight = max(goldenHeight, screenshotHeight);
20+
21+
final failureImage = Image(
22+
width: maxWidth * 2,
23+
height: maxHeight * 2,
24+
);
25+
26+
// Copy golden to top left corner.
27+
for (int x = 0; x < goldenWidth; x += 1) {
28+
for (int y = 0; y < goldenHeight; y += 1) {
29+
final goldenPixel = mismatch.golden!.image.getPixel(x, y);
30+
failureImage.setPixel(x, y, goldenPixel);
31+
}
32+
}
33+
34+
// Copy screenshot to top right corner.
35+
for (int x = 0; x < screenshotWidth; x += 1) {
36+
for (int y = 0; y < screenshotHeight; y += 1) {
37+
final screenshotPixel = mismatch.screenshot!.image.getPixel(x, y);
38+
failureImage.setPixel(maxWidth + x, y, screenshotPixel);
39+
}
40+
}
41+
42+
// Paint mismatch images.
43+
final absoluteDiffColor = ColorUint32.rgb(255, 255, 0);
44+
for (int x = 0; x < maxWidth; x += 1) {
45+
for (int y = 0; y < maxHeight; y += 1) {
46+
if (x >= goldenWidth || x >= screenshotWidth || y >= goldenHeight || y >= screenshotHeight) {
47+
// This pixel doesn't exist in the golden, or it doesn't exist in the
48+
// screenshot. Therefore, we have nothing to compare. Treat this pixel
49+
// as a max severity difference.
50+
51+
// Paint this pixel in the absolute diff image.
52+
failureImage.setPixel(x, maxHeight + y, absoluteDiffColor);
53+
54+
// Paint this pixel in the relative severity diff image.
55+
failureImage.setPixel(maxWidth + x, maxHeight + y, absoluteDiffColor);
56+
57+
continue;
58+
}
59+
60+
// Check if the screenshot matches the golden.
61+
final goldenPixel = mismatch.golden!.image.getPixel(x, y);
62+
final screenshotPixel = mismatch.screenshot!.image.getPixel(x, y);
63+
final pixelsMatch = goldenPixel == screenshotPixel;
64+
if (pixelsMatch) {
65+
continue;
66+
}
67+
68+
// Paint this pixel in the absolute diff image.
69+
failureImage.setPixel(x, maxHeight + y, absoluteDiffColor);
70+
71+
// Paint this pixel in the relative severity diff image.
72+
final mismatchPercent = calculateColorMismatchPercent(goldenPixel, screenshotPixel);
73+
final yellowAmount = ui.lerpDouble(0.2, 1.0, mismatchPercent)!;
74+
failureImage.setPixel(
75+
goldenWidth + x,
76+
goldenHeight + y,
77+
ColorUint32.rgb((255 * yellowAmount).round(), (255 * yellowAmount).round(), 0),
78+
);
79+
}
80+
}
81+
82+
return failureImage;
83+
}

lib/src/scenes/film_strip.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ class FilmStrip {
277277
// Lookup and return metadata for the position and size of each golden image
278278
// within the gallery.
279279
return GoldenSceneMetadata(
280+
description: goldenName,
280281
images: [
281282
for (final golden in renderablePhotos.keys)
282283
GoldenImageMetadata(

0 commit comments

Comments
 (0)