Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions lib/src/goldens/golden_comparisons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ GoldenCollectionMismatches compareGoldenCollections(
// For every golden, look for missing and mismatching screenshots.
for (final id in goldens.ids) {
if (!screenshots.hasId(id)) {
mismatches[id] = MissingGoldenMismatch(
golden: goldens[id],
mismatches[id] = MissingCandidateMismatch(
golden: goldens[id]!,
);
continue;
}
Expand Down Expand Up @@ -44,7 +44,7 @@ GoldenCollectionMismatches compareGoldenCollections(
for (final id in screenshots.ids) {
if (!goldens.hasId(id)) {
mismatches[id] = MissingGoldenMismatch(
screenshot: screenshots[id],
screenshot: screenshots[id]!,
);
}
}
Expand Down Expand Up @@ -132,22 +132,29 @@ class WrongSizeGoldenMismatch extends GoldenMismatch {
Map<String, dynamic> get describeStructured => throw UnimplementedError();
}

/// Attempted to compare a screenshot to a golden, but either the screenshot was never
/// generated, or the screenshot was generated for a golden that doesn't exist.
/// Attempted to compare a candidate to a golden, but the candidate was generated for a golden that doesn't exist.
class MissingGoldenMismatch extends GoldenMismatch {
MissingGoldenMismatch({
super.golden,
super.screenshot,
});
required GoldenImage screenshot,
}) : super(screenshot: screenshot);

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

@override
String get describe => "A new screenshot was generated with ID '${_existingGolden.id}', $_missingMessage";
Map<String, dynamic> get describeStructured => throw UnimplementedError();
}

/// Attempted to compare a candidante to a golden, but the candidate was never generated.
class MissingCandidateMismatch extends GoldenMismatch {
MissingCandidateMismatch({
required GoldenImage golden,
}) : super(golden: golden);

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

@override
Map<String, dynamic> get describeStructured => throw UnimplementedError();
Expand Down
4 changes: 4 additions & 0 deletions lib/src/goldens/golden_scenes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ RenderRepaintBoundary? _findNearestRepaintBoundary(Finder bounds) {
class GoldenSceneMetadata {
static GoldenSceneMetadata fromJson(Map<String, dynamic> json) {
return GoldenSceneMetadata(
description: json["description"] ?? "",
images: [
for (final imageJson in (json["images"] as List<dynamic>)) //
GoldenImageMetadata.fromJson(imageJson),
Expand All @@ -136,13 +137,16 @@ class GoldenSceneMetadata {
}

const GoldenSceneMetadata({
required this.description,
required this.images,
});

final String description;
final List<GoldenImageMetadata> images;

Map<String, dynamic> toJson() {
return {
"description": description,
"images": images.map((image) => image.toJson()).toList(growable: false),
};
}
Expand Down
83 changes: 83 additions & 0 deletions lib/src/scenes/failure_scene.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'dart:math';
import 'dart:ui' as ui;

import 'package:flutter_test_goldens/src/goldens/pixel_comparisons.dart';
import 'package:image/image.dart';

import 'package:flutter_test_goldens/flutter_test_goldens.dart';

/// Given a [mismatch] between a golden and a screenshot, generates an image
/// that shows the golden, the screenshot, and the differences between them.
Future<Image> paintGoldenMismatchImages(GoldenMismatch mismatch) async {
final goldenWidth = mismatch.golden!.image.width;
final goldenHeight = mismatch.golden!.image.height;

final screenshotWidth = mismatch.screenshot!.image.width;
final screenshotHeight = mismatch.screenshot!.image.height;

final maxWidth = max(goldenWidth, screenshotWidth);
final maxHeight = max(goldenHeight, screenshotHeight);

final failureImage = Image(
width: maxWidth * 2,
height: maxHeight * 2,
);

// Copy golden to top left corner.
for (int x = 0; x < goldenWidth; x += 1) {
for (int y = 0; y < goldenHeight; y += 1) {
final goldenPixel = mismatch.golden!.image.getPixel(x, y);
failureImage.setPixel(x, y, goldenPixel);
}
}

// Copy screenshot to top right corner.
for (int x = 0; x < screenshotWidth; x += 1) {
for (int y = 0; y < screenshotHeight; y += 1) {
final screenshotPixel = mismatch.screenshot!.image.getPixel(x, y);
failureImage.setPixel(maxWidth + x, y, screenshotPixel);
}
}

// Paint mismatch images.
final absoluteDiffColor = ColorUint32.rgb(255, 255, 0);
for (int x = 0; x < maxWidth; x += 1) {
for (int y = 0; y < maxHeight; y += 1) {
if (x >= goldenWidth || x >= screenshotWidth || y >= goldenHeight || y >= screenshotHeight) {
// This pixel doesn't exist in the golden, or it doesn't exist in the
// screenshot. Therefore, we have nothing to compare. Treat this pixel
// as a max severity difference.

// Paint this pixel in the absolute diff image.
failureImage.setPixel(x, maxHeight + y, absoluteDiffColor);

// Paint this pixel in the relative severity diff image.
failureImage.setPixel(maxWidth + x, maxHeight + y, absoluteDiffColor);

continue;
}

// Check if the screenshot matches the golden.
final goldenPixel = mismatch.golden!.image.getPixel(x, y);
final screenshotPixel = mismatch.screenshot!.image.getPixel(x, y);
final pixelsMatch = goldenPixel == screenshotPixel;
if (pixelsMatch) {
continue;
}

// Paint this pixel in the absolute diff image.
failureImage.setPixel(x, maxHeight + y, absoluteDiffColor);

// Paint this pixel in the relative severity diff image.
final mismatchPercent = calculateColorMismatchPercent(goldenPixel, screenshotPixel);
final yellowAmount = ui.lerpDouble(0.2, 1.0, mismatchPercent)!;
failureImage.setPixel(
goldenWidth + x,
goldenHeight + y,
ColorUint32.rgb((255 * yellowAmount).round(), (255 * yellowAmount).round(), 0),
);
}
}

return failureImage;
}
1 change: 1 addition & 0 deletions lib/src/scenes/film_strip.dart
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ class FilmStrip {
// Lookup and return metadata for the position and size of each golden image
// within the gallery.
return GoldenSceneMetadata(
description: goldenName,
images: [
for (final golden in renderablePhotos.keys)
GoldenImageMetadata(
Expand Down
Loading