Commit ca94bfdf authored by Ian Hickson's avatar Ian Hickson Committed by Todd Volkert

Factor our common Paint-building code used with BoxShadow (#17363)

parent 57322195
0ea80e6a0147f1a3a59ff57f460a3f038a0d2748
e3f3b6766b18e2461c89a371be6e30045d8e404f
......@@ -29,6 +29,7 @@ export 'src/painting/box_fit.dart';
export 'src/painting/box_shadow.dart';
export 'src/painting/circle_border.dart';
export 'src/painting/colors.dart';
export 'src/painting/debug.dart';
export 'src/painting/decoration.dart';
export 'src/painting/decoration_image.dart';
export 'src/painting/edge_insets.dart';
......
......@@ -686,9 +686,7 @@ class _RenderMergeableMaterialListBody extends RenderListBody {
void _paintShadows(Canvas canvas, Rect rect) {
for (BoxShadow boxShadow in boxShadows) {
final Paint paint = new Paint()
..color = boxShadow.color
..maskFilter = new MaskFilter.blur(BlurStyle.normal, boxShadow.blurSigma);
final Paint paint = boxShadow.toPaint();
// TODO(dragostis): Right now, we are only interpolating the border radii
// of the visible Material slices, not the shadows; they are not getting
// interpolated and always have the same rounded radii. Once shadow
......
......@@ -366,9 +366,7 @@ class _BoxDecorationPainter extends BoxPainter {
if (_decoration.boxShadow == null)
return;
for (BoxShadow boxShadow in _decoration.boxShadow) {
final Paint paint = new Paint()
..color = boxShadow.color
..maskFilter = new MaskFilter.blur(BlurStyle.normal, boxShadow.blurSigma);
final Paint paint = boxShadow.toPaint();
final Rect bounds = rect.shift(boxShadow.offset).inflate(boxShadow.spreadRadius);
_paintBox(canvas, bounds, paint, textDirection);
}
......
......@@ -8,13 +8,18 @@ import 'dart:ui' as ui show lerpDouble;
import 'package:flutter/foundation.dart';
import 'basic_types.dart';
import 'debug.dart';
/// A shadow cast by a box.
///
/// BoxShadow can cast non-rectangular shadows if the box is non-rectangular
/// [BoxShadow] can cast non-rectangular shadows if the box is non-rectangular
/// (e.g., has a border radius or a circular shape).
///
/// This class is similar to CSS box-shadow.
///
/// See also:
///
/// * [Canvas.drawShadow], which is a more efficient way to draw shadows.
@immutable
class BoxShadow {
/// Creates a box shadow.
......@@ -55,6 +60,24 @@ class BoxShadow {
/// See the sigma argument to [MaskFilter.blur].
double get blurSigma => convertRadiusToSigma(blurRadius);
/// Create the [Paint] object that corresponds to this shadow description.
///
/// The [offset] and [spreadRadius] are not represented in the [Paint] object.
/// To honor those as well, the shape should be inflated by [spreadRadius] pixels
/// in every direction and then translated by [offset] before being filled using
/// this [Paint].
Paint toPaint() {
final Paint result = new Paint()
..color = color
..maskFilter = new MaskFilter.blur(BlurStyle.normal, blurSigma);
assert(() {
if (debugDisableShadows)
result.maskFilter = null;
return true;
}());
return result;
}
/// Returns a new box shadow with its offset, blurRadius, and spreadRadius scaled by the given factor.
BoxShadow scale(double factor) {
return new BoxShadow(
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
/// Whether to replace all shadows with solid color blocks.
///
/// This is useful when writing golden file tests (see [matchesGoldenFile]) since
/// the rendering of shadows is not guaranteed to be pixel-for-pixel identical from
/// version to version (or even from run to run).
bool debugDisableShadows = false;
/// Returns true if none of the painting library debug variables have been changed.
///
/// This function is used by the test framework to ensure that debug variables
/// haven't been inadvertently changed.
///
/// See <https://docs.flutter.io/flutter/rendering/painting-library.html> for
/// a complete list.
///
/// The `debugDisableShadowsOverride` argument can be provided to override
/// the expected value for [debugDisableShadows]. (This exists because the
/// test framework itself overrides this value in some cases.)
bool debugAssertAllPaintingVarsUnset(String reason, { bool debugDisableShadowsOverride: false }) {
assert(() {
if (debugDisableShadows != debugDisableShadowsOverride) {
throw new FlutterError(reason);
}
return true;
}());
return true;
}
......@@ -328,12 +328,8 @@ class _ShapeDecorationPainter extends BoxPainter {
_shadowCount = _decoration.shadows.length;
_shadowPaths = new List<Path>(_shadowCount);
_shadowPaints = new List<Paint>(_shadowCount);
for (int index = 0; index < _shadowCount; index += 1) {
final BoxShadow shadow = _decoration.shadows[index];
_shadowPaints[index] = new Paint()
..color = shadow.color
..maskFilter = new MaskFilter.blur(BlurStyle.normal, shadow.blurSigma);
}
for (int index = 0; index < _shadowCount; index += 1)
_shadowPaints[index] = _decoration.shadows[index].toPaint();
}
for (int index = 0; index < _shadowCount; index += 1) {
final BoxShadow shadow = _decoration.shadows[index];
......
......@@ -818,6 +818,12 @@ class PhysicalModelLayer extends ContainerLayer {
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
///
/// In tests, the [debugDisableShadows] flag is set to true by default.
/// Several widgets and render objects force all elevations to zero when this
/// flag is set. For this reason, this property will often be set to zero in
/// tests even if the layer should be raised. To verify the actual value,
/// consider setting [debugDisableShadows] to false in your test.
double elevation;
/// The background color.
......
......@@ -1449,6 +1449,9 @@ abstract class _RenderPhysicalModelBase<T> extends _RenderCustomClip<T> {
super(child: child, clipper: clipper);
/// The z-coordinate at which to place this material.
///
/// If [debugDisableShadows] is set, this value is ignored and no shadow is
/// drawn (an outline is rendered instead).
double get elevation => _elevation;
double _elevation;
set elevation(double value) {
......@@ -1591,20 +1594,34 @@ class RenderPhysicalModel extends _RenderPhysicalModelBase<RRect> {
void paint(PaintingContext context, Offset offset) {
if (child != null) {
_updateClip();
final RRect offsetClipRRect = _clip.shift(offset);
final Rect offsetBounds = offsetClipRRect.outerRect;
final Path offsetClipPath = new Path()..addRRect(offsetClipRRect);
final RRect offsetRRect = _clip.shift(offset);
final Rect offsetBounds = offsetRRect.outerRect;
final Path offsetRRectAsPath = new Path()..addRRect(offsetRRect);
bool paintShadows = true;
assert(() {
if (debugDisableShadows) {
context.canvas.drawRRect(
offsetRRect,
new Paint()
..color = shadowColor
..style = PaintingStyle.stroke
..strokeWidth = elevation * 2.0,
);
paintShadows = false;
}
return true;
}());
if (needsCompositing) {
final PhysicalModelLayer physicalModel = new PhysicalModelLayer(
clipPath: offsetClipPath,
elevation: elevation,
clipPath: offsetRRectAsPath,
elevation: paintShadows ? elevation : 0.0,
color: color,
shadowColor: shadowColor,
);
context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds);
} else {
final Canvas canvas = context.canvas;
if (elevation != 0.0) {
if (elevation != 0.0 && paintShadows) {
// The drawShadow call doesn't add the region of the shadow to the
// picture's bounds, so we draw a hardcoded amount of extra space to
// account for the maximum potential area of the shadow.
......@@ -1614,25 +1631,25 @@ class RenderPhysicalModel extends _RenderPhysicalModelBase<RRect> {
_RenderPhysicalModelBase._transparentPaint,
);
canvas.drawShadow(
offsetClipPath,
offsetRRectAsPath,
shadowColor,
elevation,
color.alpha != 0xFF,
);
}
canvas.drawRRect(offsetClipRRect, new Paint()..color = color);
canvas.drawRRect(offsetRRect, new Paint()..color = color);
canvas.save();
canvas.clipRRect(offsetClipRRect);
canvas.clipRRect(offsetRRect);
// We only use a new layer for non-rectangular clips, on the basis that
// rectangular clips won't need antialiasing. This is not really
// correct, because if we're e.g. rotated, rectangles will also be
// aliased. Unfortunately, it's too much of a performance win to err on
// the side of correctness here.
// TODO(ianh): Find a better solution.
if (!offsetClipRRect.isRect)
if (!offsetRRect.isRect)
canvas.saveLayer(offsetBounds, _RenderPhysicalModelBase._defaultPaint);
super.paint(context, offset);
if (!offsetClipRRect.isRect)
if (!offsetRRect.isRect)
canvas.restore();
canvas.restore();
assert(context.canvas == canvas, 'canvas changed even though needsCompositing was false');
......@@ -1701,17 +1718,31 @@ class RenderPhysicalShape extends _RenderPhysicalModelBase<Path> {
_updateClip();
final Rect offsetBounds = offset & size;
final Path offsetPath = _clip.shift(offset);
bool paintShadows = true;
assert(() {
if (debugDisableShadows) {
context.canvas.drawPath(
offsetPath,
new Paint()
..color = shadowColor
..style = PaintingStyle.stroke
..strokeWidth = elevation * 2.0,
);
paintShadows = false;
}
return true;
}());
if (needsCompositing) {
final PhysicalModelLayer physicalModel = new PhysicalModelLayer(
clipPath: offsetPath,
elevation: elevation,
elevation: paintShadows ? elevation : 0.0,
color: color,
shadowColor: shadowColor,
);
context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds);
} else {
final Canvas canvas = context.canvas;
if (elevation != 0.0) {
if (elevation != 0.0 && paintShadows) {
// The drawShadow call doesn't add the region of the shadow to the
// picture's bounds, so we draw a hardcoded amount of extra space to
// account for the maximum potential area of the shadow.
......
......@@ -5,6 +5,7 @@
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'basic.dart';
import 'debug.dart';
......@@ -107,15 +108,18 @@ class BannerPainter extends CustomPainter {
/// Defaults to bold, white text.
final TextStyle textStyle;
static const BoxShadow _shadow = const BoxShadow(
color: const Color(0x7F000000),
blurRadius: 4.0,
);
bool _prepared = false;
TextPainter _textPainter;
Paint _paintShadow;
Paint _paintBanner;
void _prepare() {
_paintShadow = new Paint()
..color = const Color(0x7F000000)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0);
_paintShadow = _shadow.toPaint();
_paintBanner = new Paint()
..color = color;
_textPainter = new TextPainter(
......
......@@ -52,6 +52,7 @@ export 'package:flutter/rendering.dart' show
RelativeRect,
SemanticsBuilderCallback,
ShaderCallback,
ShapeBorderClipper,
SingleChildLayoutDelegate,
StackFit,
TextOverflow,
......@@ -731,6 +732,11 @@ class PhysicalModel extends SingleChildRenderObjectWidget {
///
/// [PhysicalModel] does the same but only supports shapes that can be expressed
/// as rectangles with rounded corners.
///
/// See also:
///
/// * [ShapeBorderClipper], which converts a [ShapeBorder] to a [CustomerClipper], as
/// needed by this widget.
class PhysicalShape extends SingleChildRenderObjectWidget {
/// Creates a physical model with an arbitrary shape clip.
///
......@@ -751,6 +757,10 @@ class PhysicalShape extends SingleChildRenderObjectWidget {
super(key: key, child: child);
/// Determines which clip to use.
///
/// If the path in question is expressed as a [ShapeBorder] subclass,
/// consider using the [ShapeBorderClipper] delegate class to adapt the
/// shape for use with this widget.
final CustomClipper<Path> clipper;
/// The z-coordinate at which to place this physical object.
......
......@@ -198,6 +198,7 @@ void main() {
});
testWidgets('MergeableMaterial paints shadows', (WidgetTester tester) async {
debugDisableShadows = false;
await tester.pumpWidget(
new MaterialApp(
home: new Scaffold(
......@@ -226,6 +227,7 @@ void main() {
find.byType(MergeableMaterial),
paints..rrect(rrect: rrect, color: boxShadow.color, hasMaskFilter: true),
);
debugDisableShadows = true;
});
testWidgets('MergeableMaterial merge gap', (WidgetTester tester) async {
......
......@@ -45,6 +45,7 @@ void main() {
testWidgets('Outline shape and border overrides', (WidgetTester tester) async {
debugDisableShadows = false;
const Color fillColor = const Color(0xFF00FF00);
const Color borderColor = const Color(0xFFFF0000);
const Color highlightedBorderColor = const Color(0xFF0000FF);
......@@ -111,6 +112,7 @@ void main() {
..clipPath(pathMatcher: coversSameAreaAs(clipPath, areaToCompare: clipRect.inflate(10.0)))
..path(color: borderColor, strokeWidth: borderWidth)
);
debugDisableShadows = true;
});
......
......@@ -325,6 +325,10 @@ abstract class PaintPattern {
/// 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
......
......@@ -248,6 +248,7 @@ void main() {
});
testWidgets('Banner widget', (WidgetTester tester) async {
debugDisableShadows = false;
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
......@@ -263,9 +264,11 @@ void main() {
..paragraph(offset: const Offset(-40.0, 29.0))
..restore()
);
debugDisableShadows = true;
});
testWidgets('Banner widget in MaterialApp', (WidgetTester tester) async {
debugDisableShadows = false;
await tester.pumpWidget(new MaterialApp(home: const Placeholder()));
expect(find.byType(CheckedModeBanner), paints
..save()
......@@ -276,5 +279,6 @@ void main() {
..paragraph(offset: const Offset(-40.0, 29.0))
..restore()
);
debugDisableShadows = true;
});
}
......@@ -344,6 +344,7 @@ void main() {
});
testWidgets('NestedScrollView and internal scrolling', (WidgetTester tester) async {
debugDisableShadows = false;
const List<String> _tabs = const <String>['Hello', 'World'];
int buildCount = 0;
await tester.pumpWidget(
......@@ -565,6 +566,7 @@ void main() {
await tester.pumpAndSettle();
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
debugDisableShadows = true;
});
testWidgets('NestedScrollView and iOS bouncing', (WidgetTester tester) async {
......
......@@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('PhysicalModel - creates a physical model layer when it needs compositing', (WidgetTester tester) async {
debugDisableShadows = false;
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new PhysicalModel(
......@@ -25,5 +26,6 @@ void main() {
expect(physicalModelLayer.shadowColor, Colors.red);
expect(physicalModelLayer.color, Colors.grey);
expect(physicalModelLayer.elevation, 1.0);
debugDisableShadows = true;
});
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io' show Platform;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
void main() {
testWidgets('Shadows on BoxDecoration', (WidgetTester tester) async {
await tester.pumpWidget(
new Center(
child: new RepaintBoundary(
child: new Container(
margin: const EdgeInsets.all(50.0),
decoration: new BoxDecoration(
boxShadow: kElevationToShadow[9],
),
height: 100.0,
width: 100.0,
),
),
),
);
await expectLater(
find.byType(Container),
matchesGoldenFile('shadow.BoxDecoration.disabled.png'),
);
debugDisableShadows = false;
tester.binding.reassembleApplication();
await tester.pump();
if (Platform.isLinux) {
// TODO(ianh): use the skip argument instead once that doesn't hang, https://github.com/dart-lang/test/issues/830
await expectLater(
find.byType(Container),
matchesGoldenFile('shadow.BoxDecoration.enabled.png'),
); // shadows render differently on different platforms
}
debugDisableShadows = true;
});
testWidgets('Shadows on ShapeDecoration', (WidgetTester tester) async {
debugDisableShadows = false;
Widget build(int elevation) {
return new Center(
child: new RepaintBoundary(
child: new Container(
margin: const EdgeInsets.all(150.0),
decoration: new ShapeDecoration(
shape: new BeveledRectangleBorder(borderRadius: new BorderRadius.circular(20.0)),
shadows: kElevationToShadow[elevation],
),
height: 100.0,
width: 100.0,
),
),
);
}
for (int elevation in kElevationToShadow.keys) {
await tester.pumpWidget(build(elevation));
await expectLater(
find.byType(Container),
matchesGoldenFile('shadow.ShapeDecoration.$elevation.png'),
);
}
debugDisableShadows = true;
}, skip: !Platform.isLinux); // shadows render differently on different platforms
testWidgets('Shadows with PhysicalLayer', (WidgetTester tester) async {
await tester.pumpWidget(
new Center(
child: new RepaintBoundary(
child: new Container(
margin: const EdgeInsets.all(150.0),
color: Colors.yellow[200],
child: new PhysicalModel(
elevation: 9.0,
color: Colors.blue[900],
child: const SizedBox(
height: 100.0,
width: 100.0,
),
),
),
),
),
);
await expectLater(
find.byType(Container),
matchesGoldenFile('shadow.PhysicalModel.disabled.png'),
);
debugDisableShadows = false;
tester.binding.reassembleApplication();
await tester.pump();
if (Platform.isLinux) {
// TODO(ianh): use the skip argument instead once that doesn't hang, https://github.com/dart-lang/test/issues/830
await expectLater(
find.byType(Container),
matchesGoldenFile('shadow.PhysicalModel.enabled.png'),
); // shadows render differently on different platforms
}
debugDisableShadows = true;
});
testWidgets('Shadows with PhysicalShape', (WidgetTester tester) async {
debugDisableShadows = false;
Widget build(double elevation) {
return new Center(
child: new RepaintBoundary(
child: new Container(
padding: const EdgeInsets.all(150.0),
color: Colors.yellow[200],
child: new PhysicalShape(
color: Colors.green[900],
clipper: new ShapeBorderClipper(shape: new BeveledRectangleBorder(borderRadius: new BorderRadius.circular(20.0))),
elevation: elevation,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
),
),
),
);
}
for (int elevation in kElevationToShadow.keys) {
await tester.pumpWidget(build(elevation.toDouble()));
await expectLater(
find.byType(Container),
matchesGoldenFile('shadow.PhysicalShape.$elevation.png'),
);
}
debugDisableShadows = true;
}, skip: !Platform.isLinux); // shadows render differently on different platforms
}
......@@ -14,6 +14,10 @@ import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
// If you are here trying to figure out how to use golden files in the Flutter
// repo itself, consider reading this wiki page:
// https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package%3Aflutter
const String _kFlutterRootKey = 'FLUTTER_ROOT';
/// Main method that can be used in a `flutter_test_config.dart` file to set
......@@ -105,20 +109,42 @@ class FlutterGoldenFileComparator implements GoldenFileComparator {
/// repository.
@visibleForTesting
class GoldensClient {
/// Create a handle to a local clone of the goldens repository.
GoldensClient({
this.fs: const LocalFileSystem(),
this.platform: const LocalPlatform(),
this.process: const LocalProcessManager(),
});
/// The file system to use for storing the local clone of the repository.
///
/// This is useful in tests, where a local file system (the default) can
/// be replaced by a memory file system.
final FileSystem fs;
/// A wrapper for the [dart:io.Platform] API.
///
/// This is useful in tests, where the system platform (the default) can
/// be replaced by a mock platform instance.
final Platform platform;
/// A controller for launching subprocesses.
///
/// This is useful in tests, where the real process manager (the default)
/// can be replaced by a mock process manager that doesn't really create
/// subprocesses.
final ProcessManager process;
RandomAccessFile _lock;
/// The local [Directory] where the Flutter repository is hosted.
///
/// Uses the [fs] file system.
Directory get flutterRoot => fs.directory(platform.environment[_kFlutterRootKey]);
/// The local [Directory] where the goldens repository is hosted.
///
/// Uses the [fs] file system.
Directory get repositoryRoot => flutterRoot.childDirectory(fs.path.join('bin', 'cache', 'pkg', 'goldens'));
/// Prepares the local clone of the `flutter/goldens` repository for golden
......@@ -222,9 +248,17 @@ class GoldensClient {
/// Exception that signals a process' exit with a non-zero exit code.
class NonZeroExitCode implements Exception {
/// Create an exception that represents a non-zero exit code.
///
/// The first argument must be non-zero.
const NonZeroExitCode(this.exitCode, this.stderr) : assert(exitCode != 0);
/// The code that the process will signal to th eoperating system.
///
/// By definiton, this is not zero.
final int exitCode;
/// The message to show on standard error.
final String stderr;
@override
......
......@@ -96,6 +96,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
/// [debugPrintOverride], which can be overridden by subclasses.
TestWidgetsFlutterBinding() {
debugPrint = debugPrintOverride;
debugDisableShadows = disableShadows;
debugCheckIntrinsicSizes = checkIntrinsicSizes;
}
......@@ -108,6 +109,15 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
@protected
DebugPrintCallback get debugPrintOverride => debugPrint;
/// The value to set [debugDisableShadows] to while tests are running.
///
/// This can be used to reduce the likelihood of golden file tests being
/// flaky, because shadow rendering is not always deterministic. The
/// [AutomatedTestWidgetsFlutterBinding] sets this to true, so that all tests
/// always run with shadows disabled.
@protected
bool get disableShadows => false;
/// The value to set [debugCheckIntrinsicSizes] to while tests are running.
///
/// This can be used to enable additional checks. For example,
......@@ -525,6 +535,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
assert(debugAssertAllGesturesVarsUnset(
'The value of a gestures debug variable was changed by the test.',
));
assert(debugAssertAllPaintingVarsUnset(
'The value of a painting debug variable was changed by the test.',
debugDisableShadowsOverride: disableShadows,
));
assert(debugAssertAllRenderVarsUnset(
'The value of a rendering debug variable was changed by the test.',
debugCheckIntrinsicSizesOverride: checkIntrinsicSizes,
......@@ -588,6 +602,9 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
@override
DebugPrintCallback get debugPrintOverride => debugPrintSynchronously;
@override
bool get disableShadows => true;
@override
bool get checkIntrinsicSizes => true;
......
......@@ -51,11 +51,19 @@ abstract class GoldenFileComparator {
///
/// This comparator is used as the backend for [matchesGoldenFile].
///
/// The default comparator, [LocalFileComparator], will treat the golden key as
/// When using `flutter test`, a comparator implemented by [LocalFileComparator]
/// is used if no other comparator is specified. It treats the golden key as
/// a relative path from the test file's directory. It will then load the
/// golden file's bytes from disk and perform a byte-for-byte comparison of the
/// encoded PNGs, returning true only if there's an exact match.
///
/// When using `flutter test --update-goldens`, the [LocalFileComparator]
/// updates the files on disk to match the rendering.
///
/// When using `flutter run`, the default comparator (null) is used. It prints
/// a message to the console but otherwise does nothing. This allows tests to
/// be developed visually on a real device.
///
/// Callers may choose to override the default comparator by setting this to a
/// custom comparator during test set-up (or using directory-level test
/// configuration). For example, some projects may wish to install a more
......@@ -119,7 +127,7 @@ class _UninitializedComparator implements GoldenFileComparator {
}
}
/// The default [GoldenFileComparator] implementation.
/// The default [GoldenFileComparator] implementation for `flutter test`.
///
/// This comparator loads golden files from the local file system, treating the
/// golden key as a relative path from the test file's directory.
......@@ -128,13 +136,16 @@ class _UninitializedComparator implements GoldenFileComparator {
/// comparison of the encoded PNGs, returning true only if there's an exact
/// match. This means it will fail the test if two PNGs represent the same
/// pixels but are encoded differently.
///
/// When using `flutter test --update-goldens`, [LocalFileComparator]
/// updates the files on disk to match the rendering.
class LocalFileComparator implements GoldenFileComparator {
/// Creates a new [LocalFileComparator] for the specified [testFile].
///
/// Golden file keys will be interpreted as file paths relative to the
/// directory in which [testFile] resides.
///
/// The [testFile] URI must represent a file.
/// The [testFile] URL must represent a file.
LocalFileComparator(Uri testFile, {path.Style pathStyle})
: basedir = _getBasedir(testFile, pathStyle),
_path = _getPath(pathStyle);
......
......@@ -177,7 +177,8 @@ Future<void> expectLater(dynamic actual, dynamic matcher, {
// We can't wrap the delegate in a guard, or we'll hit async barriers in
// [TestWidgetsFlutterBinding] while we're waiting for the matcher to complete
TestAsyncUtils.guardSync();
return test_package.expectLater(actual, matcher, reason: reason, skip: skip);
return test_package.expectLater(actual, matcher, reason: reason, skip: skip)
.then<void>((dynamic value) => null);
}
/// Class that programmatically interacts with widgets and the test environment.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment