mock_canvas.dart 59.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:ui' as ui show Image, Paragraph;
6

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/rendering.dart';
9
import 'package:flutter_test/flutter_test.dart';
10

11 12
import 'recording_canvas.dart';

13 14 15 16 17 18 19 20 21 22 23 24 25 26
/// Matches objects or functions that paint a display list that matches the
/// canvas calls described by the pattern.
///
/// Specifically, this can be applied to [RenderObject]s, [Finder]s that
/// correspond to a single [RenderObject], and functions that have either of the
/// following signatures:
///
/// ```dart
/// void function(PaintingContext context, Offset offset);
/// void function(Canvas canvas);
/// ```
///
/// In the case of functions that take a [PaintingContext] and an [Offset], the
/// [paints] matcher will always pass a zero offset.
27 28 29 30
///
/// To specify the pattern, call the methods on the returned object. For example:
///
/// ```dart
31
/// expect(myRenderObject, paints..circle(radius: 10.0)..circle(radius: 20.0));
32 33 34 35 36 37 38
/// ```
///
/// This particular pattern would verify that the render object `myRenderObject`
/// paints, among other things, two circles of radius 10.0 and 20.0 (in that
/// order).
///
/// See [PaintPattern] for a discussion of the semantics of paint patterns.
39 40
///
/// To match something which paints nothing, see [paintsNothing].
Ian Hickson's avatar
Ian Hickson committed
41 42
///
/// To match something which asserts instead of painting, see [paintsAssertion].
43
PaintPattern get paints => _TestRecordingCanvasPatternMatcher();
44

45
/// Matches objects or functions that does not paint anything on the canvas.
46
Matcher get paintsNothing => _TestRecordingCanvasPaintsNothingMatcher();
47

Ian Hickson's avatar
Ian Hickson committed
48
/// Matches objects or functions that assert when they try to paint.
49
Matcher get paintsAssertion => _TestRecordingCanvasPaintsAssertionMatcher();
Ian Hickson's avatar
Ian Hickson committed
50

51
/// Matches objects or functions that draw `methodName` exactly `count` number of times.
52
Matcher paintsExactlyCountTimes(Symbol methodName, int count) {
53
  return _TestRecordingCanvasPaintsCountMatcher(methodName, count);
54 55
}

56 57
/// Signature for the [PaintPattern.something] and [PaintPattern.everything]
/// predicate argument.
58 59 60 61 62 63 64 65 66
///
/// Used by the [paints] matcher.
///
/// The `methodName` argument is a [Symbol], and can be compared with the symbol
/// literal syntax, for example:
///
/// ```dart
/// if (methodName == #drawCircle) { ... }
/// ```
67
typedef PaintPatternPredicate = bool Function(Symbol methodName, List<dynamic> arguments);
68

69
/// The signature of [RenderObject.paint] functions.
70
typedef _ContextPainterFunction = void Function(PaintingContext context, Offset offset);
71 72

/// The signature of functions that paint directly on a canvas.
73
typedef _CanvasPainterFunction = void Function(Canvas canvas);
74

75 76 77 78 79 80 81 82
/// Builder interface for patterns used to match display lists (canvas calls).
///
/// The [paints] matcher returns a [PaintPattern] so that you can build the
/// pattern in the [expect] call.
///
/// Patterns are subset matches, meaning that any calls not described by the
/// pattern are ignored. This allows, for instance, transforms to be skipped.
abstract class PaintPattern {
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
  /// Indicates that a transform is expected next.
  ///
  /// Calls are skipped until a call to [Canvas.transform] is found. The call's
  /// arguments are compared to those provided here. If any fail to match, or if
  /// no call to [Canvas.transform] is found, then the matcher fails.
  ///
  /// Dynamic so matchers can be more easily passed in.
  ///
  /// The `matrix4` argument is dynamic so it can be either a [Matcher], or a
  /// [Float64List] of [double]s. If it is a [Float64List] of [double]s then
  /// each value in the matrix must match in the expected matrix. A deep
  /// matching [Matcher] such as [equals] can be used to test each value in the
  /// matrix with utilities such as [moreOrLessEquals].
  void transform({ dynamic matrix4 });

98 99 100 101 102
  /// Indicates that a translation transform is expected next.
  ///
  /// Calls are skipped until a call to [Canvas.translate] is found. The call's
  /// arguments are compared to those provided here. If any fail to match, or if
  /// no call to [Canvas.translate] is found, then the matcher fails.
103
  void translate({ double? x, double? y });
104 105 106 107 108 109

  /// Indicates that a scale transform is expected next.
  ///
  /// Calls are skipped until a call to [Canvas.scale] is found. The call's
  /// arguments are compared to those provided here. If any fail to match, or if
  /// no call to [Canvas.scale] is found, then the matcher fails.
110
  void scale({ double? x, double? y });
111 112 113 114 115 116 117

  /// Indicates that a rotate transform is expected next.
  ///
  /// Calls are skipped until a call to [Canvas.rotate] is found. If the `angle`
  /// argument is provided here, the call's argument is compared to it. If that
  /// fails to match, or if no call to [Canvas.rotate] is found, then the
  /// matcher fails.
118
  void rotate({ double? angle });
119 120 121 122 123 124

  /// Indicates that a save is expected next.
  ///
  /// Calls are skipped until a call to [Canvas.save] is found. If none is
  /// found, the matcher fails.
  ///
125 126 127 128 129
  /// See also:
  ///
  ///  * [restore], which indicates that a restore is expected next.
  ///  * [saveRestore], which indicates that a matching pair of save/restore
  ///    calls is expected next.
130 131 132 133 134 135 136
  void save();

  /// Indicates that a restore is expected next.
  ///
  /// Calls are skipped until a call to [Canvas.restore] is found. If none is
  /// found, the matcher fails.
  ///
137 138 139 140 141
  /// See also:
  ///
  ///  * [save], which indicates that a save is expected next.
  ///  * [saveRestore], which indicates that a matching pair of save/restore
  ///    calls is expected next.
142 143 144 145 146 147 148 149
  void restore();

