_matchers_io.dart 4.74 KB
Newer Older
1 2 3 4 5 6 7 8
// 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:typed_data';
import 'dart:ui' as ui;

import 'package:flutter/rendering.dart';
9
import 'package:flutter/widgets.dart';
10 11 12
import 'package:matcher/expect.dart';
import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports
import 'package:test_api/hooks.dart' show TestFailure;
13 14 15 16 17 18 19 20 21 22 23

import 'binding.dart';
import 'finders.dart';
import 'goldens.dart';

/// Render the closest [RepaintBoundary] of the [element] into an image.
///
/// See also:
///
///  * [OffsetLayer.toImage] which is the actual method being called.
Future<ui.Image> captureImage(Element element) {
24 25
  assert(element.renderObject != null);
  RenderObject renderObject = element.renderObject!;
26
  while (!renderObject.isRepaintBoundary) {
27
    renderObject = renderObject.parent!;
28 29
  }
  assert(!renderObject.debugNeedsPaint);
30
  final OffsetLayer layer = renderObject.debugLayer! as OffsetLayer;
31 32 33
  return layer.toImage(renderObject.paintBounds);
}

34 35 36 37 38 39 40 41 42
/// Whether or not [captureImage] is supported.
///
/// This can be used to skip tests on platforms that don't support
/// capturing images.
///
/// Currently this is true except when tests are running in the context of a web
/// browser (`flutter test --platform chrome`).
const bool canCaptureImage = true;

43 44 45 46 47 48 49 50 51 52 53 54 55
/// The matcher created by [matchesGoldenFile]. This class is enabled when the
/// test is running on a VM using conditional import.
class MatchesGoldenFile extends AsyncMatcher {
  /// Creates an instance of [MatchesGoldenFile]. Called by [matchesGoldenFile].
  const MatchesGoldenFile(this.key, this.version);

  /// Creates an instance of [MatchesGoldenFile]. Called by [matchesGoldenFile].
  MatchesGoldenFile.forStringPath(String path, this.version) : key = Uri.parse(path);

  /// The [key] to the golden image.
  final Uri key;

  /// The [version] of the golden image.
56
  final int? version;
57 58

  @override
59
  Future<String?> matchAsync(dynamic item) async {
60 61 62
    final Uri testNameUri = goldenFileComparator.getTestUri(key, version);

    Uint8List? buffer;
63 64 65
    if (item is Future<List<int>?>) {
      final List<int>? bytes = await item;
      buffer = bytes == null ? null : Uint8List.fromList(bytes);
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
    } else if (item is List<int>) {
      buffer = Uint8List.fromList(item);
    }
    if (buffer != null) {
      if (autoUpdateGoldenFiles) {
        await goldenFileComparator.update(testNameUri, buffer);
        return null;
      }
      try {
        final bool success = await goldenFileComparator.compare(buffer, testNameUri);
        return success ? null : 'does not match';
      } on TestFailure catch (ex) {
        return ex.message;
      }
    }
81
    Future<ui.Image?> imageFuture;
82
    final bool disposeImage; // set to true if the matcher created and owns the image and must therefore dispose it.
83
    if (item is Future<ui.Image?>) {
84
      imageFuture = item;
85
      disposeImage = false;
86 87
    } else if (item is ui.Image) {
      imageFuture = Future<ui.Image>.value(item);
88
      disposeImage = false;
89 90
    } else if (item is Finder) {
      final Iterable<Element> elements = item.evaluate();
91 92 93 94 95 96
      if (elements.isEmpty) {
        return 'could not be rendered because no widget was found';
      } else if (elements.length > 1) {
        return 'matched too many widgets';
      }
      imageFuture = captureImage(elements.single);
97
      disposeImage = true;
98
    } else {
99
      throw AssertionError('must provide a Finder, Image, Future<Image>, List<int>, or Future<List<int>>');
100 101
    }

102
    final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
103
    return binding.runAsync<String?>(() async {
104 105
      final ui.Image? image = await imageFuture;
      if (image == null) {
106
        throw AssertionError('Future<Image> completed to null');
107
      }
108
      try {
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
        final ByteData? bytes = await image.toByteData(format: ui.ImageByteFormat.png);
        if (bytes == null) {
          return 'could not encode screenshot.';
        }
        if (autoUpdateGoldenFiles) {
          await goldenFileComparator.update(testNameUri, bytes.buffer.asUint8List());
          return null;
        }
        try {
          final bool success = await goldenFileComparator.compare(bytes.buffer.asUint8List(), testNameUri);
          return success ? null : 'does not match';
        } on TestFailure catch (ex) {
          return ex.message;
        }
      } finally {
        if (disposeImage) {
          image.dispose();
        }
127
      }
128
    });
129 130 131 132 133 134 135 136
  }

  @override
  Description describe(Description description) {
    final Uri testNameUri = goldenFileComparator.getTestUri(key, version);
    return description.add('one widget whose rasterized image matches golden image "$testNameUri"');
  }
}