goldens_test.dart 12.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9
// 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';
10
import 'package:flutter/foundation.dart' show DiagnosticLevel, DiagnosticPropertiesBuilder, DiagnosticsNode, FlutterError;
11 12
import 'package:flutter_test/flutter_test.dart' hide test;
import 'package:flutter_test/flutter_test.dart' as test_package;
13

14
// 1x1 transparent pixel
15 16 17 18 19 20
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,
];
21 22

// 1x1 colored pixel
23 24 25 26 27 28
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,
];
29 30

// 1x2 transparent pixel
31 32 33 34 35 36
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,
];
37 38

void main() {
39
  late MemoryFileSystem fs;
40 41 42 43 44

  setUp(() {
    final FileSystemStyle style = io.Platform.isWindows
        ? FileSystemStyle.windows
        : FileSystemStyle.posix;
45
    fs = MemoryFileSystem(style: style);
46 47
  });

48 49 50 51 52 53 54 55 56 57
  /// 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);
  }

58
  void test(String description, FutureOr<void> Function() body) {
59 60
    test_package.test(description, () async {
      await io.IOOverrides.runZoned<FutureOr<void>>(
61 62 63 64 65 66 67 68 69 70 71 72 73
        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),
74
        fsWatch: (String a, int b, bool c) => throw UnsupportedError('unsupported'),
75 76 77 78 79 80 81 82
        fsWatchIsSupported: () => fs.isWatchSupported,
      );
    });
  }

  group('goldenFileComparator', () {
    test('is initialized by test framework', () {
      expect(goldenFileComparator, isNotNull);
Dan Field's avatar
Dan Field committed
83
      expect(goldenFileComparator, isA<LocalFileComparator>());
84
      final LocalFileComparator comparator = goldenFileComparator as LocalFileComparator;
85 86 87 88 89
      expect(comparator.basedir.path, contains('flutter_test'));
    });
  });

  group('LocalFileComparator', () {
90
    late LocalFileComparator comparator;
91 92

    setUp(() {
93
      comparator = LocalFileComparator(fs.file(fix('/golden_test.dart')).uri, pathStyle: fs.path.style);
94 95 96
    });

    test('calculates basedir correctly', () {
97
      expect(comparator.basedir, fs.file(fix('/')).uri);
98
      comparator = LocalFileComparator(fs.file(fix('/foo/bar/golden_test.dart')).uri, pathStyle: fs.path.style);
99
      expect(comparator.basedir, fs.directory(fix('/foo/bar/')).uri);
100 101
    });

102
    test('can be instantiated with uri that represents file in same folder', () {
103
      comparator = LocalFileComparator(Uri.parse('foo_test.dart'), pathStyle: fs.path.style);
104 105 106
      expect(comparator.basedir, Uri.parse('./'));
    });

107 108 109
    test('throws if local output is not awaited', () {
      try {
        comparator.generateFailureOutput(
110
          ComparisonResult(passed: false, diffPercent: 1.0),
111 112 113 114
          Uri.parse('foo_test.dart'),
          Uri.parse('/foo/bar/'),
        );
        TestAsyncUtils.verifyAllScopesClosed();
115
        fail('unexpectedly did not throw');
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
      } 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);
      }
    });

137
    group('compare', () {
138
      Future<bool> doComparison([ String golden = 'golden.png' ]) {
139
        final Uri uri = fs.file(fix(golden)).uri;
140
        return comparator.compare(
141
          Uint8List.fromList(_kExpectedPngBytes),
142 143 144 145 146 147
          uri,
        );
      }

      group('succeeds', () {
        test('when golden file is in same folder as test', () async {
148
          fs.file(fix('/golden.png')).writeAsBytesSync(_kExpectedPngBytes);
149 150 151 152 153
          final bool success = await doComparison();
          expect(success, isTrue);
        });

        test('when golden file is in subfolder of test', () async {
154
          fs.file(fix('/sub/foo.png'))
155
            ..createSync(recursive: true)
156
            ..writeAsBytesSync(_kExpectedPngBytes);
157 158 159
          final bool success = await doComparison('sub/foo.png');
          expect(success, isTrue);
        });
160 161 162 163 164

        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)
165
              ..writeAsBytesSync(_kExpectedPngBytes);
166
            fs.currentDirectory = fix('/foo/bar');
167
            comparator = LocalFileComparator(Uri.parse('local_test.dart'), pathStyle: fs.path.style);
168
            final bool success = await doComparison();
169 170 171 172 173 174
            expect(success, isTrue);
          });

          test('and golden file is in subfolder of test', () async {
            fs.file(fix('/foo/bar/baz/golden.png'))
              ..createSync(recursive: true)
175
              ..writeAsBytesSync(_kExpectedPngBytes);
176
            fs.currentDirectory = fix('/foo/bar');
177
            comparator = LocalFileComparator(Uri.parse('local_test.dart'), pathStyle: fs.path.style);
178 179 180 181
            final bool success = await doComparison('baz/golden.png');
            expect(success, isTrue);
          });
        });