  /// Indicates that a matching pair of save/restore calls is expected next.
  ///
  /// Calls are skipped until a call to [Canvas.save] is found, then, calls are
  /// skipped until the matching [Canvas.restore] call is found. If no matching
  /// pair of calls could be found, the matcher fails.
  ///
150 151 152 153
  /// See also:
  ///
  ///  * [save], which indicates that a save is expected next.
  ///  * [restore], which indicates that a restore is expected next.
154 155
  void saveRestore();

156
  /// Indicates that a rectangular clip is expected next.
157 158 159 160 161 162 163 164 165
  ///
  /// The next rectangular clip is examined. Any arguments that are passed to
  /// this method are compared to the actual [Canvas.clipRect] call's argument
  /// and any mismatches result in failure.
  ///
  /// If no call to [Canvas.clipRect] was made, then this results in failure.
  ///
  /// Any calls made between the last matched call (if any) and the
  /// [Canvas.clipRect] call are ignored.
166
  void clipRect({ Rect? rect });
167

168 169 170 171 172 173 174 175 176 177
  /// Indicates that a path clip is expected next.
  ///
  /// The next path clip is examined.
  /// The path that is passed to the actual [Canvas.clipPath] call is matched
  /// using [pathMatcher].
  ///
  /// If no call to [Canvas.clipPath] was made, then this results in failure.
  ///
  /// Any calls made between the last matched call (if any) and the
  /// [Canvas.clipPath] call are ignored.
178
  void clipPath({ Matcher? pathMatcher });
179

180
  /// Indicates that a rectangle is expected next.
181
  ///
182 183 184
  /// The next rectangle is examined. Any arguments that are passed to this
  /// method are compared to the actual [Canvas.drawRect] call's arguments
  /// and any mismatches result in failure.
185
  ///
186
  /// If no call to [Canvas.drawRect] was made, then this results in failure.
187 188
  ///
  /// Any calls made between the last matched call (if any) and the
189
  /// [Canvas.drawRect] call are ignored.
190 191 192 193 194 195
  ///
  /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
  /// `style`) are compared against the state of the [Paint] object after the
  /// painting has completed, not at the time of the call. If the same [Paint]
  /// object is reused multiple times, then this may not match the actual
  /// arguments as they were seen by the method.
196
  void rect({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style });
197

198 199 200 201 202 203 204 205 206 207
  /// Indicates that a rounded rectangle clip is expected next.
  ///
  /// The next rounded rectangle clip is examined. Any arguments that are passed
  /// to this method are compared to the actual [Canvas.clipRRect] call's
  /// argument and any mismatches result in failure.
  ///
  /// If no call to [Canvas.clipRRect] was made, then this results in failure.
  ///
  /// Any calls made between the last matched call (if any) and the
  /// [Canvas.clipRRect] call are ignored.
208
  void clipRRect({ RRect? rrect });
209

210 211 212 213 214 215 216 217 218 219
  /// Indicates that a rounded rectangle is expected next.
  ///
  /// The next rounded rectangle is examined. Any arguments that are passed to
  /// this method are compared to the actual [Canvas.drawRRect] call's arguments
  /// and any mismatches result in failure.
  ///
  /// If no call to [Canvas.drawRRect] was made, then this results in failure.
  ///
  /// Any calls made between the last matched call (if any) and the
  /// [Canvas.drawRRect] call are ignored.
220 221 222 223 224 225
  ///
  /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
  /// `style`) are compared against the state of the [Paint] object after the
  /// painting has completed, not at the time of the call. If the same [Paint]
  /// object is reused multiple times, then this may not match the actual
  /// arguments as they were seen by the method.
226
  void rrect({ RRect? rrect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style });
227

228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
  /// Indicates that a rounded rectangle outline is expected next.
  ///
  /// The next call to [Canvas.drawRRect] is examined. Any arguments that are
  /// passed to this method are compared to the actual [Canvas.drawRRect] call's
  /// arguments and any mismatches result in failure.
  ///
  /// If no call to [Canvas.drawRRect] was made, then this results in failure.
  ///
  /// Any calls made between the last matched call (if any) and the
  /// [Canvas.drawRRect] call are ignored.
  ///
  /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
  /// `style`) are compared against the state of the [Paint] object after the
  /// painting has completed, not at the time of the call. If the same [Paint]
  /// object is reused multiple times, then this may not match the actual
  /// arguments as they were seen by the method.
244
  void drrect({ RRect? outer, RRect? inner, Color? color, double strokeWidth, bool hasMaskFilter, PaintingStyle style });
245

246 247 248 249 250 251 252 253 254 255
  /// Indicates that a circle is expected next.
  ///
  /// The next circle is examined. Any arguments that are passed to this method
  /// are compared to the actual [Canvas.drawCircle] call's arguments and any
  /// mismatches result in failure.
  ///
  /// If no call to [Canvas.drawCircle] was made, then this results in failure.
  ///
  /// Any calls made between the last matched call (if any) and the
  /// [Canvas.drawCircle] call are ignored.
256 257 258 259 260 261
  ///
  /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
  /// `style`) are compared against the state of the [Paint] object after the
  /// painting has completed, not at the time of the call. If the same [Paint]
  /// object is reused multiple times, then this may not match the actual
  /// arguments as they were seen by the method.
262
  void circle({ double? x, double? y, double? radius, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style });
263

264 265 266 267 268 269
  /// Indicates that a path is expected next.
  ///
  /// The next path is examined. Any arguments that are passed to this method
  /// are compared to the actual [Canvas.drawPath] call's `paint` argument, and
  /// any mismatches result in failure.
  ///
Ian Hickson's avatar
Ian Hickson committed
270 271 272 273
  /// To introspect the Path object (as it stands after the painting has
  /// completed), the `includes` and `excludes` arguments can be provided to
  /// specify points that should be considered inside or outside the path
  /// (respectively).
274 275 276 277 278
  ///
  /// If no call to [Canvas.drawPath] was made, then this results in failure.
  ///
  /// Any calls made between the last matched call (if any) and the
  /// [Canvas.drawPath] call are ignored.
279 280 281 282 283 284
  ///
  /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
  /// `style`) are compared against the state of the [Paint] object after the
  /// painting has completed, not at the time of the call. If the same [Paint]
  /// object is reused multiple times, then this may not match the actual
  /// arguments as they were seen by the method.
285
  void path({ Iterable<Offset>? includes, Iterable<Offset>? excludes, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style });
286

Ian Hickson's avatar
Ian Hickson committed
287 288 289
  /// Indicates that a line is expected next.
  ///
  /// The next line is examined. Any arguments that are passed to this method
290 291
  /// are compared to the actual [Canvas.drawLine] call's `p1`, `p2`, and
  /// `paint` arguments, and any mismatches result in failure.
Ian Hickson's avatar
Ian Hickson committed
292 293 294 295 296
  ///
  /// If no call to [Canvas.drawLine] was made, then this results in failure.
  ///
  /// Any calls made between the last matched call (if any) and the
  /// [Canvas.drawLine] call are ignored.
297 298 299 300 301 302
  ///
  /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
  /// `style`) are compared against the state of the [Paint] object after the
  /// painting has completed, not at the time of the call. If the same [Paint]
  /// object is reused multiple times, then this may not match the actual
  /// arguments as they were seen by the method.
303
  void line({ Offset? p1, Offset? p2, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style });
Ian Hickson's avatar
Ian Hickson committed
304

305 306 307 308 309 310 311 312 313 314
  /// Indicates that an arc is expected next.
  ///
  /// The next arc is examined. Any arguments that are passed to this method
  /// are compared to the actual [Canvas.drawArc] call's `paint` argument, and
  /// any mismatches result in failure.
  ///
  /// If no call to [Canvas.drawArc] was made, then this results in failure.
  ///
  /// Any calls made between the last matched call (if any) and the
  /// [Canvas.drawArc] call are ignored.
315 316 317 318 319 320
  ///
  /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
  /// `style`) are compared against the state of the [Paint] object after the
  /// painting has completed, not at the time of the call. If the same [Paint]
  /// object is reused multiple times, then this may not match the actual
  /// arguments as they were seen by the method.
321
  void arc({ Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style });
322

323 324 325 326 327 328
  /// Indicates that a paragraph is expected next.
  ///
  /// Calls are skipped until a call to [Canvas.drawParagraph] is found. Any
  /// arguments that are passed to this method are compared to the actual
  /// [Canvas.drawParagraph] call's argument, and any mismatches result in failure.
  ///
329 330 331 332 333 334 335
  /// The `offset` argument can be either an [Offset] or a [Matcher]. If it is
  /// an [Offset] then the actual value must match the expected offset
  /// precisely. If it is a [Matcher] then the comparison is made according to
  /// the semantics of the [Matcher]. For example, [within] can be used to
  /// assert that the actual offset is within a given distance from the expected
  /// offset.
  ///
336
  /// If no call to [Canvas.drawParagraph] was made, then this results in failure.
337
  void paragraph({ ui.Paragraph? paragraph, dynamic offset });
338

339 340 341 342 343 344
  /// Indicates that a shadow is expected next.
  ///
  /// The next shadow is examined. Any arguments that are passed to this method
  /// are compared to the actual [Canvas.drawShadow] call's `paint` argument,
  /// and any mismatches result in failure.
  ///
345 346 347 348
  /// In tests, shadows from framework features such as [BoxShadow] or
  /// [Material] are disabled by default, and thus this predicate would not
  /// match. The [debugDisableShadows] flag controls this.
  ///
349 350 351 352 353 354 355 356 357
  /// To introspect the Path object (as it stands after the painting has
  /// completed), the `includes` and `excludes` arguments can be provided to
  /// specify points that should be considered inside or outside the path
  /// (respectively).
  ///
  /// If no call to [Canvas.drawShadow] was made, then this results in failure.
  ///
  /// Any calls made between the last matched call (if any) and the
  /// [Canvas.drawShadow] call are ignored.
358
  void shadow({ Iterable<Offset>? includes, Iterable<Offset>? excludes, Color? color, double? elevation, bool? transparentOccluder });
359

Ian Hickson's avatar
Ian Hickson committed
360 361
  /// Indicates that an image is expected next.
  ///
362 363 364 365 366 367 368 369 370 371 372 373 374 375
  /// The next call to [Canvas.drawImage] is examined, and its arguments
  /// compared to those passed to _this_ method.
  ///
  /// If no call to [Canvas.drawImage] was made, then this results in
  /// failure.
  ///
  /// Any calls made between the last matched call (if any) and the
  /// [Canvas.drawImage] call are ignored.
  ///
  /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
  /// `style`) are compared against the state of the [Paint] object after the
  /// painting has completed, not at the time of the call. If the same [Paint]
  /// object is reused multiple times, then this may not match the actual
  /// arguments as they were seen by the method.
376
  void image({ ui.Image? image, double? x, double? y, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style });
377 378 379

  /// Indicates that an image subsection is expected next.
  ///
Ian Hickson's avatar
Ian Hickson committed
380 381 382 383 384 385 386 387 388 389 390 391 392 393
  /// The next call to [Canvas.drawImageRect] is examined, and its arguments
  /// compared to those passed to _this_ method.
  ///
  /// If no call to [Canvas.drawImageRect] was made, then this results in
  /// failure.
  ///
  /// Any calls made between the last matched call (if any) and the
  /// [Canvas.drawImageRect] call are ignored.
  ///
  /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
  /// `style`) are compared against the state of the [Paint] object after the
  /// painting has completed, not at the time of the call. If the same [Paint]
  /// object is reused multiple times, then this may not match the actual
  /// arguments as they were seen by the method.
394
  void drawImageRect({ ui.Image? image, Rect? source, Rect? destination, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style });
Ian Hickson's avatar
Ian Hickson committed
395

396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
  /// Provides a custom matcher.
  ///
  /// Each method call after the last matched call (if any) will be passed to
  /// the given predicate, along with the values of its (positional) arguments.
  ///
  /// For each one, the predicate must either return a boolean or throw a [String].
  ///
  /// If the predicate returns true, the call is considered a successful match
  /// and the next step in the pattern is examined. If this was the last step,
  /// then any calls that were not yet matched are ignored and the [paints]
  /// [Matcher] is considered a success.
  ///
  /// If the predicate returns false, then the call is considered uninteresting
  /// and the predicate will be called again for the next [Canvas] call that was
  /// made by the [RenderObject] under test. If this was the last call, then the
  /// [paints] [Matcher] is considered to have failed.
  ///
  /// If the predicate throws a [String], then the [paints] [Matcher] is
  /// considered to have failed. The thrown string is used in the message
  /// displayed from the test framework and should be complete sentence
  /// describing the problem.
  void something(PaintPatternPredicate predicate);
418 419 420 421 422 423 424 425 426 427 428

  /// Provides a custom matcher.
  ///
  /// Each method call after the last matched call (if any) will be passed to
  /// the given predicate, along with the values of its (positional) arguments.
  ///
  /// For each one, the predicate must either return a boolean or throw a [String].
  ///
  /// The predicate will be applied to each [Canvas] call until it returns false
  /// or all of the method calls have been tested.
  ///
429 430 431 432
  /// If the predicate returns false, then the [paints] [Matcher] is considered
  /// to have failed. If all calls are tested without failing, then the [paints]
  /// [Matcher] is considered a success.
  ///
433 434 435 436 437
  /// If the predicate throws a [String], then the [paints] [Matcher] is
  /// considered to have failed. The thrown string is used in the message
  /// displayed from the test framework and should be complete sentence
  /// describing the problem.
  void everything(PaintPatternPredicate predicate);
438 439
}

Ian Hickson's avatar
Ian Hickson committed
440 441 442
/// Matches a [Path] that contains (as defined by [Path.contains]) the given
/// `includes` points and does not contain the given `excludes` points.
Matcher isPathThat({
443 444
  Iterable<Offset> includes = const <Offset>[],
  Iterable<Offset> excludes = const <Offset>[],
Ian Hickson's avatar
Ian Hickson committed
445
}) {
446
  return _PathMatcher(includes.toList(), excludes.toList());
Ian Hickson's avatar
Ian Hickson committed
447 448 449 450 451 452 453 454 455
}

class _PathMatcher extends Matcher {
  _PathMatcher(this.includes, this.excludes);

  List<Offset> includes;
  List<Offset> excludes;

  @override
456
  bool matches(Object? object, Map<dynamic, dynamic> matchState) {
Ian Hickson's avatar
Ian Hickson committed
457 458 459 460
    if (object is! Path) {
      matchState[this] = 'The given object ($object) was not a Path.';
      return false;
    }
461
    final Path path = object;
462
    final List<String> errors = <String>[
463
      for (final Offset offset in includes)
464 465
        if (!path.contains(offset))
          'Offset $offset should be inside the path, but is not.',
466
      for (final Offset offset in excludes)
467 468 469
        if (path.contains(offset))
          'Offset $offset should be outside the path, but is not.',
    ];
470
    if (errors.isEmpty) {
Ian Hickson's avatar
Ian Hickson committed
471
      return true;
472
    }
Ian Hickson's avatar
Ian Hickson committed
473 474 475 476 477 478 479 480
    matchState[this] = 'Not all the given points were inside or outside the path as expected:\n  ${errors.join("\n  ")}';
    return false;
  }

  @override
  Description describe(Description description) {
    String points(List<Offset> list) {
      final int count = list.length;
481
      if (count == 1) {
Ian Hickson's avatar
Ian Hickson committed
482
        return 'one particular point';
483
      }
Ian Hickson's avatar
Ian Hickson committed
484 485 486 487 488 489 490 491 492 493 494 495
      return '$count particular points';
    }
    return description.add('A Path that contains ${points(includes)} but does not contain ${points(excludes)}.');
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description description,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
496
    return description.add(matchState[this] as String);
Ian Hickson's avatar
Ian Hickson committed
497 498 499
  }
}

500 501 502 503 504 505 506
class _MismatchedCall {
  const _MismatchedCall(this.message, this.callIntroduction, this.call) : assert(call != null);
  final String message;
  final String callIntroduction;
  final RecordedInvocation call;
}

507
bool _evaluatePainter(Object? object, Canvas canvas, PaintingContext context) {
Ian Hickson's avatar
Ian Hickson committed
508 509 510 511 512 513 514 515 516
  if (object is _ContextPainterFunction) {
    final _ContextPainterFunction function = object;
    function(context, Offset.zero);
  } else if (object is _CanvasPainterFunction) {
    final _CanvasPainterFunction function = object;
    function(canvas);
  } else {
    if (object is Finder) {
      TestAsyncUtils.guardSync();
517
      final Finder finder = object;
Ian Hickson's avatar
Ian Hickson committed
518 519 520 521 522 523 524 525 526 527 528 529
      object = finder.evaluate().single.renderObject;
    }
    if (object is RenderObject) {
      final RenderObject renderObject = object;
      renderObject.paint(context, Offset.zero);
    } else {
      return false;
    }
  }
  return true;
}

