_matchers_web.dart 5.69 KB
Newer Older
1 2 3 4 5 6
// 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:ui' as ui;

7
import 'package:flutter/foundation.dart';
8 9
import 'package:flutter/rendering.dart';
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

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

/// An unsupported method that exists for API compatibility.
Future<ui.Image> captureImage(Element element) {
  throw UnsupportedError('captureImage is not supported on the web.');
}

23 24 25 26 27 28 29 30 31
/// 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 = false;

32 33 34 35 36 37 38 39 40 41 42 43 44
/// The matcher created by [matchesGoldenFile]. This class is enabled when the
/// test is running in a web browser 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.
45
  final int? version;
46 47

  @override
48
  Future<String?> matchAsync(dynamic item) async {
49 50 51
    if (item is! Finder) {
      return 'web goldens only supports matching finders.';
    }
52
    final Iterable<Element> elements = item.evaluate();
53 54 55 56 57 58 59 60
    if (elements.isEmpty) {
      return 'could not be rendered because no widget was found';
    } else if (elements.length > 1) {
      return 'matched too many widgets';
    }
    final Element element = elements.single;
    final RenderObject renderObject = _findRepaintBoundary(element);
    final Size size = renderObject.paintBounds.size;
61
    final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
62
    final ui.FlutterView view = binding.platformDispatcher.implicitView!;
63
    final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView == view);
64

65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
    if (isCanvasKit) {
      // In CanvasKit, use Layer.toImage to generate the screenshot.
      final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
      return binding.runAsync<String?>(() async {
        assert(element.renderObject != null);
        RenderObject renderObject = element.renderObject!;
        while (!renderObject.isRepaintBoundary) {
          renderObject = renderObject.parent!;
        }
        assert(!renderObject.debugNeedsPaint);
        final OffsetLayer layer = renderObject.debugLayer! as OffsetLayer;
        final ui.Image image = await layer.toImage(renderObject.paintBounds);
        try {
          final ByteData? bytes = await image.toByteData(format: ui.ImageByteFormat.png);
          if (bytes == null) {
            return 'could not encode screenshot.';
          }
          if (autoUpdateGoldenFiles) {
            await webGoldenComparator.updateBytes(bytes.buffer.asUint8List(), key);
            return null;
          }
          try {
            final bool success = await webGoldenComparator.compareBytes(bytes.buffer.asUint8List(), key);
            return success ? null : 'does not match';
          } on TestFailure catch (ex) {
            return ex.message;
          }
        } finally {
          image.dispose();
        }
      });
    } else {
      // In the HTML renderer, we don't have the ability to render an element
      // to an image directly. Instead, we will use `window.render()` to render
      // only the element being requested, and send a request to the test server
      // requesting it to take a screenshot through the browser's debug interface.
      _renderElement(view, renderObject);
      final String? result = await binding.runAsync<String?>(() async {
        if (autoUpdateGoldenFiles) {
          await webGoldenComparator.update(size.width, size.height, key);
          return null;
        }
        try {
          final bool success = await webGoldenComparator.compare(size.width, size.height, key);
          return success ? null : 'does not match';
        } on TestFailure catch (ex) {
          return ex.message;
        }
      });
      _renderElement(view, renderView);
      return result;
    }
117 118 119 120 121 122 123 124 125 126
  }

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

RenderObject _findRepaintBoundary(Element element) {
127 128
  assert(element.renderObject != null);
  RenderObject renderObject = element.renderObject!;
129
  while (!renderObject.isRepaintBoundary) {
130
    renderObject = renderObject.parent!;
131 132 133 134
  }
  return renderObject;
}

135
void _renderElement(ui.FlutterView window, RenderObject renderObject) {
136 137
  assert(renderObject.debugLayer != null);
  final Layer layer = renderObject.debugLayer!;
138 139 140 141 142 143 144 145 146 147 148
  final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
  if (layer is OffsetLayer) {
    sceneBuilder.pushOffset(-layer.offset.dx, -layer.offset.dy);
  }
  // ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
  layer.updateSubtreeNeedsAddToScene();
  // ignore: invalid_use_of_protected_member
  layer.addToScene(sceneBuilder);
  sceneBuilder.pop();
  window.render(sceneBuilder.build());
}