recording_canvas.dart 6.45 KB
Newer Older
1 2 3 4
// Copyright 2017 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.

5
import 'package:flutter/foundation.dart';
6 7
import 'package:flutter/rendering.dart';

8 9 10 11 12 13 14 15
/// An [Invocation] and the [stack] trace that led to it.
///
/// Used by [TestRecordingCanvas] to trace canvas calls.
class RecordedInvocation {
  /// Create a record for an invocation list.
  const RecordedInvocation(this.invocation, { this.stack });

  /// The method that was called and its arguments.
16 17 18 19 20 21
  ///
  /// The arguments preserve identity, but not value. Thus, if two invocations
  /// were made with the same [Paint] object, but with that object configured
  /// differently each time, then they will both have the same object as their
  /// argument, and inspecting that object will return the object's current
  /// values (mostly likely those passed to the second call).
22 23 24 25 26 27 28 29 30
  final Invocation invocation;

  /// The stack trace at the time of the method call.
  final StackTrace stack;

  @override
  String toString() => _describeInvocation(invocation);

  /// Converts [stack] to a string using the [FlutterError.defaultStackFilter] logic.
31
  String stackToString({ String indent = '' }) {
32 33 34 35 36 37 38
    assert(indent != null);
    return indent + FlutterError.defaultStackFilter(
      stack.toString().trimRight().split('\n')
    ).join('\n$indent');
  }
}

39 40
/// A [Canvas] for tests that records its method calls.
///
41
/// This class can be used in conjunction with [TestRecordingPaintingContext]
42 43 44 45 46 47 48 49 50 51 52
/// to record the [Canvas] method calls made by a renderer. For example:
///
/// ```dart
/// RenderBox box = tester.renderObject(find.text('ABC'));
/// TestRecordingCanvas canvas = new TestRecordingCanvas();
/// TestRecordingPaintingContext context = new TestRecordingPaintingContext(canvas);
/// box.paint(context, Offset.zero);
/// // Now test the expected canvas.invocations.
/// ```
///
/// In some cases it may be useful to define a subclass that overrides the
53
/// [Canvas] methods the test is checking and squirrels away the parameters
54
/// that the test requires.
55 56 57
///
/// For simple tests, consider using the [paints] matcher, which overlays a
/// pattern matching API over [TestRecordingCanvas].
58 59
class TestRecordingCanvas implements Canvas {
  /// All of the method calls on this canvas.
60
  final List<RecordedInvocation> invocations = <RecordedInvocation>[];
61 62 63 64 65 66 67 68 69

  int _saveCount = 0;

  @override
  int getSaveCount() => _saveCount;

  @override
  void save() {
    _saveCount += 1;
70
    invocations.add(new RecordedInvocation(new _MethodCall(#save), stack: StackTrace.current));
71 72
  }

73 74 75
  @override
  void saveLayer(Rect bounds, Paint paint) {
    _saveCount += 1;
76
    invocations.add(new RecordedInvocation(new _MethodCall(#saveLayer, <dynamic>[bounds, paint]), stack: StackTrace.current));
77 78
  }

79 80 81 82
  @override
  void restore() {
    _saveCount -= 1;
    assert(_saveCount >= 0);
83
    invocations.add(new RecordedInvocation(new _MethodCall(#restore), stack: StackTrace.current));
84 85 86 87
  }

  @override
  void noSuchMethod(Invocation invocation) {
88
    invocations.add(new RecordedInvocation(invocation, stack: StackTrace.current));
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
  }
}

/// A [PaintingContext] for tests that use [TestRecordingCanvas].
class TestRecordingPaintingContext implements PaintingContext {
  /// Creates a [PaintingContext] for tests that use [TestRecordingCanvas].
  TestRecordingPaintingContext(this.canvas);

  @override
  final Canvas canvas;

  @override
  void paintChild(RenderObject child, Offset offset) {
    child.paint(this, offset);
  }

  @override
  void pushClipRect(bool needsCompositing, Offset offset, Rect clipRect, PaintingContextCallback painter) {
    canvas.save();
    canvas.clipRect(clipRect.shift(offset));
    painter(this, offset);
110 111 112
    canvas.restore();
  }

113 114 115 116 117 118 119 120
  @override
  void pushClipRRect(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect, PaintingContextCallback painter) {
    canvas.save();
    canvas.clipRRect(clipRRect.shift(offset));
    painter(this, offset);
    canvas.restore();
  }

121 122 123 124 125 126
  @override
  void pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath, PaintingContextCallback painter) {
    canvas
      ..save()
      ..clipPath(clipPath.shift(offset));
    painter(this, offset);
127 128 129
    canvas.restore();
  }

130 131 132 133 134 135 136 137
  @override
  void pushTransform(bool needsCompositing, Offset offset, Matrix4 transform, PaintingContextCallback painter) {
    canvas.save();
    canvas.transform(transform.storage);
    painter(this, offset);
    canvas.restore();
  }

138 139 140 141 142
  @override
  void pushLayer(Layer childLayer, PaintingContextCallback painter, Offset offset, {Rect childPaintBounds}) {
    painter(this, offset);
  }

143
  @override
144
  void noSuchMethod(Invocation invocation) { }
145 146 147
}

class _MethodCall implements Invocation {
148
  _MethodCall(this._name, [ this._arguments = const <dynamic>[], this._typeArguments = const <Type> []]);
149
  final Symbol _name;
150
  final List<dynamic> _arguments;
151
  final List<Type> _typeArguments;
152 153 154 155 156 157 158 159 160 161 162 163 164
  @override
  bool get isAccessor => false;
  @override
  bool get isGetter => false;
  @override
  bool get isMethod => true;
  @override
  bool get isSetter => false;
  @override
  Symbol get memberName => _name;
  @override
  Map<Symbol, dynamic> get namedArguments => <Symbol, dynamic>{};
  @override
165
  List<dynamic> get positionalArguments => _arguments;
166 167
  @override
  List<Type> get typeArguments => _typeArguments;
168
}
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204

String _valueName(Object value) {
  if (value is double)
    return value.toStringAsFixed(1);
  return value.toString();
}

// Workaround for https://github.com/dart-lang/sdk/issues/28372
String _symbolName(Symbol symbol) {
  // WARNING: Assumes a fixed format for Symbol.toString which is *not*
  // guaranteed anywhere.
  final String s = '$symbol';
  return s.substring(8, s.length - 2);
}

// Workaround for https://github.com/dart-lang/sdk/issues/28373
String _describeInvocation(Invocation call) {
  final StringBuffer buffer = new StringBuffer();
  buffer.write(_symbolName(call.memberName));
  if (call.isSetter) {
    buffer.write(call.positionalArguments[0].toString());
  } else if (call.isMethod) {
    buffer.write('(');
    buffer.writeAll(call.positionalArguments.map<String>(_valueName), ', ');
    String separator = call.positionalArguments.isEmpty ? '' : ', ';
    call.namedArguments.forEach((Symbol name, Object value) {
      buffer.write(separator);
      buffer.write(_symbolName(name));
      buffer.write(': ');
      buffer.write(_valueName(value));
      separator = ', ';
    });
    buffer.write(')');
  }
  return buffer.toString();
}