Commit fad36baa authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Fix WidgetsApp to wrap its contents in a Directionality (#12018)

...rather than only wrapping the children.

Also, improvements to Banner to better support RTL, and mock_canvas to
make debugging things easier.
parent c46347b6
...@@ -104,7 +104,7 @@ enum _WordWrapParseMode { inSpace, inWord, atBreak } ...@@ -104,7 +104,7 @@ enum _WordWrapParseMode { inSpace, inWord, atBreak }
/// ///
/// The default [debugPrint] implementation uses this for its line wrapping. /// The default [debugPrint] implementation uses this for its line wrapping.
Iterable<String> debugWordWrap(String message, int width, { String wrapIndent: '' }) sync* { Iterable<String> debugWordWrap(String message, int width, { String wrapIndent: '' }) sync* {
if (message.length < width || message[0] == '#') { if (message.length < width || message.trimLeft()[0] == '#') {
yield message; yield message;
return; return;
} }
......
...@@ -2293,7 +2293,8 @@ class RenderCustomPaint extends RenderProxyBox { ...@@ -2293,7 +2293,8 @@ class RenderCustomPaint extends RenderProxyBox {
int debugPreviousCanvasSaveCount; int debugPreviousCanvasSaveCount;
canvas.save(); canvas.save();
assert(() { debugPreviousCanvasSaveCount = canvas.getSaveCount(); return true; }); assert(() { debugPreviousCanvasSaveCount = canvas.getSaveCount(); return true; });
canvas.translate(offset.dx, offset.dy); if (offset != Offset.zero)
canvas.translate(offset.dx, offset.dy);
painter.paint(canvas, size); painter.paint(canvas, size);
assert(() { assert(() {
// This isn't perfect. For example, we can't catch the case of // This isn't perfect. For example, we can't catch the case of
......
...@@ -296,28 +296,18 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv ...@@ -296,28 +296,18 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget result = new MediaQuery( Widget result = new Navigator(
data: new MediaQueryData.fromWindow(ui.window), key: _navigator,
child: new Localizations( initialRoute: widget.initialRoute ?? ui.window.defaultRouteName,
locale: widget.locale ?? _locale, onGenerateRoute: widget.onGenerateRoute,
delegates: _localizationsDelegates.toList(), onUnknownRoute: widget.onUnknownRoute,
child: new Title( observers: widget.navigatorObservers,
title: widget.title,
color: widget.color,
child: new Navigator(
key: _navigator,
initialRoute: widget.initialRoute ?? ui.window.defaultRouteName,
onGenerateRoute: widget.onGenerateRoute,
onUnknownRoute: widget.onUnknownRoute,
observers: widget.navigatorObservers
)
)
)
); );
if (widget.textStyle != null) { if (widget.textStyle != null) {
result = new DefaultTextStyle( result = new DefaultTextStyle(
style: widget.textStyle, style: widget.textStyle,
child: result child: result,
); );
} }
...@@ -335,7 +325,6 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv ...@@ -335,7 +325,6 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
); );
} }
if (performanceOverlay != null) { if (performanceOverlay != null) {
result = new Stack( result = new Stack(
children: <Widget>[ children: <Widget>[
...@@ -344,11 +333,13 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv ...@@ -344,11 +333,13 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
] ]
); );
} }
if (widget.showSemanticsDebugger) { if (widget.showSemanticsDebugger) {
result = new SemanticsDebugger( result = new SemanticsDebugger(
child: result, child: result,
); );
} }
assert(() { assert(() {
if (widget.debugShowWidgetInspector || WidgetsApp.debugShowWidgetInspectorOverride) { if (widget.debugShowWidgetInspector || WidgetsApp.debugShowWidgetInspectorOverride) {
result = new WidgetInspector( result = new WidgetInspector(
...@@ -364,7 +355,17 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv ...@@ -364,7 +355,17 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
return true; return true;
}); });
return result; return new MediaQuery(
data: new MediaQueryData.fromWindow(ui.window),
child: new Localizations(
locale: widget.locale ?? _locale,
delegates: _localizationsDelegates.toList(),
child: new Title(
title: widget.title,
color: widget.color,
child: result,
),
),
);
} }
} }
...@@ -23,25 +23,30 @@ const TextStyle _kTextStyle = const TextStyle( ...@@ -23,25 +23,30 @@ const TextStyle _kTextStyle = const TextStyle(
); );
/// Where to show a [Banner]. /// Where to show a [Banner].
///
/// The start and end locations are relative to the ambient [Directionality]
/// (which can be overridden by [Banner.layoutDirection]).
enum BannerLocation { enum BannerLocation {
/// Show the banner in the top-right corner when the ambient [Directionality] /// Show the banner in the top-right corner when the ambient [Directionality]
/// is [TextDirection.rtl] and in the top-left corner when the ambient /// (or [Banner.layoutDirection]) is [TextDirection.rtl] and in the top-left
/// [Directionality] is [TextDirection.ltr]. /// corner when the ambient [Directionality] is [TextDirection.ltr].
topStart, topStart,
/// Show the banner in the top-left corner when the ambient [Directionality] /// Show the banner in the top-left corner when the ambient [Directionality]
/// is [TextDirection.rtl] and in the top-right corner when the ambient /// (or [Banner.layoutDirection]) is [TextDirection.rtl] and in the top-right
/// [Directionality] is [TextDirection.ltr]. /// corner when the ambient [Directionality] is [TextDirection.ltr].
topEnd, topEnd,
/// Show the banner in the bottom-right corner when the ambient /// Show the banner in the bottom-right corner when the ambient
/// [Directionality] is [TextDirection.rtl] and in the bottom-left corner when /// [Directionality] (or [Banner.layoutDirection]) is [TextDirection.rtl] and
/// the ambient [Directionality] is [TextDirection.ltr]. /// in the bottom-left corner when the ambient [Directionality] is
/// [TextDirection.ltr].
bottomStart, bottomStart,
/// Show the banner in the bottom-left corner when the ambient /// Show the banner in the bottom-left corner when the ambient
/// [Directionality] is [TextDirection.rtl] and in the bottom-right corner when /// [Directionality] (or [Banner.layoutDirection]) is [TextDirection.rtl] and
/// the ambient [Directionality] is [TextDirection.ltr]. /// in the bottom-right corner when the ambient [Directionality] is
/// [TextDirection.ltr].
bottomEnd, bottomEnd,
} }
...@@ -49,11 +54,13 @@ enum BannerLocation { ...@@ -49,11 +54,13 @@ enum BannerLocation {
class BannerPainter extends CustomPainter { class BannerPainter extends CustomPainter {
/// Creates a banner painter. /// Creates a banner painter.
/// ///
/// The [message], [textDirection], and [location] arguments must not be null. /// The [message], [textDirection], [location], and [layoutDirection]
/// arguments must not be null.
BannerPainter({ BannerPainter({
@required this.message, @required this.message,
@required this.textDirection, @required this.textDirection,
@required this.location, @required this.location,
@required this.layoutDirection,
this.color: _kColor, this.color: _kColor,
this.textStyle: _kTextStyle, this.textStyle: _kTextStyle,
}) : assert(message != null), }) : assert(message != null),
...@@ -74,12 +81,21 @@ class BannerPainter extends CustomPainter { ...@@ -74,12 +81,21 @@ class BannerPainter extends CustomPainter {
/// context, the English phrase will be on the right and the Hebrow phrase on /// context, the English phrase will be on the right and the Hebrow phrase on
/// its left. /// its left.
/// ///
/// This value is also used to interpret the [location] of the banner. /// See also [layoutDirection], which controls the interpretation of values in
/// [location].
final TextDirection textDirection; final TextDirection textDirection;
/// Where to show the banner (e.g., the upper right corder). /// Where to show the banner (e.g., the upper right corder).
final BannerLocation location; final BannerLocation location;
/// The directionality of the layout.
///
/// This value is used to interpret the [location] of the banner.
///
/// See also [textDirection], which controls the reading direction of the
/// [message].
final TextDirection layoutDirection;
/// The color to paint behind the [message]. /// The color to paint behind the [message].
/// ///
/// Defaults to a dark red. /// Defaults to a dark red.
...@@ -136,8 +152,8 @@ class BannerPainter extends CustomPainter { ...@@ -136,8 +152,8 @@ class BannerPainter extends CustomPainter {
double _translationX(double width) { double _translationX(double width) {
assert(location != null); assert(location != null);
assert(textDirection != null); assert(layoutDirection != null);
switch (textDirection) { switch (layoutDirection) {
case TextDirection.rtl: case TextDirection.rtl:
switch (location) { switch (location) {
case BannerLocation.bottomEnd: case BannerLocation.bottomEnd:
...@@ -181,8 +197,8 @@ class BannerPainter extends CustomPainter { ...@@ -181,8 +197,8 @@ class BannerPainter extends CustomPainter {
double get _rotation { double get _rotation {
assert(location != null); assert(location != null);
assert(textDirection != null); assert(layoutDirection != null);
switch (textDirection) { switch (layoutDirection) {
case TextDirection.rtl: case TextDirection.rtl:
switch (location) { switch (location) {
case BannerLocation.bottomStart: case BannerLocation.bottomStart:
...@@ -215,7 +231,8 @@ class BannerPainter extends CustomPainter { ...@@ -215,7 +231,8 @@ class BannerPainter extends CustomPainter {
/// ///
/// See also: /// See also:
/// ///
/// * [CheckedModeBanner]. /// * [CheckedModeBanner], which the [WidgetsApp] widget includes by default in
/// debug mode, to show a banner that says "SLOW MODE".
class Banner extends StatelessWidget { class Banner extends StatelessWidget {
/// Creates a banner. /// Creates a banner.
/// ///
...@@ -226,6 +243,7 @@ class Banner extends StatelessWidget { ...@@ -226,6 +243,7 @@ class Banner extends StatelessWidget {
@required this.message, @required this.message,
this.textDirection, this.textDirection,
@required this.location, @required this.location,
this.layoutDirection,
this.color: _kColor, this.color: _kColor,
this.textStyle: _kTextStyle, this.textStyle: _kTextStyle,
}) : assert(message != null), }) : assert(message != null),
...@@ -250,11 +268,24 @@ class Banner extends StatelessWidget { ...@@ -250,11 +268,24 @@ class Banner extends StatelessWidget {
/// its left. /// its left.
/// ///
/// Defaults to the ambient [Directionality], if any. /// Defaults to the ambient [Directionality], if any.
///
/// See also [layoutDirection], which controls the interpretation of the
/// [location].
final TextDirection textDirection; final TextDirection textDirection;
/// Where to show the banner (e.g., the upper right corder). /// Where to show the banner (e.g., the upper right corder).
final BannerLocation location; final BannerLocation location;
/// The directionality of the layout.
///
/// This is used to resolve the [location] values.
///
/// Defaults to the ambient [Directionality], if any.
///
/// See also [textDirection], which controls the reading direction of the
/// [message].
final TextDirection layoutDirection;
/// The color of the banner. /// The color of the banner.
final Color color; final Color color;
...@@ -268,6 +299,7 @@ class Banner extends StatelessWidget { ...@@ -268,6 +299,7 @@ class Banner extends StatelessWidget {
message: message, message: message,
textDirection: textDirection ?? Directionality.of(context), textDirection: textDirection ?? Directionality.of(context),
location: location, location: location,
layoutDirection: layoutDirection ?? Directionality.of(context),
color: color, color: color,
textStyle: textStyle, textStyle: textStyle,
), ),
...@@ -281,6 +313,7 @@ class Banner extends StatelessWidget { ...@@ -281,6 +313,7 @@ class Banner extends StatelessWidget {
description.add(new StringProperty('message', message, showName: false)); description.add(new StringProperty('message', message, showName: false));
description.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); description.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
description.add(new EnumProperty<BannerLocation>('location', location)); description.add(new EnumProperty<BannerLocation>('location', location));
description.add(new EnumProperty<TextDirection>('layoutDirection', layoutDirection, defaultValue: null));
description.add(new DiagnosticsProperty<Color>('color', color, showName: false)); description.add(new DiagnosticsProperty<Color>('color', color, showName: false));
textStyle?.debugFillProperties(description, prefix: 'text '); textStyle?.debugFillProperties(description, prefix: 'text ');
} }
......
...@@ -248,6 +248,13 @@ abstract class PaintPattern { ...@@ -248,6 +248,13 @@ abstract class PaintPattern {
void something(PaintPatternPredicate predicate); void something(PaintPatternPredicate predicate);
} }
class _MismatchedCall {
const _MismatchedCall(this.message, this.callIntroduction, this.call) : assert(call != null);
final String message;
final String callIntroduction;
final RecordedInvocation call;
}
abstract class _TestRecordingCanvasMatcher extends Matcher { abstract class _TestRecordingCanvasMatcher extends Matcher {
@override @override
bool matches(Object object, Map<dynamic, dynamic> matchState) { bool matches(Object object, Map<dynamic, dynamic> matchState) {
...@@ -276,17 +283,16 @@ abstract class _TestRecordingCanvasMatcher extends Matcher { ...@@ -276,17 +283,16 @@ abstract class _TestRecordingCanvasMatcher extends Matcher {
final StringBuffer description = new StringBuffer(); final StringBuffer description = new StringBuffer();
final bool result = _evaluatePredicates(canvas.invocations, description); final bool result = _evaluatePredicates(canvas.invocations, description);
if (!result) { if (!result) {
const String indent = '\n '; // the length of ' Which: ' in spaces, plus two more
if (canvas.invocations.isNotEmpty) if (canvas.invocations.isNotEmpty)
description.write(' The complete display list was:'); description.write('The complete display list was:');
for (Invocation call in canvas.invocations) for (RecordedInvocation call in canvas.invocations)
description.write('$indent${_describeInvocation(call)}'); description.write('\n * $call');
matchState[this] = 'did not match the pattern.\n$description';
} }
matchState[this] = description.toString();
return result; return result;
} }
bool _evaluatePredicates(Iterable<Invocation> calls, StringBuffer description); bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description);
@override @override
Description describeMismatch( Description describeMismatch(
...@@ -306,10 +312,13 @@ class _TestRecordingCanvasPaintsNothingMatcher extends _TestRecordingCanvasMatch ...@@ -306,10 +312,13 @@ class _TestRecordingCanvasPaintsNothingMatcher extends _TestRecordingCanvasMatch
} }
@override @override
bool _evaluatePredicates(Iterable<Invocation> calls, StringBuffer description) { bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) {
if (calls.isEmpty) if (calls.isEmpty)
return true; return true;
description.write('painted the following.'); description.write(
'painted something, the first call having the following stack:\n'
'${calls.first.stackToString(indent: " ")}\n'
);
return false; return false;
} }
} }
...@@ -409,32 +418,42 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp ...@@ -409,32 +418,42 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp
} }
@override @override
bool _evaluatePredicates(Iterable<Invocation> calls, StringBuffer description) { bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) {
if (calls.isEmpty) { if (calls.isEmpty) {
description.write('painted nothing.'); description.writeln('It painted nothing.');
return false; return false;
} }
if (_predicates.isEmpty) { if (_predicates.isEmpty) {
description.write( description.writeln(
'painted something, but you must now add a pattern to the paints matcher ' '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.' 'in the test to verify that it matches the important parts of the following.'
); );
return false; return false;
} }
final Iterator<_PaintPredicate> predicate = _predicates.iterator; final Iterator<_PaintPredicate> predicate = _predicates.iterator;
final Iterator<Invocation> call = calls.iterator..moveNext(); final Iterator<RecordedInvocation> call = calls.iterator..moveNext();
try { try {
while (predicate.moveNext()) { while (predicate.moveNext()) {
if (call.current == null) { if (call.current == null) {
throw 'painted less on its canvas than the paint pattern expected. ' throw 'It painted less on its canvas than the paint pattern expected. '
'The first missing paint call was: ${predicate.current}'; 'The first missing paint call was: ${predicate.current}';
} }
predicate.current.match(call); predicate.current.match(call);
} }
assert(predicate.current == null); assert(predicate.current == null);
// We allow painting more than expected. // 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) { } on String catch (s) {
description.write(s); description.writeln(s);
if (call.current != null) {
description.write('The stack of the offending call was:\n${call.current.stackToString(indent: " ")}\n');
} else {
description.write('The stack of the first call was:\n${calls.first.stackToString(indent: " ")}\n');
}
return false; return false;
} }
return true; return true;
...@@ -442,7 +461,7 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp ...@@ -442,7 +461,7 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp
} }
abstract class _PaintPredicate { abstract class _PaintPredicate {
void match(Iterator<Invocation> call); void match(Iterator<RecordedInvocation> call);
@override @override
String toString() { String toString() {
...@@ -468,20 +487,24 @@ abstract class _DrawCommandPaintPredicate extends _PaintPredicate { ...@@ -468,20 +487,24 @@ abstract class _DrawCommandPaintPredicate extends _PaintPredicate {
String get methodName => _symbolName(symbol); String get methodName => _symbolName(symbol);
@override @override
void match(Iterator<Invocation> call) { void match(Iterator<RecordedInvocation> call) {
int others = 0; int others = 0;
final Invocation firstCall = call.current; final RecordedInvocation firstCall = call.current;
while (!call.current.isMethod || call.current.memberName != symbol) { while (!call.current.invocation.isMethod || call.current.invocation.memberName != symbol) {
others += 1; others += 1;
if (!call.moveNext()) if (!call.moveNext())
throw 'called $others other method${ others == 1 ? "" : "s" } on the canvas, ' throw new _MismatchedCall(
'the first of which was ${_describeInvocation(firstCall)}, but did not ' 'It called $others other method${ others == 1 ? "" : "s" } on the canvas, '
'call $methodName at the time where $this was expected.'; 'the first of which was $firstCall, but did not '
'call $methodName at the time where $this was expected.',
'The stack for the call to $firstCall was:',
firstCall,
);
} }
final int actualArgumentCount = call.current.positionalArguments.length; final int actualArgumentCount = call.current.invocation.positionalArguments.length;
if (actualArgumentCount != argumentCount) if (actualArgumentCount != argumentCount)
throw 'called $methodName with $actualArgumentCount argument${actualArgumentCount == 1 ? "" : "s"}; expected $argumentCount.'; throw 'It called $methodName with $actualArgumentCount argument${actualArgumentCount == 1 ? "" : "s"}; expected $argumentCount.';
verifyArguments(call.current.positionalArguments); verifyArguments(call.current.invocation.positionalArguments);
call.moveNext(); call.moveNext();
} }
...@@ -490,17 +513,17 @@ abstract class _DrawCommandPaintPredicate extends _PaintPredicate { ...@@ -490,17 +513,17 @@ abstract class _DrawCommandPaintPredicate extends _PaintPredicate {
void verifyArguments(List<dynamic> arguments) { void verifyArguments(List<dynamic> arguments) {
final Paint paintArgument = arguments[paintArgumentIndex]; final Paint paintArgument = arguments[paintArgumentIndex];
if (color != null && paintArgument.color != color) if (color != null && paintArgument.color != color)
throw 'called $methodName with a paint whose color, ${paintArgument.color}, was not exactly the expected color ($color).'; throw 'It called $methodName with a paint whose color, ${paintArgument.color}, was not exactly the expected color ($color).';
if (strokeWidth != null && paintArgument.strokeWidth != strokeWidth) if (strokeWidth != null && paintArgument.strokeWidth != strokeWidth)
throw 'called $methodName with a paint whose strokeWidth, ${paintArgument.strokeWidth}, was not exactly the expected strokeWidth ($strokeWidth).'; throw '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 != null && (paintArgument.maskFilter != null) != hasMaskFilter) {
if (hasMaskFilter) if (hasMaskFilter)
throw 'called $methodName with a paint that did not have a mask filter, despite expecting one.'; throw 'It called $methodName with a paint that did not have a mask filter, despite expecting one.';
else else
throw 'called $methodName with a paint that did have a mask filter, despite not expecting one.'; throw 'It called $methodName with a paint that did have a mask filter, despite not expecting one.';
} }
if (style != null && paintArgument.style != style) if (style != null && paintArgument.style != style)
throw 'called $methodName with a paint whose style, ${paintArgument.style}, was not exactly the expected style ($style).'; throw 'It called $methodName with a paint whose style, ${paintArgument.style}, was not exactly the expected style ($style).';
} }
@override @override
...@@ -544,7 +567,7 @@ class _OneParameterPaintPredicate<T> extends _DrawCommandPaintPredicate { ...@@ -544,7 +567,7 @@ class _OneParameterPaintPredicate<T> extends _DrawCommandPaintPredicate {
super.verifyArguments(arguments); super.verifyArguments(arguments);
final T actual = arguments[0]; final T actual = arguments[0];
if (expected != null && actual != expected) if (expected != null && actual != expected)
throw 'called $methodName with $T, $actual, which was not exactly the expected $T ($expected).'; throw 'It called $methodName with $T, $actual, which was not exactly the expected $T ($expected).';
} }
@override @override
...@@ -601,16 +624,16 @@ class _CirclePaintPredicate extends _DrawCommandPaintPredicate { ...@@ -601,16 +624,16 @@ class _CirclePaintPredicate extends _DrawCommandPaintPredicate {
if (x != null && y != null) { if (x != null && y != null) {
final Offset point = new Offset(x, y); final Offset point = new Offset(x, y);
if (point != pointArgument) if (point != pointArgument)
throw 'called $methodName with a center coordinate, $pointArgument, which was not exactly the expected coordinate ($point).'; throw 'It called $methodName with a center coordinate, $pointArgument, which was not exactly the expected coordinate ($point).';
} else { } else {
if (x != null && pointArgument.dx != x) if (x != null && pointArgument.dx != x)
throw 'called $methodName with a center coordinate, $pointArgument, whose x-coordinate not exactly the expected coordinate (${x.toStringAsFixed(1)}).'; throw 'It called $methodName with a center coordinate, $pointArgument, whose x-coordinate not exactly the expected coordinate (${x.toStringAsFixed(1)}).';
if (y != null && pointArgument.dy != y) if (y != null && pointArgument.dy != y)
throw 'called $methodName with a center coordinate, $pointArgument, whose y-coordinate not exactly the expected coordinate (${y.toStringAsFixed(1)}).'; throw 'It called $methodName with a center coordinate, $pointArgument, whose y-coordinate not exactly the expected coordinate (${y.toStringAsFixed(1)}).';
} }
final double radiusArgument = arguments[1]; final double radiusArgument = arguments[1];
if (radius != null && radiusArgument != radius) if (radius != null && radiusArgument != radius)
throw 'called $methodName with radius, ${radiusArgument.toStringAsFixed(1)}, which was not exactly the expected radius (${radius.toStringAsFixed(1)}).'; throw 'It called $methodName with radius, ${radiusArgument.toStringAsFixed(1)}, which was not exactly the expected radius (${radius.toStringAsFixed(1)}).';
} }
@override @override
...@@ -654,24 +677,24 @@ class _SomethingPaintPredicate extends _PaintPredicate { ...@@ -654,24 +677,24 @@ class _SomethingPaintPredicate extends _PaintPredicate {
final PaintPatternPredicate predicate; final PaintPatternPredicate predicate;
@override @override
void match(Iterator<Invocation> call) { void match(Iterator<RecordedInvocation> call) {
assert(predicate != null); assert(predicate != null);
Invocation currentCall; RecordedInvocation currentCall;
do { do {
currentCall = call.current; currentCall = call.current;
if (currentCall == null) if (currentCall == null)
throw 'did not call anything that was matched by the predicate passed to a "something" step of the paint pattern.'; throw 'It did not call anything that was matched by the predicate passed to a "something" step of the paint pattern.';
if (!currentCall.isMethod) if (!currentCall.invocation.isMethod)
throw 'called ${_describeInvocation(currentCall)}, which was not a method, when the paint pattern expected a method call'; throw 'It called $currentCall, which was not a method, when the paint pattern expected a method call';
call.moveNext(); call.moveNext();
} while (!_runPredicate(currentCall.memberName, currentCall.positionalArguments)); } while (!_runPredicate(currentCall.invocation.memberName, currentCall.invocation.positionalArguments));
} }
bool _runPredicate(Symbol methodName, List<dynamic> arguments) { bool _runPredicate(Symbol methodName, List<dynamic> arguments) {
try { try {
return predicate(methodName, arguments); return predicate(methodName, arguments);
} on String catch (s) { } on String catch (s) {
throw 'painted something that the predicate passed to a "something" step ' throw 'It painted something that the predicate passed to a "something" step '
'in the paint pattern considered incorrect:\n $s\n '; 'in the paint pattern considered incorrect:\n $s\n ';
} }
} }
...@@ -688,23 +711,28 @@ class _FunctionPaintPredicate extends _PaintPredicate { ...@@ -688,23 +711,28 @@ class _FunctionPaintPredicate extends _PaintPredicate {
final List<dynamic> arguments; final List<dynamic> arguments;
@override @override
void match(Iterator<Invocation> call) { void match(Iterator<RecordedInvocation> call) {
int others = 0; int others = 0;
final Invocation firstCall = call.current; final RecordedInvocation firstCall = call.current;
while (!call.current.isMethod || call.current.memberName != symbol) { while (!call.current.invocation.isMethod || call.current.invocation.memberName != symbol) {
others += 1; others += 1;
if (!call.moveNext()) if (!call.moveNext())
throw 'called $others other method${ others == 1 ? "" : "s" } on the canvas, ' throw new _MismatchedCall(
'the first of which was ${_describeInvocation(firstCall)}, but did not ' 'It called $others other method${ others == 1 ? "" : "s" } on the canvas, '
'call ${_symbolName(symbol)}() at the time where $this was expected.'; '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,
);
} }
if (call.current.positionalArguments.length != arguments.length) if (call.current.invocation.positionalArguments.length != arguments.length)
throw 'called ${_symbolName(symbol)} with ${call.current.positionalArguments.length} arguments; expected ${arguments.length}.'; throw 'It called ${_symbolName(symbol)} with ${call.current.invocation.positionalArguments.length} arguments; expected ${arguments.length}.';
for (int index = 0; index < arguments.length; index += 1) { for (int index = 0; index < arguments.length; index += 1) {
final dynamic actualArgument = call.current.positionalArguments[index]; final dynamic actualArgument = call.current.invocation.positionalArguments[index];
final dynamic desiredArgument = arguments[index]; final dynamic desiredArgument = arguments[index];
if (desiredArgument != null && desiredArgument != actualArgument) if (desiredArgument != null && desiredArgument != actualArgument)
throw 'called ${_symbolName(symbol)} with argument $index having value ${_valueName(actualArgument)} when ${_valueName(desiredArgument)} was expected.'; throw 'It called ${_symbolName(symbol)} with argument $index having value ${_valueName(actualArgument)} when ${_valueName(desiredArgument)} was expected.';
} }
call.moveNext(); call.moveNext();
} }
...@@ -720,24 +748,29 @@ class _FunctionPaintPredicate extends _PaintPredicate { ...@@ -720,24 +748,29 @@ class _FunctionPaintPredicate extends _PaintPredicate {
class _SaveRestorePairPaintPredicate extends _PaintPredicate { class _SaveRestorePairPaintPredicate extends _PaintPredicate {
@override @override
void match(Iterator<Invocation> call) { void match(Iterator<RecordedInvocation> call) {
int others = 0; int others = 0;
final Invocation firstCall = call.current; final RecordedInvocation firstCall = call.current;
while (!call.current.isMethod || call.current.memberName != #save) { while (!call.current.invocation.isMethod || call.current.invocation.memberName != #save) {
others += 1; others += 1;
if (!call.moveNext()) if (!call.moveNext())
throw 'called $others other method${ others == 1 ? "" : "s" } on the canvas, ' throw new _MismatchedCall(
'the first of which was ${_describeInvocation(firstCall)}, but did not ' 'It called $others other method${ others == 1 ? "" : "s" } on the canvas, '
'call save() at the time where $this was expected.'; 'the first of which was $firstCall, but did not '
'call save() at the time where $this was expected.',
'The first method that was called when the call to save() '
'was expected, $firstCall, was called with the following stack:',
firstCall,
);
} }
int depth = 1; int depth = 1;
while (depth > 0) { while (depth > 0) {
if (!call.moveNext()) if (!call.moveNext())
throw 'did not have a matching restore() for the save() that was found where $this was expected.'; throw 'It did not have a matching restore() for the save() that was found where $this was expected.';
if (call.current.isMethod) { if (call.current.invocation.isMethod) {
if (call.current.memberName == #save) if (call.current.invocation.memberName == #save)
depth += 1; depth += 1;
else if (call.current.memberName == #restore) else if (call.current.invocation.memberName == #restore)
depth -= 1; depth -= 1;
} }
} }
...@@ -761,25 +794,3 @@ String _symbolName(Symbol symbol) { ...@@ -761,25 +794,3 @@ String _symbolName(Symbol symbol) {
final String s = '$symbol'; final String s = '$symbol';
return s.substring(8, s.length - 2); return s.substring(8, s.length - 2);
} }
// Workaround for https://github.com/dart-lang/sdk/issues/28373
String _describeInvocation(Invocation call) {
final StringBuffer buffer = new StringBuffer();
buffer.write(_symbolName(call.memberName));
if (call.isSetter) {
buffer.write(call.positionalArguments[0].toString());
} else if (call.isMethod) {
buffer.write('(');
buffer.writeAll(call.positionalArguments.map<String>(_valueName), ', ');
String separator = call.positionalArguments.isEmpty ? '' : ', ';
call.namedArguments.forEach((Symbol name, Object value) {
buffer.write(separator);
buffer.write(_symbolName(name));
buffer.write(': ');
buffer.write(_valueName(value));
separator = ', ';
});
buffer.write(')');
}
return buffer.toString();
}
...@@ -2,8 +2,34 @@ ...@@ -2,8 +2,34 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
/// An [Invocation] and the [stack] trace that led to it.
///
/// Used by [TestRecordingCanvas] to trace canvas calls.
class RecordedInvocation {
/// Create a record for an invocation list.
const RecordedInvocation(this.invocation, { this.stack });
/// The method that was called and its arguments.
final Invocation invocation;
/// The stack trace at the time of the method call.
final StackTrace stack;
@override
String toString() => _describeInvocation(invocation);
/// Converts [stack] to a string using the [FlutterError.defaultStackFilter] logic.
String stackToString({ String indent: '' }) {
assert(indent != null);
return indent + FlutterError.defaultStackFilter(
stack.toString().trimRight().split('\n')
).join('\n$indent');
}
}
/// A [Canvas] for tests that records its method calls. /// A [Canvas] for tests that records its method calls.
/// ///
/// This class can be used in conjuction with [TestRecordingPaintingContext] /// This class can be used in conjuction with [TestRecordingPaintingContext]
...@@ -25,7 +51,7 @@ import 'package:flutter/rendering.dart'; ...@@ -25,7 +51,7 @@ import 'package:flutter/rendering.dart';
/// pattern matching API over [TestRecordingCanvas]. /// pattern matching API over [TestRecordingCanvas].
class TestRecordingCanvas implements Canvas { class TestRecordingCanvas implements Canvas {
/// All of the method calls on this canvas. /// All of the method calls on this canvas.
final List<Invocation> invocations = <Invocation>[]; final List<RecordedInvocation> invocations = <RecordedInvocation>[];
int _saveCount = 0; int _saveCount = 0;
...@@ -35,25 +61,25 @@ class TestRecordingCanvas implements Canvas { ...@@ -35,25 +61,25 @@ class TestRecordingCanvas implements Canvas {
@override @override
void save() { void save() {
_saveCount += 1; _saveCount += 1;
invocations.add(new _MethodCall(#save)); invocations.add(new RecordedInvocation(new _MethodCall(#save), stack: StackTrace.current));
} }
@override @override
void saveLayer(Rect bounds, Paint paint) { void saveLayer(Rect bounds, Paint paint) {
_saveCount += 1; _saveCount += 1;
invocations.add(new _MethodCall(#saveLayer, <dynamic>[bounds, paint])); invocations.add(new RecordedInvocation(new _MethodCall(#saveLayer, <dynamic>[bounds, paint]), stack: StackTrace.current));
} }
@override @override
void restore() { void restore() {
_saveCount -= 1; _saveCount -= 1;
assert(_saveCount >= 0); assert(_saveCount >= 0);
invocations.add(new _MethodCall(#restore)); invocations.add(new RecordedInvocation(new _MethodCall(#restore), stack: StackTrace.current));
} }
@override @override
void noSuchMethod(Invocation invocation) { void noSuchMethod(Invocation invocation) {
invocations.add(invocation); invocations.add(new RecordedInvocation(invocation, stack: StackTrace.current));
} }
} }
...@@ -101,3 +127,39 @@ class _MethodCall implements Invocation { ...@@ -101,3 +127,39 @@ class _MethodCall implements Invocation {
@override @override
List<dynamic> get positionalArguments => _arguments; List<dynamic> get positionalArguments => _arguments;
} }
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 String s = '$symbol';
return s.substring(8, s.length - 2);
}
// Workaround for https://github.com/dart-lang/sdk/issues/28373
String _describeInvocation(Invocation call) {
final StringBuffer buffer = new StringBuffer();
buffer.write(_symbolName(call.memberName));
if (call.isSetter) {
buffer.write(call.positionalArguments[0].toString());
} else if (call.isMethod) {
buffer.write('(');
buffer.writeAll(call.positionalArguments.map<String>(_valueName), ', ');
String separator = call.positionalArguments.isEmpty ? '' : ', ';
call.namedArguments.forEach((Symbol name, Object value) {
buffer.write(separator);
buffer.write(_symbolName(name));
buffer.write(': ');
buffer.write(_valueName(value));
separator = ', ';
});
buffer.write(')');
}
return buffer.toString();
}
...@@ -27,14 +27,11 @@ class TestRoute<T> extends PageRoute<T> { ...@@ -27,14 +27,11 @@ class TestRoute<T> extends PageRoute<T> {
Future<Null> pumpApp(WidgetTester tester) async { Future<Null> pumpApp(WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new Directionality( new WidgetsApp(
textDirection: TextDirection.ltr, color: const Color(0xFF333333),
child: new WidgetsApp( onGenerateRoute: (RouteSettings settings) {
color: const Color(0xFF333333), return new TestRoute<Null>(settings: settings, child: new Container());
onGenerateRoute: (RouteSettings settings) { },
return new TestRoute<Null>(settings: settings, child: new Container());
},
),
), ),
); );
} }
......
...@@ -4,8 +4,10 @@ ...@@ -4,8 +4,10 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart';
import 'package:test/test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
class TestCanvas implements Canvas { class TestCanvas implements Canvas {
final List<Invocation> invocations = <Invocation>[]; final List<Invocation> invocations = <Invocation>[];
...@@ -17,11 +19,16 @@ class TestCanvas implements Canvas { ...@@ -17,11 +19,16 @@ class TestCanvas implements Canvas {
} }
void main() { void main() {
// the textDirection values below are intentionally sometimes different and
// sometimes the same as the layoutDirection, to make sure that they don't
// affect the layout.
test('A Banner with a location of topStart paints in the top left (LTR)', () { test('A Banner with a location of topStart paints in the top left (LTR)', () {
final BannerPainter bannerPainter = new BannerPainter( final BannerPainter bannerPainter = new BannerPainter(
message: 'foo', message: 'foo',
textDirection: TextDirection.ltr, textDirection: TextDirection.rtl,
location: BannerLocation.topStart location: BannerLocation.topStart,
layoutDirection: TextDirection.ltr,
); );
final TestCanvas canvas = new TestCanvas(); final TestCanvas canvas = new TestCanvas();
...@@ -47,8 +54,9 @@ void main() { ...@@ -47,8 +54,9 @@ void main() {
test('A Banner with a location of topStart paints in the top right (RTL)', () { test('A Banner with a location of topStart paints in the top right (RTL)', () {
final BannerPainter bannerPainter = new BannerPainter( final BannerPainter bannerPainter = new BannerPainter(
message: 'foo', message: 'foo',
textDirection: TextDirection.rtl, textDirection: TextDirection.ltr,
location: BannerLocation.topStart, location: BannerLocation.topStart,
layoutDirection: TextDirection.rtl,
); );
final TestCanvas canvas = new TestCanvas(); final TestCanvas canvas = new TestCanvas();
...@@ -75,7 +83,8 @@ void main() { ...@@ -75,7 +83,8 @@ void main() {
final BannerPainter bannerPainter = new BannerPainter( final BannerPainter bannerPainter = new BannerPainter(
message: 'foo', message: 'foo',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
location: BannerLocation.topEnd location: BannerLocation.topEnd,
layoutDirection: TextDirection.ltr,
); );
final TestCanvas canvas = new TestCanvas(); final TestCanvas canvas = new TestCanvas();
...@@ -103,6 +112,7 @@ void main() { ...@@ -103,6 +112,7 @@ void main() {
message: 'foo', message: 'foo',
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
location: BannerLocation.topEnd, location: BannerLocation.topEnd,
layoutDirection: TextDirection.rtl,
); );
final TestCanvas canvas = new TestCanvas(); final TestCanvas canvas = new TestCanvas();
...@@ -129,7 +139,8 @@ void main() { ...@@ -129,7 +139,8 @@ void main() {
final BannerPainter bannerPainter = new BannerPainter( final BannerPainter bannerPainter = new BannerPainter(
message: 'foo', message: 'foo',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
location: BannerLocation.bottomStart location: BannerLocation.bottomStart,
layoutDirection: TextDirection.ltr,
); );
final TestCanvas canvas = new TestCanvas(); final TestCanvas canvas = new TestCanvas();
...@@ -157,6 +168,7 @@ void main() { ...@@ -157,6 +168,7 @@ void main() {
message: 'foo', message: 'foo',
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
location: BannerLocation.bottomStart, location: BannerLocation.bottomStart,
layoutDirection: TextDirection.rtl,
); );
final TestCanvas canvas = new TestCanvas(); final TestCanvas canvas = new TestCanvas();
...@@ -182,8 +194,9 @@ void main() { ...@@ -182,8 +194,9 @@ void main() {
test('A Banner with a location of bottomEnd paints in the bottom right (LTR)', () { test('A Banner with a location of bottomEnd paints in the bottom right (LTR)', () {
final BannerPainter bannerPainter = new BannerPainter( final BannerPainter bannerPainter = new BannerPainter(
message: 'foo', message: 'foo',
textDirection: TextDirection.ltr, textDirection: TextDirection.rtl,
location: BannerLocation.bottomEnd location: BannerLocation.bottomEnd,
layoutDirection: TextDirection.ltr,
); );
final TestCanvas canvas = new TestCanvas(); final TestCanvas canvas = new TestCanvas();
...@@ -209,8 +222,9 @@ void main() { ...@@ -209,8 +222,9 @@ void main() {
test('A Banner with a location of bottomEnd paints in the bottom left (RTL)', () { test('A Banner with a location of bottomEnd paints in the bottom left (RTL)', () {
final BannerPainter bannerPainter = new BannerPainter( final BannerPainter bannerPainter = new BannerPainter(
message: 'foo', message: 'foo',
textDirection: TextDirection.rtl, textDirection: TextDirection.ltr,
location: BannerLocation.bottomEnd, location: BannerLocation.bottomEnd,
layoutDirection: TextDirection.rtl,
); );
final TestCanvas canvas = new TestCanvas(); final TestCanvas canvas = new TestCanvas();
...@@ -233,4 +247,34 @@ void main() { ...@@ -233,4 +247,34 @@ void main() {
expect(rotateCommand.positionalArguments[0], equals(math.PI / 4.0)); expect(rotateCommand.positionalArguments[0], equals(math.PI / 4.0));
}); });
testWidgets('Banner widget', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: const Banner(message: 'Hello', location: BannerLocation.topEnd),
),
);
expect(find.byType(CustomPaint), paints
..save
..translate(x: 800.0, y: 0.0)
..rotate(angle: math.PI / 4.0)
..rect(rect: new Rect.fromLTRB(-40.0, 28.0, 40.0, 40.0), color: const Color(0x7f000000), hasMaskFilter: true)
..rect(rect: new Rect.fromLTRB(-40.0, 28.0, 40.0, 40.0), color: const Color(0xa0b71c1c), hasMaskFilter: false)
..paragraph(offset: const Offset(-40.0, 29.0))
..restore
);
});
testWidgets('Banner widget in MaterialApp', (WidgetTester tester) async {
await tester.pumpWidget(new MaterialApp(home: const Placeholder()));
expect(find.byType(CheckedModeBanner), paints
..save
..translate(x: 800.0, y: 0.0)
..rotate(angle: math.PI / 4.0)
..rect(rect: new Rect.fromLTRB(-40.0, 28.0, 40.0, 40.0), color: const Color(0x7f000000), hasMaskFilter: true)
..rect(rect: new Rect.fromLTRB(-40.0, 28.0, 40.0, 40.0), color: const Color(0xa0b71c1c), hasMaskFilter: false)
..paragraph(offset: const Offset(-40.0, 24.0))
..restore
);
});
} }
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