mock_canvas.dart 57.9 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.

Ian Hickson's avatar
Ian Hickson committed
5
import 'dart:ui' as ui show Paragraph, Image;
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 429 430 431 432 433

  /// 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.
  ///
  /// 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);
434 435
}

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

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

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

  @override
452
  bool matches(Object? object, Map<dynamic, dynamic> matchState) {
Ian Hickson's avatar
Ian Hickson committed
453 454 455 456
    if (object is! Path) {
      matchState[this] = 'The given object ($object) was not a Path.';
      return false;
    }
457
    final Path path = object;
458
    final List<String> errors = <String>[
459
      for (final Offset offset in includes)
460 461
        if (!path.contains(offset))
          'Offset $offset should be inside the path, but is not.',
462
      for (final Offset offset in excludes)
463 464 465
        if (path.contains(offset))
          'Offset $offset should be outside the path, but is not.',
    ];
Ian Hickson's avatar
Ian Hickson committed
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489
    if (errors.isEmpty)
      return true;
    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;
      if (count == 1)
        return 'one particular point';
      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,
  ) {
490
    return description.add(matchState[this] as String);
Ian Hickson's avatar
Ian Hickson committed
491 492 493
  }
}

494 495 496 497 498 499 500
class _MismatchedCall {
  const _MismatchedCall(this.message, this.callIntroduction, this.call) : assert(call != null);
  final String message;
  final String callIntroduction;
  final RecordedInvocation call;
}

501
bool _evaluatePainter(Object? object, Canvas canvas, PaintingContext context) {
Ian Hickson's avatar
Ian Hickson committed
502 503 504 505 506 507 508 509 510
  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();
511
      final Finder finder = object;
Ian Hickson's avatar
Ian Hickson committed
512 513 514 515 516 517 518 519 520 521 522 523
      object = finder.evaluate().single.renderObject;
    }
    if (object is RenderObject) {
      final RenderObject renderObject = object;
      renderObject.paint(context, Offset.zero);
    } else {
      return false;
    }
  }
  return true;
}

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

557
  bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description);
558 559 560 561 562 563 564 565

  @override
  Description describeMismatch(
    dynamic item,
    Description description,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
566
    return description.add(matchState[this] as String);
567 568 569
  }
}

570 571
class _TestRecordingCanvasPaintsCountMatcher extends _TestRecordingCanvasMatcher {
  _TestRecordingCanvasPaintsCountMatcher(Symbol methodName, int count)
572 573
    : _methodName = methodName,
      _count = count;
574

575 576 577
  final Symbol _methodName;
  final int _count;

578 579 580 581 582 583
  @override
  Description describe(Description description) {
    return description.add('Object or closure painting $_methodName exactly $_count times');
  }

  @override
584
  bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) {
585
    int count = 0;
586
    for (final RecordedInvocation call in calls) {
587 588 589 590 591 592 593 594 595 596 597
      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;
  }
}

598 599 600 601 602 603 604
class _TestRecordingCanvasPaintsNothingMatcher extends _TestRecordingCanvasMatcher {
  @override
  Description describe(Description description) {
    return description.add('An object or closure that paints nothing.');
  }

  @override
605
  bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) {
606 607
    final Iterable<RecordedInvocation> paintingCalls = _filterCanvasCalls(calls);
    if (paintingCalls.isEmpty)
608
      return true;
609 610
    description.write(
      'painted something, the first call having the following stack:\n'
611
      '${paintingCalls.first.stackToString(indent: "  ")}\n',
612
    );
613 614
    return false;
  }
615

616
  static const List<Symbol> _nonPaintingOperations = <Symbol> [
617 618 619 620 621 622 623
    #save,
    #restore,
  ];

  // Filters out canvas calls that are not painting anything.
  static Iterable<RecordedInvocation> _filterCanvasCalls(Iterable<RecordedInvocation> canvasCalls) {
    return canvasCalls.where((RecordedInvocation canvasCall) =>
624
      !_nonPaintingOperations.contains(canvasCall.invocation.memberName),
625 626
    );
  }
627 628
}

