| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| /// @docImport 'package:flutter/material.dart'; | |
| /// | |
| /// @docImport 'matchers.dart'; | |
| library; | |
| import 'dart:ui' as ui show Image, Paragraph; | |
| import 'package:flutter/foundation.dart'; | |
| import 'package:flutter/widgets.dart'; | |
| import 'package:matcher/expect.dart'; | |
| import 'finders.dart'; | |
| import 'recording_canvas.dart'; | |
| import 'test_async_utils.dart'; | |
| // Examples can assume: | |
| // late RenderObject myRenderObject; | |
| // late Symbol methodName; | |
| /// 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 exampleOne(PaintingContext context, Offset offset) { } | |
| /// void exampleTwo(Canvas canvas) { } | |
| /// ``` | |
| /// | |
| /// In the case of functions that take a [PaintingContext] and an [Offset], the | |
| /// [paints] matcher will always pass a zero offset. | |
| /// | |
| /// To specify the pattern, call the methods on the returned object. For example: | |
| /// | |
| /// ```dart | |
| /// expect(myRenderObject, paints..circle(radius: 10.0)..circle(radius: 20.0)); | |
| /// ``` | |
| /// | |
| /// 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. | |
| /// | |
| /// To match something which paints nothing, see [paintsNothing]. | |
| /// | |
| /// To match something which asserts instead of painting, see [paintsAssertion]. | |
| PaintPattern get paints => _TestRecordingCanvasPatternMatcher(); | |
| /// Matches objects or functions that does not paint anything on the canvas. | |
| Matcher get paintsNothing => _TestRecordingCanvasPaintsNothingMatcher(); | |
| /// Matches objects or functions that assert when they try to paint. | |
| Matcher get paintsAssertion => _TestRecordingCanvasPaintsAssertionMatcher(); | |
| /// Matches objects or functions that draw `methodName` exactly `count` number | |
| /// of times. | |
| Matcher paintsExactlyCountTimes(Symbol methodName, int count) { | |
| return _TestRecordingCanvasPaintsCountMatcher(methodName, count); | |
| } | |
| /// Signature for the [PaintPattern.something] and [PaintPattern.everything] | |
| /// predicate argument. | |
| /// | |
| /// 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) { | |
| /// // ... | |
| /// } | |
| /// ``` | |
| typedef PaintPatternPredicate = bool Function(Symbol methodName, List<dynamic> arguments); | |
| /// The signature of [RenderObject.paint] functions. | |
| typedef _ContextPainterFunction = void Function(PaintingContext context, Offset offset); | |
| /// The signature of functions that paint directly on a canvas. | |
| typedef _CanvasPainterFunction = void Function(Canvas canvas); | |
| bool _colorsMatch(Color x, Color? y) { | |
| if (y == null) { | |
| return false; | |
| } else { | |
| const double limit = 1 / 255; | |
| return x.colorSpace == y.colorSpace && | |
| (x.a - y.a).abs() < limit && | |
| (x.r - y.r).abs() < limit && | |
| (x.g - y.g).abs() < limit && | |
| (x.b - y.b).abs() < limit; | |
| } | |
| } | |
| /// 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 { | |
| /// 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}); | |
| /// 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. | |
| void translate({double? x, double? y}); | |
| /// 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. | |
| void scale({double? x, double? y}); | |
| /// 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. | |
| void rotate({double? angle}); | |
| /// 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. | |
| /// | |
| /// 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. | |
| 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. | |
| /// | |
| /// 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. | |
| 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. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [save], which indicates that a save is expected next. | |
| /// * [restore], which indicates that a restore is expected next. | |
| void saveRestore(); | |
| /// Indicates that a rectangular clip is expected next. | |
| /// | |
| /// 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. | |
| void clipRect({Rect? rect}); | |
| /// 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. | |
| void clipPath({Matcher? pathMatcher}); | |
| /// Indicates that a rectangle is expected next. | |
| /// | |
| /// 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. | |
| /// | |
| /// If no call to [Canvas.drawRect] was made, then this results in failure. | |
| /// | |
| /// Any calls made between the last matched call (if any) and the | |
| /// [Canvas.drawRect] 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. | |
| void rect({ | |
| Rect? rect, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }); | |
| /// 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. | |
| void clipRRect({RRect? rrect}); | |
| /// 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. | |
| /// | |
| /// 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. | |
| void rrect({ | |
| RRect? rrect, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }); | |
| /// 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. | |
| void drrect({ | |
| RRect? outer, | |
| RRect? inner, | |
| Color? color, | |
| double strokeWidth, | |
| bool hasMaskFilter, | |
| PaintingStyle style, | |
| }); | |
| /// Indicates that a rounded superellipse is expected next. | |
| /// | |
| /// The next rounded superellipse is examined. Any arguments that are passed | |
| /// to this method are compared to the actual [Canvas.drawRSuperellipse] | |
| /// call's arguments and any mismatches result in failure. | |
| /// | |
| /// If no call to [Canvas.drawRSuperellipse] was made, then this results in | |
| /// failure. | |
| /// | |
| /// Any calls made between the last matched call (if any) and the | |
| /// [Canvas.drawRSuperellipse] call are ignored. | |
| /// | |
| /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`) | |
| /// 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. | |
| void rsuperellipse({ | |
| RSuperellipse? rsuperellipse, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| }); | |
| /// Indicates that a rounded superellipse clip is expected next. | |
| /// | |
| /// The next rounded superellipse clip is examined. Any arguments that are | |
| /// passed to this method are compared to the actual | |
| /// [Canvas.clipRSuperellipse] call's argument and any mismatches result in | |
| /// failure. | |
| /// | |
| /// If no call to [Canvas.clipRSuperellipse] was made, then this results in failure. | |
| /// | |
| /// Any calls made between the last matched call (if any) and the | |
| /// [Canvas.clipRSuperellipse] call are ignored. | |
| void clipRSuperellipse({RSuperellipse? rsuperellipse}); | |
| /// 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. | |
| /// | |
| /// 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. | |
| void circle({ | |
| double? x, | |
| double? y, | |
| double? radius, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }); | |
| /// 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. | |
| /// | |
| /// 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.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. | |
| /// | |
| /// 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. | |
| void path({ | |
| Iterable<Offset>? includes, | |
| Iterable<Offset>? excludes, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }); | |
| /// Indicates that a line is expected next. | |
| /// | |
| /// The next line is examined. Any arguments that are passed to this method | |
| /// are compared to the actual [Canvas.drawLine] call's `p1`, `p2`, and | |
| /// `paint` arguments, and any mismatches result in failure. | |
| /// | |
| /// 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. | |
| /// | |
| /// 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. | |
| void line({ | |
| Offset? p1, | |
| Offset? p2, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }); | |
| /// 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. | |
| /// | |
| /// 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. | |
| void arc({ | |
| Rect? rect, | |
| double? startAngle, | |
| double? sweepAngle, | |
| bool? useCenter, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| StrokeCap? strokeCap, | |
| }); | |
| /// 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. | |
| /// | |
| /// 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. | |
| /// | |
| /// If no call to [Canvas.drawParagraph] was made, then this results in | |
| /// failure. | |
| void paragraph({ui.Paragraph? paragraph, dynamic offset}); | |
| /// 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. | |
| /// | |
| /// 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. | |
| /// | |
| /// 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. | |
| void shadow({ | |
| Iterable<Offset>? includes, | |
| Iterable<Offset>? excludes, | |
| Color? color, | |
| double? elevation, | |
| bool? transparentOccluder, | |
| }); | |
| /// Indicates that an image is expected next. | |
| /// | |
| /// 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. | |
| void image({ | |
| ui.Image? image, | |
| double? x, | |
| double? y, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }); | |
| /// Indicates that an image subsection is expected next. | |
| /// | |
| /// 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. | |
| void drawImageRect({ | |
| ui.Image? image, | |
| Rect? source, | |
| Rect? destination, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }); | |
| /// 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); | |
| /// 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 returns false, then the [paints] [Matcher] is considered | |
| /// to have failed. If all calls are tested without failing, then the [paints] | |
| /// [Matcher] is considered a success. | |
| /// | |
| /// 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); | |
| } | |
| /// Matches a [Path] that contains (as defined by [Path.contains]) the given | |
| /// `includes` points and does not contain the given `excludes` points. | |
| Matcher isPathThat({ | |
| Iterable<Offset> includes = const <Offset>[], | |
| Iterable<Offset> excludes = const <Offset>[], | |
| }) { | |
| return _PathMatcher(includes.toList(), excludes.toList()); | |
| } | |
| class _PathMatcher extends Matcher { | |
| _PathMatcher(this.includes, this.excludes); | |
| List<Offset> includes; | |
| List<Offset> excludes; | |
| @override | |
| bool matches(Object? object, Map<dynamic, dynamic> matchState) { | |
| if (object is! Path) { | |
| matchState[this] = 'The given object ($object) was not a Path.'; | |
| return false; | |
| } | |
| final Path path = object; | |
| final errors = <String>[ | |
| for (final Offset offset in includes) | |
| if (!path.contains(offset)) 'Offset $offset should be inside the path, but is not.', | |
| for (final Offset offset in excludes) | |
| if (path.contains(offset)) 'Offset $offset should be outside the path, but is not.', | |
| ]; | |
| 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, | |
| ) { | |
| return description.add(matchState[this] as String); | |
| } | |
| } | |
| class _MismatchedCall extends Error { | |
| _MismatchedCall(this.message, this.callIntroduction, this.call); | |
| final String message; | |
| final String callIntroduction; | |
| final RecordedInvocation call; | |
| } | |
| bool _evaluatePainter(Object? object, Canvas canvas, PaintingContext context) { | |
| switch (object) { | |
| case final _ContextPainterFunction function: | |
| function(context, Offset.zero); | |
| case final _CanvasPainterFunction function: | |
| function(canvas); | |
| case final Finder finder: | |
| TestAsyncUtils.guardSync(); | |
| final RenderObject? result = finder.evaluate().single.renderObject; | |
| return (result?..paint(context, Offset.zero)) != null; | |
| case final RenderObject renderObject: | |
| renderObject.paint(context, Offset.zero); | |
| default: | |
| return false; | |
| } | |
| return true; | |
| } | |
| abstract class _TestRecordingCanvasMatcher extends Matcher { | |
| @override | |
| bool matches(Object? object, Map<dynamic, dynamic> matchState) { | |
| final canvas = TestRecordingCanvas(); | |
| final context = TestRecordingPaintingContext(canvas); | |
| final description = StringBuffer(); | |
| var prefixMessage = 'unexpectedly failed.'; | |
| var result = false; | |
| try { | |
| if (!_evaluatePainter(object, canvas, context)) { | |
| matchState[this] = | |
| 'was not one of the supported objects for the ' | |
| '"paints" matcher.'; | |
| return false; | |
| } | |
| 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; | |
| } | |
| if (!result) { | |
| if (canvas.invocations.isNotEmpty) { | |
| description.write('The complete display list was:'); | |
| for (final RecordedInvocation call in canvas.invocations) { | |
| description.write('\n * $call'); | |
| } | |
| } | |
| matchState[this] = '$prefixMessage\n$description'; | |
| } | |
| return result; | |
| } | |
| bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description); | |
| @override | |
| Description describeMismatch( | |
| dynamic item, | |
| Description description, | |
| Map<dynamic, dynamic> matchState, | |
| bool verbose, | |
| ) { | |
| return description.add(matchState[this] as String); | |
| } | |
| } | |
| class _TestRecordingCanvasPaintsCountMatcher extends _TestRecordingCanvasMatcher { | |
| _TestRecordingCanvasPaintsCountMatcher(Symbol methodName, int count) | |
| : _methodName = methodName, | |
| _count = count; | |
| final Symbol _methodName; | |
| final int _count; | |
| @override | |
| Description describe(Description description) { | |
| return description.add('Object or closure painting $_methodName exactly $_count times'); | |
| } | |
| @override | |
| bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) { | |
| var count = 0; | |
| for (final call in calls) { | |
| 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; | |
| } | |
| } | |
| class _TestRecordingCanvasPaintsNothingMatcher extends _TestRecordingCanvasMatcher { | |
| @override | |
| Description describe(Description description) { | |
| return description.add('An object or closure that paints nothing.'); | |
| } | |
| @override | |
| bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) { | |
| final Iterable<RecordedInvocation> paintingCalls = _filterCanvasCalls(calls); | |
| if (paintingCalls.isEmpty) { | |
| return true; | |
| } | |
| description.write( | |
| 'painted something, the first call having the following stack:\n' | |
| '${paintingCalls.first.stackToString(indent: " ")}\n', | |
| ); | |
| return false; | |
| } | |
| static const List<Symbol> _nonPaintingOperations = <Symbol>[#save, #restore]; | |
| // Filters out canvas calls that are not painting anything. | |
| static Iterable<RecordedInvocation> _filterCanvasCalls(Iterable<RecordedInvocation> canvasCalls) { | |
| return canvasCalls.where( | |
| (RecordedInvocation canvasCall) => | |
| !_nonPaintingOperations.contains(canvasCall.invocation.memberName), | |
| ); | |
| } | |
| } | |
| class _TestRecordingCanvasPaintsAssertionMatcher extends Matcher { | |
| @override | |
| bool matches(Object? object, Map<dynamic, dynamic> matchState) { | |
| final canvas = TestRecordingCanvas(); | |
| final context = TestRecordingPaintingContext(canvas); | |
| final description = StringBuffer(); | |
| var prefixMessage = 'unexpectedly failed.'; | |
| var 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:'); | |
| for (final RecordedInvocation call in canvas.invocations) { | |
| 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, | |
| ) { | |
| return description.add(matchState[this] as String); | |
| } | |
| } | |
| class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher | |
| implements PaintPattern { | |
| final List<_PaintPredicate> _predicates = <_PaintPredicate>[]; | |
| @override | |
| void transform({dynamic matrix4}) { | |
| _predicates.add(_FunctionPaintPredicate(#transform, <dynamic>[matrix4])); | |
| } | |
| @override | |
| void translate({double? x, double? y}) { | |
| _predicates.add(_FunctionPaintPredicate(#translate, <dynamic>[x, y])); | |
| } | |
| @override | |
| void scale({double? x, double? y}) { | |
| _predicates.add(_FunctionPaintPredicate(#scale, <dynamic>[x, y])); | |
| } | |
| @override | |
| void rotate({double? angle}) { | |
| _predicates.add(_FunctionPaintPredicate(#rotate, <dynamic>[angle])); | |
| } | |
| @override | |
| void save() { | |
| _predicates.add(_FunctionPaintPredicate(#save, <dynamic>[])); | |
| } | |
| @override | |
| void restore() { | |
| _predicates.add(_FunctionPaintPredicate(#restore, <dynamic>[])); | |
| } | |
| @override | |
| void saveRestore() { | |
| _predicates.add(_SaveRestorePairPaintPredicate()); | |
| } | |
| @override | |
| void clipRect({Rect? rect}) { | |
| _predicates.add(_FunctionPaintPredicate(#clipRect, <dynamic>[rect])); | |
| } | |
| @override | |
| void clipPath({Matcher? pathMatcher}) { | |
| _predicates.add(_FunctionPaintPredicate(#clipPath, <dynamic>[pathMatcher])); | |
| } | |
| @override | |
| void rect({ | |
| Rect? rect, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }) { | |
| _predicates.add( | |
| _RectPaintPredicate( | |
| rect: rect, | |
| color: color, | |
| strokeWidth: strokeWidth, | |
| hasMaskFilter: hasMaskFilter, | |
| style: style, | |
| ), | |
| ); | |
| } | |
| @override | |
| void clipRRect({RRect? rrect}) { | |
| _predicates.add(_FunctionPaintPredicate(#clipRRect, <dynamic>[rrect])); | |
| } | |
| @override | |
| void rrect({ | |
| RRect? rrect, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }) { | |
| _predicates.add( | |
| _RRectPaintPredicate( | |
| rrect: rrect, | |
| color: color, | |
| strokeWidth: strokeWidth, | |
| hasMaskFilter: hasMaskFilter, | |
| style: style, | |
| ), | |
| ); | |
| } | |
| @override | |
| void drrect({ | |
| RRect? outer, | |
| RRect? inner, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }) { | |
| _predicates.add( | |
| _DRRectPaintPredicate( | |
| outer: outer, | |
| inner: inner, | |
| color: color, | |
| strokeWidth: strokeWidth, | |
| hasMaskFilter: hasMaskFilter, | |
| style: style, | |
| ), | |
| ); | |
| } | |
| @override | |
| void rsuperellipse({ | |
| RSuperellipse? rsuperellipse, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| }) { | |
| _predicates.add( | |
| _RSuperellipsePaintPredicate( | |
| rsuperellipse: rsuperellipse, | |
| color: color, | |
| strokeWidth: strokeWidth, | |
| hasMaskFilter: hasMaskFilter, | |
| ), | |
| ); | |
| } | |
| @override | |
| void clipRSuperellipse({RSuperellipse? rsuperellipse}) { | |
| _predicates.add(_FunctionPaintPredicate(#clipRSuperellipse, <dynamic>[rsuperellipse])); | |
| } | |
| @override | |
| void circle({ | |
| double? x, | |
| double? y, | |
| double? radius, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }) { | |
| _predicates.add( | |
| _CirclePaintPredicate( | |
| x: x, | |
| y: y, | |
| radius: radius, | |
| color: color, | |
| strokeWidth: strokeWidth, | |
| hasMaskFilter: hasMaskFilter, | |
| style: style, | |
| ), | |
| ); | |
| } | |
| @override | |
| void path({ | |
| Iterable<Offset>? includes, | |
| Iterable<Offset>? excludes, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }) { | |
| _predicates.add( | |
| _PathPaintPredicate( | |
| includes: includes, | |
| excludes: excludes, | |
| color: color, | |
| strokeWidth: strokeWidth, | |
| hasMaskFilter: hasMaskFilter, | |
| style: style, | |
| ), | |
| ); | |
| } | |
| @override | |
| void line({ | |
| Offset? p1, | |
| Offset? p2, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }) { | |
| _predicates.add( | |
| _LinePaintPredicate( | |
| p1: p1, | |
| p2: p2, | |
| color: color, | |
| strokeWidth: strokeWidth, | |
| hasMaskFilter: hasMaskFilter, | |
| style: style, | |
| ), | |
| ); | |
| } | |
| @override | |
| void arc({ | |
| Rect? rect, | |
| double? startAngle, | |
| double? sweepAngle, | |
| bool? useCenter, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| StrokeCap? strokeCap, | |
| }) { | |
| _predicates.add( | |
| _ArcPaintPredicate( | |
| rect: rect, | |
| startAngle: startAngle, | |
| sweepAngle: sweepAngle, | |
| useCenter: useCenter, | |
| color: color, | |
| strokeWidth: strokeWidth, | |
| hasMaskFilter: hasMaskFilter, | |
| style: style, | |
| strokeCap: strokeCap, | |
| ), | |
| ); | |
| } | |
| @override | |
| void paragraph({ui.Paragraph? paragraph, dynamic offset}) { | |
| _predicates.add(_FunctionPaintPredicate(#drawParagraph, <dynamic>[paragraph, offset])); | |
| } | |
| @override | |
| void shadow({ | |
| Iterable<Offset>? includes, | |
| Iterable<Offset>? excludes, | |
| Color? color, | |
| double? elevation, | |
| bool? transparentOccluder, | |
| }) { | |
| _predicates.add( | |
| _ShadowPredicate( | |
| includes: includes, | |
| excludes: excludes, | |
| color: color, | |
| elevation: elevation, | |
| transparentOccluder: transparentOccluder, | |
| ), | |
| ); | |
| } | |
| @override | |
| void image({ | |
| ui.Image? image, | |
| double? x, | |
| double? y, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }) { | |
| _predicates.add( | |
| _DrawImagePaintPredicate( | |
| image: image, | |
| x: x, | |
| y: y, | |
| color: color, | |
| strokeWidth: strokeWidth, | |
| hasMaskFilter: hasMaskFilter, | |
| style: style, | |
| ), | |
| ); | |
| } | |
| @override | |
| void drawImageRect({ | |
| ui.Image? image, | |
| Rect? source, | |
| Rect? destination, | |
| Color? color, | |
| double? strokeWidth, | |
| bool? hasMaskFilter, | |
| PaintingStyle? style, | |
| }) { | |
| _predicates.add( | |
| _DrawImageRectPaintPredicate( | |
| image: image, | |
| source: source, | |
| destination: destination, | |
| color: color, | |
| strokeWidth: strokeWidth, | |
| hasMaskFilter: hasMaskFilter, | |
| style: style, | |
| ), | |
| ); | |
| } | |
| @override | |
| void something(PaintPatternPredicate predicate) { | |
| _predicates.add(_SomethingPaintPredicate(predicate)); | |
| } | |
| @override | |
| void everything(PaintPatternPredicate predicate) { | |
| _predicates.add(_EverythingPaintPredicate(predicate)); | |
| } | |
| @override | |
| Description describe(Description description) { | |
| if (_predicates.isEmpty) { | |
| return description.add('An object or closure and a paint pattern.'); | |
| } | |
| description.add('Object or closure painting:\n'); | |
| return description.addAll( | |
| '', | |
| '\n', | |
| '', | |
| _predicates.map<String>((_PaintPredicate predicate) => predicate.toString()), | |
| ); | |
| } | |
| @override | |
| bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) { | |
| if (calls.isEmpty) { | |
| description.writeln('It painted nothing.'); | |
| return false; | |
| } | |
| if (_predicates.isEmpty) { | |
| description.writeln( | |
| 'It painted something, but you must now add a pattern to the paints ' | |
| 'matcher in the test to verify that it matches the important parts of ' | |
| 'the following.', | |
| ); | |
| return false; | |
| } | |
| final Iterator<_PaintPredicate> predicate = _predicates.iterator; | |
| final Iterator<RecordedInvocation> call = calls.iterator..moveNext(); | |
| try { | |
| while (predicate.moveNext()) { | |
| predicate.current.match(call); | |
| } | |
| // We allow painting more than expected. | |
| } on _MismatchedCall catch (data) { | |
| description.writeln(data.message); | |
| description.writeln(data.callIntroduction); | |
| description.writeln(data.call.stackToString(indent: ' ')); | |
| return false; | |
| } on String catch (s) { | |
| description.writeln(s); | |
| try { | |
| description.write( | |
| 'The stack of the offending call was:\n${call.current.stackToString(indent: " ")}\n', | |
| ); | |
| } on TypeError catch (_) { | |
| // All calls have been evaluated | |
| } | |
| return false; | |
| } | |
| return true; | |
| } | |
| } | |
| abstract class _PaintPredicate { | |
| void match(Iterator<RecordedInvocation> call); | |
| @protected | |
| void checkMethod(Iterator<RecordedInvocation> call, Symbol symbol) { | |
| var others = 0; | |
| final RecordedInvocation firstCall = call.current; | |
| while (!call.current.invocation.isMethod || call.current.invocation.memberName != symbol) { | |
| others += 1; | |
| if (!call.moveNext()) { | |
| throw _MismatchedCall( | |
| '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, | |
| ); | |
| } | |
| } | |
| } | |
| @override | |
| String toString() { | |
| throw FlutterError('$runtimeType does not implement toString.'); | |
| } | |
| } | |
| abstract class _DrawCommandPaintPredicate extends _PaintPredicate { | |
| _DrawCommandPaintPredicate( | |
| this.symbol, | |
| this.name, | |
| this.argumentCount, | |
| this.paintArgumentIndex, { | |
| this.color, | |
| this.strokeWidth, | |
| this.hasMaskFilter, | |
| this.style, | |
| this.strokeCap, | |
| }); | |
| final Symbol symbol; | |
| final String name; | |
| final int argumentCount; | |
| final int paintArgumentIndex; | |
| final Color? color; | |
| final double? strokeWidth; | |
| final bool? hasMaskFilter; | |
| final PaintingStyle? style; | |
| final StrokeCap? strokeCap; | |
| String get methodName => _symbolName(symbol); | |
| @override | |
| void match(Iterator<RecordedInvocation> call) { | |
| checkMethod(call, symbol); | |
| final int actualArgumentCount = call.current.invocation.positionalArguments.length; | |
| if (actualArgumentCount != argumentCount) { | |
| throw FlutterError( | |
| 'It called $methodName with $actualArgumentCount ' | |
| 'argument${actualArgumentCount == 1 ? "" : "s"}; expected ' | |
| '$argumentCount.', | |
| ); | |
| } | |
| verifyArguments(call.current.invocation.positionalArguments); | |
| call.moveNext(); | |
| } | |
| @protected | |
| @mustCallSuper | |
| void verifyArguments(List<dynamic> arguments) { | |
| final paintArgument = arguments[paintArgumentIndex] as Paint; | |
| if (color != null && !_colorsMatch(paintArgument.color, color)) { | |
| throw FlutterError( | |
| 'It called $methodName with a paint whose color, ' | |
| '${paintArgument.color}, was not exactly the expected color ($color).', | |
| ); | |
| } | |
| if (strokeWidth != null && paintArgument.strokeWidth != strokeWidth) { | |
| throw FlutterError( | |
| 'It called $methodName with a paint whose strokeWidth, ' | |
| '${paintArgument.strokeWidth}, was not exactly the expected ' | |
| 'strokeWidth ($strokeWidth).', | |
| ); | |
| } | |
| if (hasMaskFilter != null && (paintArgument.maskFilter != null) != hasMaskFilter) { | |
| if (hasMaskFilter!) { | |
| throw FlutterError( | |
| 'It called $methodName with a paint that did not have a mask filter, ' | |
| 'despite expecting one.', | |
| ); | |
| } else { | |
| throw FlutterError( | |
| 'It called $methodName with a paint that had a mask filter, ' | |
| 'despite not expecting one.', | |
| ); | |
| } | |
| } | |
| if (style != null && paintArgument.style != style) { | |
| throw FlutterError( | |
| 'It called $methodName with a paint whose style, ' | |
| '${paintArgument.style}, was not exactly the expected style ($style).', | |
| ); | |
| } | |
| if (strokeCap != null && paintArgument.strokeCap != strokeCap) { | |
| throw FlutterError( | |
| 'It called $methodName with a paint whose strokeCap, ' | |
| '${paintArgument.strokeCap}, was not exactly the expected ' | |
| 'strokeCap ($strokeCap).', | |
| ); | |
| } | |
| } | |
| @override | |
| String toString() { | |
| final description = <String>[]; | |
| 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'); | |
| } | |
| if (strokeWidth != null) { | |
| description.add('strokeWidth: $strokeWidth'); | |
| } | |
| if (hasMaskFilter != null) { | |
| description.add(hasMaskFilter! ? 'a mask filter' : 'no mask filter'); | |
| } | |
| if (style != null) { | |
| description.add('$style'); | |
| } | |
| } | |
| } | |
| class _OneParameterPaintPredicate<T> extends _DrawCommandPaintPredicate { | |
| _OneParameterPaintPredicate( | |
| Symbol symbol, | |
| String name, { | |
| required this.expected, | |
| required super.color, | |
| required super.strokeWidth, | |
| required super.hasMaskFilter, | |
| required super.style, | |
| }) : super(symbol, name, 2, 1); | |
| final T? expected; | |
| @override | |
| void verifyArguments(List<dynamic> arguments) { | |
| super.verifyArguments(arguments); | |
| final actual = arguments[0] as T; | |
| if (expected != null && actual != expected) { | |
| throw FlutterError( | |
| 'It called $methodName with $T, $actual, which was not exactly the ' | |
| 'expected $T ($expected).', | |
| ); | |
| } | |
| } | |
| @override | |
| void debugFillDescription(List<String> description) { | |
| super.debugFillDescription(description); | |
| if (expected != null) { | |
| if (expected.toString().contains(T.toString())) { | |
| description.add('$expected'); | |
| } else { | |
| description.add('$T: $expected'); | |
| } | |
| } | |
| } | |
| } | |
| class _TwoParameterPaintPredicate<T1, T2> extends _DrawCommandPaintPredicate { | |
| _TwoParameterPaintPredicate( | |
| Symbol symbol, | |
| String name, { | |
| required this.expected1, | |
| required this.expected2, | |
| required super.color, | |
| required super.strokeWidth, | |
| required super.hasMaskFilter, | |
| required super.style, | |
| }) : super(symbol, name, 3, 2); | |
| final T1? expected1; | |
| final T2? expected2; | |
| @override | |
| void verifyArguments(List<dynamic> arguments) { | |
| super.verifyArguments(arguments); | |
| final actual1 = arguments[0] as T1; | |
| if (expected1 != null && actual1 != expected1) { | |
| throw FlutterError( | |
| 'It called $methodName with its first argument (a $T1), $actual1, ' | |
| 'which was not exactly the expected $T1 ($expected1).', | |
| ); | |
| } | |
| final actual2 = arguments[1] as T2; | |
| if (expected2 != null && actual2 != expected2) { | |
| throw FlutterError( | |
| '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'); | |
| } | |
| } | |
| } | |
| } | |
| class _RectPaintPredicate extends _OneParameterPaintPredicate<Rect> { | |
| _RectPaintPredicate({ | |
| Rect? rect, | |
| super.color, | |
| super.strokeWidth, | |
| super.hasMaskFilter, | |
| super.style, | |
| }) : super(#drawRect, 'a rectangle', expected: rect); | |
| } | |
| class _RRectPaintPredicate extends _DrawCommandPaintPredicate { | |
| _RRectPaintPredicate({ | |
| this.rrect, | |
| super.color, | |
| super.strokeWidth, | |
| super.hasMaskFilter, | |
| super.style, | |
| }) : super(#drawRRect, 'a rounded rectangle', 2, 1); | |
| final RRect? rrect; | |
| @override | |
| void verifyArguments(List<dynamic> arguments) { | |
| super.verifyArguments(arguments); | |
| const eps = .0001; | |
| final actual = arguments[0] as RRect; | |
| if (rrect != null && | |
| ((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)) { | |
| throw FlutterError( | |
| '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'); | |
| } | |
| } | |
| } | |
| class _DRRectPaintPredicate extends _TwoParameterPaintPredicate<RRect, RRect> { | |
| _DRRectPaintPredicate({ | |
| RRect? inner, | |
| RRect? outer, | |
| super.color, | |
| super.strokeWidth, | |
| super.hasMaskFilter, | |
| super.style, | |
| }) : super(#drawDRRect, 'a rounded rectangle outline', expected1: outer, expected2: inner); | |
| } | |
| class _RSuperellipsePaintPredicate extends _DrawCommandPaintPredicate { | |
| _RSuperellipsePaintPredicate({ | |
| this.rsuperellipse, | |
| super.color, | |
| super.strokeWidth, | |
| super.hasMaskFilter, | |
| }) : super(#drawRSuperellipse, 'a rounded superellipse', 2, 1); | |
| final RSuperellipse? rsuperellipse; | |
| @override | |
| void verifyArguments(List<dynamic> arguments) { | |
| super.verifyArguments(arguments); | |
| final rsuperellipseArgument = arguments[0] as RSuperellipse; | |
| if (rsuperellipse != null && rsuperellipseArgument != rsuperellipse) { | |
| throw FlutterError( | |
| 'It called $methodName with a rounded superellipse, ' | |
| '$rsuperellipseArgument, which was not exactly the expected rounded ' | |
| 'superellipse ($rsuperellipse).', | |
| ); | |
| } | |
| } | |
| @override | |
| void debugFillDescription(List<String> description) { | |
| super.debugFillDescription(description); | |
| if (rsuperellipse != null) { | |
| description.add('rounded superellipse $rsuperellipse'); | |
| } | |
| } | |
| } | |
| class _CirclePaintPredicate extends _DrawCommandPaintPredicate { | |
| _CirclePaintPredicate({ | |
| this.x, | |
| this.y, | |
| this.radius, | |
| super.color, | |
| super.strokeWidth, | |
| super.hasMaskFilter, | |
| super.style, | |
| }) : super(#drawCircle, 'a circle', 3, 2); | |
| final double? x; | |
| final double? y; | |
| final double? radius; | |
| @override | |
| void verifyArguments(List<dynamic> arguments) { | |
| super.verifyArguments(arguments); | |
| final pointArgument = arguments[0] as Offset; | |
| if (x != null && y != null) { | |
| final point = Offset(x!, y!); | |
| if (point != pointArgument) { | |
| throw FlutterError( | |
| 'It called $methodName with a center coordinate, $pointArgument, ' | |
| 'which was not exactly the expected coordinate ($point).', | |
| ); | |
| } | |
| } else { | |
| if (x != null && pointArgument.dx != x) { | |
| throw FlutterError( | |
| 'It called $methodName with a center coordinate, $pointArgument, ' | |
| 'whose x-coordinate was not exactly the expected coordinate ' | |
| '(${x!.toStringAsFixed(1)}).', | |
| ); | |
| } | |
| if (y != null && pointArgument.dy != y) { | |
| throw FlutterError( | |
| 'It called $methodName with a center coordinate, $pointArgument, ' | |
| 'whose y-coordinate was not exactly the expected coordinate ' | |
| '(${y!.toStringAsFixed(1)}).', | |
| ); | |
| } | |
| } | |
| final radiusArgument = arguments[1] as double; | |
| if (radius != null && radiusArgument != radius) { | |
| throw FlutterError( | |
| 'It called $methodName with radius, ' | |
| '${radiusArgument.toStringAsFixed(1)}, which was not exactly the ' | |
| 'expected radius (${radius!.toStringAsFixed(1)}).', | |
| ); | |
| } | |
| } | |
| @override | |
| void debugFillDescription(List<String> description) { | |
| super.debugFillDescription(description); | |
| if (x != null && y != null) { | |
| description.add('point ${Offset(x!, y!)}'); | |
| } else { | |
| if (x != null) { | |
| description.add('x-coordinate ${x!.toStringAsFixed(1)}'); | |
| } | |
| if (y != null) { | |
| description.add('y-coordinate ${y!.toStringAsFixed(1)}'); | |
| } | |
| } | |
| if (radius != null) { | |
| description.add('radius ${radius!.toStringAsFixed(1)}'); | |
| } | |
| } | |
| } | |
| class _PathPaintPredicate extends _DrawCommandPaintPredicate { | |
| _PathPaintPredicate({ | |
| this.includes, | |
| this.excludes, | |
| super.color, | |
| super.strokeWidth, | |
| super.hasMaskFilter, | |
| super.style, | |
| }) : super(#drawPath, 'a path', 2, 1); | |
| final Iterable<Offset>? includes; | |
| final Iterable<Offset>? excludes; | |
| @override | |
| void verifyArguments(List<dynamic> arguments) { | |
| super.verifyArguments(arguments); | |
| final pathArgument = arguments[0] as Path; | |
| if (includes != null) { | |
| for (final Offset offset in includes!) { | |
| if (!pathArgument.contains(offset)) { | |
| throw FlutterError( | |
| 'It called $methodName with a path that unexpectedly did not ' | |
| 'contain $offset.', | |
| ); | |
| } | |
| } | |
| } | |
| if (excludes != null) { | |
| for (final Offset offset in excludes!) { | |
| if (pathArgument.contains(offset)) { | |
| throw FlutterError( | |
| 'It called $methodName with a path that unexpectedly contained ' | |
| '$offset.', | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| @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'); | |
| } | |
| } | |
| } | |
| // TODO(ianh): add arguments to test the length, angle, that kind of thing | |
| class _LinePaintPredicate extends _DrawCommandPaintPredicate { | |
| _LinePaintPredicate({ | |
| this.p1, | |
| this.p2, | |
| super.color, | |
| super.strokeWidth, | |
| super.hasMaskFilter, | |
| super.style, | |
| }) : super(#drawLine, 'a line', 3, 2); | |
| final Offset? p1; | |
| final Offset? p2; | |
| @override | |
| void verifyArguments(List<dynamic> arguments) { | |
| super.verifyArguments(arguments); // Checks the 3rd argument, a Paint | |
| if (arguments.length != 3) { | |
| throw FlutterError('It called $methodName with ${arguments.length} arguments; expected 3.'); | |
| } | |
| final p1Argument = arguments[0] as Offset; | |
| final p2Argument = arguments[1] as Offset; | |
| if (p1 != null && p1Argument != p1) { | |
| throw FlutterError( | |
| 'It called $methodName with p1 endpoint, $p1Argument, which was not ' | |
| 'exactly the expected endpoint ($p1).', | |
| ); | |
| } | |
| if (p2 != null && p2Argument != p2) { | |
| throw FlutterError( | |
| 'It called $methodName with p2 endpoint, $p2Argument, which was not ' | |
| 'exactly the expected endpoint ($p2).', | |
| ); | |
| } | |
| } | |
| @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'); | |
| } | |
| } | |
| } | |
| class _ArcPaintPredicate extends _DrawCommandPaintPredicate { | |
| _ArcPaintPredicate({ | |
| this.rect, | |
| this.startAngle, | |
| this.sweepAngle, | |
| this.useCenter, | |
| super.color, | |
| super.strokeWidth, | |
| super.hasMaskFilter, | |
| super.style, | |
| super.strokeCap, | |
| }) : super(#drawArc, 'an arc', 5, 4); | |
| final Rect? rect; | |
| final double? startAngle; | |
| final double? sweepAngle; | |
| final bool? useCenter; | |
| @override | |
| void verifyArguments(List<dynamic> arguments) { | |
| super.verifyArguments(arguments); | |
| final rectArgument = arguments[0] as Rect; | |
| if (rect != null && rectArgument != rect) { | |
| throw FlutterError( | |
| 'It called $methodName with a paint whose rect, $rectArgument, was not ' | |
| 'exactly the expected rect ($rect).', | |
| ); | |
| } | |
| final startAngleArgument = arguments[1] as double; | |
| if (startAngle != null && startAngleArgument != startAngle) { | |
| throw FlutterError( | |
| 'It called $methodName with a start angle, $startAngleArgument, which ' | |
| 'was not exactly the expected start angle ($startAngle).', | |
| ); | |
| } | |
| final sweepAngleArgument = arguments[2] as double; | |
| if (sweepAngle != null && sweepAngleArgument != sweepAngle) { | |
| throw FlutterError( | |
| 'It called $methodName with a sweep angle, $sweepAngleArgument, which ' | |
| 'was not exactly the expected sweep angle ($sweepAngle).', | |
| ); | |
| } | |
| final useCenterArgument = arguments[3] as bool; | |
| if (useCenter != null && useCenterArgument != useCenter) { | |
| throw FlutterError( | |
| 'It called $methodName with a useCenter value, $useCenterArgument, ' | |
| 'which was not exactly the expected value ($useCenter).', | |
| ); | |
| } | |
| } | |
| @override | |
| void debugFillDescription(List<String> description) { | |
| super.debugFillDescription(description); | |
| if (rect != null) { | |
| description.add('rect $rect'); | |
| } | |
| if (startAngle != null) { | |
| description.add('startAngle $startAngle'); | |
| } | |
| if (sweepAngle != null) { | |
| description.add('sweepAngle $sweepAngle'); | |
| } | |
| if (useCenter != null) { | |
| description.add('useCenter $useCenter'); | |
| } | |
| } | |
| } | |
| class _ShadowPredicate extends _PaintPredicate { | |
| _ShadowPredicate({ | |
| this.includes, | |
| this.excludes, | |
| this.color, | |
| this.elevation, | |
| this.transparentOccluder, | |
| }); | |
| final Iterable<Offset>? includes; | |
| final Iterable<Offset>? excludes; | |
| final Color? color; | |
| final double? elevation; | |
| final bool? transparentOccluder; | |
| static const Symbol symbol = #drawShadow; | |
| String get methodName => _symbolName(symbol); | |
| @protected | |
| void verifyArguments(List<dynamic> arguments) { | |
| if (arguments.length != 4) { | |
| throw FlutterError('It called $methodName with ${arguments.length} arguments; expected 4.'); | |
| } | |
| final pathArgument = arguments[0] as Path; | |
| if (includes != null) { | |
| for (final Offset offset in includes!) { | |
| if (!pathArgument.contains(offset)) { | |
| throw FlutterError( | |
| 'It called $methodName with a path that unexpectedly did not ' | |
| 'contain $offset.', | |
| ); | |
| } | |
| } | |
| } | |
| if (excludes != null) { | |
| for (final Offset offset in excludes!) { | |
| if (pathArgument.contains(offset)) { | |
| throw FlutterError( | |
| 'It called $methodName with a path that unexpectedly contained ' | |
| '$offset.', | |
| ); | |
| } | |
| } | |
| } | |
| final actualColor = arguments[1] as Color; | |
| if (color != null && !_colorsMatch(actualColor, color)) { | |
| throw FlutterError( | |
| 'It called $methodName with a color, $actualColor, which was not ' | |
| 'exactly the expected color ($color).', | |
| ); | |
| } | |
| final actualElevation = arguments[2] as double; | |
| if (elevation != null && actualElevation != elevation) { | |
| throw FlutterError( | |
| 'It called $methodName with an elevation, $actualElevation, which was ' | |
| 'not exactly the expected value ($elevation).', | |
| ); | |
| } | |
| final actualTransparentOccluder = arguments[3] as bool; | |
| if (transparentOccluder != null && actualTransparentOccluder != transparentOccluder) { | |
| throw FlutterError( | |
| '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 description = <String>[]; | |
| debugFillDescription(description); | |
| String result = methodName; | |
| if (description.isNotEmpty) { | |
| result += ' with ${description.join(", ")}'; | |
| } | |
| return result; | |
| } | |
| } | |
| class _DrawImagePaintPredicate extends _DrawCommandPaintPredicate { | |
| _DrawImagePaintPredicate({ | |
| this.image, | |
| this.x, | |
| this.y, | |
| super.color, | |
| super.strokeWidth, | |
| super.hasMaskFilter, | |
| super.style, | |
| }) : super(#drawImage, 'an image', 3, 2); | |
| final ui.Image? image; | |
| final double? x; | |
| final double? y; | |
| @override | |
| void verifyArguments(List<dynamic> arguments) { | |
| super.verifyArguments(arguments); | |
| final imageArgument = arguments[0] as ui.Image; | |
| if (image != null && !image!.isCloneOf(imageArgument)) { | |
| throw FlutterError( | |
| 'It called $methodName with an image, $imageArgument, which was not ' | |
| 'exactly the expected image ($image).', | |
| ); | |
| } | |
| final pointArgument = arguments[0] as Offset; | |
| if (x != null && y != null) { | |
| final point = Offset(x!, y!); | |
| if (point != pointArgument) { | |
| throw FlutterError( | |
| 'It called $methodName with an offset coordinate, $pointArgument, ' | |
| 'which was not exactly the expected coordinate ($point).', | |
| ); | |
| } | |
| } else { | |
| if (x != null && pointArgument.dx != x) { | |
| throw FlutterError( | |
| 'It called $methodName with an offset coordinate, $pointArgument, ' | |
| 'whose x-coordinate was not exactly the expected coordinate ' | |
| '(${x!.toStringAsFixed(1)}).', | |
| ); | |
| } | |
| if (y != null && pointArgument.dy != y) { | |
| throw FlutterError( | |
| 'It called $methodName with an offset coordinate, $pointArgument, ' | |
| 'whose y-coordinate was not exactly the expected coordinate ' | |
| '(${y!.toStringAsFixed(1)}).', | |
| ); | |
| } | |
| } | |
| } | |
| @override | |
| void debugFillDescription(List<String> description) { | |
| super.debugFillDescription(description); | |
| if (image != null) { | |
| description.add('image $image'); | |
| } | |
| if (x != null && y != null) { | |
| description.add('point ${Offset(x!, y!)}'); | |
| } else { | |
| if (x != null) { | |
| description.add('x-coordinate ${x!.toStringAsFixed(1)}'); | |
| } | |
| if (y != null) { | |
| description.add('y-coordinate ${y!.toStringAsFixed(1)}'); | |
| } | |
| } | |
| } | |
| } | |
| class _DrawImageRectPaintPredicate extends _DrawCommandPaintPredicate { | |
| _DrawImageRectPaintPredicate({ | |
| this.image, | |
| this.source, | |
| this.destination, | |
| super.color, | |
| super.strokeWidth, | |
| super.hasMaskFilter, | |
| super.style, | |
| }) : super(#drawImageRect, 'an image', 4, 3); | |
| final ui.Image? image; | |
| final Rect? source; | |
| final Rect? destination; | |
| @override | |
| void verifyArguments(List<dynamic> arguments) { | |
| super.verifyArguments(arguments); | |
| final imageArgument = arguments[0] as ui.Image; | |
| if (image != null && !image!.isCloneOf(imageArgument)) { | |
| throw FlutterError( | |
| 'It called $methodName with an image, $imageArgument, which was not ' | |
| 'exactly the expected image ($image).', | |
| ); | |
| } | |
| final sourceArgument = arguments[1] as Rect; | |
| if (source != null && sourceArgument != source) { | |
| throw FlutterError( | |
| 'It called $methodName with a source rectangle, $sourceArgument, which ' | |
| 'was not exactly the expected rectangle ($source).', | |
| ); | |
| } | |
| final destinationArgument = arguments[2] as Rect; | |
| if (destination != null && destinationArgument != destination) { | |
| throw FlutterError( | |
| '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'); | |
| } | |
| } | |
| } | |
| class _SomethingPaintPredicate extends _PaintPredicate { | |
| _SomethingPaintPredicate(this.predicate); | |
| final PaintPatternPredicate predicate; | |
| @override | |
| void match(Iterator<RecordedInvocation> call) { | |
| RecordedInvocation currentCall; | |
| var testedAllCalls = false; | |
| do { | |
| if (testedAllCalls) { | |
| throw FlutterError( | |
| 'It painted methods that the predicate passed to a "something" step, ' | |
| 'in the paint pattern, none of which were considered correct.', | |
| ); | |
| } | |
| currentCall = call.current; | |
| if (!currentCall.invocation.isMethod) { | |
| throw FlutterError( | |
| 'It called $currentCall, which was not a method, when the paint ' | |
| 'pattern expected a method call', | |
| ); | |
| } | |
| testedAllCalls = !call.moveNext(); | |
| } while (!_runPredicate( | |
| currentCall.invocation.memberName, | |
| currentCall.invocation.positionalArguments, | |
| )); | |
| } | |
| bool _runPredicate(Symbol methodName, List<dynamic> arguments) { | |
| try { | |
| return predicate(methodName, arguments); | |
| } on String catch (s) { | |
| throw FlutterError( | |
| 'It painted something that the predicate passed to a "something" step ' | |
| 'in the paint pattern considered incorrect:\n $s\n ', | |
| ); | |
| } | |
| } | |
| @override | |
| String toString() => 'a "something" step'; | |
| } | |
| class _EverythingPaintPredicate extends _PaintPredicate { | |
| _EverythingPaintPredicate(this.predicate); | |
| final PaintPatternPredicate predicate; | |
| @override | |
| void match(Iterator<RecordedInvocation> call) { | |
| do { | |
| final RecordedInvocation currentCall = call.current; | |
| if (!currentCall.invocation.isMethod) { | |
| throw FlutterError( | |
| 'It called $currentCall, which was not a method, when the paint ' | |
| 'pattern expected a method call', | |
| ); | |
| } | |
| if (!_runPredicate( | |
| currentCall.invocation.memberName, | |
| currentCall.invocation.positionalArguments, | |
| )) { | |
| throw FlutterError( | |
| 'It painted something that the predicate passed to an "everything" ' | |
| 'step in the paint pattern considered incorrect.\n', | |
| ); | |
| } | |
| } while (call.moveNext()); | |
| } | |
| bool _runPredicate(Symbol methodName, List<dynamic> arguments) { | |
| try { | |
| return predicate(methodName, arguments); | |
| } on String catch (s) { | |
| throw FlutterError( | |
| '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'; | |
| } | |
| class _FunctionPaintPredicate extends _PaintPredicate { | |
| _FunctionPaintPredicate(this.symbol, this.arguments); | |
| final Symbol symbol; | |
| final List<dynamic> arguments; | |
| @override | |
| void match(Iterator<RecordedInvocation> call) { | |
| checkMethod(call, symbol); | |
| if (call.current.invocation.positionalArguments.length != arguments.length) { | |
| throw FlutterError( | |
| 'It called ${_symbolName(symbol)} with ' | |
| '${call.current.invocation.positionalArguments.length} arguments; ' | |
| 'expected ${arguments.length}.', | |
| ); | |
| } | |
| for (var index = 0; index < arguments.length; index += 1) { | |
| final dynamic actualArgument = call.current.invocation.positionalArguments[index]; | |
| final dynamic desiredArgument = arguments[index]; | |
| if (desiredArgument is Matcher) { | |
| expect(actualArgument, desiredArgument); | |
| } else if (desiredArgument != null && desiredArgument != actualArgument) { | |
| throw FlutterError( | |
| 'It called ${_symbolName(symbol)} with argument $index having value ' | |
| '${_valueName(actualArgument)} when ${_valueName(desiredArgument)} ' | |
| 'was expected.', | |
| ); | |
| } | |
| } | |
| call.moveNext(); | |
| } | |
| @override | |
| String toString() { | |
| final adjectives = <String>[ | |
| for (int index = 0; index < arguments.length; index += 1) | |
| if (arguments[index] != null) _valueName(arguments[index]) else '...', | |
| ]; | |
| return '${_symbolName(symbol)}(${adjectives.join(", ")})'; | |
| } | |
| } | |
| class _SaveRestorePairPaintPredicate extends _PaintPredicate { | |
| @override | |
| void match(Iterator<RecordedInvocation> call) { | |
| checkMethod(call, #save); | |
| var depth = 1; | |
| while (depth > 0) { | |
| if (!call.moveNext()) { | |
| throw FlutterError( | |
| '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) { | |
| depth += 1; | |
| } else if (call.current.invocation.memberName == #restore) { | |
| depth -= 1; | |
| } | |
| } | |
| } | |
| call.moveNext(); | |
| } | |
| @override | |
| String toString() => 'a matching save/restore pair'; | |
| } | |
| String _valueName(Object? value) { | |
| if (value is double) { | |
| return value.toStringAsFixed(1); | |
| } | |
| return value.toString(); | |
| } | |
| // Workaround for https://github.com/dart-lang/sdk/issues/28372 | |
| String _symbolName(Symbol symbol) { | |
| // WARNING: Assumes a fixed format for Symbol.toString which is *not* | |
| // guaranteed anywhere. | |
| final s = '$symbol'; | |
| return s.substring(8, s.length - 2); | |
| } |