| // 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. |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| import 'dart:typed_data'; |
| |
| import 'package:image/image.dart'; |
| import 'package:path/path.dart' as path; |
| // ignore: deprecated_member_use |
| import 'package:test_api/test_api.dart' as test_package show TestFailure; |
| |
| import 'goldens.dart'; |
| |
| /// The default [GoldenFileComparator] implementation for `flutter test`. |
| /// |
| /// 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. This comparator loads golden |
| /// files from the local file system, treating the golden key as a relative |
| /// path from the test file's directory. |
| /// |
| /// 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 |
| /// output to illustrate the difference, described in further detail below. |
| /// |
| /// When using `flutter test --update-goldens`, [LocalFileComparator] |
| /// updates the golden files on disk to match the rendering. |
| /// |
| /// ## Local Output from Golden File Testing |
| /// |
| /// The [LocalFileComparator] will output test feedback when a golden file test |
| /// fails. This output takes the form of differential images contained within a |
| /// `failures` directory that will be generated in the same location specified |
| /// by the golden key. The differential images include the master and test |
| /// images that were compared, as well as an isolated diff of detected pixels, |
| /// and a masked diff that overlays these detected pixels over the master image. |
| /// |
| /// The following images are examples of a test failure output: |
| /// |
| /// | File Name | Image Output | |
| /// |----------------------------|---------------| |
| /// | testName_masterImage.png |  | |
| /// | testName_testImage.png |  | |
| /// | testName_isolatedDiff.png |  | |
| /// | testName_maskedDiff.png |  | |
| /// |
| /// See also: |
| /// |
| /// * [GoldenFileComparator], the abstract class that [LocalFileComparator] |
| /// implements. |
| /// * [matchesGoldenFile], the function from [flutter_test] that invokes the |
| /// comparator. |
| class LocalFileComparator extends GoldenFileComparator with LocalComparisonOutput { |
| /// Creates a new [LocalFileComparator] for the specified [testFile]. |
| /// |
| /// Golden file keys will be interpreted as file paths relative to the |
| /// directory in which [testFile] resides. |
| /// |
| /// The [testFile] URL must represent a file. |
| LocalFileComparator(Uri testFile, {path.Style pathStyle}) |
| : basedir = _getBasedir(testFile, pathStyle), |
| _path = _getPath(pathStyle); |
| |
| static path.Context _getPath(path.Style style) { |
| return path.Context(style: style ?? path.Style.platform); |
| } |
| |
| static Uri _getBasedir(Uri testFile, path.Style pathStyle) { |
| final path.Context context = _getPath(pathStyle); |
| final String testFilePath = context.fromUri(testFile); |
| final String testDirectoryPath = context.dirname(testFilePath); |
| return context.toUri(testDirectoryPath + context.separator); |
| } |
| |
| /// The directory in which the test was loaded. |
| /// |
| /// Golden file keys will be interpreted as file paths relative to this |
| /// directory. |
| final Uri basedir; |
| |
| /// Path context exists as an instance variable rather than just using the |
| /// system path context in order to support testing, where we can spoof the |
| /// platform to test behaviors with arbitrary path styles. |
| final path.Context _path; |
| |
| @override |
| Future<bool> compare(Uint8List imageBytes, Uri golden) async { |
| final File goldenFile = _getGoldenFile(golden); |
| if (!goldenFile.existsSync()) { |
| throw test_package.TestFailure( |
| 'Could not be compared against non-existent file: "$golden"' |
| ); |
| } |
| final List<int> goldenBytes = await goldenFile.readAsBytes(); |
| final ComparisonResult result = GoldenFileComparator.compareLists( |
| imageBytes, |
| goldenBytes, |
| ); |
| |
| if (!result.passed) { |
| generateFailureOutput(result, golden, basedir); |
| } |
| return result.passed; |
| } |
| |
| @override |
| Future<void> update(Uri golden, Uint8List imageBytes) async { |
| final File goldenFile = _getGoldenFile(golden); |
| await goldenFile.parent.create(recursive: true); |
| await goldenFile.writeAsBytes(imageBytes, flush: true); |
| } |
| |
| File _getGoldenFile(Uri golden) { |
| return File(_path.join(_path.fromUri(basedir), _path.fromUri(golden.path))); |
| } |
| } |
| |
| /// A class for use in golden file comparators that run locally and provide |
| /// output. |
| class LocalComparisonOutput { |
| /// Writes out diffs from the [ComparisonResult] of a golden file test. |
| /// |
| /// Will throw an error if a null result is provided. |
| void generateFailureOutput( |
| ComparisonResult result, |
| Uri golden, |
| Uri basedir, { |
| String key = '', |
| }) { |
| String additionalFeedback = ''; |
| if (result.diffs != null) { |
| additionalFeedback = '\nFailure feedback can be found at ' |
| '${path.join(basedir.path, 'failures')}'; |
| final Map<String, Image> diffs = result.diffs.cast<String, Image>(); |
| diffs.forEach((String name, Image image) { |
| final File output = getFailureFile( |
| key.isEmpty ? name : name + '_' + key, |
| golden, |
| basedir, |
| ); |
| output.parent.createSync(recursive: true); |
| output.writeAsBytesSync(encodePng(image)); |
| }); |
| } |
| throw test_package.TestFailure( |
| 'Golden "$golden": ${result.error}$additionalFeedback' |
| ); |
| } |
| |
| /// Returns the appropriate file for a given diff from a [ComparisonResult]. |
| File getFailureFile(String failure, Uri golden, Uri basedir) { |
| final String fileName = golden.pathSegments.last; |
| final String testName = fileName.split(path.extension(fileName))[0] |
| + '_' |
| + failure |
| + '.png'; |
| return File(path.join( |
| path.fromUri(basedir), |
| path.fromUri(Uri.parse('failures/$testName')), |
| )); |
| } |
| } |