Ian Hickson's avatar
Ian Hickson committed
629 630
class _TestRecordingCanvasPaintsAssertionMatcher extends Matcher {
  @override
631
  bool matches(Object? object, Map<dynamic, dynamic> matchState) {
632 633 634
    final TestRecordingCanvas canvas = TestRecordingCanvas();
    final TestRecordingPaintingContext context = TestRecordingPaintingContext(canvas);
    final StringBuffer description = StringBuffer();
Ian Hickson's avatar
Ian Hickson committed
635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
    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:');
654
        for (final RecordedInvocation call in canvas.invocations)
Ian Hickson's avatar
Ian Hickson committed
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673
          description.write('\n  * $call');
      }
      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,
  ) {
674
    return description.add(matchState[this] as String);
Ian Hickson's avatar
Ian Hickson committed
675 676 677
  }
}

678
class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher implements PaintPattern {
679 680
  final List<_PaintPredicate> _predicates = <_PaintPredicate>[];

681 682
  @override
  void transform({ dynamic matrix4 }) {
683
    _predicates.add(_FunctionPaintPredicate(#transform, <dynamic>[matrix4]));
684 685
  }

686
  @override
687
  void translate({ double? x, double? y }) {
688
    _predicates.add(_FunctionPaintPredicate(#translate, <dynamic>[x, y]));
689 690 691
  }

  @override
692
  void scale({ double? x, double? y }) {
693
    _predicates.add(_FunctionPaintPredicate(#scale, <dynamic>[x, y]));
694 695 696
  }

  @override
697
  void rotate({ double? angle }) {
698
    _predicates.add(_FunctionPaintPredicate(#rotate, <dynamic>[angle]));
699 700 701 702
  }

  @override
  void save() {
703
    _predicates.add(_FunctionPaintPredicate(#save, <dynamic>[]));
704 705 706 707
  }

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

  @override
  void saveRestore() {
713
    _predicates.add(_SaveRestorePairPaintPredicate());
714 715
  }

716
  @override
717
  void clipRect({ Rect? rect }) {
718
    _predicates.add(_FunctionPaintPredicate(#clipRect, <dynamic>[rect]));
719 720
  }

721
  @override
722
  void clipPath({ Matcher? pathMatcher }) {
723
    _predicates.add(_FunctionPaintPredicate(#clipPath, <dynamic>[pathMatcher]));
724 725
  }

726
  @override
727
  void rect({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
728
    _predicates.add(_RectPaintPredicate(rect: rect, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
729 730
  }

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

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

741
  @override
742
  void drrect({ RRect? outer, RRect? inner, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
743
    _predicates.add(_DRRectPaintPredicate(outer: outer, inner: inner, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
744 745
  }

746
  @override
747
  void circle({ double? x, double? y, double? radius, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
748
    _predicates.add(_CirclePaintPredicate(x: x, y: y, radius: radius, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
749 750
  }

751
  @override
752
  void path({ Iterable<Offset>? includes, Iterable<Offset>? excludes, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
753
    _predicates.add(_PathPaintPredicate(includes: includes, excludes: excludes, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
754 755
  }

Ian Hickson's avatar
Ian Hickson committed
756
  @override
757
  void line({ Offset? p1, Offset? p2, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
758
    _predicates.add(_LinePaintPredicate(p1: p1, p2: p2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
Ian Hickson's avatar
Ian Hickson committed
759 760
  }

761
  @override
762
  void arc({ Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
763
    _predicates.add(_ArcPaintPredicate(color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
764 765
  }

766
  @override
767
  void paragraph({ ui.Paragraph? paragraph, dynamic offset }) {
768
    _predicates.add(_FunctionPaintPredicate(#drawParagraph, <dynamic>[paragraph, offset]));
769 770
  }

771
  @override
772
  void shadow({ Iterable<Offset>? includes, Iterable<Offset>? excludes, Color? color, double? elevation, bool? transparentOccluder }) {
773
    _predicates.add(_ShadowPredicate(includes: includes, excludes: excludes, color: color, elevation: elevation, transparentOccluder: transparentOccluder));
774 775
  }

776
  @override
777
  void image({ ui.Image? image, double? x, double? y, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
778
    _predicates.add(_DrawImagePaintPredicate(image: image, x: x, y: y, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
779 780
  }

Ian Hickson's avatar
Ian Hickson committed
781
  @override
782
  void drawImageRect({ ui.Image? image, Rect? source, Rect? destination, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
783
    _predicates.add(_DrawImageRectPaintPredicate(image: image, source: source, destination: destination, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
Ian Hickson's avatar
Ian Hickson committed
784 785
  }

786 787
  @override
  void something(PaintPatternPredicate predicate) {
788
    _predicates.add(_SomethingPaintPredicate(predicate));
789 790
  }

791 792
  @override
  void everything(PaintPatternPredicate predicate) {
793
    _predicates.add(_EverythingPaintPredicate(predicate));
794 795
  }

796 797
  @override
  Description describe(Description description) {
798 799
    if (_predicates.isEmpty)
      return description.add('An object or closure and a paint pattern.');
800
    description.add('Object or closure painting:\n');
801
    return description.addAll(
802
      '', '\n', '',
803
      _predicates.map<String>((_PaintPredicate predicate) => predicate.toString()),
804 805 806 807
    );
  }

  @override
808
  bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) {
809
    if (calls.isEmpty) {
810
      description.writeln('It painted nothing.');
811 812
      return false;
    }
813
    if (_predicates.isEmpty) {
814 815
      description.writeln(
        'It painted something, but you must now add a pattern to the paints matcher '
816
        'in the test to verify that it matches the important parts of the following.',
817 818 819
      );
      return false;
    }
820
    final Iterator<_PaintPredicate> predicate = _predicates.iterator;
821
    final Iterator<RecordedInvocation> call = calls.iterator..moveNext();
822 823 824 825 826
    try {
      while (predicate.moveNext()) {
        predicate.current.match(call);
      }
      // We allow painting more than expected.
827 828 829 830 831
    } on _MismatchedCall catch (data) {
      description.writeln(data.message);
      description.writeln(data.callIntroduction);
      description.writeln(data.call.stackToString(indent: '  '));
      return false;
832
    } on String catch (s) {
833
      description.writeln(s);
834
      description.write('The stack of the offending call was:\n${call.current.stackToString(indent: "  ")}\n');
835 836 837 838 839 840 841
      return false;
    }
    return true;
  }
}

abstract class _PaintPredicate {
842
  void match(Iterator<RecordedInvocation> call);
843

844 845 846 847 848 849 850
  @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;
      if (!call.moveNext())
851
        throw _MismatchedCall(
852 853 854 855 856 857 858 859 860 861
          '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,
        );
    }
  }

862 863
  @override
  String toString() {
864
    throw FlutterError('$runtimeType does not implement toString.');
865 866 867 868 869
  }
}

abstract class _DrawCommandPaintPredicate extends _PaintPredicate {
  _DrawCommandPaintPredicate(
870 871 872 873 874 875 876 877 878
    this.symbol,
    this.name,
    this.argumentCount,
    this.paintArgumentIndex, {
    this.color,
    this.strokeWidth,
    this.hasMaskFilter,
    this.style,
  });
879 880 881 882 883

  final Symbol symbol;
  final String name;
  final int argumentCount;
  final int paintArgumentIndex;
884 885 886 887
  final Color? color;
  final double? strokeWidth;
  final bool? hasMaskFilter;
  final PaintingStyle? style;
888 889 890 891

  String get methodName => _symbolName(symbol);

  @override
892
  void match(Iterator<RecordedInvocation> call) {
893
    checkMethod(call, symbol);
894
    final int actualArgumentCount = call.current.invocation.positionalArguments.length;
895
    if (actualArgumentCount != argumentCount)
896 897
      throw 'It called $methodName with $actualArgumentCount argument${actualArgumentCount == 1 ? "" : "s"}; expected $argumentCount.';
    verifyArguments(call.current.invocation.positionalArguments);
898 899 900 901 902 903
    call.moveNext();
  }

  @protected
  @mustCallSuper
  void verifyArguments(List<dynamic> arguments) {
904
    final Paint paintArgument = arguments[paintArgumentIndex] as Paint;
905
    if (color != null && paintArgument.color != color)
906
      throw 'It called $methodName with a paint whose color, ${paintArgument.color}, was not exactly the expected color ($color).';
907
    if (strokeWidth != null && paintArgument.strokeWidth != strokeWidth)
908
      throw 'It called $methodName with a paint whose strokeWidth, ${paintArgument.strokeWidth}, was not exactly the expected strokeWidth ($strokeWidth).';
909
    if (hasMaskFilter != null && (paintArgument.maskFilter != null) != hasMaskFilter) {
910
      if (hasMaskFilter!)
911
        throw 'It called $methodName with a paint that did not have a mask filter, despite expecting one.';
912
      else
913
        throw 'It called $methodName with a paint that did have a mask filter, despite not expecting one.';
914 915
    }
    if (style != null && paintArgument.style != style)
916
      throw 'It called $methodName with a paint whose style, ${paintArgument.style}, was not exactly the expected style ($style).';
917 918 919 920
  }

  @override
  String toString() {
921
    final List<String> description = <String>[];
922 923 924 925 926 927 928 929 930 931 932 933
    debugFillDescription(description);
    String result = name;
    if (description.isNotEmpty)
      result += ' with ${description.join(", ")}';
    return result;
  }

  @protected
  @mustCallSuper
  void debugFillDescription(List<String> description) {
    if (color != null)
      description.add('$color');
934 935
    if (strokeWidth != null)
      description.add('strokeWidth: $strokeWidth');
936
    if (hasMaskFilter != null)
937
      description.add(hasMaskFilter! ? 'a mask filter' : 'no mask filter');
938 939 940 941 942
    if (style != null)
      description.add('$style');
  }
}

943
class _OneParameterPaintPredicate<T> extends _DrawCommandPaintPredicate {
944 945 946
  _OneParameterPaintPredicate(
    Symbol symbol,
    String name, {
947 948 949 950 951
    required this.expected,
    required Color? color,
    required double? strokeWidth,
    required bool? hasMaskFilter,
    required PaintingStyle? style,
952 953 954 955 956 957 958 959 960 961
  })  : super(
          symbol,
          name,
          2,
          1,
          color: color,
          strokeWidth: strokeWidth,
          hasMaskFilter: hasMaskFilter,
          style: style,
        );
962

963
  final T? expected;
964 965 966 967

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
968
    final T actual = arguments[0] as T;
969
    if (expected != null && actual != expected)
970
      throw 'It called $methodName with $T, $actual, which was not exactly the expected $T ($expected).';
971 972 973 974 975
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
976 977 978 979 980 981 982
    if (expected != null) {
      if (expected.toString().contains(T.toString())) {
        description.add('$expected');
      } else {
        description.add('$T: $expected');
      }
    }
983 984 985
  }
}

986
class _TwoParameterPaintPredicate<T1, T2> extends _DrawCommandPaintPredicate {
987 988 989
  _TwoParameterPaintPredicate(
    Symbol symbol,
    String name, {
990 991 992 993 994 995
    required this.expected1,
    required this.expected2,
    required Color? color,
    required double? strokeWidth,
    required bool? hasMaskFilter,
    required PaintingStyle? style,
996 997 998 999 1000 1001 1002 1003 1004 1005
  })  : super(
          symbol,
          name,
          3,
          2,
          color: color,
          strokeWidth: strokeWidth,
          hasMaskFilter: hasMaskFilter,
          style: style,
        );
1006

1007
  final T1? expected1;
1008

1009
  final T2? expected2;
1010 1011 1012 1013

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
1014
    final T1 actual1 = arguments[0] as T1;
1015 1016
    if (expected1 != null && actual1 != expected1)
      throw 'It called $methodName with its first argument (a $T1), $actual1, which was not exactly the expected $T1 ($expected1).';
1017
    final T2 actual2 = arguments[1] as T2;
1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040
    if (expected2 != null && actual2 != expected2)
      throw 'It called $methodName with its second argument (a $T2), $actual2, which was not exactly the expected $T2 ($expected2).';
  }

  @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');
      }
    }
  }
}
1041 1042

class _RectPaintPredicate extends _OneParameterPaintPredicate<Rect> {
1043
  _RectPaintPredicate({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1044 1045 1046 1047
    #drawRect,
    'a rectangle',
    expected: rect,
    color: color,
1048
    strokeWidth: strokeWidth,
1049 1050 1051 1052 1053
    hasMaskFilter: hasMaskFilter,
    style: style,
  );
}

1054
class _RRectPaintPredicate extends _DrawCommandPaintPredicate {
1055
  _RRectPaintPredicate({ this.rrect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1056 1057
    #drawRRect,
    'a rounded rectangle',
1058 1059
    2,
    1,
1060
    color: color,
1061
    strokeWidth: strokeWidth,
1062
    hasMaskFilter: hasMaskFilter,
1063
    style: style,
1064
  );
1065

1066
  final RRect? rrect;
1067 1068 1069 1070 1071

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
    const double eps = .0001;
1072
    final RRect actual = arguments[0] as RRect;
1073
    if (rrect != null &&
1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085
       ((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)) {
1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096
      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');
    }
  }
1097 1098
}

1099
class _DRRectPaintPredicate extends _TwoParameterPaintPredicate<RRect, RRect> {
1100
  _DRRectPaintPredicate({ RRect? inner, RRect? outer, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111
    #drawDRRect,
    'a rounded rectangle outline',
    expected1: outer,
    expected2: inner,
    color: color,
    strokeWidth: strokeWidth,
    hasMaskFilter: hasMaskFilter,
    style: style,
  );
}

1112
class _CirclePaintPredicate extends _DrawCommandPaintPredicate {
1113
  _CirclePaintPredicate({ this.x, this.y, this.radius, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1114
    #drawCircle, 'a circle', 3, 2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style,
1115 1116
  );

1117 1118 1119
  final double? x;
  final double? y;
  final double? radius;
1120 1121 1122 1123

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
1124
    final Offset pointArgument = arguments[0] as Offset;
1125
    if (x != null && y != null) {
1126
      final Offset point = Offset(x!, y!);
1127
      if (point != pointArgument)
1128
        throw 'It called $methodName with a center coordinate, $pointArgument, which was not exactly the expected coordinate ($point).';
1129
    } else {
1130
      if (x != null && pointArgument.dx != x)
1131
        throw 'It called $methodName with a center coordinate, $pointArgument, whose x-coordinate not exactly the expected coordinate (${x!.toStringAsFixed(1)}).';
1132
      if (y != null && pointArgument.dy != y)
1133
        throw 'It called $methodName with a center coordinate, $pointArgument, whose y-coordinate not exactly the expected coordinate (${y!.toStringAsFixed(1)}).';
1134
    }
1135
    final double radiusArgument = arguments[1] as double;
1136
    if (radius != null && radiusArgument != radius)
1137
      throw 'It called $methodName with radius, ${radiusArgument.toStringAsFixed(1)}, which was not exactly the expected radius (${radius!.toStringAsFixed(1)}).';
1138 1139 1140 1141 1142 1143
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (x != null && y != null) {
1144
      description.add('point ${Offset(x!, y!)}');
1145 1146
    } else {
      if (x != null)
1147
        description.add('x-coordinate ${x!.toStringAsFixed(1)}');
1148
      if (y != null)
1149
        description.add('y-coordinate ${y!.toStringAsFixed(1)}');
1150 1151
    }
    if (radius != null)
1152
      description.add('radius ${radius!.toStringAsFixed(1)}');
1153 1154 1155
  }
}

1156
class _PathPaintPredicate extends _DrawCommandPaintPredicate {
1157
  _PathPaintPredicate({ this.includes, this.excludes, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1158
    #drawPath, 'a path', 2, 1, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style,
1159
  );
Ian Hickson's avatar
Ian Hickson committed
1160

1161 1162
  final Iterable<Offset>? includes;
  final Iterable<Offset>? excludes;
Ian Hickson's avatar
Ian Hickson committed
1163 1164 1165 1166

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
1167
    final Path pathArgument = arguments[0] as Path;
Ian Hickson's avatar
Ian Hickson committed
1168
    if (includes != null) {
1169
      for (final Offset offset in includes!) {
Ian Hickson's avatar
Ian Hickson committed
1170
        if (!pathArgument.contains(offset))
1171
          throw 'It called $methodName with a path that unexpectedly did not contain $offset.';
Ian Hickson's avatar
Ian Hickson committed
1172 1173 1174
      }
    }
    if (excludes != null) {
1175
      for (final Offset offset in excludes!) {
Ian Hickson's avatar
Ian Hickson committed
1176
        if (pathArgument.contains(offset))
1177
          throw 'It called $methodName with a path that unexpectedly contained $offset.';
Ian Hickson's avatar
Ian Hickson committed
1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192
      }
    }
  }

  @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');
    }
  }
1193 1194
}

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

1201 1202
  final Offset? p1;
  final Offset? p2;
1203 1204 1205 1206 1207 1208

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments); // Checks the 3rd argument, a Paint
    if (arguments.length != 3)
      throw 'It called $methodName with ${arguments.length} arguments; expected 3.';
1209 1210
    final Offset p1Argument = arguments[0] as Offset;
    final Offset p2Argument = arguments[1] as Offset;
1211
    if (p1 != null && p1Argument != p1) {
1212
      throw 'It called $methodName with p1 endpoint, $p1Argument, which was not exactly the expected endpoint ($p1).';
1213 1214
    }
    if (p2 != null && p2Argument != p2) {
1215
      throw 'It called $methodName with p2 endpoint, $p2Argument, which was not exactly the expected endpoint ($p2).';
1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226
    }
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (p1 != null)
      description.add('end point p1: $p1');
    if (p2 != null)
      description.add('end point p2: $p2');
  }
Ian Hickson's avatar
Ian Hickson committed
1227 1228
}

1229
class _ArcPaintPredicate extends _DrawCommandPaintPredicate {
1230
  _ArcPaintPredicate({ Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1231
    #drawArc, 'an arc', 5, 4, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style,
1232 1233 1234
  );
}

1235 1236 1237
class _ShadowPredicate extends _PaintPredicate {
  _ShadowPredicate({ this.includes, this.excludes, this.color, this.elevation, this.transparentOccluder });

1238 1239 1240 1241 1242
  final Iterable<Offset>? includes;
  final Iterable<Offset>? excludes;
  final Color? color;
  final double? elevation;
  final bool? transparentOccluder;
1243 1244 1245 1246 1247 1248 1249 1250

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

  @protected
  void verifyArguments(List<dynamic> arguments) {
    if (arguments.length != 4)
      throw 'It called $methodName with ${arguments.length} arguments; expected 4.';
1251
    final Path pathArgument = arguments[0] as Path;
1252
    if (includes != null) {
1253
      for (final Offset offset in includes!) {
1254 1255 1256 1257 1258
        if (!pathArgument.contains(offset))
          throw 'It called $methodName with a path that unexpectedly did not contain $offset.';
      }
    }
    if (excludes != null) {
1259
      for (final Offset offset in excludes!) {
1260 1261 1262 1263
        if (pathArgument.contains(offset))
          throw 'It called $methodName with a path that unexpectedly contained $offset.';
      }
    }
1264
    final Color actualColor = arguments[1] as Color;
1265 1266
    if (color != null && actualColor != color)
      throw 'It called $methodName with a color, $actualColor, which was not exactly the expected color ($color).';
1267
    final double actualElevation = arguments[2] as double;
1268 1269
    if (elevation != null && actualElevation != elevation)
      throw 'It called $methodName with an elevation, $actualElevation, which was not exactly the expected value ($elevation).';
1270
    final bool actualTransparentOccluder = arguments[3] as bool;
1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309
    if (transparentOccluder != null && actualTransparentOccluder != transparentOccluder)
      throw 'It called $methodName with a transparentOccluder value, $actualTransparentOccluder, which was not exactly the expected value ($transparentOccluder).';
  }

  @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');
    }
    if (color != null)
      description.add('$color');
    if (elevation != null)
      description.add('elevation: $elevation');
    if (transparentOccluder != null)
      description.add('transparentOccluder: $transparentOccluder');
  }

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

1310
class _DrawImagePaintPredicate extends _DrawCommandPaintPredicate {
1311
  _DrawImagePaintPredicate({ this.image, this.x, this.y, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1312
    #drawImage, 'an image', 3, 2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style,
1313 1314
  );

1315 1316 1317
  final ui.Image? image;
  final double? x;
  final double? y;
1318 1319 1320 1321

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
1322
    final ui.Image imageArgument = arguments[0] as ui.Image;
1323
    if (image != null && !image!.isCloneOf(imageArgument))
1324
      throw 'It called $methodName with an image, $imageArgument, which was not exactly the expected image ($image).';
1325
    final Offset pointArgument = arguments[0] as Offset;
1326
    if (x != null && y != null) {
1327
      final Offset point = Offset(x!, y!);
1328 1329 1330 1331
      if (point != pointArgument)
        throw 'It called $methodName with an offset coordinate, $pointArgument, which was not exactly the expected coordinate ($point).';
    } else {
      if (x != null && pointArgument.dx != x)
1332
        throw 'It called $methodName with an offset coordinate, $pointArgument, whose x-coordinate not exactly the expected coordinate (${x!.toStringAsFixed(1)}).';
1333
      if (y != null && pointArgument.dy != y)
1334
        throw 'It called $methodName with an offset coordinate, $pointArgument, whose y-coordinate not exactly the expected coordinate (${y!.toStringAsFixed(1)}).';
1335 1336 1337 1338 1339 1340 1341 1342 1343
    }
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (image != null)
      description.add('image $image');
    if (x != null && y != null) {
1344
      description.add('point ${Offset(x!, y!)}');
1345 1346
    } else {
      if (x != null)
1347
        description.add('x-coordinate ${x!.toStringAsFixed(1)}');
1348
      if (y != null)
1349
        description.add('y-coordinate ${y!.toStringAsFixed(1)}');
1350 1351 1352 1353
    }
  }
}

Ian Hickson's avatar
Ian Hickson committed
1354
class _DrawImageRectPaintPredicate extends _DrawCommandPaintPredicate {
1355
  _DrawImageRectPaintPredicate({ this.image, this.source, this.destination, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1356
    #drawImageRect, 'an image', 4, 3, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style,
Ian Hickson's avatar
Ian Hickson committed
1357 1358
  );

1359 1360 1361
  final ui.Image? image;
  final Rect? source;
  final Rect? destination;
Ian Hickson's avatar
Ian Hickson committed
1362 1363 1364 1365

  @override
  void verifyArguments(List<dynamic> arguments) {
    super.verifyArguments(arguments);
1366
    final ui.Image imageArgument = arguments[0] as ui.Image;
1367
    if (image != null && !image!.isCloneOf(imageArgument))
Ian Hickson's avatar
Ian Hickson committed
1368
      throw 'It called $methodName with an image, $imageArgument, which was not exactly the expected image ($image).';
1369
    final Rect sourceArgument = arguments[1] as Rect;
Ian Hickson's avatar
Ian Hickson committed
1370 1371
    if (source != null && sourceArgument != source)
      throw 'It called $methodName with a source rectangle, $sourceArgument, which was not exactly the expected rectangle ($source).';
1372
    final Rect destinationArgument = arguments[2] as Rect;
Ian Hickson's avatar
Ian Hickson committed
1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388
    if (destination != null && destinationArgument != destination)
      throw 'It called $methodName with a destination rectangle, $destinationArgument, which was not exactly the expected rectangle ($destination).';
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (image != null)
      description.add('image $image');
    if (source != null)
      description.add('source $source');
    if (destination != null)
      description.add('destination $destination');
  }
}

1389 1390 1391 1392 1393 1394
class _SomethingPaintPredicate extends _PaintPredicate {
  _SomethingPaintPredicate(this.predicate);

  final PaintPatternPredicate predicate;

  @override
1395
  void match(Iterator<RecordedInvocation> call) {
1396
    assert(predicate != null);
1397
    RecordedInvocation currentCall;
1398 1399
    do {
      currentCall = call.current;
1400 1401
      if (!currentCall.invocation.isMethod)
        throw 'It called $currentCall, which was not a method, when the paint pattern expected a method call';
1402
    } while (call.moveNext() && !_runPredicate(currentCall.invocation.memberName, currentCall.invocation.positionalArguments));
1403 1404 1405 1406 1407 1408
  }

  bool _runPredicate(Symbol methodName, List<dynamic> arguments) {
    try {
      return predicate(methodName, arguments);
    } on String catch (s) {
1409
      throw 'It painted something that the predicate passed to a "something" step '
1410 1411 1412 1413 1414 1415 1416 1417
            'in the paint pattern considered incorrect:\n      $s\n  ';
    }
  }

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

1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447
class _EverythingPaintPredicate extends _PaintPredicate {
  _EverythingPaintPredicate(this.predicate);

  final PaintPatternPredicate predicate;

  @override
  void match(Iterator<RecordedInvocation> call) {
    assert(predicate != null);
    while (call.moveNext()) {
      final RecordedInvocation currentCall = call.current;
      if (!currentCall.invocation.isMethod)
        throw 'It called $currentCall, which was not a method, when the paint pattern expected a method call';
      if (!_runPredicate(currentCall.invocation.memberName, currentCall.invocation.positionalArguments))
        return;
    }
  }

  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';
}

1448 1449 1450 1451 1452 1453 1454 1455
class _FunctionPaintPredicate extends _PaintPredicate {
  _FunctionPaintPredicate(this.symbol, this.arguments);

  final Symbol symbol;

  final List<dynamic> arguments;

  @override
1456
  void match(Iterator<RecordedInvocation> call) {
1457
    checkMethod(call, symbol);
1458 1459
    if (call.current.invocation.positionalArguments.length != arguments.length)
      throw 'It called ${_symbolName(symbol)} with ${call.current.invocation.positionalArguments.length} arguments; expected ${arguments.length}.';
1460
    for (int index = 0; index < arguments.length; index += 1) {
1461
      final dynamic actualArgument = call.current.invocation.positionalArguments[index];
1462
      final dynamic desiredArgument = arguments[index];
1463 1464 1465 1466

      if (desiredArgument is Matcher) {
        expect(actualArgument, desiredArgument);
      } else if (desiredArgument != null && desiredArgument != actualArgument) {
1467
        throw 'It called ${_symbolName(symbol)} with argument $index having value ${_valueName(actualArgument)} when ${_valueName(desiredArgument)} was expected.';
1468
      }
1469 1470 1471 1472 1473 1474
    }
    call.moveNext();
  }

  @override
  String toString() {
1475 1476 1477 1478
    final List<String> adjectives = <String>[
      for (int index = 0; index < arguments.length; index += 1)
        arguments[index] != null ? _valueName(arguments[index]) : '...',
    ];
1479 1480 1481 1482 1483 1484
    return '${_symbolName(symbol)}(${adjectives.join(", ")})';
  }
}

class _SaveRestorePairPaintPredicate extends _PaintPredicate {
  @override
1485
  void match(Iterator<RecordedInvocation> call) {
1486
    checkMethod(call, #save);
1487 1488 1489
    int depth = 1;
    while (depth > 0) {
      if (!call.moveNext())
1490 1491 1492
        throw 'It did not have a matching restore() for the save() that was found where $this was expected.';
      if (call.current.invocation.isMethod) {
        if (call.current.invocation.memberName == #save)
1493
          depth += 1;
1494
        else if (call.current.invocation.memberName == #restore)
1495 1496 1497 1498 1499 1500 1501 1502 1503 1504
          depth -= 1;
      }
    }
    call.moveNext();
  }

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

1505
String _valueName(Object? value) {
1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517
  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);
}