182 183 184
      });

      group('fails', () {
185

186
        test('and generates correct output in the correct base location', () async {
187 188
          comparator = LocalFileComparator(Uri.parse('local_test.dart'), pathStyle: fs.path.style);
          await fs.file(fix('/golden.png')).writeAsBytes(_kColorFailurePngBytes);
189 190
          await expectLater(
            () => doComparison(),
191
            throwsA(isFlutterError.having(
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
              (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);
213 214 215 216 217 218
        });

        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)
219
            ..writeAsBytesSync(_kColorFailurePngBytes);
220 221
          await expectLater(
            () => doComparison('subdir/golden.png'),
222
            throwsA(isFlutterError.having(
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
              (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);
244 245
        });

246
        test('when golden file does not exist', () async {
247 248 249 250 251 252 253 254
          await expectLater(
            () => doComparison(),
            throwsA(isA<TestFailure>().having(
              (TestFailure error) => error.message,
              'message',
              contains('Could not be compared against non-existent file'),
            )),
          );
255 256
        });

257 258
        test('when images are not the same size', () async{
          await fs.file(fix('/golden.png')).writeAsBytes(_kSizeFailurePngBytes);
259 260
          await expectLater(
            () => doComparison(),
261
            throwsA(isFlutterError.having(
262 263 264 265 266
              (FlutterError error) => error.message,
              'message',
              contains('image sizes do not match'),
            )),
          );
267 268
        });

269 270
        test('when pixels do not match', () async{
          await fs.file(fix('/golden.png')).writeAsBytes(_kColorFailurePngBytes);
271 272
          await expectLater(
            () => doComparison(),
273
            throwsA(isFlutterError.having(
274 275 276 277 278
              (FlutterError error) => error.message,
              'message',
              contains('% diff detected'),
            )),
          );
279 280 281
        });

        test('when golden bytes are empty', () async {
282
          await fs.file(fix('/golden.png')).writeAsBytes(<int>[]);
283 284
          await expectLater(
            () => doComparison(),
285
            throwsA(isFlutterError.having(
286 287 288 289 290
              (FlutterError error) => error.message,
              'message',
              contains('null image provided'),
            )),
          );
291 292 293 294 295 296
        });
      });
    });

    group('update', () {
      test('updates existing file', () async {
297
        fs.file(fix('/golden.png')).writeAsBytesSync(_kExpectedPngBytes);
298
        const List<int> newBytes = <int>[11, 12, 13];
299
        await comparator.update(fs.file('golden.png').uri, Uint8List.fromList(newBytes));
300
        expect(fs.file(fix('/golden.png')).readAsBytesSync(), newBytes);
301 302 303
      });

      test('creates non-existent file', () async {
304
        expect(fs.file(fix('/foo.png')).existsSync(), isFalse);
305
        const List<int> newBytes = <int>[11, 12, 13];
306
        await comparator.update(fs.file('foo.png').uri, Uint8List.fromList(newBytes));
307 308
        expect(fs.file(fix('/foo.png')).existsSync(), isTrue);
        expect(fs.file(fix('/foo.png')).readAsBytesSync(), newBytes);
309 310
      });
    });
311 312 313 314 315 316 317 318 319 320 321 322 323

    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'));
      });
    });
324 325
  });
}