1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/material.dart';
6///
7/// @docImport 'matchers.dart';
8library;
9
10import 'dart:ui' as ui show Image, Paragraph;
11
12import 'package:flutter/foundation.dart';
13import 'package:flutter/widgets.dart';
14import 'package:matcher/expect.dart';
15
16import 'finders.dart';
17import 'recording_canvas.dart';
18import 'test_async_utils.dart';
19
20// Examples can assume:
21// late RenderObject myRenderObject;
22// late Symbol methodName;
23
24/// Matches objects or functions that paint a display list that matches the
25/// canvas calls described by the pattern.
26///
27/// Specifically, this can be applied to [RenderObject]s, [Finder]s that
28/// correspond to a single [RenderObject], and functions that have either of the
29/// following signatures:
30///
31/// ```dart
32/// void exampleOne(PaintingContext context, Offset offset) { }
33/// void exampleTwo(Canvas canvas) { }
34/// ```
35///
36/// In the case of functions that take a [PaintingContext] and an [Offset], the
37/// [paints] matcher will always pass a zero offset.
38///
39/// To specify the pattern, call the methods on the returned object. For example:
40///
41/// ```dart
42/// expect(myRenderObject, paints..circle(radius: 10.0)..circle(radius: 20.0));
43/// ```
44///
45/// This particular pattern would verify that the render object `myRenderObject`
46/// paints, among other things, two circles of radius 10.0 and 20.0 (in that
47/// order).
48///
49/// See [PaintPattern] for a discussion of the semantics of paint patterns.
50///
51/// To match something which paints nothing, see [paintsNothing].
52///
53/// To match something which asserts instead of painting, see [paintsAssertion].
54PaintPattern get paints => _TestRecordingCanvasPatternMatcher();
55
56/// Matches objects or functions that does not paint anything on the canvas.
57Matcher get paintsNothing => _TestRecordingCanvasPaintsNothingMatcher();
58
59/// Matches objects or functions that assert when they try to paint.
60Matcher get paintsAssertion => _TestRecordingCanvasPaintsAssertionMatcher();
61
62/// Matches objects or functions that draw `methodName` exactly `count` number
63/// of times.
64Matcher paintsExactlyCountTimes(Symbol methodName, int count) {
65 return _TestRecordingCanvasPaintsCountMatcher(methodName, count);
66}
67
68/// Signature for the [PaintPattern.something] and [PaintPattern.everything]
69/// predicate argument.
70///
71/// Used by the [paints] matcher.
72///
73/// The `methodName` argument is a [Symbol], and can be compared with the symbol
74/// literal syntax, for example:
75///
76/// ```dart
77/// if (methodName == #drawCircle) {
78/// // ...
79/// }
80/// ```
81typedef PaintPatternPredicate = bool Function(Symbol methodName, List<dynamic> arguments);
82
83/// The signature of [RenderObject.paint] functions.
84typedef _ContextPainterFunction = void Function(PaintingContext context, Offset offset);
85
86/// The signature of functions that paint directly on a canvas.
87typedef _CanvasPainterFunction = void Function(Canvas canvas);
88
89bool _colorsMatch(Color x, Color? y) {
90 if (y == null) {
91 return false;
92 } else {
93 const double limit = 1 / 255;
94 return x.colorSpace == y.colorSpace &&
95 (x.a - y.a).abs() < limit &&
96 (x.r - y.r).abs() < limit &&
97 (x.g - y.g).abs() < limit &&
98 (x.b - y.b).abs() < limit;
99 }
100}
101
102/// Builder interface for patterns used to match display lists (canvas calls).
103///
104/// The [paints] matcher returns a [PaintPattern] so that you can build the
105/// pattern in the [expect] call.
106///
107/// Patterns are subset matches, meaning that any calls not described by the
108/// pattern are ignored. This allows, for instance, transforms to be skipped.
109abstract class PaintPattern {
110 /// Indicates that a transform is expected next.
111 ///
112 /// Calls are skipped until a call to [Canvas.transform] is found. The call's
113 /// arguments are compared to those provided here. If any fail to match, or if
114 /// no call to [Canvas.transform] is found, then the matcher fails.
115 ///
116 /// Dynamic so matchers can be more easily passed in.
117 ///
118 /// The `matrix4` argument is dynamic so it can be either a [Matcher], or a
119 /// [Float64List] of [double]s. If it is a [Float64List] of [double]s then
120 /// each value in the matrix must match in the expected matrix. A deep
121 /// matching [Matcher] such as [equals] can be used to test each value in the
122 /// matrix with utilities such as [moreOrLessEquals].
123 void transform({dynamic matrix4});
124
125 /// Indicates that a translation transform is expected next.
126 ///
127 /// Calls are skipped until a call to [Canvas.translate] is found. The call's
128 /// arguments are compared to those provided here. If any fail to match, or if
129 /// no call to [Canvas.translate] is found, then the matcher fails.
130 void translate({double? x, double? y});
131
132 /// Indicates that a scale transform is expected next.
133 ///
134 /// Calls are skipped until a call to [Canvas.scale] is found. The call's
135 /// arguments are compared to those provided here. If any fail to match, or if
136 /// no call to [Canvas.scale] is found, then the matcher fails.
137 void scale({double? x, double? y});
138
139 /// Indicates that a rotate transform is expected next.
140 ///
141 /// Calls are skipped until a call to [Canvas.rotate] is found. If the `angle`
142 /// argument is provided here, the call's argument is compared to it. If that
143 /// fails to match, or if no call to [Canvas.rotate] is found, then the
144 /// matcher fails.
145 void rotate({double? angle});
146
147 /// Indicates that a save is expected next.
148 ///
149 /// Calls are skipped until a call to [Canvas.save] is found. If none is
150 /// found, the matcher fails.
151 ///
152 /// See also:
153 ///
154 /// * [restore], which indicates that a restore is expected next.
155 /// * [saveRestore], which indicates that a matching pair of save/restore
156 /// calls is expected next.
157 void save();
158
159 /// Indicates that a restore is expected next.
160 ///
161 /// Calls are skipped until a call to [Canvas.restore] is found. If none is
162 /// found, the matcher fails.
163 ///
164 /// See also:
165 ///
166 /// * [save], which indicates that a save is expected next.
167 /// * [saveRestore], which indicates that a matching pair of save/restore
168 /// calls is expected next.
169 void restore();
170
171 /// Indicates that a matching pair of save/restore calls is expected next.
172 ///
173 /// Calls are skipped until a call to [Canvas.save] is found, then, calls are
174 /// skipped until the matching [Canvas.restore] call is found. If no matching
175 /// pair of calls could be found, the matcher fails.
176 ///
177 /// See also:
178 ///
179 /// * [save], which indicates that a save is expected next.
180 /// * [restore], which indicates that a restore is expected next.
181 void saveRestore();
182
183 /// Indicates that a rectangular clip is expected next.
184 ///
185 /// The next rectangular clip is examined. Any arguments that are passed to
186 /// this method are compared to the actual [Canvas.clipRect] call's argument
187 /// and any mismatches result in failure.
188 ///
189 /// If no call to [Canvas.clipRect] was made, then this results in failure.
190 ///
191 /// Any calls made between the last matched call (if any) and the
192 /// [Canvas.clipRect] call are ignored.
193 void clipRect({Rect? rect});
194
195 /// Indicates that a path clip is expected next.
196 ///
197 /// The next path clip is examined.
198 /// The path that is passed to the actual [Canvas.clipPath] call is matched
199 /// using [pathMatcher].
200 ///
201 /// If no call to [Canvas.clipPath] was made, then this results in failure.
202 ///
203 /// Any calls made between the last matched call (if any) and the
204 /// [Canvas.clipPath] call are ignored.
205 void clipPath({Matcher? pathMatcher});
206
207 /// Indicates that a rectangle is expected next.
208 ///
209 /// The next rectangle is examined. Any arguments that are passed to this
210 /// method are compared to the actual [Canvas.drawRect] call's arguments
211 /// and any mismatches result in failure.
212 ///
213 /// If no call to [Canvas.drawRect] was made, then this results in failure.
214 ///
215 /// Any calls made between the last matched call (if any) and the
216 /// [Canvas.drawRect] call are ignored.
217 ///
218 /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
219 /// `style`) are compared against the state of the [Paint] object after the
220 /// painting has completed, not at the time of the call. If the same [Paint]
221 /// object is reused multiple times, then this may not match the actual
222 /// arguments as they were seen by the method.
223 void rect({
224 Rect? rect,
225 Color? color,
226 double? strokeWidth,
227 bool? hasMaskFilter,
228 PaintingStyle? style,
229 });
230
231 /// Indicates that a rounded rectangle clip is expected next.
232 ///
233 /// The next rounded rectangle clip is examined. Any arguments that are passed
234 /// to this method are compared to the actual [Canvas.clipRRect] call's
235 /// argument and any mismatches result in failure.
236 ///
237 /// If no call to [Canvas.clipRRect] was made, then this results in failure.
238 ///
239 /// Any calls made between the last matched call (if any) and the
240 /// [Canvas.clipRRect] call are ignored.
241 void clipRRect({RRect? rrect});
242
243 /// Indicates that a rounded rectangle is expected next.
244 ///
245 /// The next rounded rectangle is examined. Any arguments that are passed to
246 /// this method are compared to the actual [Canvas.drawRRect] call's arguments
247 /// and any mismatches result in failure.
248 ///
249 /// If no call to [Canvas.drawRRect] was made, then this results in failure.
250 ///
251 /// Any calls made between the last matched call (if any) and the
252 /// [Canvas.drawRRect] call are ignored.
253 ///
254 /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
255 /// `style`) are compared against the state of the [Paint] object after the
256 /// painting has completed, not at the time of the call. If the same [Paint]
257 /// object is reused multiple times, then this may not match the actual
258 /// arguments as they were seen by the method.
259 void rrect({
260 RRect? rrect,
261 Color? color,
262 double? strokeWidth,
263 bool? hasMaskFilter,
264 PaintingStyle? style,
265 });
266
267 /// Indicates that a rounded rectangle outline is expected next.
268 ///
269 /// The next call to [Canvas.drawRRect] is examined. Any arguments that are
270 /// passed to this method are compared to the actual [Canvas.drawRRect] call's
271 /// arguments and any mismatches result in failure.
272 ///
273 /// If no call to [Canvas.drawRRect] was made, then this results in failure.
274 ///
275 /// Any calls made between the last matched call (if any) and the
276 /// [Canvas.drawRRect] call are ignored.
277 ///
278 /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
279 /// `style`) are compared against the state of the [Paint] object after the
280 /// painting has completed, not at the time of the call. If the same [Paint]
281 /// object is reused multiple times, then this may not match the actual
282 /// arguments as they were seen by the method.
283 void drrect({
284 RRect? outer,
285 RRect? inner,
286 Color? color,
287 double strokeWidth,
288 bool hasMaskFilter,
289 PaintingStyle style,
290 });
291
292 /// Indicates that a rounded superellipse is expected next.
293 ///
294 /// The next rounded superellipse is examined. Any arguments that are passed
295 /// to this method are compared to the actual [Canvas.drawRSuperellipse]
296 /// call's arguments and any mismatches result in failure.
297 ///
298 /// If no call to [Canvas.drawRSuperellipse] was made, then this results in
299 /// failure.
300 ///
301 /// Any calls made between the last matched call (if any) and the
302 /// [Canvas.drawRSuperellipse] call are ignored.
303 ///
304 /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`)
305 /// are compared against the state of the [Paint] object after the
306 /// painting has completed, not at the time of the call. If the same [Paint]
307 /// object is reused multiple times, then this may not match the actual
308 /// arguments as they were seen by the method.
309 void rsuperellipse({
310 RSuperellipse? rsuperellipse,
311 Color? color,
312 double? strokeWidth,
313 bool? hasMaskFilter,
314 });
315
316 /// Indicates that a rounded superellipse clip is expected next.
317 ///
318 /// The next rounded superellipse clip is examined. Any arguments that are
319 /// passed to this method are compared to the actual
320 /// [Canvas.clipRSuperellipse] call's argument and any mismatches result in
321 /// failure.
322 ///
323 /// If no call to [Canvas.clipRSuperellipse] was made, then this results in failure.
324 ///
325 /// Any calls made between the last matched call (if any) and the
326 /// [Canvas.clipRSuperellipse] call are ignored.
327 void clipRSuperellipse({RSuperellipse? rsuperellipse});
328
329 /// Indicates that a circle is expected next.
330 ///
331 /// The next circle is examined. Any arguments that are passed to this method
332 /// are compared to the actual [Canvas.drawCircle] call's arguments and any
333 /// mismatches result in failure.
334 ///
335 /// If no call to [Canvas.drawCircle] was made, then this results in failure.
336 ///
337 /// Any calls made between the last matched call (if any) and the
338 /// [Canvas.drawCircle] call are ignored.
339 ///
340 /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
341 /// `style`) are compared against the state of the [Paint] object after the
342 /// painting has completed, not at the time of the call. If the same [Paint]
343 /// object is reused multiple times, then this may not match the actual
344 /// arguments as they were seen by the method.
345 void circle({
346 double? x,
347 double? y,
348 double? radius,
349 Color? color,
350 double? strokeWidth,
351 bool? hasMaskFilter,
352 PaintingStyle? style,
353 });
354
355 /// Indicates that a path is expected next.
356 ///
357 /// The next path is examined. Any arguments that are passed to this method
358 /// are compared to the actual [Canvas.drawPath] call's `paint` argument, and
359 /// any mismatches result in failure.
360 ///
361 /// To introspect the Path object (as it stands after the painting has
362 /// completed), the `includes` and `excludes` arguments can be provided to
363 /// specify points that should be considered inside or outside the path
364 /// (respectively).
365 ///
366 /// If no call to [Canvas.drawPath] was made, then this results in failure.
367 ///
368 /// Any calls made between the last matched call (if any) and the
369 /// [Canvas.drawPath] call are ignored.
370 ///
371 /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
372 /// `style`) are compared against the state of the [Paint] object after the
373 /// painting has completed, not at the time of the call. If the same [Paint]
374 /// object is reused multiple times, then this may not match the actual
375 /// arguments as they were seen by the method.
376 void path({
377 Iterable<Offset>? includes,
378 Iterable<Offset>? excludes,
379 Color? color,
380 double? strokeWidth,
381 bool? hasMaskFilter,
382 PaintingStyle? style,
383 });
384
385 /// Indicates that a line is expected next.
386 ///
387 /// The next line is examined. Any arguments that are passed to this method
388 /// are compared to the actual [Canvas.drawLine] call's `p1`, `p2`, and
389 /// `paint` arguments, and any mismatches result in failure.
390 ///
391 /// If no call to [Canvas.drawLine] was made, then this results in failure.
392 ///
393 /// Any calls made between the last matched call (if any) and the
394 /// [Canvas.drawLine] call are ignored.
395 ///
396 /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
397 /// `style`) are compared against the state of the [Paint] object after the
398 /// painting has completed, not at the time of the call. If the same [Paint]
399 /// object is reused multiple times, then this may not match the actual
400 /// arguments as they were seen by the method.
401 void line({
402 Offset? p1,
403 Offset? p2,
404 Color? color,
405 double? strokeWidth,
406 bool? hasMaskFilter,
407 PaintingStyle? style,
408 });
409
410 /// Indicates that an arc is expected next.
411 ///
412 /// The next arc is examined. Any arguments that are passed to this method
413 /// are compared to the actual [Canvas.drawArc] call's `paint` argument, and
414 /// any mismatches result in failure.
415 ///
416 /// If no call to [Canvas.drawArc] was made, then this results in failure.
417 ///
418 /// Any calls made between the last matched call (if any) and the
419 /// [Canvas.drawArc] call are ignored.
420 ///
421 /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
422 /// `style`) are compared against the state of the [Paint] object after the
423 /// painting has completed, not at the time of the call. If the same [Paint]
424 /// object is reused multiple times, then this may not match the actual
425 /// arguments as they were seen by the method.
426 void arc({
427 Rect? rect,
428 double? startAngle,
429 double? sweepAngle,
430 bool? useCenter,
431 Color? color,
432 double? strokeWidth,
433 bool? hasMaskFilter,
434 PaintingStyle? style,
435 StrokeCap? strokeCap,
436 });
437
438 /// Indicates that a paragraph is expected next.
439 ///
440 /// Calls are skipped until a call to [Canvas.drawParagraph] is found. Any
441 /// arguments that are passed to this method are compared to the actual
442 /// [Canvas.drawParagraph] call's argument, and any mismatches result in
443 /// failure.
444 ///
445 /// The `offset` argument can be either an [Offset] or a [Matcher]. If it is
446 /// an [Offset] then the actual value must match the expected offset
447 /// precisely. If it is a [Matcher] then the comparison is made according to
448 /// the semantics of the [Matcher]. For example, [within] can be used to
449 /// assert that the actual offset is within a given distance from the expected
450 /// offset.
451 ///
452 /// If no call to [Canvas.drawParagraph] was made, then this results in
453 /// failure.
454 void paragraph({ui.Paragraph? paragraph, dynamic offset});
455
456 /// Indicates that a shadow is expected next.
457 ///
458 /// The next shadow is examined. Any arguments that are passed to this method
459 /// are compared to the actual [Canvas.drawShadow] call's `paint` argument,
460 /// and any mismatches result in failure.
461 ///
462 /// In tests, shadows from framework features such as [BoxShadow] or
463 /// [Material] are disabled by default, and thus this predicate would not
464 /// match. The [debugDisableShadows] flag controls this.
465 ///
466 /// To introspect the Path object (as it stands after the painting has
467 /// completed), the `includes` and `excludes` arguments can be provided to
468 /// specify points that should be considered inside or outside the path
469 /// (respectively).
470 ///
471 /// If no call to [Canvas.drawShadow] was made, then this results in failure.
472 ///
473 /// Any calls made between the last matched call (if any) and the
474 /// [Canvas.drawShadow] call are ignored.
475 void shadow({
476 Iterable<Offset>? includes,
477 Iterable<Offset>? excludes,
478 Color? color,
479 double? elevation,
480 bool? transparentOccluder,
481 });
482
483 /// Indicates that an image is expected next.
484 ///
485 /// The next call to [Canvas.drawImage] is examined, and its arguments
486 /// compared to those passed to _this_ method.
487 ///
488 /// If no call to [Canvas.drawImage] was made, then this results in
489 /// failure.
490 ///
491 /// Any calls made between the last matched call (if any) and the
492 /// [Canvas.drawImage] call are ignored.
493 ///
494 /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
495 /// `style`) are compared against the state of the [Paint] object after the
496 /// painting has completed, not at the time of the call. If the same [Paint]
497 /// object is reused multiple times, then this may not match the actual
498 /// arguments as they were seen by the method.
499 void image({
500 ui.Image? image,
501 double? x,
502 double? y,
503 Color? color,
504 double? strokeWidth,
505 bool? hasMaskFilter,
506 PaintingStyle? style,
507 });
508
509 /// Indicates that an image subsection is expected next.
510 ///
511 /// The next call to [Canvas.drawImageRect] is examined, and its arguments
512 /// compared to those passed to _this_ method.
513 ///
514 /// If no call to [Canvas.drawImageRect] was made, then this results in
515 /// failure.
516 ///
517 /// Any calls made between the last matched call (if any) and the
518 /// [Canvas.drawImageRect] call are ignored.
519 ///
520 /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`,
521 /// `style`) are compared against the state of the [Paint] object after the
522 /// painting has completed, not at the time of the call. If the same [Paint]
523 /// object is reused multiple times, then this may not match the actual
524 /// arguments as they were seen by the method.
525 void drawImageRect({
526 ui.Image? image,
527 Rect? source,
528 Rect? destination,
529 Color? color,
530 double? strokeWidth,
531 bool? hasMaskFilter,
532 PaintingStyle? style,
533 });
534
535 /// Provides a custom matcher.
536 ///
537 /// Each method call after the last matched call (if any) will be passed to
538 /// the given predicate, along with the values of its (positional) arguments.
539 ///
540 /// For each one, the predicate must either return a boolean or throw a
541 /// [String].
542 ///
543 /// If the predicate returns true, the call is considered a successful match
544 /// and the next step in the pattern is examined. If this was the last step,
545 /// then any calls that were not yet matched are ignored and the [paints]
546 /// [Matcher] is considered a success.
547 ///
548 /// If the predicate returns false, then the call is considered uninteresting
549 /// and the predicate will be called again for the next [Canvas] call that was
550 /// made by the [RenderObject] under test. If this was the last call, then the
551 /// [paints] [Matcher] is considered to have failed.
552 ///
553 /// If the predicate throws a [String], then the [paints] [Matcher] is
554 /// considered to have failed. The thrown string is used in the message
555 /// displayed from the test framework and should be complete sentence
556 /// describing the problem.
557 void something(PaintPatternPredicate predicate);
558
559 /// Provides a custom matcher.
560 ///
561 /// Each method call after the last matched call (if any) will be passed to
562 /// the given predicate, along with the values of its (positional) arguments.
563 ///
564 /// For each one, the predicate must either return a boolean or throw a
565 /// [String].
566 ///
567 /// The predicate will be applied to each [Canvas] call until it returns false
568 /// or all of the method calls have been tested.
569 ///
570 /// If the predicate returns false, then the [paints] [Matcher] is considered
571 /// to have failed. If all calls are tested without failing, then the [paints]
572 /// [Matcher] is considered a success.
573 ///
574 /// If the predicate throws a [String], then the [paints] [Matcher] is
575 /// considered to have failed. The thrown string is used in the message
576 /// displayed from the test framework and should be complete sentence
577 /// describing the problem.
578 void everything(PaintPatternPredicate predicate);
579}
580
581/// Matches a [Path] that contains (as defined by [Path.contains]) the given
582/// `includes` points and does not contain the given `excludes` points.
583Matcher isPathThat({
584 Iterable<Offset> includes = const <Offset>[],
585 Iterable<Offset> excludes = const <Offset>[],
586}) {
587 return _PathMatcher(includes.toList(), excludes.toList());
588}
589
590class _PathMatcher extends Matcher {
591 _PathMatcher(this.includes, this.excludes);
592
593 List<Offset> includes;
594 List<Offset> excludes;
595
596 @override
597 bool matches(Object? object, Map<dynamic, dynamic> matchState) {
598 if (object is! Path) {
599 matchState[this] = 'The given object ($object) was not a Path.';
600 return false;
601 }
602 final Path path = object;
603 final List<String> errors = <String>[
604 for (final Offset offset in includes)
605 if (!path.contains(offset)) 'Offset $offset should be inside the path, but is not.',
606 for (final Offset offset in excludes)
607 if (path.contains(offset)) 'Offset $offset should be outside the path, but is not.',
608 ];
609 if (errors.isEmpty) {
610 return true;
611 }
612 matchState[this] =
613 'Not all the given points were inside or outside the '
614 'path as expected:\n ${errors.join("\n ")}';
615 return false;
616 }
617
618 @override
619 Description describe(Description description) {
620 String points(List<Offset> list) {
621 final int count = list.length;
622 if (count == 1) {
623 return 'one particular point';
624 }
625 return '$count particular points';
626 }
627
628 return description.add(
629 'A Path that contains ${points(includes)} but does '
630 'not contain ${points(excludes)}.',
631 );
632 }
633
634 @override
635 Description describeMismatch(
636 dynamic item,
637 Description description,
638 Map<dynamic, dynamic> matchState,
639 bool verbose,
640 ) {
641 return description.add(matchState[this] as String);
642 }
643}
644
645class _MismatchedCall extends Error {
646 _MismatchedCall(this.message, this.callIntroduction, this.call);
647 final String message;
648 final String callIntroduction;
649 final RecordedInvocation call;
650}
651
652bool _evaluatePainter(Object? object, Canvas canvas, PaintingContext context) {
653 switch (object) {
654 case final _ContextPainterFunction function:
655 function(context, Offset.zero);
656 case final _CanvasPainterFunction function:
657 function(canvas);
658 case final Finder finder:
659 TestAsyncUtils.guardSync();
660 final RenderObject? result = finder.evaluate().single.renderObject;
661 return (result?..paint(context, Offset.zero)) != null;
662 case final RenderObject renderObject:
663 renderObject.paint(context, Offset.zero);
664 default:
665 return false;
666 }
667 return true;
668}
669
670abstract class _TestRecordingCanvasMatcher extends Matcher {
671 @override
672 bool matches(Object? object, Map<dynamic, dynamic> matchState) {
673 final TestRecordingCanvas canvas = TestRecordingCanvas();
674 final TestRecordingPaintingContext context = TestRecordingPaintingContext(canvas);
675 final StringBuffer description = StringBuffer();
676 String prefixMessage = 'unexpectedly failed.';
677 bool result = false;
678 try {
679 if (!_evaluatePainter(object, canvas, context)) {
680 matchState[this] =
681 'was not one of the supported objects for the '
682 '"paints" matcher.';
683 return false;
684 }
685 result = _evaluatePredicates(canvas.invocations, description);
686 if (!result) {
687 prefixMessage = 'did not match the pattern.';
688 }
689 } catch (error, stack) {
690 prefixMessage = 'threw the following exception:';
691 description.writeln(error.toString());
692 description.write(stack.toString());
693 result = false;
694 }
695 if (!result) {
696 if (canvas.invocations.isNotEmpty) {
697 description.write('The complete display list was:');
698 for (final RecordedInvocation call in canvas.invocations) {
699 description.write('\n * $call');
700 }
701 }
702 matchState[this] = '$prefixMessage\n$description';
703 }
704 return result;
705 }
706
707 bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description);
708
709 @override
710 Description describeMismatch(
711 dynamic item,
712 Description description,
713 Map<dynamic, dynamic> matchState,
714 bool verbose,
715 ) {
716 return description.add(matchState[this] as String);
717 }
718}
719
720class _TestRecordingCanvasPaintsCountMatcher extends _TestRecordingCanvasMatcher {
721 _TestRecordingCanvasPaintsCountMatcher(Symbol methodName, int count)
722 : _methodName = methodName,
723 _count = count;
724
725 final Symbol _methodName;
726 final int _count;
727
728 @override
729 Description describe(Description description) {
730 return description.add('Object or closure painting $_methodName exactly $_count times');
731 }
732
733 @override
734 bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) {
735 int count = 0;
736 for (final RecordedInvocation call in calls) {
737 if (call.invocation.isMethod && call.invocation.memberName == _methodName) {
738 count++;
739 }
740 }
741 if (count != _count) {
742 description.write('It painted $_methodName $count times instead of $_count times.');
743 }
744 return count == _count;
745 }
746}
747
748class _TestRecordingCanvasPaintsNothingMatcher extends _TestRecordingCanvasMatcher {
749 @override
750 Description describe(Description description) {
751 return description.add('An object or closure that paints nothing.');
752 }
753
754 @override
755 bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) {
756 final Iterable<RecordedInvocation> paintingCalls = _filterCanvasCalls(calls);
757 if (paintingCalls.isEmpty) {
758 return true;
759 }
760 description.write(
761 'painted something, the first call having the following stack:\n'
762 '${paintingCalls.first.stackToString(indent: " ")}\n',
763 );
764 return false;
765 }
766
767 static const List<Symbol> _nonPaintingOperations = <Symbol>[#save, #restore];
768
769 // Filters out canvas calls that are not painting anything.
770 static Iterable<RecordedInvocation> _filterCanvasCalls(Iterable<RecordedInvocation> canvasCalls) {
771 return canvasCalls.where(
772 (RecordedInvocation canvasCall) =>
773 !_nonPaintingOperations.contains(canvasCall.invocation.memberName),
774 );
775 }
776}
777
778class _TestRecordingCanvasPaintsAssertionMatcher extends Matcher {
779 @override
780 bool matches(Object? object, Map<dynamic, dynamic> matchState) {
781 final TestRecordingCanvas canvas = TestRecordingCanvas();
782 final TestRecordingPaintingContext context = TestRecordingPaintingContext(canvas);
783 final StringBuffer description = StringBuffer();
784 String prefixMessage = 'unexpectedly failed.';
785 bool result = false;
786 try {
787 if (!_evaluatePainter(object, canvas, context)) {
788 matchState[this] =
789 'was not one of the supported objects for the '
790 '"paints" matcher.';
791 return false;
792 }
793 prefixMessage = 'did not assert.';
794 } on AssertionError {
795 result = true;
796 } catch (error, stack) {
797 prefixMessage = 'threw the following exception:';
798 description.writeln(error.toString());
799 description.write(stack.toString());
800 result = false;
801 }
802 if (!result) {
803 if (canvas.invocations.isNotEmpty) {
804 description.write('The complete display list was:');
805 for (final RecordedInvocation call in canvas.invocations) {
806 description.write('\n * $call');
807 }
808 }
809 matchState[this] = '$prefixMessage\n$description';
810 }
811 return result;
812 }
813
814 @override
815 Description describe(Description description) {
816 return description.add('An object or closure that asserts when it tries to paint.');
817 }
818
819 @override
820 Description describeMismatch(
821 dynamic item,
822 Description description,
823 Map<dynamic, dynamic> matchState,
824 bool verbose,
825 ) {
826 return description.add(matchState[this] as String);
827 }
828}
829
830class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher
831 implements PaintPattern {
832 final List<_PaintPredicate> _predicates = <_PaintPredicate>[];
833
834 @override
835 void transform({dynamic matrix4}) {
836 _predicates.add(_FunctionPaintPredicate(#transform, <dynamic>[matrix4]));
837 }
838
839 @override
840 void translate({double? x, double? y}) {
841 _predicates.add(_FunctionPaintPredicate(#translate, <dynamic>[x, y]));
842 }
843
844 @override
845 void scale({double? x, double? y}) {
846 _predicates.add(_FunctionPaintPredicate(#scale, <dynamic>[x, y]));
847 }
848
849 @override
850 void rotate({double? angle}) {
851 _predicates.add(_FunctionPaintPredicate(#rotate, <dynamic>[angle]));
852 }
853
854 @override
855 void save() {
856 _predicates.add(_FunctionPaintPredicate(#save, <dynamic>[]));
857 }
858
859 @override
860 void restore() {
861 _predicates.add(_FunctionPaintPredicate(#restore, <dynamic>[]));
862 }
863
864 @override
865 void saveRestore() {
866 _predicates.add(_SaveRestorePairPaintPredicate());
867 }
868
869 @override
870 void clipRect({Rect? rect}) {
871 _predicates.add(_FunctionPaintPredicate(#clipRect, <dynamic>[rect]));
872 }
873
874 @override
875 void clipPath({Matcher? pathMatcher}) {
876 _predicates.add(_FunctionPaintPredicate(#clipPath, <dynamic>[pathMatcher]));
877 }
878
879 @override
880 void rect({
881 Rect? rect,
882 Color? color,
883 double? strokeWidth,
884 bool? hasMaskFilter,
885 PaintingStyle? style,
886 }) {
887 _predicates.add(
888 _RectPaintPredicate(
889 rect: rect,
890 color: color,
891 strokeWidth: strokeWidth,
892 hasMaskFilter: hasMaskFilter,
893 style: style,
894 ),
895 );
896 }
897
898 @override
899 void clipRRect({RRect? rrect}) {
900 _predicates.add(_FunctionPaintPredicate(#clipRRect, <dynamic>[rrect]));
901 }
902
903 @override
904 void rrect({
905 RRect? rrect,
906 Color? color,
907 double? strokeWidth,
908 bool? hasMaskFilter,
909 PaintingStyle? style,
910 }) {
911 _predicates.add(
912 _RRectPaintPredicate(
913 rrect: rrect,
914 color: color,
915 strokeWidth: strokeWidth,
916 hasMaskFilter: hasMaskFilter,
917 style: style,
918 ),
919 );
920 }
921
922 @override
923 void drrect({
924 RRect? outer,
925 RRect? inner,
926 Color? color,
927 double? strokeWidth,
928 bool? hasMaskFilter,
929 PaintingStyle? style,
930 }) {
931 _predicates.add(
932 _DRRectPaintPredicate(
933 outer: outer,
934 inner: inner,
935 color: color,
936 strokeWidth: strokeWidth,
937 hasMaskFilter: hasMaskFilter,
938 style: style,
939 ),
940 );
941 }
942
943 @override
944 void rsuperellipse({
945 RSuperellipse? rsuperellipse,
946 Color? color,
947 double? strokeWidth,
948 bool? hasMaskFilter,
949 }) {
950 _predicates.add(
951 _RSuperellipsePaintPredicate(
952 rsuperellipse: rsuperellipse,
953 color: color,
954 strokeWidth: strokeWidth,
955 hasMaskFilter: hasMaskFilter,
956 ),
957 );
958 }
959
960 @override
961 void clipRSuperellipse({RSuperellipse? rsuperellipse}) {
962 _predicates.add(_FunctionPaintPredicate(#clipRSuperellipse, <dynamic>[rsuperellipse]));
963 }
964
965 @override
966 void circle({
967 double? x,
968 double? y,
969 double? radius,
970 Color? color,
971 double? strokeWidth,
972 bool? hasMaskFilter,
973 PaintingStyle? style,
974 }) {
975 _predicates.add(
976 _CirclePaintPredicate(
977 x: x,
978 y: y,
979 radius: radius,
980 color: color,
981 strokeWidth: strokeWidth,
982 hasMaskFilter: hasMaskFilter,
983 style: style,
984 ),
985 );
986 }
987
988 @override
989 void path({
990 Iterable<Offset>? includes,
991 Iterable<Offset>? excludes,
992 Color? color,
993 double? strokeWidth,
994 bool? hasMaskFilter,
995 PaintingStyle? style,
996 }) {
997 _predicates.add(
998 _PathPaintPredicate(
999 includes: includes,
1000 excludes: excludes,
1001 color: color,
1002 strokeWidth: strokeWidth,
1003 hasMaskFilter: hasMaskFilter,
1004 style: style,
1005 ),
1006 );
1007 }
1008
1009 @override
1010 void line({
1011 Offset? p1,
1012 Offset? p2,
1013 Color? color,
1014 double? strokeWidth,
1015 bool? hasMaskFilter,
1016 PaintingStyle? style,
1017 }) {
1018 _predicates.add(
1019 _LinePaintPredicate(
1020 p1: p1,
1021 p2: p2,
1022 color: color,
1023 strokeWidth: strokeWidth,
1024 hasMaskFilter: hasMaskFilter,
1025 style: style,
1026 ),
1027 );
1028 }
1029
1030 @override
1031 void arc({
1032 Rect? rect,
1033 double? startAngle,
1034 double? sweepAngle,
1035 bool? useCenter,
1036 Color? color,
1037 double? strokeWidth,
1038 bool? hasMaskFilter,
1039 PaintingStyle? style,
1040 StrokeCap? strokeCap,
1041 }) {
1042 _predicates.add(
1043 _ArcPaintPredicate(
1044 rect: rect,
1045 startAngle: startAngle,
1046 sweepAngle: sweepAngle,
1047 useCenter: useCenter,
1048 color: color,
1049 strokeWidth: strokeWidth,
1050 hasMaskFilter: hasMaskFilter,
1051 style: style,
1052 strokeCap: strokeCap,
1053 ),
1054 );
1055 }
1056
1057 @override
1058 void paragraph({ui.Paragraph? paragraph, dynamic offset}) {
1059 _predicates.add(_FunctionPaintPredicate(#drawParagraph, <dynamic>[paragraph, offset]));
1060 }
1061
1062 @override
1063 void shadow({
1064 Iterable<Offset>? includes,
1065 Iterable<Offset>? excludes,
1066 Color? color,
1067 double? elevation,
1068 bool? transparentOccluder,
1069 }) {
1070 _predicates.add(
1071 _ShadowPredicate(
1072 includes: includes,
1073 excludes: excludes,
1074 color: color,
1075 elevation: elevation,
1076 transparentOccluder: transparentOccluder,
1077 ),
1078 );
1079 }
1080
1081 @override
1082 void image({
1083 ui.Image? image,
1084 double? x,
1085 double? y,
1086 Color? color,
1087 double? strokeWidth,
1088 bool? hasMaskFilter,
1089 PaintingStyle? style,
1090 }) {
1091 _predicates.add(
1092 _DrawImagePaintPredicate(
1093 image: image,
1094 x: x,
1095 y: y,
1096 color: color,
1097 strokeWidth: strokeWidth,
1098 hasMaskFilter: hasMaskFilter,
1099 style: style,
1100 ),
1101 );
1102 }
1103
1104 @override
1105 void drawImageRect({
1106 ui.Image? image,
1107 Rect? source,
1108 Rect? destination,
1109 Color? color,
1110 double? strokeWidth,
1111 bool? hasMaskFilter,
1112 PaintingStyle? style,
1113 }) {
1114 _predicates.add(
1115 _DrawImageRectPaintPredicate(
1116 image: image,
1117 source: source,
1118 destination: destination,
1119 color: color,
1120 strokeWidth: strokeWidth,
1121 hasMaskFilter: hasMaskFilter,
1122 style: style,
1123 ),
1124 );
1125 }
1126
1127 @override
1128 void something(PaintPatternPredicate predicate) {
1129 _predicates.add(_SomethingPaintPredicate(predicate));
1130 }
1131
1132 @override
1133 void everything(PaintPatternPredicate predicate) {
1134 _predicates.add(_EverythingPaintPredicate(predicate));
1135 }
1136
1137 @override
1138 Description describe(Description description) {
1139 if (_predicates.isEmpty) {
1140 return description.add('An object or closure and a paint pattern.');
1141 }
1142 description.add('Object or closure painting:\n');
1143 return description.addAll(
1144 '',
1145 '\n',
1146 '',
1147 _predicates.map<String>((_PaintPredicate predicate) => predicate.toString()),
1148 );
1149 }
1150
1151 @override
1152 bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) {
1153 if (calls.isEmpty) {
1154 description.writeln('It painted nothing.');
1155 return false;
1156 }
1157 if (_predicates.isEmpty) {
1158 description.writeln(
1159 'It painted something, but you must now add a pattern to the paints '
1160 'matcher in the test to verify that it matches the important parts of '
1161 'the following.',
1162 );
1163 return false;
1164 }
1165 final Iterator<_PaintPredicate> predicate = _predicates.iterator;
1166 final Iterator<RecordedInvocation> call = calls.iterator..moveNext();
1167 try {
1168 while (predicate.moveNext()) {
1169 predicate.current.match(call);
1170 }
1171 // We allow painting more than expected.
1172 } on _MismatchedCall catch (data) {
1173 description.writeln(data.message);
1174 description.writeln(data.callIntroduction);
1175 description.writeln(data.call.stackToString(indent: ' '));
1176 return false;
1177 } on String catch (s) {
1178 description.writeln(s);
1179 try {
1180 description.write(
1181 'The stack of the offending call was:\n${call.current.stackToString(indent: " ")}\n',
1182 );
1183 } on TypeError catch (_) {
1184 // All calls have been evaluated
1185 }
1186 return false;
1187 }
1188 return true;
1189 }
1190}
1191
1192abstract class _PaintPredicate {
1193 void match(Iterator<RecordedInvocation> call);
1194
1195 @protected
1196 void checkMethod(Iterator<RecordedInvocation> call, Symbol symbol) {
1197 int others = 0;
1198 final RecordedInvocation firstCall = call.current;
1199 while (!call.current.invocation.isMethod || call.current.invocation.memberName != symbol) {
1200 others += 1;
1201 if (!call.moveNext()) {
1202 throw _MismatchedCall(
1203 'It called $others other method${others == 1 ? "" : "s"} on the '
1204 'canvas, the first of which was $firstCall, but did not call '
1205 '${_symbolName(symbol)}() at the time where $this was expected.',
1206 'The first method that was called when the call to '
1207 '${_symbolName(symbol)}() was expected, $firstCall, was called with '
1208 'the following stack:',
1209 firstCall,
1210 );
1211 }
1212 }
1213 }
1214
1215 @override
1216 String toString() {
1217 throw FlutterError('$runtimeType does not implement toString.');
1218 }
1219}
1220
1221abstract class _DrawCommandPaintPredicate extends _PaintPredicate {
1222 _DrawCommandPaintPredicate(
1223 this.symbol,
1224 this.name,
1225 this.argumentCount,
1226 this.paintArgumentIndex, {
1227 this.color,
1228 this.strokeWidth,
1229 this.hasMaskFilter,
1230 this.style,
1231 this.strokeCap,
1232 });
1233
1234 final Symbol symbol;
1235 final String name;
1236 final int argumentCount;
1237 final int paintArgumentIndex;
1238 final Color? color;
1239 final double? strokeWidth;
1240 final bool? hasMaskFilter;
1241 final PaintingStyle? style;
1242 final StrokeCap? strokeCap;
1243
1244 String get methodName => _symbolName(symbol);
1245
1246 @override
1247 void match(Iterator<RecordedInvocation> call) {
1248 checkMethod(call, symbol);
1249 final int actualArgumentCount = call.current.invocation.positionalArguments.length;
1250 if (actualArgumentCount != argumentCount) {
1251 throw FlutterError(
1252 'It called $methodName with $actualArgumentCount '
1253 'argument${actualArgumentCount == 1 ? "" : "s"}; expected '
1254 '$argumentCount.',
1255 );
1256 }
1257 verifyArguments(call.current.invocation.positionalArguments);
1258 call.moveNext();
1259 }
1260
1261 @protected
1262 @mustCallSuper
1263 void verifyArguments(List<dynamic> arguments) {
1264 final Paint paintArgument = arguments[paintArgumentIndex] as Paint;
1265 if (color != null && !_colorsMatch(paintArgument.color, color)) {
1266 throw FlutterError(
1267 'It called $methodName with a paint whose color, '
1268 '${paintArgument.color}, was not exactly the expected color ($color).',
1269 );
1270 }
1271 if (strokeWidth != null && paintArgument.strokeWidth != strokeWidth) {
1272 throw FlutterError(
1273 'It called $methodName with a paint whose strokeWidth, '
1274 '${paintArgument.strokeWidth}, was not exactly the expected '
1275 'strokeWidth ($strokeWidth).',
1276 );
1277 }
1278 if (hasMaskFilter != null && (paintArgument.maskFilter != null) != hasMaskFilter) {
1279 if (hasMaskFilter!) {
1280 throw FlutterError(
1281 'It called $methodName with a paint that did not have a mask filter, '
1282 'despite expecting one.',
1283 );
1284 } else {
1285 throw FlutterError(
1286 'It called $methodName with a paint that had a mask filter, '
1287 'despite not expecting one.',
1288 );
1289 }
1290 }
1291 if (style != null && paintArgument.style != style) {
1292 throw FlutterError(
1293 'It called $methodName with a paint whose style, '
1294 '${paintArgument.style}, was not exactly the expected style ($style).',
1295 );
1296 }
1297 if (strokeCap != null && paintArgument.strokeCap != strokeCap) {
1298 throw FlutterError(
1299 'It called $methodName with a paint whose strokeCap, '
1300 '${paintArgument.strokeCap}, was not exactly the expected '
1301 'strokeCap ($strokeCap).',
1302 );
1303 }
1304 }
1305
1306 @override
1307 String toString() {
1308 final List<String> description = <String>[];
1309 debugFillDescription(description);
1310 String result = name;
1311 if (description.isNotEmpty) {
1312 result += ' with ${description.join(", ")}';
1313 }
1314 return result;
1315 }
1316
1317 @protected
1318 @mustCallSuper
1319 void debugFillDescription(List<String> description) {
1320 if (color != null) {
1321 description.add('$color');
1322 }
1323 if (strokeWidth != null) {
1324 description.add('strokeWidth: $strokeWidth');
1325 }
1326 if (hasMaskFilter != null) {
1327 description.add(hasMaskFilter! ? 'a mask filter' : 'no mask filter');
1328 }
1329 if (style != null) {
1330 description.add('$style');
1331 }
1332 }
1333}
1334
1335class _OneParameterPaintPredicate<T> extends _DrawCommandPaintPredicate {
1336 _OneParameterPaintPredicate(
1337 Symbol symbol,
1338 String name, {
1339 required this.expected,
1340 required super.color,
1341 required super.strokeWidth,
1342 required super.hasMaskFilter,
1343 required super.style,
1344 }) : super(symbol, name, 2, 1);
1345
1346 final T? expected;
1347
1348 @override
1349 void verifyArguments(List<dynamic> arguments) {
1350 super.verifyArguments(arguments);
1351 final T actual = arguments[0] as T;
1352 if (expected != null && actual != expected) {
1353 throw FlutterError(
1354 'It called $methodName with $T, $actual, which was not exactly the '
1355 'expected $T ($expected).',
1356 );
1357 }
1358 }
1359
1360 @override
1361 void debugFillDescription(List<String> description) {
1362 super.debugFillDescription(description);
1363 if (expected != null) {
1364 if (expected.toString().contains(T.toString())) {
1365 description.add('$expected');
1366 } else {
1367 description.add('$T: $expected');
1368 }
1369 }
1370 }
1371}
1372
1373class _TwoParameterPaintPredicate<T1, T2> extends _DrawCommandPaintPredicate {
1374 _TwoParameterPaintPredicate(
1375 Symbol symbol,
1376 String name, {
1377 required this.expected1,
1378 required this.expected2,
1379 required super.color,
1380 required super.strokeWidth,
1381 required super.hasMaskFilter,
1382 required super.style,
1383 }) : super(symbol, name, 3, 2);
1384
1385 final T1? expected1;
1386
1387 final T2? expected2;
1388
1389 @override
1390 void verifyArguments(List<dynamic> arguments) {
1391 super.verifyArguments(arguments);
1392 final T1 actual1 = arguments[0] as T1;
1393 if (expected1 != null && actual1 != expected1) {
1394 throw FlutterError(
1395 'It called $methodName with its first argument (a $T1), $actual1, '
1396 'which was not exactly the expected $T1 ($expected1).',
1397 );
1398 }
1399 final T2 actual2 = arguments[1] as T2;
1400 if (expected2 != null && actual2 != expected2) {
1401 throw FlutterError(
1402 'It called $methodName with its second argument (a $T2), $actual2, '
1403 'which was not exactly the expected $T2 ($expected2).',
1404 );
1405 }
1406 }
1407
1408 @override
1409 void debugFillDescription(List<String> description) {
1410 super.debugFillDescription(description);
1411 if (expected1 != null) {
1412 if (expected1.toString().contains(T1.toString())) {
1413 description.add('$expected1');
1414 } else {
1415 description.add('$T1: $expected1');
1416 }
1417 }
1418 if (expected2 != null) {
1419 if (expected2.toString().contains(T2.toString())) {
1420 description.add('$expected2');
1421 } else {
1422 description.add('$T2: $expected2');
1423 }
1424 }
1425 }
1426}
1427
1428class _RectPaintPredicate extends _OneParameterPaintPredicate<Rect> {
1429 _RectPaintPredicate({
1430 Rect? rect,
1431 super.color,
1432 super.strokeWidth,
1433 super.hasMaskFilter,
1434 super.style,
1435 }) : super(#drawRect, 'a rectangle', expected: rect);
1436}
1437
1438class _RRectPaintPredicate extends _DrawCommandPaintPredicate {
1439 _RRectPaintPredicate({
1440 this.rrect,
1441 super.color,
1442 super.strokeWidth,
1443 super.hasMaskFilter,
1444 super.style,
1445 }) : super(#drawRRect, 'a rounded rectangle', 2, 1);
1446
1447 final RRect? rrect;
1448
1449 @override
1450 void verifyArguments(List<dynamic> arguments) {
1451 super.verifyArguments(arguments);
1452 const double eps = .0001;
1453 final RRect actual = arguments[0] as RRect;
1454 if (rrect != null &&
1455 ((actual.left - rrect!.left).abs() > eps ||
1456 (actual.right - rrect!.right).abs() > eps ||
1457 (actual.top - rrect!.top).abs() > eps ||
1458 (actual.bottom - rrect!.bottom).abs() > eps ||
1459 (actual.blRadiusX - rrect!.blRadiusX).abs() > eps ||
1460 (actual.blRadiusY - rrect!.blRadiusY).abs() > eps ||
1461 (actual.brRadiusX - rrect!.brRadiusX).abs() > eps ||
1462 (actual.brRadiusY - rrect!.brRadiusY).abs() > eps ||
1463 (actual.tlRadiusX - rrect!.tlRadiusX).abs() > eps ||
1464 (actual.tlRadiusY - rrect!.tlRadiusY).abs() > eps ||
1465 (actual.trRadiusX - rrect!.trRadiusX).abs() > eps ||
1466 (actual.trRadiusY - rrect!.trRadiusY).abs() > eps)) {
1467 throw FlutterError(
1468 'It called $methodName with RRect, $actual, which was not exactly the '
1469 'expected RRect ($rrect).',
1470 );
1471 }
1472 }
1473
1474 @override
1475 void debugFillDescription(List<String> description) {
1476 super.debugFillDescription(description);
1477 if (rrect != null) {
1478 description.add('RRect: $rrect');
1479 }
1480 }
1481}
1482
1483class _DRRectPaintPredicate extends _TwoParameterPaintPredicate<RRect, RRect> {
1484 _DRRectPaintPredicate({
1485 RRect? inner,
1486 RRect? outer,
1487 super.color,
1488 super.strokeWidth,
1489 super.hasMaskFilter,
1490 super.style,
1491 }) : super(#drawDRRect, 'a rounded rectangle outline', expected1: outer, expected2: inner);
1492}
1493
1494class _RSuperellipsePaintPredicate extends _DrawCommandPaintPredicate {
1495 _RSuperellipsePaintPredicate({
1496 this.rsuperellipse,
1497 super.color,
1498 super.strokeWidth,
1499 super.hasMaskFilter,
1500 }) : super(#drawRSuperellipse, 'a rounded superellipse', 2, 1);
1501
1502 final RSuperellipse? rsuperellipse;
1503
1504 @override
1505 void verifyArguments(List<dynamic> arguments) {
1506 super.verifyArguments(arguments);
1507 final RSuperellipse rsuperellipseArgument = arguments[0] as RSuperellipse;
1508 if (rsuperellipse != null && rsuperellipseArgument != rsuperellipse) {
1509 throw FlutterError(
1510 'It called $methodName with a rounded superellipse, '
1511 '$rsuperellipseArgument, which was not exactly the expected rounded '
1512 'superellipse ($rsuperellipse).',
1513 );
1514 }
1515 }
1516
1517 @override
1518 void debugFillDescription(List<String> description) {
1519 super.debugFillDescription(description);
1520 if (rsuperellipse != null) {
1521 description.add('rounded superellipse $rsuperellipse');
1522 }
1523 }
1524}
1525
1526class _CirclePaintPredicate extends _DrawCommandPaintPredicate {
1527 _CirclePaintPredicate({
1528 this.x,
1529 this.y,
1530 this.radius,
1531 super.color,
1532 super.strokeWidth,
1533 super.hasMaskFilter,
1534 super.style,
1535 }) : super(#drawCircle, 'a circle', 3, 2);
1536
1537 final double? x;
1538 final double? y;
1539 final double? radius;
1540
1541 @override
1542 void verifyArguments(List<dynamic> arguments) {
1543 super.verifyArguments(arguments);
1544 final Offset pointArgument = arguments[0] as Offset;
1545 if (x != null && y != null) {
1546 final Offset point = Offset(x!, y!);
1547 if (point != pointArgument) {
1548 throw FlutterError(
1549 'It called $methodName with a center coordinate, $pointArgument, '
1550 'which was not exactly the expected coordinate ($point).',
1551 );
1552 }
1553 } else {
1554 if (x != null && pointArgument.dx != x) {
1555 throw FlutterError(
1556 'It called $methodName with a center coordinate, $pointArgument, '
1557 'whose x-coordinate was not exactly the expected coordinate '
1558 '(${x!.toStringAsFixed(1)}).',
1559 );
1560 }
1561 if (y != null && pointArgument.dy != y) {
1562 throw FlutterError(
1563 'It called $methodName with a center coordinate, $pointArgument, '
1564 'whose y-coordinate was not exactly the expected coordinate '
1565 '(${y!.toStringAsFixed(1)}).',
1566 );
1567 }
1568 }
1569 final double radiusArgument = arguments[1] as double;
1570 if (radius != null && radiusArgument != radius) {
1571 throw FlutterError(
1572 'It called $methodName with radius, '
1573 '${radiusArgument.toStringAsFixed(1)}, which was not exactly the '
1574 'expected radius (${radius!.toStringAsFixed(1)}).',
1575 );
1576 }
1577 }
1578
1579 @override
1580 void debugFillDescription(List<String> description) {
1581 super.debugFillDescription(description);
1582 if (x != null && y != null) {
1583 description.add('point ${Offset(x!, y!)}');
1584 } else {
1585 if (x != null) {
1586 description.add('x-coordinate ${x!.toStringAsFixed(1)}');
1587 }
1588 if (y != null) {
1589 description.add('y-coordinate ${y!.toStringAsFixed(1)}');
1590 }
1591 }
1592 if (radius != null) {
1593 description.add('radius ${radius!.toStringAsFixed(1)}');
1594 }
1595 }
1596}
1597
1598class _PathPaintPredicate extends _DrawCommandPaintPredicate {
1599 _PathPaintPredicate({
1600 this.includes,
1601 this.excludes,
1602 super.color,
1603 super.strokeWidth,
1604 super.hasMaskFilter,
1605 super.style,
1606 }) : super(#drawPath, 'a path', 2, 1);
1607
1608 final Iterable<Offset>? includes;
1609 final Iterable<Offset>? excludes;
1610
1611 @override
1612 void verifyArguments(List<dynamic> arguments) {
1613 super.verifyArguments(arguments);
1614 final Path pathArgument = arguments[0] as Path;
1615 if (includes != null) {
1616 for (final Offset offset in includes!) {
1617 if (!pathArgument.contains(offset)) {
1618 throw FlutterError(
1619 'It called $methodName with a path that unexpectedly did not '
1620 'contain $offset.',
1621 );
1622 }
1623 }
1624 }
1625 if (excludes != null) {
1626 for (final Offset offset in excludes!) {
1627 if (pathArgument.contains(offset)) {
1628 throw FlutterError(
1629 'It called $methodName with a path that unexpectedly contained '
1630 '$offset.',
1631 );
1632 }
1633 }
1634 }
1635 }
1636
1637 @override
1638 void debugFillDescription(List<String> description) {
1639 super.debugFillDescription(description);
1640 if (includes != null && excludes != null) {
1641 description.add('that contains $includes and does not contain $excludes');
1642 } else if (includes != null) {
1643 description.add('that contains $includes');
1644 } else if (excludes != null) {
1645 description.add('that does not contain $excludes');
1646 }
1647 }
1648}
1649
1650// TODO(ianh): add arguments to test the length, angle, that kind of thing
1651class _LinePaintPredicate extends _DrawCommandPaintPredicate {
1652 _LinePaintPredicate({
1653 this.p1,
1654 this.p2,
1655 super.color,
1656 super.strokeWidth,
1657 super.hasMaskFilter,
1658 super.style,
1659 }) : super(#drawLine, 'a line', 3, 2);
1660
1661 final Offset? p1;
1662 final Offset? p2;
1663
1664 @override
1665 void verifyArguments(List<dynamic> arguments) {
1666 super.verifyArguments(arguments); // Checks the 3rd argument, a Paint
1667 if (arguments.length != 3) {
1668 throw FlutterError('It called $methodName with ${arguments.length} arguments; expected 3.');
1669 }
1670 final Offset p1Argument = arguments[0] as Offset;
1671 final Offset p2Argument = arguments[1] as Offset;
1672 if (p1 != null && p1Argument != p1) {
1673 throw FlutterError(
1674 'It called $methodName with p1 endpoint, $p1Argument, which was not '
1675 'exactly the expected endpoint ($p1).',
1676 );
1677 }
1678 if (p2 != null && p2Argument != p2) {
1679 throw FlutterError(
1680 'It called $methodName with p2 endpoint, $p2Argument, which was not '
1681 'exactly the expected endpoint ($p2).',
1682 );
1683 }
1684 }
1685
1686 @override
1687 void debugFillDescription(List<String> description) {
1688 super.debugFillDescription(description);
1689 if (p1 != null) {
1690 description.add('end point p1: $p1');
1691 }
1692 if (p2 != null) {
1693 description.add('end point p2: $p2');
1694 }
1695 }
1696}
1697
1698class _ArcPaintPredicate extends _DrawCommandPaintPredicate {
1699 _ArcPaintPredicate({
1700 this.rect,
1701 this.startAngle,
1702 this.sweepAngle,
1703 this.useCenter,
1704 super.color,
1705 super.strokeWidth,
1706 super.hasMaskFilter,
1707 super.style,
1708 super.strokeCap,
1709 }) : super(#drawArc, 'an arc', 5, 4);
1710
1711 final Rect? rect;
1712
1713 final double? startAngle;
1714
1715 final double? sweepAngle;
1716
1717 final bool? useCenter;
1718
1719 @override
1720 void verifyArguments(List<dynamic> arguments) {
1721 super.verifyArguments(arguments);
1722 final Rect rectArgument = arguments[0] as Rect;
1723 if (rect != null && rectArgument != rect) {
1724 throw FlutterError(
1725 'It called $methodName with a paint whose rect, $rectArgument, was not '
1726 'exactly the expected rect ($rect).',
1727 );
1728 }
1729 final double startAngleArgument = arguments[1] as double;
1730 if (startAngle != null && startAngleArgument != startAngle) {
1731 throw FlutterError(
1732 'It called $methodName with a start angle, $startAngleArgument, which '
1733 'was not exactly the expected start angle ($startAngle).',
1734 );
1735 }
1736 final double sweepAngleArgument = arguments[2] as double;
1737 if (sweepAngle != null && sweepAngleArgument != sweepAngle) {
1738 throw FlutterError(
1739 'It called $methodName with a sweep angle, $sweepAngleArgument, which '
1740 'was not exactly the expected sweep angle ($sweepAngle).',
1741 );
1742 }
1743 final bool useCenterArgument = arguments[3] as bool;
1744 if (useCenter != null && useCenterArgument != useCenter) {
1745 throw FlutterError(
1746 'It called $methodName with a useCenter value, $useCenterArgument, '
1747 'which was not exactly the expected value ($useCenter).',
1748 );
1749 }
1750 }
1751
1752 @override
1753 void debugFillDescription(List<String> description) {
1754 super.debugFillDescription(description);
1755 if (rect != null) {
1756 description.add('rect $rect');
1757 }
1758 if (startAngle != null) {
1759 description.add('startAngle $startAngle');
1760 }
1761 if (sweepAngle != null) {
1762 description.add('sweepAngle $sweepAngle');
1763 }
1764 if (useCenter != null) {
1765 description.add('useCenter $useCenter');
1766 }
1767 }
1768}
1769
1770class _ShadowPredicate extends _PaintPredicate {
1771 _ShadowPredicate({
1772 this.includes,
1773 this.excludes,
1774 this.color,
1775 this.elevation,
1776 this.transparentOccluder,
1777 });
1778
1779 final Iterable<Offset>? includes;
1780 final Iterable<Offset>? excludes;
1781 final Color? color;
1782 final double? elevation;
1783 final bool? transparentOccluder;
1784
1785 static const Symbol symbol = #drawShadow;
1786 String get methodName => _symbolName(symbol);
1787
1788 @protected
1789 void verifyArguments(List<dynamic> arguments) {
1790 if (arguments.length != 4) {
1791 throw FlutterError('It called $methodName with ${arguments.length} arguments; expected 4.');
1792 }
1793 final Path pathArgument = arguments[0] as Path;
1794 if (includes != null) {
1795 for (final Offset offset in includes!) {
1796 if (!pathArgument.contains(offset)) {
1797 throw FlutterError(
1798 'It called $methodName with a path that unexpectedly did not '
1799 'contain $offset.',
1800 );
1801 }
1802 }
1803 }
1804 if (excludes != null) {
1805 for (final Offset offset in excludes!) {
1806 if (pathArgument.contains(offset)) {
1807 throw FlutterError(
1808 'It called $methodName with a path that unexpectedly contained '
1809 '$offset.',
1810 );
1811 }
1812 }
1813 }
1814 final Color actualColor = arguments[1] as Color;
1815 if (color != null && !_colorsMatch(actualColor, color)) {
1816 throw FlutterError(
1817 'It called $methodName with a color, $actualColor, which was not '
1818 'exactly the expected color ($color).',
1819 );
1820 }
1821 final double actualElevation = arguments[2] as double;
1822 if (elevation != null && actualElevation != elevation) {
1823 throw FlutterError(
1824 'It called $methodName with an elevation, $actualElevation, which was '
1825 'not exactly the expected value ($elevation).',
1826 );
1827 }
1828 final bool actualTransparentOccluder = arguments[3] as bool;
1829 if (transparentOccluder != null && actualTransparentOccluder != transparentOccluder) {
1830 throw FlutterError(
1831 'It called $methodName with a transparentOccluder value, '
1832 '$actualTransparentOccluder, which was not exactly the expected value '
1833 '($transparentOccluder).',
1834 );
1835 }
1836 }
1837
1838 @override
1839 void match(Iterator<RecordedInvocation> call) {
1840 checkMethod(call, symbol);
1841 verifyArguments(call.current.invocation.positionalArguments);
1842 call.moveNext();
1843 }
1844
1845 @protected
1846 void debugFillDescription(List<String> description) {
1847 if (includes != null && excludes != null) {
1848 description.add('that contains $includes and does not contain $excludes');
1849 } else if (includes != null) {
1850 description.add('that contains $includes');
1851 } else if (excludes != null) {
1852 description.add('that does not contain $excludes');
1853 }
1854 if (color != null) {
1855 description.add('$color');
1856 }
1857 if (elevation != null) {
1858 description.add('elevation: $elevation');
1859 }
1860 if (transparentOccluder != null) {
1861 description.add('transparentOccluder: $transparentOccluder');
1862 }
1863 }
1864
1865 @override
1866 String toString() {
1867 final List<String> description = <String>[];
1868 debugFillDescription(description);
1869 String result = methodName;
1870 if (description.isNotEmpty) {
1871 result += ' with ${description.join(", ")}';
1872 }
1873 return result;
1874 }
1875}
1876
1877class _DrawImagePaintPredicate extends _DrawCommandPaintPredicate {
1878 _DrawImagePaintPredicate({
1879 this.image,
1880 this.x,
1881 this.y,
1882 super.color,
1883 super.strokeWidth,
1884 super.hasMaskFilter,
1885 super.style,
1886 }) : super(#drawImage, 'an image', 3, 2);
1887
1888 final ui.Image? image;
1889 final double? x;
1890 final double? y;
1891
1892 @override
1893 void verifyArguments(List<dynamic> arguments) {
1894 super.verifyArguments(arguments);
1895 final ui.Image imageArgument = arguments[0] as ui.Image;
1896 if (image != null && !image!.isCloneOf(imageArgument)) {
1897 throw FlutterError(
1898 'It called $methodName with an image, $imageArgument, which was not '
1899 'exactly the expected image ($image).',
1900 );
1901 }
1902 final Offset pointArgument = arguments[0] as Offset;
1903 if (x != null && y != null) {
1904 final Offset point = Offset(x!, y!);
1905 if (point != pointArgument) {
1906 throw FlutterError(
1907 'It called $methodName with an offset coordinate, $pointArgument, '
1908 'which was not exactly the expected coordinate ($point).',
1909 );
1910 }
1911 } else {
1912 if (x != null && pointArgument.dx != x) {
1913 throw FlutterError(
1914 'It called $methodName with an offset coordinate, $pointArgument, '
1915 'whose x-coordinate was not exactly the expected coordinate '
1916 '(${x!.toStringAsFixed(1)}).',
1917 );
1918 }
1919 if (y != null && pointArgument.dy != y) {
1920 throw FlutterError(
1921 'It called $methodName with an offset coordinate, $pointArgument, '
1922 'whose y-coordinate was not exactly the expected coordinate '
1923 '(${y!.toStringAsFixed(1)}).',
1924 );
1925 }
1926 }
1927 }
1928
1929 @override
1930 void debugFillDescription(List<String> description) {
1931 super.debugFillDescription(description);
1932 if (image != null) {
1933 description.add('image $image');
1934 }
1935 if (x != null && y != null) {
1936 description.add('point ${Offset(x!, y!)}');
1937 } else {
1938 if (x != null) {
1939 description.add('x-coordinate ${x!.toStringAsFixed(1)}');
1940 }
1941 if (y != null) {
1942 description.add('y-coordinate ${y!.toStringAsFixed(1)}');
1943 }
1944 }
1945 }
1946}
1947
1948class _DrawImageRectPaintPredicate extends _DrawCommandPaintPredicate {
1949 _DrawImageRectPaintPredicate({
1950 this.image,
1951 this.source,
1952 this.destination,
1953 super.color,
1954 super.strokeWidth,
1955 super.hasMaskFilter,
1956 super.style,
1957 }) : super(#drawImageRect, 'an image', 4, 3);
1958
1959 final ui.Image? image;
1960 final Rect? source;
1961 final Rect? destination;
1962
1963 @override
1964 void verifyArguments(List<dynamic> arguments) {
1965 super.verifyArguments(arguments);
1966 final ui.Image imageArgument = arguments[0] as ui.Image;
1967 if (image != null && !image!.isCloneOf(imageArgument)) {
1968 throw FlutterError(
1969 'It called $methodName with an image, $imageArgument, which was not '
1970 'exactly the expected image ($image).',
1971 );
1972 }
1973 final Rect sourceArgument = arguments[1] as Rect;
1974 if (source != null && sourceArgument != source) {
1975 throw FlutterError(
1976 'It called $methodName with a source rectangle, $sourceArgument, which '
1977 'was not exactly the expected rectangle ($source).',
1978 );
1979 }
1980 final Rect destinationArgument = arguments[2] as Rect;
1981 if (destination != null && destinationArgument != destination) {
1982 throw FlutterError(
1983 'It called $methodName with a destination rectangle, '
1984 '$destinationArgument, which was not exactly the expected rectangle '
1985 '($destination).',
1986 );
1987 }
1988 }
1989
1990 @override
1991 void debugFillDescription(List<String> description) {
1992 super.debugFillDescription(description);
1993 if (image != null) {
1994 description.add('image $image');
1995 }
1996 if (source != null) {
1997 description.add('source $source');
1998 }
1999 if (destination != null) {
2000 description.add('destination $destination');
2001 }
2002 }
2003}
2004
2005class _SomethingPaintPredicate extends _PaintPredicate {
2006 _SomethingPaintPredicate(this.predicate);
2007
2008 final PaintPatternPredicate predicate;
2009
2010 @override
2011 void match(Iterator<RecordedInvocation> call) {
2012 RecordedInvocation currentCall;
2013 bool testedAllCalls = false;
2014 do {
2015 if (testedAllCalls) {
2016 throw FlutterError(
2017 'It painted methods that the predicate passed to a "something" step, '
2018 'in the paint pattern, none of which were considered correct.',
2019 );
2020 }
2021 currentCall = call.current;
2022 if (!currentCall.invocation.isMethod) {
2023 throw FlutterError(
2024 'It called $currentCall, which was not a method, when the paint '
2025 'pattern expected a method call',
2026 );
2027 }
2028 testedAllCalls = !call.moveNext();
2029 } while (!_runPredicate(
2030 currentCall.invocation.memberName,
2031 currentCall.invocation.positionalArguments,
2032 ));
2033 }
2034
2035 bool _runPredicate(Symbol methodName, List<dynamic> arguments) {
2036 try {
2037 return predicate(methodName, arguments);
2038 } on String catch (s) {
2039 throw FlutterError(
2040 'It painted something that the predicate passed to a "something" step '
2041 'in the paint pattern considered incorrect:\n $s\n ',
2042 );
2043 }
2044 }
2045
2046 @override
2047 String toString() => 'a "something" step';
2048}
2049
2050class _EverythingPaintPredicate extends _PaintPredicate {
2051 _EverythingPaintPredicate(this.predicate);
2052
2053 final PaintPatternPredicate predicate;
2054
2055 @override
2056 void match(Iterator<RecordedInvocation> call) {
2057 do {
2058 final RecordedInvocation currentCall = call.current;
2059 if (!currentCall.invocation.isMethod) {
2060 throw FlutterError(
2061 'It called $currentCall, which was not a method, when the paint '
2062 'pattern expected a method call',
2063 );
2064 }
2065 if (!_runPredicate(
2066 currentCall.invocation.memberName,
2067 currentCall.invocation.positionalArguments,
2068 )) {
2069 throw FlutterError(
2070 'It painted something that the predicate passed to an "everything" '
2071 'step in the paint pattern considered incorrect.\n',
2072 );
2073 }
2074 } while (call.moveNext());
2075 }
2076
2077 bool _runPredicate(Symbol methodName, List<dynamic> arguments) {
2078 try {
2079 return predicate(methodName, arguments);
2080 } on String catch (s) {
2081 throw FlutterError(
2082 'It painted something that the predicate passed to an "everything" step '
2083 'in the paint pattern considered incorrect:\n $s\n ',
2084 );
2085 }
2086 }
2087
2088 @override
2089 String toString() => 'an "everything" step';
2090}
2091
2092class _FunctionPaintPredicate extends _PaintPredicate {
2093 _FunctionPaintPredicate(this.symbol, this.arguments);
2094
2095 final Symbol symbol;
2096
2097 final List<dynamic> arguments;
2098
2099 @override
2100 void match(Iterator<RecordedInvocation> call) {
2101 checkMethod(call, symbol);
2102 if (call.current.invocation.positionalArguments.length != arguments.length) {
2103 throw FlutterError(
2104 'It called ${_symbolName(symbol)} with '
2105 '${call.current.invocation.positionalArguments.length} arguments; '
2106 'expected ${arguments.length}.',
2107 );
2108 }
2109 for (int index = 0; index < arguments.length; index += 1) {
2110 final dynamic actualArgument = call.current.invocation.positionalArguments[index];
2111 final dynamic desiredArgument = arguments[index];
2112
2113 if (desiredArgument is Matcher) {
2114 expect(actualArgument, desiredArgument);
2115 } else if (desiredArgument != null && desiredArgument != actualArgument) {
2116 throw FlutterError(
2117 'It called ${_symbolName(symbol)} with argument $index having value '
2118 '${_valueName(actualArgument)} when ${_valueName(desiredArgument)} '
2119 'was expected.',
2120 );
2121 }
2122 }
2123 call.moveNext();
2124 }
2125
2126 @override
2127 String toString() {
2128 final List<String> adjectives = <String>[
2129 for (int index = 0; index < arguments.length; index += 1)
2130 arguments[index] != null ? _valueName(arguments[index]) : '...',
2131 ];
2132 return '${_symbolName(symbol)}(${adjectives.join(", ")})';
2133 }
2134}
2135
2136class _SaveRestorePairPaintPredicate extends _PaintPredicate {
2137 @override
2138 void match(Iterator<RecordedInvocation> call) {
2139 checkMethod(call, #save);
2140 int depth = 1;
2141 while (depth > 0) {
2142 if (!call.moveNext()) {
2143 throw FlutterError(
2144 'It did not have a matching restore() for the save() that was found '
2145 'where $this was expected.',
2146 );
2147 }
2148 if (call.current.invocation.isMethod) {
2149 if (call.current.invocation.memberName == #save) {
2150 depth += 1;
2151 } else if (call.current.invocation.memberName == #restore) {
2152 depth -= 1;
2153 }
2154 }
2155 }
2156 call.moveNext();
2157 }
2158
2159 @override
2160 String toString() => 'a matching save/restore pair';
2161}
2162
2163String _valueName(Object? value) {
2164 if (value is double) {
2165 return value.toStringAsFixed(1);
2166 }
2167 return value.toString();
2168}
2169
2170// Workaround for https://github.com/dart-lang/sdk/issues/28372
2171String _symbolName(Symbol symbol) {
2172 // WARNING: Assumes a fixed format for Symbol.toString which is *not*
2173 // guaranteed anywhere.
2174 final String s = '$symbol';
2175 return s.substring(8, s.length - 2);
2176}
2177

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com