530 531
abstract class _TestRecordingCanvasMatcher extends Matcher {
  @override
532
  bool matches(Object? object, Map<dynamic, dynamic> matchState) {
533 534 535
    final TestRecordingCanvas canvas = TestRecordingCanvas();
    final TestRecordingPaintingContext context = TestRecordingPaintingContext(canvas);
    final StringBuffer description = StringBuffer();
536 537 538
    String prefixMessage = 'unexpectedly failed.';
    bool result = false;
    try {
Ian Hickson's avatar
Ian Hickson committed
539 540 541
      if (!_evaluatePainter(object, canvas, context)) {
        matchState[this] = 'was not one of the supported objects for the "paints" matcher.';
        return false;
542
      }
543
      result = _evaluatePredicates(canvas.invocations, description);
544
      if (!result) {
545
        prefixMessage = 'did not match the pattern.';
546
      }
547 548 549 550 551
    } catch (error, stack) {
      prefixMessage = 'threw the following exception:';
      description.writeln(error.toString());
      description.write(stack.toString());
      result = false;
552 553
    }
    if (!result) {
554
      if (canvas.invocations.isNotEmpty) {
555
        description.write('The complete display list was:');
556
        for (final RecordedInvocation call in canvas.invocations) {
557
          description.write('\n  * $call');
558
        }
559 560
      }
      matchState[this] = '$prefixMessage\n$description';
561 562 563 564
    }
    return result;
  }

565
  bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description);
566 567 568 569 570 571 572 573

  @override
  Description describeMismatch(
    dynamic item,
    Description description,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
574
    return description.add(matchState[this] as String);
575 576 577
  }
}

578 579
class _TestRecordingCanvasPaintsCountMatcher extends _TestRecordingCanvasMatcher {
  _TestRecordingCanvasPaintsCountMatcher(Symbol methodName, int count)
580 581
    : _methodName = methodName,
      _count = count;
582

583 584 585
  final Symbol _methodName;
  final int _count;

586 587 588 589 590 591
  @override
  Description describe(Description description) {
    return description.add('Object or closure painting $_methodName exactly $_count times');
  }

  @override
592
  bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) {
593
    int count = 0;
594
    for (final RecordedInvocation call in calls) {
595 596 597 598 599 600 601 602 603 604 605
      if (call.invocation.isMethod && call.invocation.memberName == _methodName) {
        count++;
      }
    }
    if (count != _count) {
      description.write('It painted $_methodName $count times instead of $_count times.');
    }
    return count == _count;
  }
}

606 607 608 609 610 611 612
class _TestRecordingCanvasPaintsNothingMatcher extends _TestRecordingCanvasMatcher {
  @override
  Description describe(Description description) {
    return description.add('An object or closure that paints nothing.');
  }

  @override
613
  bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) {
614
    final Iterable<RecordedInvocation> paintingCalls = _filterCanvasCalls(calls);
615
    if (paintingCalls.isEmpty) {
616
      return true;
617
    }
618 619
    description.write(
      'painted something, the first call having the following stack:\n'
620
      '${paintingCalls.first.stackToString(indent: "  ")}\n',
621
    );
622 623
    return false;
  }
624

625
  static const List<Symbol> _nonPaintingOperations = <Symbol> [
626 627 628 629 630 631 632
    #save,
    #restore,
  ];

  // Filters out canvas calls that are not painting anything.
  static Iterable<RecordedInvocation> _filterCanvasCalls(Iterable<RecordedInvocation> canvasCalls) {
    return canvasCalls.where((RecordedInvocation canvasCall) =>
633
      !_nonPaintingOperations.contains(canvasCall.invocation.memberName),
634 635
    );
  }
636 637
}

Ian Hickson's avatar
Ian Hickson committed
638 639
class _TestRecordingCanvasPaintsAssertionMatcher extends Matcher {
  @override
640
  bool matches(Object? object, Map<dynamic, dynamic> matchState) {
641 642 643
    final TestRecordingCanvas canvas = TestRecordingCanvas();
    final TestRecordingPaintingContext context = TestRecordingPaintingContext(canvas);
    final StringBuffer description = StringBuffer();
Ian Hickson's avatar
Ian Hickson committed
644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662
    String prefixMessage = 'unexpectedly failed.';
    bool result = false;
    try {
      if (!_evaluatePainter(object, canvas, context)) {
        matchState[this] = 'was not one of the supported objects for the "paints" matcher.';
        return false;
      }
      prefixMessage = 'did not assert.';
    } on AssertionError {
      result = true;
    } catch (error, stack) {
      prefixMessage = 'threw the following exception:';
      description.writeln(error.toString());
      description.write(stack.toString());
      result = false;
    }
    if (!result) {
      if (canvas.invocations.isNotEmpty) {
        description.write('The complete display list was:');
663
        for (final RecordedInvocation call in canvas.invocations) {
Ian Hickson's avatar
Ian Hickson committed
664
          description.write('\n  * $call');
665
        }
Ian Hickson's avatar
Ian Hickson committed
666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683
      }
      matchState[this] = '$prefixMessage\n$description';
    }
    return result;
  }

  @override
  Description describe(Description description) {
    return description.add('An object or closure that asserts when it tries to paint.');
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description description,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
684
    return description.add(matchState[this] as String);
Ian Hickson's avatar
Ian Hickson committed
685 686 687
  }
}

