// 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 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 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 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 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], or /// [List] 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 matchAsync(Object? item) async { final Uri testNameUri = goldenFileComparator.getTestUri(key, version); final Uint8List buffer; if (item is FutureOr>) { buffer = Uint8List.fromList(await item); } else if (item is FutureOr) { 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"'); } }