2176 lines
67 KiB
Dart
2176 lines
67 KiB
Dart
// 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)
|
|
arguments[index] != null ? _valueName(arguments[index]) : '...',
|
|
];
|
|
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);
|
|
}
|