| // Copyright 2022 The Flutter Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. | |
| // ignore_for_file: avoid_print | |
| import 'dart:io'; | |
| import 'dart:typed_data'; | |
| import 'package:collection/collection.dart'; | |
| import 'package:flutter_driver/flutter_driver.dart'; | |
| import 'package:image/image.dart'; | |
| import 'package:integration_test/integration_test_driver_extended.dart'; | |
| const _goldensDirectoryPath = 'integration_test/test_infra/goldens'; | |
| const _failuresDirectoryPath = '$_goldensDirectoryPath/failures'; | |
| const _defaultDiffPercentage = 1.0; | |
| const _defaultDiffTolerance = 0.003; | |
| // The following timeouts were copied from | |
| // https://developer.mozilla.org/en-US/docs/Web/WebDriver/Reference/Timeouts | |
| const _defaultWebDriverPageLoadTimeoutInMins = 5; | |
| const _defaultWebDriverScriptTimeoutInSeconds = 30; | |
| Future<void> main() async { | |
| final driver = await FlutterDriver.connect(); | |
| // Double the WebDriver timeout limits to handle test flakiness. | |
| // See https://github.com/flutter/devtools/issues/9474 | |
| await driver.webDriver.timeouts.setScriptTimeout( | |
| const Duration(seconds: _defaultWebDriverScriptTimeoutInSeconds * 2), | |
| ); | |
| await driver.webDriver.timeouts.setPageLoadTimeout( | |
| const Duration(minutes: _defaultWebDriverPageLoadTimeoutInMins * 2), | |
| ); | |
| await integrationDriver( | |
| driver: driver, | |
| onScreenshot: | |
| ( | |
| String screenshotName, | |
| List<int> screenshotBytes, [ | |
| Map<String, Object?>? args, | |
| ]) async { | |
| final shouldUpdateGoldens = args?['update_goldens'] == true; | |
| // TODO(https://github.com/flutter/flutter/issues/118470): remove this. | |
| // We need this to ensure all golden image checks run. Without this | |
| // workaround, the flutter integration test framework will crash on the | |
| // failed expectation. | |
| final lastScreenshot = args?['last_screenshot'] == true; | |
| final goldenFile = File('$_goldensDirectoryPath/$screenshotName.png'); | |
| if (shouldUpdateGoldens) { | |
| if (!goldenFile.existsSync()) { | |
| // Create the goldens directory if it does not exist. | |
| Directory(_goldensDirectoryPath).createSync(); | |
| } | |
| goldenFile.writeAsBytesSync(screenshotBytes, flush: true); | |
| print('Golden image updated: $screenshotName.png'); | |
| return true; | |
| } | |
| bool equal = false; | |
| double percentDiff = 1.0; | |
| if (goldenFile.existsSync()) { | |
| final goldenBytes = goldenFile.readAsBytesSync(); | |
| equal = const DeepCollectionEquality().equals( | |
| goldenBytes, | |
| screenshotBytes, | |
| ); | |
| if (!equal) { | |
| percentDiff = _percentDiff(goldenBytes, screenshotBytes); | |
| } | |
| } | |
| final failuresDirectory = Directory(_failuresDirectoryPath); | |
| if (!equal) { | |
| final percentDiffDisplay = | |
| '${(percentDiff * 100).toStringAsFixed(4)}%'; | |
| if (percentDiff < _defaultDiffTolerance) { | |
| print( | |
| 'Warning: $screenshotName.png differed from the golden image by ' | |
| '$percentDiffDisplay. Since this is less than the acceptable ' | |
| 'tolerance ${(_defaultDiffTolerance * 100).toStringAsFixed(4)}%, ' | |
| 'the test still passes.', | |
| ); | |
| return true; | |
| } | |
| print( | |
| 'Golden image test failed: $screenshotName.png. The test image ' | |
| 'differed from the golden image by $percentDiffDisplay.', | |
| ); | |
| // Create the goldens and failures directories if they do not exist. | |
| Directory(_goldensDirectoryPath).createSync(); | |
| failuresDirectory.createSync(); | |
| File( | |
| '$_failuresDirectoryPath/$screenshotName.png', | |
| ).writeAsBytesSync(screenshotBytes); | |
| } | |
| if (lastScreenshot && | |
| failuresDirectory.existsSync() && | |
| failuresDirectory.listSync().isNotEmpty) { | |
| return false; | |
| } | |
| return true; | |
| }, | |
| ); | |
| } | |
| double _percentDiff(Uint8List goldenBytes, List<int> screenshotBytes) { | |
| final goldenImage = decodeImage(goldenBytes); | |
| final screenshotImage = decodeImage(Uint8List.fromList(screenshotBytes)); | |
| if (goldenImage == null || screenshotImage == null) { | |
| print('Cannot decode one or both of the golden images.'); | |
| return _defaultDiffPercentage; | |
| } | |
| if (goldenImage.height != screenshotImage.height || | |
| goldenImage.width != screenshotImage.width) { | |
| print( | |
| 'The golden images have a different height or width. ' | |
| 'Golden: ${goldenImage.sizeDisplay}\n' | |
| 'Screenshot: ${screenshotImage.sizeDisplay}\n', | |
| ); | |
| return _defaultDiffPercentage; | |
| } | |
| final goldenImageBytes = goldenImage.getBytes(); | |
| final screenshotImageBytes = screenshotImage.getBytes(); | |
| if (goldenImageBytes.length != screenshotImageBytes.length) { | |
| print( | |
| 'The golden images have a different byte lengths. ' | |
| 'Golden: ${goldenImageBytes.length} bytes\n' | |
| 'Screenshot: ${screenshotImageBytes.length} bytes\n', | |
| ); | |
| return _defaultDiffPercentage; | |
| } | |
| // This image diff calculation code is used by the Flutter test matcher | |
| // [matchesReferenceImage]. The small bit of code copied here is pulled out | |
| // for convenient reuse. | |
| int delta = 0; | |
| for (int i = 0; i < goldenImageBytes.length; i += 4) { | |
| if (goldenImageBytes[i] != screenshotImageBytes[i] || | |
| goldenImageBytes[i + 1] != screenshotImageBytes[i + 1] || | |
| goldenImageBytes[i + 2] != screenshotImageBytes[i + 2] || | |
| goldenImageBytes[i + 3] != screenshotImageBytes[i + 3]) { | |
| delta++; | |
| } | |
| } | |
| return delta / goldenImageBytes.length / 4; | |
| } | |
| extension _ImageExtension on Image { | |
| String get sizeDisplay => | |
| 'Size(width: ${width.toDouble()}, height: ${height.toDouble()})'; | |
| } |