mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

Closes https://github.com/flutter/flutter/issues/152376. The gradient, for posterity, looks like this: <img src="https://github.com/user-attachments/assets/bed9599a-4e16-499c-af79-b51980095e89" width="150"> Let's see if CI agrees! /cc @johnmccutchan
259 lines
9.4 KiB
Dart
259 lines
9.4 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
/// A partial copy of [flutter_test.matchesGoldenFile] and supporting code.
|
|
///
|
|
/// Flutter driver runs in the standalone Dart VM, which does not have access to
|
|
/// the Flutter test library or `dart:ui`. This file provides a subset of the
|
|
/// functionality of `flutter_test`'s `matchesGoldenFile` function, and we can
|
|
/// consider refactoring this code to be shared between the two libraries
|
|
/// (https://github.com/flutter/flutter/issues/152257).
|
|
///
|
|
/// ## TODOs
|
|
///
|
|
/// - [ ] Figure out a code-sharing strategy with `flutter_test`.
|
|
///
|
|
/// The basics, such as the matcher and APIs, could definitely be shared.
|
|
///
|
|
/// - [ ] Consider how local image diffing could be shared, if at all.
|
|
///
|
|
/// The `flutter_test` implementation uses `dart:ui`, which is not available
|
|
/// today in a Flutter driver script. We could either (a) provide a different
|
|
/// implementation for Flutter driver, or (b) provide a way, such as using
|
|
/// `flutter_tester` to run the tests in a Flutter context, to use the same
|
|
/// implementation.
|
|
///
|
|
/// - [ ] Teach `flutter drive` to use and provide `--update-goldens` flag.
|
|
///
|
|
/// @docImport 'package:flutter_test/flutter_test.dart' as flutter_test;
|
|
library;
|
|
|
|
import 'dart:async';
|
|
import 'dart:io' as io;
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:matcher/matcher.dart';
|
|
|
|
// Similar to `flutter_test`, we ignore the implementation import.
|
|
// ignore: implementation_imports
|
|
import 'package:matcher/src/expect/async_matcher.dart';
|
|
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:test_api/test_api.dart';
|
|
|
|
import 'driver.dart';
|
|
|
|
/// Whether golden files should be automatically updated during tests rather
|
|
/// than compared to the image bytes recorded by the tests.
|
|
///
|
|
/// When this is `true`, [matchesGoldenFile] will always report a successful
|
|
/// match, because the bytes being tested implicitly become the new golden.
|
|
bool autoUpdateGoldenFiles = false;
|
|
|
|
/// Compares pixels against those of a golden image file.
|
|
///
|
|
/// This comparator is used as the backend for [matchesGoldenFile].
|
|
///
|
|
/// By default, an exact pixel match to a local golden file is used.
|
|
GoldenFileComparator goldenFileComparator = const NaiveLocalFileComparator._();
|
|
|
|
/// Compares image pixels against a golden image file.
|
|
///
|
|
/// Instances of this comparator will be used as the backend for
|
|
/// [matchesGoldenFile].
|
|
abstract mixin class GoldenFileComparator {
|
|
/// Compares the pixels of decoded png [imageBytes] against the golden file
|
|
/// identified by [golden].
|
|
///
|
|
/// The returned future completes with a boolean value that indicates whether
|
|
/// the pixels decoded from [imageBytes] match the golden file's pixels.
|
|
///
|
|
/// In the case of comparison mismatch, the comparator may choose to throw a
|
|
/// [TestFailure] if it wants to control the failure message, often in the
|
|
/// form of a [ComparisonResult] that provides detailed information about the
|
|
/// mismatch.
|
|
///
|
|
/// The method by which [golden] is located and by which its bytes are loaded
|
|
/// is left up to the implementation class. For instance, some implementations
|
|
/// may load files from the local file system, whereas others may load files
|
|
/// over the network or from a remote repository.
|
|
Future<bool> compare(Uint8List imageBytes, Uri golden);
|
|
|
|
/// Updates the golden file identified by [golden] with [imageBytes].
|
|
///
|
|
/// This will be invoked in lieu of [compare] when [autoUpdateGoldenFiles]
|
|
/// is `true` (which gets set automatically by the test framework when the
|
|
/// user runs `flutter drive --update-goldens`).
|
|
///
|
|
/// The method by which [golden] is located and by which its bytes are written
|
|
/// is left up to the implementation class.
|
|
Future<void> update(Uri golden, Uint8List imageBytes);
|
|
|
|
/// Returns a new golden file [Uri] to incorporate any [version] number with
|
|
/// the [key].
|
|
///
|
|
/// The [version] is an optional int that can be used to differentiate
|
|
/// historical golden files.
|
|
///
|
|
/// Version numbers are used in golden file tests for package:flutter. You can
|
|
/// learn more about these tests [here](https://github.com/flutter/flutter/blob/main/docs/contributing/testing/Writing-a-golden-file-test-for-package-flutter.md).
|
|
Uri getTestUri(Uri key, int? version) {
|
|
if (version == null) {
|
|
return key;
|
|
}
|
|
final String keyString = key.toString();
|
|
final String extension = path.extension(keyString);
|
|
return Uri.parse('${keyString.split(extension).join()}.$version$extension');
|
|
}
|
|
}
|
|
|
|
/// The default [GoldenFileComparator] implementation for `flutter drive`.
|
|
///
|
|
/// This comparator performs a pixel-for-pixel comparison of the decoded PNGs,
|
|
/// returning true only if there's an exact match. In cases where the captured
|
|
/// test image does not match the golden file, this comparator will provide a
|
|
/// fairly unhelpful error message, which could be improved in the future.
|
|
final class NaiveLocalFileComparator with GoldenFileComparator {
|
|
const NaiveLocalFileComparator._();
|
|
|
|
@override
|
|
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
|
|
final io.File goldenFile = _getTestFilePath(golden);
|
|
final Uint8List goldenBytes;
|
|
try {
|
|
goldenBytes = await goldenFile.readAsBytes();
|
|
} on io.PathNotFoundException {
|
|
throw TestFailure('Golden file not found: ${goldenFile.path}');
|
|
}
|
|
|
|
if (goldenBytes.length != imageBytes.length) {
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < goldenBytes.length; i++) {
|
|
if (goldenBytes[i] != imageBytes[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Future<void> update(Uri golden, Uint8List imageBytes) async {
|
|
final io.File goldenFile = _getTestFilePath(golden);
|
|
await goldenFile.parent.create(recursive: true);
|
|
await goldenFile.writeAsBytes(imageBytes);
|
|
}
|
|
|
|
/// Returns a path relative to the test script.
|
|
///
|
|
/// This is hacky and unreliable, but it's the best we can do until we have
|
|
/// more integration with the `flutter` CLI (which does all the heavy lifting
|
|
/// for us in `flutter_test`).
|
|
io.File _getTestFilePath(Uri golden) {
|
|
final String testScriptPath = io.Platform.script.toFilePath();
|
|
final String testScriptDir = path.dirname(testScriptPath);
|
|
return io.File(path.join(testScriptDir, golden.path));
|
|
}
|
|
}
|
|
|
|
// Examples can assume:
|
|
// import 'package:flutter_driver/src/native/driver.dart';
|
|
// import 'package:flutter_driver/src/native/goldens.dart';
|
|
// import 'package:test/test.dart';
|
|
// late NativeDriver nativeDriver;
|
|
|
|
/// Asserts that a [NativeScreenshot], [Future<NativeScreenshot>], or
|
|
/// [List<int>] matches the golden image file indentified by [key], with an
|
|
/// optional [version] number].
|
|
///
|
|
/// The [key] may be either a [Uri] or a [String] representation of a URL.
|
|
///
|
|
/// The [version] is a number that can be used to differentiate historical
|
|
/// golden files. This parameter is optional.
|
|
///
|
|
/// This is an asynchronous matcher, meaning that callers should use
|
|
/// [flutter_test.expectLater] when using this matcher and await the future
|
|
/// returned by [flutter_test.expectLater].
|
|
///
|
|
/// ## Golden File Testing
|
|
///
|
|
/// The term __golden file__ refers to a master image that is considered the
|
|
/// true renmdering of a given widget, state, application, or other visual
|
|
/// representation you have chosen to capture.
|
|
///
|
|
/// The master golden image files are tested against can be created or updated
|
|
/// by running `flutter drive --update-goldens` on the test.
|
|
///
|
|
/// {@tool snippet}
|
|
/// Sample invocations of [matchesGoldenFile].
|
|
///
|
|
/// ```dart
|
|
/// await expectLater(
|
|
/// nativeDriver.screenshot(),
|
|
/// matchesGoldenFile('save.png'),
|
|
/// );
|
|
/// ```
|
|
/// {@end-tool}
|
|
AsyncMatcher matchesGoldenFile(Object key, {int? version}) {
|
|
return switch (key) {
|
|
Uri() => _MatchesGoldenFile(key, version),
|
|
String() => _MatchesGoldenFile.forStringPath(key, version),
|
|
_ => throw ArgumentError(
|
|
'Unexpected type for golden file: ${key.runtimeType}'),
|
|
};
|
|
}
|
|
|
|
/// The matcher created by [matchesGoldenFile].
|
|
final class _MatchesGoldenFile extends AsyncMatcher {
|
|
/// Creates an instance of [MatchesGoldenFile].
|
|
const _MatchesGoldenFile(this.key, this.version);
|
|
|
|
/// Creates an instance of [MatchesGoldenFile] from a [String] path.
|
|
_MatchesGoldenFile.forStringPath(String path, this.version)
|
|
: key = Uri.parse(path);
|
|
|
|
/// The [key] to the golden image.
|
|
final Uri key;
|
|
|
|
/// The [version] of the golden image.
|
|
final int? version;
|
|
|
|
@override
|
|
Future<String?> matchAsync(Object? item) async {
|
|
final Uri testNameUri = goldenFileComparator.getTestUri(key, version);
|
|
|
|
final Uint8List buffer;
|
|
if (item is FutureOr<List<int>>) {
|
|
buffer = Uint8List.fromList(await item);
|
|
} else if (item is FutureOr<NativeScreenshot>) {
|
|
buffer = await (await item).readAsBytes();
|
|
} else {
|
|
throw ArgumentError(
|
|
'Unexpected type for golden file: ${item.runtimeType}',
|
|
);
|
|
}
|
|
|
|
if (autoUpdateGoldenFiles) {
|
|
await goldenFileComparator.update(testNameUri, buffer);
|
|
return null;
|
|
}
|
|
try {
|
|
final bool success = await goldenFileComparator.compare(
|
|
buffer,
|
|
testNameUri,
|
|
);
|
|
return success ? null : 'does not match';
|
|
} on TestFailure catch (e) {
|
|
return e.message;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Description describe(Description description) {
|
|
return description.add('app screenshot image matches golden file "$key"');
|
|
}
|
|
}
|