Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions lib/src/goldens/golden_collections.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:ui';

import 'package:flutter_test_goldens/src/goldens/golden_scenes.dart';
import 'package:image/image.dart' as img;

/// A collection of in-memory golden images or screenshot images.
Expand Down
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