| // 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). |
| part of '../native_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. |
| /// |
| /// Defaults to `true` if the environment variable `UPDATE_GOLDENS` is either |
| /// `true` or `1` (case insensitive). |
| bool autoUpdateGoldenFiles = () { |
| final String? updateGoldens = io.Platform.environment['UPDATE_GOLDENS']; |
| return switch (updateGoldens?.toLowerCase()) { |
| '1' || 'true' => true, |
| _ => 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 class GoldenFileComparator { |
| /// @nodoc |
| const 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 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 extends 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: ${path.relative(goldenFile.path)}.\n' |
| '\n' |
| 'For local development, you must establish a local baseline image before ' |
| 'running tests, otherwise the test will always fail. Use UPDATE_GOLDENS=1 ' |
| 'when running "flutter drive" to establish a baseline, and then subequent ' |
| '"flutter drive" instances will be tested against that (local) golden.\n' |
| '\n' |
| 'See the documentation at dev/integration_tests/android_engine_test/README.md for ' |
| 'details.', |
| ); |
| } |
| |
| if (goldenBytes.length != imageBytes.length) { |
| return false; |
| } |
| |
| for (var 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 identified 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 rendering 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"'); |
| } |
| } |