// 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' as io; import 'dart:typed_data'; import 'package:file/memory.dart'; import 'package:flutter/foundation.dart' show DiagnosticLevel, DiagnosticPropertiesBuilder, DiagnosticsNode, FlutterError; import 'package:flutter_test/flutter_test.dart' hide test; import 'package:flutter_test/flutter_test.dart' as test_package; // 1x1 transparent pixel const List<int> _kExpectedPngBytes = <int>[ 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84, 120, 1, 99, 97, 0, 2, 0, 0, 25, 0, 5, 144, 240, 54, 245, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, ]; // 1x1 colored pixel const List<int> _kColorFailurePngBytes = <int>[ 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84, 120, 1, 99, 249, 207, 240, 255, 63, 0, 7, 18, 3, 2, 164, 147, 160, 197, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, ]; // 1x2 transparent pixel const List<int> _kSizeFailurePngBytes = <int>[ 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0,0, 2, 8, 6, 0, 0, 0, 153, 129, 182, 39, 0, 0, 0, 14, 73, 68, 65, 84, 120, 1, 99, 97, 0, 2, 22, 16, 1, 0, 0, 70, 0, 9, 112, 117, 150, 160, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, ]; void main() { late MemoryFileSystem fs; setUp(() { final FileSystemStyle style = io.Platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix; fs = MemoryFileSystem(style: style); }); /// Converts posix-style paths to the style associated with [fs]. /// /// This allows us to deal in posix-style paths in the tests. String fix(String path) { if (path.startsWith('/')) { path = '${fs.style.drive}$path'; } return path.replaceAll('/', fs.path.separator); } void test(String description, FutureOr<void> Function() body) { test_package.test(description, () async { await io.IOOverrides.runZoned<FutureOr<void>>( body, createDirectory: (String path) => fs.directory(path), createFile: (String path) => fs.file(path), createLink: (String path) => fs.link(path), getCurrentDirectory: () => fs.currentDirectory, setCurrentDirectory: (String path) => fs.currentDirectory = path, getSystemTempDirectory: () => fs.systemTempDirectory, stat: (String path) => fs.stat(path), statSync: (String path) => fs.statSync(path), fseIdentical: (String p1, String p2) => fs.identical(p1, p2), fseIdenticalSync: (String p1, String p2) => fs.identicalSync(p1, p2), fseGetType: (String path, bool followLinks) => fs.type(path, followLinks: followLinks), fseGetTypeSync: (String path, bool followLinks) => fs.typeSync(path, followLinks: followLinks), fsWatch: (String a, int b, bool c) => throw UnsupportedError('unsupported'), fsWatchIsSupported: () => fs.isWatchSupported, ); }); } group('goldenFileComparator', () { test('is initialized by test framework', () { expect(goldenFileComparator, isNotNull); expect(goldenFileComparator, isA<LocalFileComparator>()); final LocalFileComparator comparator = goldenFileComparator as LocalFileComparator; expect(comparator.basedir.path, contains('flutter_test')); }); }); group('LocalFileComparator', () { late LocalFileComparator comparator; setUp(() { comparator = LocalFileComparator(fs.file(fix('/golden_test.dart')).uri, pathStyle: fs.path.style); }); test('calculates basedir correctly', () { expect(comparator.basedir, fs.file(fix('/')).uri); comparator = LocalFileComparator(fs.file(fix('/foo/bar/golden_test.dart')).uri, pathStyle: fs.path.style); expect(comparator.basedir, fs.directory(fix('/foo/bar/')).uri); }); test('can be instantiated with uri that represents file in same folder', () { comparator = LocalFileComparator(Uri.parse('foo_test.dart'), pathStyle: fs.path.style); expect(comparator.basedir, Uri.parse('./')); }); test('throws if local output is not awaited', () { try { comparator.generateFailureOutput( ComparisonResult(passed: false, diffPercent: 1.0), Uri.parse('foo_test.dart'), Uri.parse('/foo/bar/'), ); TestAsyncUtils.verifyAllScopesClosed(); fail('unexpectedly did not throw'); } on FlutterError catch (e) { final List<String> lines = e.message.split('\n'); expectSync(lines[0], 'Asynchronous call to guarded function leaked.'); expectSync(lines[1], 'You must use "await" with all Future-returning test APIs.'); expectSync( lines[2], matches(r'^The guarded method "generateFailureOutput" from class ' r'LocalComparisonOutput was called from .*goldens_test.dart on line ' r'[0-9]+, but never completed before its parent scope closed\.$'), ); expectSync(lines.length, 3); final DiagnosticPropertiesBuilder propertiesBuilder = DiagnosticPropertiesBuilder(); e.debugFillProperties(propertiesBuilder); final List<DiagnosticsNode> information = propertiesBuilder.properties; expectSync(information.length, 3); expectSync(information[0].level, DiagnosticLevel.summary); expectSync(information[1].level, DiagnosticLevel.hint); expectSync(information[2].level, DiagnosticLevel.info); } }); group('compare', () { Future<bool> doComparison([ String golden = 'golden.png' ]) { final Uri uri = fs.file(fix(golden)).uri; return comparator.compare( Uint8List.fromList(_kExpectedPngBytes), uri, ); } group('succeeds', () { test('when golden file is in same folder as test', () async { fs.file(fix('/golden.png')).writeAsBytesSync(_kExpectedPngBytes); final bool success = await doComparison(); expect(success, isTrue); }); test('when golden file is in subfolder of test', () async { fs.file(fix('/sub/foo.png')) ..createSync(recursive: true) ..writeAsBytesSync(_kExpectedPngBytes); final bool success = await doComparison('sub/foo.png'); expect(success, isTrue); }); group('when comparator instantiated with uri that represents file in same folder', () { test('and golden file is in same folder as test', () async { fs.file(fix('/foo/bar/golden.png')) ..createSync(recursive: true) ..writeAsBytesSync(_kExpectedPngBytes); fs.currentDirectory = fix('/foo/bar'); comparator = LocalFileComparator(Uri.parse('local_test.dart'), pathStyle: fs.path.style); final bool success = await doComparison(); expect(success, isTrue); }); test('and golden file is in subfolder of test', () async { fs.file(fix('/foo/bar/baz/golden.png')) ..createSync(recursive: true) ..writeAsBytesSync(_kExpectedPngBytes); fs.currentDirectory = fix('/foo/bar'); comparator = LocalFileComparator(Uri.parse('local_test.dart'), pathStyle: fs.path.style); final bool success = await doComparison('baz/golden.png'); expect(success, isTrue); }); }); }); group('fails', () { test('and generates correct output in the correct base location', () async { comparator = LocalFileComparator(Uri.parse('local_test.dart'), pathStyle: fs.path.style); await fs.file(fix('/golden.png')).writeAsBytes(_kColorFailurePngBytes); await expectLater( () => doComparison(), throwsA(isFlutterError.having( (FlutterError error) => error.message, 'message', contains('% diff detected'), )), ); final io.File master = fs.file( fix('/failures/golden_masterImage.png') ); final io.File test = fs.file( fix('/failures/golden_testImage.png') ); final io.File isolated = fs.file( fix('/failures/golden_isolatedDiff.png') ); final io.File masked = fs.file( fix('/failures/golden_maskedDiff.png') ); expect(master.existsSync(), isTrue); expect(test.existsSync(), isTrue); expect(isolated.existsSync(), isTrue); expect(masked.existsSync(), isTrue); }); test('and generates correct output when files are in a subdirectory', () async { comparator = LocalFileComparator(Uri.parse('local_test.dart'), pathStyle: fs.path.style); fs.file(fix('subdir/golden.png')) ..createSync(recursive:true) ..writeAsBytesSync(_kColorFailurePngBytes); await expectLater( () => doComparison('subdir/golden.png'), throwsA(isFlutterError.having( (FlutterError error) => error.message, 'message', contains('% diff detected'), )), ); final io.File master = fs.file( fix('/failures/golden_masterImage.png') ); final io.File test = fs.file( fix('/failures/golden_testImage.png') ); final io.File isolated = fs.file( fix('/failures/golden_isolatedDiff.png') ); final io.File masked = fs.file( fix('/failures/golden_maskedDiff.png') ); expect(master.existsSync(), isTrue); expect(test.existsSync(), isTrue); expect(isolated.existsSync(), isTrue); expect(masked.existsSync(), isTrue); }); test('when golden file does not exist', () async { await expectLater( () => doComparison(), throwsA(isA<TestFailure>().having( (TestFailure error) => error.message, 'message', contains('Could not be compared against non-existent file'), )), ); }); test('when images are not the same size', () async{ await fs.file(fix('/golden.png')).writeAsBytes(_kSizeFailurePngBytes); await expectLater( () => doComparison(), throwsA(isFlutterError.having( (FlutterError error) => error.message, 'message', contains('image sizes do not match'), )), ); }); test('when pixels do not match', () async{ await fs.file(fix('/golden.png')).writeAsBytes(_kColorFailurePngBytes); await expectLater( () => doComparison(), throwsA(isFlutterError.having( (FlutterError error) => error.message, 'message', contains('% diff detected'), )), ); }); test('when golden bytes are empty', () async { await fs.file(fix('/golden.png')).writeAsBytes(<int>[]); await expectLater( () => doComparison(), throwsA(isFlutterError.having( (FlutterError error) => error.message, 'message', contains('null image provided'), )), ); }); }); }); group('update', () { test('updates existing file', () async { fs.file(fix('/golden.png')).writeAsBytesSync(_kExpectedPngBytes); const List<int> newBytes = <int>[11, 12, 13]; await comparator.update(fs.file('golden.png').uri, Uint8List.fromList(newBytes)); expect(fs.file(fix('/golden.png')).readAsBytesSync(), newBytes); }); test('creates non-existent file', () async { expect(fs.file(fix('/foo.png')).existsSync(), isFalse); const List<int> newBytes = <int>[11, 12, 13]; await comparator.update(fs.file('foo.png').uri, Uint8List.fromList(newBytes)); expect(fs.file(fix('/foo.png')).existsSync(), isTrue); expect(fs.file(fix('/foo.png')).readAsBytesSync(), newBytes); }); }); group('getTestUri', () { test('updates file name with version number', () { final Uri key = Uri.parse('foo.png'); final Uri key1 = comparator.getTestUri(key, 1); expect(key1, Uri.parse('foo.1.png')); }); test('does nothing for null version number', () { final Uri key = Uri.parse('foo.png'); final Uri keyNull = comparator.getTestUri(key, null); expect(keyNull, Uri.parse('foo.png')); }); }); }); }