recording_canvas.dart 5.44 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 31 32 33 34 35 36 37 38
  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.
  String stackToString({ String indent: '' }) {
    assert(indent != null);
    return indent + FlutterError.defaultStackFilter(
      stack.toString().trimRight().split('\n')
    ).join('\n$indent');
  }
}

39 40 41 42 43 44 45 46 47 48 49 50 51 52
/// A [Canvas] for tests that records its method calls.
///
/// This class can be used in conjuction with [TestRecordingPaintingContext]
/// 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 110 111 112 113
  }
}

/// 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);
    canvas.restore();
  }

  @override
114
  void noSuchMethod(Invocation invocation) { }
115 116 117
}

class _MethodCall implements Invocation {
118
  _MethodCall(this._name, [ this._arguments = const <dynamic>[] ]);
119
  final Symbol _name;
120
  final List<dynamic> _arguments;
121 122 123 124 125 126 127 128 129 130 131 132 133
  @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
134
  List<dynamic> get positionalArguments => _arguments;
135
}
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171

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();
}