blob: eeb38f4aea8f3c87e79a513e7723b21ce4cd5f48 [file] [log] [blame]
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// 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;
Future<void> main() async {
final driver = await FlutterDriver.connect();
await integrationDriver(
driver: driver,
onScreenshot: (
String screenshotName,
List<int> screenshotBytes, [
Map<String, Object?>? args,
]) async {
final bool 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 bool 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);
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()})';
}