688
class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher implements PaintPattern {
689 690
  final List<_PaintPredicate> _predicates = <_PaintPredicate>[];

691 692
  @override
  void transform({ dynamic matrix4 }) {
693
    _predicates.add(_FunctionPaintPredicate(#transform, <dynamic>[matrix4]));
694 695
  }

696
  @override
697
  void translate({ double? x, double? y }) {
698
    _predicates.add(_FunctionPaintPredicate(#translate, <dynamic>[x, y]));
699 700 701
  }

  @override
702
  void scale({ double? x, double? y }) {
703
    _predicates.add(_FunctionPaintPredicate(#scale, <dynamic>[x, y]));
704 705 706
  }

  @override
707
  void rotate({ double? angle }) {
708
    _predicates.add(_FunctionPaintPredicate(#rotate, <dynamic>[angle]));
709 710 711 712
  }

  @override
  void save() {
713
    _predicates.add(_FunctionPaintPredicate(#save, <dynamic>[]));
714 715 716 717
  }

  @override
  void restore() {
718
    _predicates.add(_FunctionPaintPredicate(#restore, <dynamic>[]));
719 720 721 722
  }

  @override
  void saveRestore() {
723
    _predicates.add(_SaveRestorePairPaintPredicate());
724 725
  }

726
  @override
727
  void clipRect({ Rect? rect }) {
728
    _predicates.add(_FunctionPaintPredicate(#clipRect, <dynamic>[rect]));
729 730
  }

731
  @override
732
  void clipPath({ Matcher? pathMatcher }) {
733
    _predicates.add(_FunctionPaintPredicate(#clipPath, <dynamic>[pathMatcher]));
734 735
  }

736
  @override
737
  void rect({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
738
    _predicates.add(_RectPaintPredicate(rect: rect, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
739 740
  }

741
  @override
742
  void clipRRect({ RRect? rrect }) {
743
    _predicates.add(_FunctionPaintPredicate(#clipRRect, <dynamic>[rrect]));
744 745
  }

746
  @override
747
  void rrect({ RRect? rrect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
748
    _predicates.add(_RRectPaintPredicate(rrect: rrect, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
749 750
  }

751
  @override
752
  void drrect({ RRect? outer, RRect? inner, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
753
    _predicates.add(_DRRectPaintPredicate(outer: outer, inner: inner, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
754 755
  }

756
  @override
757
  void circle({ double? x, double? y, double? radius, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
758
    _predicates.add(_CirclePaintPredicate(x: x, y: y, radius: radius, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
759 760
  }

761
  @override
762
  void path({ Iterable<Offset>? includes, Iterable<Offset>? excludes, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
763
    _predicates.add(_PathPaintPredicate(includes: includes, excludes: excludes, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
764 765
  }

Ian Hickson's avatar
Ian Hickson committed
766
  @override
767
  void line({ Offset? p1, Offset? p2, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
768
    _predicates.add(_LinePaintPredicate(p1: p1, p2: p2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
Ian Hickson's avatar
Ian Hickson committed
769 770
  }

771
  @override
772
  void arc({ Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
773
    _predicates.add(_ArcPaintPredicate(color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
774 775
  }

776
  @override
777
  void paragraph({ ui.Paragraph? paragraph, dynamic offset }) {
778
    _predicates.add(_FunctionPaintPredicate(#drawParagraph, <dynamic>[paragraph, offset]));
779 780
  }

781
  @override
782
  void shadow({ Iterable<Offset>? includes, Iterable<Offset>? excludes, Color? color, double? elevation, bool? transparentOccluder }) {
783
    _predicates.add(_ShadowPredicate(includes: includes, excludes: excludes, color: color, elevation: elevation, transparentOccluder: transparentOccluder));
784 785
  }

786
  @override
787
  void image({ ui.Image? image, double? x, double? y, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
788
    _predicates.add(_DrawImagePaintPredicate(image: image, x: x, y: y, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
789 790
  }

Ian Hickson's avatar
Ian Hickson committed
791
  @override
792
  void drawImageRect({ ui.Image? image, Rect? source, Rect? destination, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
793
    _predicates.add(_DrawImageRectPaintPredicate(image: image, source: source, destination: destination, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
Ian Hickson's avatar
Ian Hickson committed
794 795
  }

796 797
  @override
  void something(PaintPatternPredicate predicate) {
798
    _predicates.add(_SomethingPaintPredicate(predicate));
799 800
  }

801 802
  @override
  void everything(PaintPatternPredicate predicate) {
803
    _predicates.add(_EverythingPaintPredicate(predicate));
804 805
  }

806 807
  @override
  Description describe(Description description) {
808
    if (_predicates.isEmpty) {
809
      return description.add('An object or closure and a paint pattern.');
810
    }
811
    description.add('Object or closure painting:\n');
812
    return description.addAll(
813
      '', '\n', '',
814
      _predicates.map<String>((_PaintPredicate predicate) => predicate.toString()),
815 816 817 818
    );
  }

  @override
819
  bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) {
820
    if (calls.isEmpty) {
821
      description.writeln('It painted nothing.');
822 823
      return false;
    }
824
    if (_predicates.isEmpty) {
825 826
      description.writeln(
        'It painted something, but you must now add a pattern to the paints matcher '
827
        'in the test to verify that it matches the important parts of the following.',
828 829 830
      );
      return false;
    }
831
    final Iterator<_PaintPredicate> predicate = _predicates.iterator;
832
    final Iterator<RecordedInvocation> call = calls.iterator..moveNext();
833 834 835 836 837
    try {
      while (predicate.moveNext()) {
        predicate.current.match(call);
      }
      // We allow painting more than expected.
838 839 840 841 842
    } on _MismatchedCall catch (data) {
      description.writeln(data.message);
      description.writeln(data.callIntroduction);
      description.writeln(data.call.stackToString(indent: '  '));
      return false;
843
    } on String catch (s) {
844
      description.writeln(s);
845 846 847 848 849
      try {
        description.write('The stack of the offending call was:\n${call.current.stackToString(indent: "  ")}\n');
      } on TypeError catch (_) {
        // All calls have been evaluated
      }
850 851 852 853 854 855 856
      return false;
    }
    return true;
  }
}

abstract class _PaintPredicate {
857
  void match(Iterator<RecordedInvocation> call);
858

859 860 861 862 863 864
  @protected
  void checkMethod(Iterator<RecordedInvocation> call, Symbol symbol) {
    int others = 0;
    final RecordedInvocation firstCall = call.current;
    while (!call.current.invocation.isMethod || call.current.invocation.memberName != symbol) {
      others += 1;
865
      if (!call.moveNext()) {
866
        throw _MismatchedCall(
867 868 869 870 871 872 873
          'It called $others other method${ others == 1 ? "" : "s" } on the canvas, '
          'the first of which was $firstCall, but did not '
          'call ${_symbolName(symbol)}() at the time where $this was expected.',
          'The first method that was called when the call to ${_symbolName(symbol)}() '
          'was expected, $firstCall, was called with the following stack:',
          firstCall,
        );
874
      }
875 876 877
    }
  }

878 879
  @override
  String toString() {
880
    throw FlutterError('$runtimeType does not implement toString.');
881 882 883 884 885
  }
}

abstract class _DrawCommandPaintPredicate extends _PaintPredicate {
  _DrawCommandPaintPredicate(
886 887 888 889 890 891 892 893 894
    this.symbol,
    this.name,
    this.argumentCount,
    this.paintArgumentIndex, {
    this.color,
    this.strokeWidth,
    this.hasMaskFilter,
    this.style,
  });
895 896 897 898 899

  final Symbol symbol;
  final String name;
  final int argumentCount;
  final int paintArgumentIndex;
900 901 902 903
  final Color? color;
  final double? strokeWidth;
  final bool? hasMaskFilter;
  final PaintingStyle? style;
904 905 906 907

  String get methodName => _symbolName(symbol);

  @override
908
  void match(Iterator<RecordedInvocation> call) {
909
    checkMethod(call, symbol);
910
    final int actualArgumentCount = call.current.invocation.positionalArguments.length;
911
    if (actualArgumentCount != argumentCount) {
912
      throw 'It called $methodName with $actualArgumentCount argument${actualArgumentCount == 1 ? "" : "s"}; expected $argumentCount.';
913
    }
914
    verifyArguments(call.current.invocation.positionalArguments);
915 916 917 918 919 920
    call.moveNext();
  }

  @protected
  @mustCallSuper
  void verifyArguments(List<dynamic> arguments) {
921
    final Paint paintArgument = arguments[paintArgumentIndex] as Paint;
922
    if (color != null && paintArgument.color != color) {
923
      throw 'It called $methodName with a paint whose color, ${paintArgument.color}, was not exactly the expected color ($color).';
924 925
    }
    if (strokeWidth != null && paintArgument.strokeWidth != strokeWidth) {
926
      throw 'It called $methodName with a paint whose strokeWidth, ${paintArgument.strokeWidth}, was not exactly the expected strokeWidth ($strokeWidth).';
927
    }
928
    if (hasMaskFilter != null && (paintArgument.maskFilter != null) != hasMaskFilter) {
929
      if (hasMaskFilter!) {
930
        throw 'It called $methodName with a paint that did not have a mask filter, despite expecting one.';
931
      } else {
932
        throw 'It called $methodName with a paint that did have a mask filter, despite not expecting one.';
933
      }
934
    }
935
    if (style != null && paintArgument.style != style) {
936
      throw 'It called $methodName with a paint whose style, ${paintArgument.style}, was not exactly the expected style ($style).';
937
    }
938 939 940 941
  }

  @override
  String toString() {
942
    final List<String> description = <String>[];
943 944
    debugFillDescription(description);
    String result = name;
945
    if (description.isNotEmpty) {
946
      result += ' with ${description.join(", ")}';
947
    }
948 949 950 951 952 953
    return result;
  }

  @protected
  @mustCallSuper
  void debugFillDescription(List<String> description) {
954
    if (color != null) {
955
      description.add('$color');
956 957
    }
    if (strokeWidth != null) {
958
      description.add('strokeWidth: $strokeWidth');
959 960
    }
    if (hasMaskFilter != null) {
961
      description.add(hasMaskFilter! ? 'a mask filter' : 'no mask filter');
962 963
    }
    if (style != null) {
964
      description.add('$style');
965
    }
966 967 968
  }
}

969
class _OneParameterPaintPredicate<T> extends _DrawCommandPaintPredicate {
970 971 972
  _OneParameterPaintPredicate(
    Symbol symbol,
    String name, {
973 974 975 976 977
    required this.expected,
    required Color? color,
    required double? strokeWidth,
    required bool? hasMaskFilter,
    required PaintingStyle? style,
978 979 980 981 982 983 984 985 986 987
  })  : super(
          symbol,
          name,
          2,
          1,
          color: color,
          strokeWidth: strokeWidth,
          hasMaskFilter: hasMaskFilter,
          style: style,
        );
988

989
  final T? expected;
990 991 992 993

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
994
    final T actual = arguments[0] as T;
995
    if (expected != null && actual != expected) {
996
      throw 'It called $methodName with $T, $actual, which was not exactly the expected $T ($expected).';
997
    }
998 999 1000 1001 1002
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
1003 1004 1005 1006 1007 1008 1009
    if (expected != null) {
      if (expected.toString().contains(T.toString())) {
        description.add('$expected');
      } else {
        description.add('$T: $expected');
      }
    }
1010 1011 1012
  }
}

1013
class _TwoParameterPaintPredicate<T1, T2> extends _DrawCommandPaintPredicate {
1014 1015 1016
  _TwoParameterPaintPredicate(
    Symbol symbol,
    String name, {
1017 1018 1019 1020 1021 1022
    required this.expected1,
    required this.expected2,
    required Color? color,
    required double? strokeWidth,
    required bool? hasMaskFilter,
    required PaintingStyle? style,
1023 1024 1025 1026 1027 1028 1029 1030 1031 1032
  })  : super(
          symbol,
          name,
          3,
          2,
          color: color,
          strokeWidth: strokeWidth,
          hasMaskFilter: hasMaskFilter,
          style: style,
        );
1033

1034
  final T1? expected1;
1035

1036
  final T2? expected2;
1037 1038 1039 1040

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
1041
    final T1 actual1 = arguments[0] as T1;
1042
    if (expected1 != null && actual1 != expected1) {
1043
      throw 'It called $methodName with its first argument (a $T1), $actual1, which was not exactly the expected $T1 ($expected1).';
1044
    }
1045
    final T2 actual2 = arguments[1] as T2;
1046
    if (expected2 != null && actual2 != expected2) {
1047
      throw 'It called $methodName with its second argument (a $T2), $actual2, which was not exactly the expected $T2 ($expected2).';
1048
    }
1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (expected1 != null) {
      if (expected1.toString().contains(T1.toString())) {
        description.add('$expected1');
      } else {
        description.add('$T1: $expected1');
      }
    }
    if (expected2 != null) {
      if (expected2.toString().contains(T2.toString())) {
        description.add('$expected2');
      } else {
        description.add('$T2: $expected2');
      }
    }
  }
}
1070 1071

class _RectPaintPredicate extends _OneParameterPaintPredicate<Rect> {
1072
  _RectPaintPredicate({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1073 1074 1075 1076
    #drawRect,
    'a rectangle',
    expected: rect,
    color: color,
1077
    strokeWidth: strokeWidth,
1078 1079 1080 1081 1082
    hasMaskFilter: hasMaskFilter,
    style: style,
  );
}

1083
class _RRectPaintPredicate extends _DrawCommandPaintPredicate {
1084
  _RRectPaintPredicate({ this.rrect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1085 1086
    #drawRRect,
    'a rounded rectangle',
1087 1088
    2,
    1,
1089
    color: color,
1090
    strokeWidth: strokeWidth,
1091
    hasMaskFilter: hasMaskFilter,
1092
    style: style,
1093
  );
1094

1095
  final RRect? rrect;
1096 1097 1098 1099 1100

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
    const double eps = .0001;
1101
    final RRect actual = arguments[0] as RRect;
1102
    if (rrect != null &&
1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114
       ((actual.left - rrect!.left).abs() > eps ||
        (actual.right - rrect!.right).abs() > eps ||
        (actual.top - rrect!.top).abs() > eps ||
        (actual.bottom - rrect!.bottom).abs() > eps ||
        (actual.blRadiusX - rrect!.blRadiusX).abs() > eps ||
        (actual.blRadiusY - rrect!.blRadiusY).abs() > eps ||
        (actual.brRadiusX - rrect!.brRadiusX).abs() > eps ||
        (actual.brRadiusY - rrect!.brRadiusY).abs() > eps ||
        (actual.tlRadiusX - rrect!.tlRadiusX).abs() > eps ||
        (actual.tlRadiusY - rrect!.tlRadiusY).abs() > eps ||
        (actual.trRadiusX - rrect!.trRadiusX).abs() > eps ||
        (actual.trRadiusY - rrect!.trRadiusY).abs() > eps)) {
1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125
      throw 'It called $methodName with RRect, $actual, which was not exactly the expected RRect ($rrect).';
    }
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (rrect != null) {
      description.add('RRect: $rrect');
    }
  }
1126 1127
}

1128
class _DRRectPaintPredicate extends _TwoParameterPaintPredicate<RRect, RRect> {
1129
  _DRRectPaintPredicate({ RRect? inner, RRect? outer, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140
    #drawDRRect,
    'a rounded rectangle outline',
    expected1: outer,
    expected2: inner,
    color: color,
    strokeWidth: strokeWidth,
    hasMaskFilter: hasMaskFilter,
    style: style,
  );
}

1141
class _CirclePaintPredicate extends _DrawCommandPaintPredicate {
1142
  _CirclePaintPredicate({ this.x, this.y, this.radius, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1143
    #drawCircle, 'a circle', 3, 2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style,
1144 1145
  );

1146 1147 1148
  final double? x;
  final double? y;
  final double? radius;
1149 1150 1151 1152

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
1153
    final Offset pointArgument = arguments[0] as Offset;
1154
    if (x != null && y != null) {
1155
      final Offset point = Offset(x!, y!);
1156
      if (point != pointArgument) {
1157
        throw 'It called $methodName with a center coordinate, $pointArgument, which was not exactly the expected coordinate ($point).';
1158
      }
1159
    } else {
1160
      if (x != null && pointArgument.dx != x) {
1161
        throw 'It called $methodName with a center coordinate, $pointArgument, whose x-coordinate not exactly the expected coordinate (${x!.toStringAsFixed(1)}).';
1162 1163
      }
      if (y != null && pointArgument.dy != y) {
1164
        throw 'It called $methodName with a center coordinate, $pointArgument, whose y-coordinate not exactly the expected coordinate (${y!.toStringAsFixed(1)}).';
1165
      }
1166
    }
1167
    final double radiusArgument = arguments[1] as double;
1168
    if (radius != null && radiusArgument != radius) {
1169
      throw 'It called $methodName with radius, ${radiusArgument.toStringAsFixed(1)}, which was not exactly the expected radius (${radius!.toStringAsFixed(1)}).';
1170
    }
1171 1172 1173 1174 1175 1176
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (x != null && y != null) {
1177
      description.add('point ${Offset(x!, y!)}');
1178
    } else {
1179
      if (x != null) {
1180
        description.add('x-coordinate ${x!.toStringAsFixed(1)}');
1181 1182
      }
      if (y != null) {
1183
        description.add('y-coordinate ${y!.toStringAsFixed(1)}');
1184
      }
1185
    }
1186
    if (radius != null) {
1187
      description.add('radius ${radius!.toStringAsFixed(1)}');
1188
    }
1189 1190 1191
  }
}

1192
class _PathPaintPredicate extends _DrawCommandPaintPredicate {
1193
  _PathPaintPredicate({ this.includes, this.excludes, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1194
    #drawPath, 'a path', 2, 1, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style,
1195
  );
Ian Hickson's avatar
Ian Hickson committed
1196

1197 1198
  final Iterable<Offset>? includes;
  final Iterable<Offset>? excludes;
Ian Hickson's avatar
Ian Hickson committed
1199 1200 1201 1202

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
1203
    final Path pathArgument = arguments[0] as Path;
Ian Hickson's avatar
Ian Hickson committed
1204
    if (includes != null) {
1205
      for (final Offset offset in includes!) {
1206
        if (!pathArgument.contains(offset)) {
1207
          throw 'It called $methodName with a path that unexpectedly did not contain $offset.';
1208
        }
Ian Hickson's avatar
Ian Hickson committed
1209 1210 1211
      }
    }
    if (excludes != null) {
1212
      for (final Offset offset in excludes!) {
1213
        if (pathArgument.contains(offset)) {
1214
          throw 'It called $methodName with a path that unexpectedly contained $offset.';
1215
        }
Ian Hickson's avatar
Ian Hickson committed
1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230
      }
    }
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (includes != null && excludes != null) {
      description.add('that contains $includes and does not contain $excludes');
    } else if (includes != null) {
      description.add('that contains $includes');
    } else if (excludes != null) {
      description.add('that does not contain $excludes');
    }
  }
1231 1232
}

1233
// TODO(ianh): add arguments to test the length, angle, that kind of thing
Ian Hickson's avatar
Ian Hickson committed
1234
class _LinePaintPredicate extends _DrawCommandPaintPredicate {
1235
  _LinePaintPredicate({ this.p1, this.p2, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1236
    #drawLine, 'a line', 3, 2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style,
Ian Hickson's avatar
Ian Hickson committed
1237
  );
1238

1239 1240
  final Offset? p1;
  final Offset? p2;
1241 1242 1243 1244

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments); // Checks the 3rd argument, a Paint
1245
    if (arguments.length != 3) {
1246
      throw 'It called $methodName with ${arguments.length} arguments; expected 3.';
1247
    }
1248 1249
    final Offset p1Argument = arguments[0] as Offset;
    final Offset p2Argument = arguments[1] as Offset;
1250
    if (p1 != null && p1Argument != p1) {
1251
      throw 'It called $methodName with p1 endpoint, $p1Argument, which was not exactly the expected endpoint ($p1).';
1252 1253
    }
    if (p2 != null && p2Argument != p2) {
1254
      throw 'It called $methodName with p2 endpoint, $p2Argument, which was not exactly the expected endpoint ($p2).';
1255 1256 1257 1258 1259 1260
    }
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
1261
    if (p1 != null) {
1262
      description.add('end point p1: $p1');
1263 1264
    }
    if (p2 != null) {
1265
      description.add('end point p2: $p2');
1266
    }
1267
  }
Ian Hickson's avatar
Ian Hickson committed
1268 1269
}

1270
class _ArcPaintPredicate extends _DrawCommandPaintPredicate {
1271
  _ArcPaintPredicate({ Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1272
    #drawArc, 'an arc', 5, 4, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style,
1273 1274 1275
  );
}

1276 1277 1278
class _ShadowPredicate extends _PaintPredicate {
  _ShadowPredicate({ this.includes, this.excludes, this.color, this.elevation, this.transparentOccluder });

1279 1280 1281 1282 1283
  final Iterable<Offset>? includes;
  final Iterable<Offset>? excludes;
  final Color? color;
  final double? elevation;
  final bool? transparentOccluder;
1284 1285 1286 1287 1288 1289

  static const Symbol symbol = #drawShadow;
  String get methodName => _symbolName(symbol);

  @protected
  void verifyArguments(List<dynamic> arguments) {
1290
    if (arguments.length != 4) {
1291
      throw 'It called $methodName with ${arguments.length} arguments; expected 4.';
1292
    }
1293
    final Path pathArgument = arguments[0] as Path;
1294
    if (includes != null) {
1295
      for (final Offset offset in includes!) {
1296
        if (!pathArgument.contains(offset)) {
1297
          throw 'It called $methodName with a path that unexpectedly did not contain $offset.';
1298
        }
1299 1300 1301
      }
    }
    if (excludes != null) {
1302
      for (final Offset offset in excludes!) {
1303
        if (pathArgument.contains(offset)) {
1304
          throw 'It called $methodName with a path that unexpectedly contained $offset.';
1305
        }
1306 1307
      }
    }
1308
    final Color actualColor = arguments[1] as Color;
1309
    if (color != null && actualColor != color) {
1310
      throw 'It called $methodName with a color, $actualColor, which was not exactly the expected color ($color).';
1311
    }
1312
    final double actualElevation = arguments[2] as double;
1313
    if (elevation != null && actualElevation != elevation) {
1314
      throw 'It called $methodName with an elevation, $actualElevation, which was not exactly the expected value ($elevation).';
1315
    }
1316
    final bool actualTransparentOccluder = arguments[3] as bool;
1317
    if (transparentOccluder != null && actualTransparentOccluder != transparentOccluder) {
1318
      throw 'It called $methodName with a transparentOccluder value, $actualTransparentOccluder, which was not exactly the expected value ($transparentOccluder).';
1319
    }
1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337
  }

  @override
  void match(Iterator<RecordedInvocation> call) {
    checkMethod(call, symbol);
    verifyArguments(call.current.invocation.positionalArguments);
    call.moveNext();
  }

  @protected
  void debugFillDescription(List<String> description) {
    if (includes != null && excludes != null) {
      description.add('that contains $includes and does not contain $excludes');
    } else if (includes != null) {
      description.add('that contains $includes');
    } else if (excludes != null) {
      description.add('that does not contain $excludes');
    }
1338
    if (color != null) {
1339
      description.add('$color');
1340 1341
    }
    if (elevation != null) {
1342
      description.add('elevation: $elevation');
1343 1344
    }
    if (transparentOccluder != null) {
1345
      description.add('transparentOccluder: $transparentOccluder');
1346
    }
1347 1348 1349 1350 1351 1352 1353
  }

  @override
  String toString() {
    final List<String> description = <String>[];
    debugFillDescription(description);
    String result = methodName;
1354
    if (description.isNotEmpty) {
1355
      result += ' with ${description.join(", ")}';
1356
    }
1357 1358 1359 1360
    return result;
  }
}

1361
class _DrawImagePaintPredicate extends _DrawCommandPaintPredicate {
1362
  _DrawImagePaintPredicate({ this.image, this.x, this.y, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1363
    #drawImage, 'an image', 3, 2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style,
1364 1365
  );

1366 1367 1368
  final ui.Image? image;
  final double? x;
  final double? y;
1369 1370 1371 1372

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
1373
    final ui.Image imageArgument = arguments[0] as ui.Image;
1374
    if (image != null && !image!.isCloneOf(imageArgument)) {
1375
      throw 'It called $methodName with an image, $imageArgument, which was not exactly the expected image ($image).';
1376
    }
1377
    final Offset pointArgument = arguments[0] as Offset;
1378
    if (x != null && y != null) {
1379
      final Offset point = Offset(x!, y!);
1380
      if (point != pointArgument) {
1381
        throw 'It called $methodName with an offset coordinate, $pointArgument, which was not exactly the expected coordinate ($point).';
1382
      }
1383
    } else {
1384
      if (x != null && pointArgument.dx != x) {
1385
        throw 'It called $methodName with an offset coordinate, $pointArgument, whose x-coordinate not exactly the expected coordinate (${x!.toStringAsFixed(1)}).';
1386 1387
      }
      if (y != null && pointArgument.dy != y) {
1388
        throw 'It called $methodName with an offset coordinate, $pointArgument, whose y-coordinate not exactly the expected coordinate (${y!.toStringAsFixed(1)}).';
1389
      }
1390 1391 1392 1393 1394 1395
    }
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
1396
    if (image != null) {
1397
      description.add('image $image');
1398
    }
1399
    if (x != null && y != null) {
1400
      description.add('point ${Offset(x!, y!)}');
1401
    } else {
1402
      if (x != null) {
1403
        description.add('x-coordinate ${x!.toStringAsFixed(1)}');
1404 1405
      }
      if (y != null) {
1406
        description.add('y-coordinate ${y!.toStringAsFixed(1)}');
1407
      }
1408 1409 1410 1411
    }
  }
}

Ian Hickson's avatar
Ian Hickson committed
1412
class _DrawImageRectPaintPredicate extends _DrawCommandPaintPredicate {
1413
  _DrawImageRectPaintPredicate({ this.image, this.source, this.destination, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1414
    #drawImageRect, 'an image', 4, 3, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style,
Ian Hickson's avatar
Ian Hickson committed
1415 1416
  );

1417 1418 1419
  final ui.Image? image;
  final Rect? source;
  final Rect? destination;
Ian Hickson's avatar
Ian Hickson committed
1420 1421 1422 1423

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
1424
    final ui.Image imageArgument = arguments[0] as ui.Image;
1425
    if (image != null && !image!.isCloneOf(imageArgument)) {
Ian Hickson's avatar
Ian Hickson committed
1426
      throw 'It called $methodName with an image, $imageArgument, which was not exactly the expected image ($image).';
1427
    }
1428
    final Rect sourceArgument = arguments[1] as Rect;
1429
    if (source != null && sourceArgument != source) {
Ian Hickson's avatar
Ian Hickson committed
1430
      throw 'It called $methodName with a source rectangle, $sourceArgument, which was not exactly the expected rectangle ($source).';
1431
    }
1432
    final Rect destinationArgument = arguments[2] as Rect;
1433
    if (destination != null && destinationArgument != destination) {
Ian Hickson's avatar
Ian Hickson committed
1434
      throw 'It called $methodName with a destination rectangle, $destinationArgument, which was not exactly the expected rectangle ($destination).';
1435
    }
Ian Hickson's avatar
Ian Hickson committed
1436 1437 1438 1439 1440
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
1441
    if (image != null) {
Ian Hickson's avatar
Ian Hickson committed
1442
      description.add('image $image');
1443 1444
    }
    if (source != null) {
Ian Hickson's avatar
Ian Hickson committed
1445
      description.add('source $source');
1446 1447
    }
    if (destination != null) {
Ian Hickson's avatar
Ian Hickson committed
1448
      description.add('destination $destination');
1449
    }
Ian Hickson's avatar
Ian Hickson committed
1450 1451 1452
  }
}

1453 1454 1455 1456 1457 1458
class _SomethingPaintPredicate extends _PaintPredicate {
  _SomethingPaintPredicate(this.predicate);

  final PaintPatternPredicate predicate;

  @override
1459 1460
  void match(Iterator<RecordedInvocation> call) {
    RecordedInvocation currentCall;
1461
    bool testedAllCalls = false;
1462
    do {
1463 1464 1465 1466
      if (testedAllCalls) {
        throw 'It painted methods that the predicate passed to a "something" step, '
              'in the paint pattern, none of which were considered correct.';
      }
1467
      currentCall = call.current;
1468
      if (!currentCall.invocation.isMethod) {
1469
        throw 'It called $currentCall, which was not a method, when the paint pattern expected a method call';
1470
      }
1471 1472
      testedAllCalls = !call.moveNext();
    } while (!_runPredicate(currentCall.invocation.memberName, currentCall.invocation.positionalArguments));
1473 1474 1475 1476 1477 1478
  }

  bool _runPredicate(Symbol methodName, List<dynamic> arguments) {
    try {
      return predicate(methodName, arguments);
    } on String catch (s) {
1479
      throw 'It painted something that the predicate passed to a "something" step '
1480 1481 1482 1483 1484 1485 1486 1487
            'in the paint pattern considered incorrect:\n      $s\n  ';
    }
  }

  @override
  String toString() => 'a "something" step';
}

1488 1489 1490 1491 1492 1493 1494
class _EverythingPaintPredicate extends _PaintPredicate {
  _EverythingPaintPredicate(this.predicate);

  final PaintPatternPredicate predicate;

  @override
  void match(Iterator<RecordedInvocation> call) {
1495
    do {
1496
      final RecordedInvocation currentCall = call.current;
1497
      if (!currentCall.invocation.isMethod) {
1498
        throw 'It called $currentCall, which was not a method, when the paint pattern expected a method call';
1499 1500
      }
      if (!_runPredicate(currentCall.invocation.memberName, currentCall.invocation.positionalArguments)) {
1501 1502
        throw 'It painted something that the predicate passed to an "everything" step '
              'in the paint pattern considered incorrect.\n';
1503
      }
1504
    } while (call.moveNext());
1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519
  }

  bool _runPredicate(Symbol methodName, List<dynamic> arguments) {
    try {
      return predicate(methodName, arguments);
    } on String catch (s) {
      throw 'It painted something that the predicate passed to an "everything" step '
            'in the paint pattern considered incorrect:\n      $s\n  ';
    }
  }

  @override
  String toString() => 'an "everything" step';
}

1520 1521 1522 1523 1524 1525 1526 1527
class _FunctionPaintPredicate extends _PaintPredicate {
  _FunctionPaintPredicate(this.symbol, this.arguments);

  final Symbol symbol;

  final List<dynamic> arguments;

  @override
1528
  void match(Iterator<RecordedInvocation> call) {
1529
    checkMethod(call, symbol);
1530
    if (call.current.invocation.positionalArguments.length != arguments.length) {
1531
      throw 'It called ${_symbolName(symbol)} with ${call.current.invocation.positionalArguments.length} arguments; expected ${arguments.length}.';
1532
    }
1533
    for (int index = 0; index < arguments.length; index += 1) {
1534
      final dynamic actualArgument = call.current.invocation.positionalArguments[index];
1535
      final dynamic desiredArgument = arguments[index];
1536 1537 1538 1539

      if (desiredArgument is Matcher) {
        expect(actualArgument, desiredArgument);
      } else if (desiredArgument != null && desiredArgument != actualArgument) {
1540
        throw 'It called ${_symbolName(symbol)} with argument $index having value ${_valueName(actualArgument)} when ${_valueName(desiredArgument)} was expected.';
1541
      }
1542 1543 1544 1545 1546 1547
    }
    call.moveNext();
  }

  @override
  String toString() {
1548 1549 1550 1551
    final List<String> adjectives = <String>[
      for (int index = 0; index < arguments.length; index += 1)
        arguments[index] != null ? _valueName(arguments[index]) : '...',
    ];
1552 1553 1554 1555 1556 1557
    return '${_symbolName(symbol)}(${adjectives.join(", ")})';
  }
}

class _SaveRestorePairPaintPredicate extends _PaintPredicate {
  @override
1558
  void match(Iterator<RecordedInvocation> call) {
1559
    checkMethod(call, #save);
1560 1561
    int depth = 1;
    while (depth > 0) {
1562
      if (!call.moveNext()) {
1563
        throw 'It did not have a matching restore() for the save() that was found where $this was expected.';
1564
      }
1565
      if (call.current.invocation.isMethod) {
1566
        if (call.current.invocation.memberName == #save) {
1567
          depth += 1;
1568
        } else if (call.current.invocation.memberName == #restore) {
1569
          depth -= 1;
1570
        }
1571 1572 1573 1574 1575 1576 1577 1578 1579
      }
    }
    call.moveNext();
  }

  @override
  String toString() => 'a matching save/restore pair';
}

1580
String _valueName(Object? value) {
1581
  if (value is double) {
1582
    return value.toStringAsFixed(1);
1583
  }
1584 1585 1586 1587 1588 1589 1590 1591 1592 1593
  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);
}