Unverified Commit b31ca1aa authored by Albertus Angga Raharja's avatar Albertus Angga Raharja Committed by GitHub

Add more structure to errors (continuation of #34684) (#42640)

* Add structured errors in Animations, TabView, ChangeNotifier

* Add structured error on MaterialPageRoute, BoxBorder, DecorationImagePainter, TextSpan

* Add structured errors in Debug

* Fix test errors

* Add structured errors in Scaffold and Stepper

* Add structured errors in part of Rendering Layer

* Fix failing test due to FloatingPoint precision

* Fix failing tests due to precision error and not using final

* Fix failing test due to floating precision error with RegEx instead

* Add structured error in CustomLayout and increase test coverage

* Add structured error & its test in ListBody

* Add structured error in ProxyBox and increase test coverage

* Add structured error message in Viewport

* Fix styles and add more assertions on ErrorHint and DiagnosticProperty

* Add structured error in scheduler/binding and scheduler/ticker
Signed-off-by: 's avatarAlbertus Angga Raharja <albertusangga@google.com>

* Add structured error in AssetBundle and TextInput
Signed-off-by: 's avatarAlbertus Angga Raharja <albertusangga@google.com>

* Add structured errors in several widgets #1
Signed-off-by: 's avatarAlbertus Angga Raharja <albertusangga@google.com>

* Remove unused import
Signed-off-by: 's avatarAlbertus Angga Raharja <albertusangga@google.com>

* Add assertions on hint messages
Signed-off-by: 's avatarAlbertus Angga Raharja <albertusangga@google.com>

* Fix catch spacing
Signed-off-by: 's avatarAlbertus Angga Raharja <albertusangga@google.com>

* Add structured error in several widgets part 2 and increase code coverage
Signed-off-by: 's avatarAlbertus Angga Raharja <albertusangga@google.com>

* Add structured error in flutter_test/widget_tester

* Fix floating precision accuracy by using RegExp
Signed-off-by: 's avatarAlbertus Angga Raharja <albertusangga@google.com>

* Remove todo to add tests in Scaffold showBottomSheet
Signed-off-by: 's avatarAlbertus Angga Raharja <albertusangga@google.com>

* Fix reviews by indenting lines and fixing the assertion orders
Signed-off-by: 's avatarAlbertus Angga Raharja <albertusangga@google.com>

* Fix failing tests due to renaming class
Signed-off-by: 's avatarAlbertus Angga Raharja <albertusangga@google.com>

* Try skipping the NetworkBundleTest
Signed-off-by: 's avatarAlbertus Angga Raharja <albertusangga@google.com>

* Remove leading space in material/debug error hint
Signed-off-by: 's avatarAlbertus Angga Raharja <albertusangga@google.com>
parent 734ddd31
...@@ -439,12 +439,14 @@ class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<do ...@@ -439,12 +439,14 @@ class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<do
final double transformedValue = activeCurve.transform(t); final double transformedValue = activeCurve.transform(t);
final double roundedTransformedValue = transformedValue.round().toDouble(); final double roundedTransformedValue = transformedValue.round().toDouble();
if (roundedTransformedValue != t) { if (roundedTransformedValue != t) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Invalid curve endpoint at $t.\n' ErrorSummary('Invalid curve endpoint at $t.'),
'Curves must map 0.0 to near zero and 1.0 to near one but ' ErrorDescription(
'${activeCurve.runtimeType} mapped $t to $transformedValue, which ' 'Curves must map 0.0 to near zero and 1.0 to near one but '
'is near $roundedTransformedValue.' '${activeCurve.runtimeType} mapped $t to $transformedValue, which '
); 'is near $roundedTransformedValue.'
)
]);
} }
return true; return true;
}()); }());
......
...@@ -192,29 +192,33 @@ class _CupertinoTabViewState extends State<CupertinoTabView> { ...@@ -192,29 +192,33 @@ class _CupertinoTabViewState extends State<CupertinoTabView> {
Route<dynamic> _onUnknownRoute(RouteSettings settings) { Route<dynamic> _onUnknownRoute(RouteSettings settings) {
assert(() { assert(() {
if (widget.onUnknownRoute == null) { if (widget.onUnknownRoute == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Could not find a generator for route $settings in the $runtimeType.\n' ErrorSummary('Could not find a generator for route $settings in the $runtimeType.'),
'Generators for routes are searched for in the following order:\n' ErrorDescription(
' 1. For the "/" route, the "builder" property, if non-null, is used.\n' 'Generators for routes are searched for in the following order:\n'
' 2. Otherwise, the "routes" table is used, if it has an entry for ' ' 1. For the "/" route, the "builder" property, if non-null, is used.\n'
'the route.\n' ' 2. Otherwise, the "routes" table is used, if it has an entry for '
' 3. Otherwise, onGenerateRoute is called. It should return a ' 'the route.\n'
'non-null value for any valid route not handled by "builder" and "routes".\n' ' 3. Otherwise, onGenerateRoute is called. It should return a '
' 4. Finally if all else fails onUnknownRoute is called.\n' 'non-null value for any valid route not handled by "builder" and "routes".\n'
'Unfortunately, onUnknownRoute was not set.' ' 4. Finally if all else fails onUnknownRoute is called.\n'
); 'Unfortunately, onUnknownRoute was not set.'
)
]);
} }
return true; return true;
}()); }());
final Route<dynamic> result = widget.onUnknownRoute(settings); final Route<dynamic> result = widget.onUnknownRoute(settings);
assert(() { assert(() {
if (result == null) { if (result == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The onUnknownRoute callback returned null.\n' ErrorSummary('The onUnknownRoute callback returned null.'),
'When the $runtimeType requested the route $settings from its ' ErrorDescription(
'onUnknownRoute callback, the callback returned null. Such callbacks ' 'When the $runtimeType requested the route $settings from its '
'must never return null.' 'onUnknownRoute callback, the callback returned null. Such callbacks '
); 'must never return null.'
)
]);
} }
return true; return true;
}()); }());
......
...@@ -102,10 +102,10 @@ class ChangeNotifier implements Listenable { ...@@ -102,10 +102,10 @@ class ChangeNotifier implements Listenable {
bool _debugAssertNotDisposed() { bool _debugAssertNotDisposed() {
assert(() { assert(() {
if (_listeners == null) { if (_listeners == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'A $runtimeType was used after being disposed.\n' ErrorSummary('A $runtimeType was used after being disposed.'),
'Once you have called dispose() on a $runtimeType, it can no longer be used.' ErrorDescription('Once you have called dispose() on a $runtimeType, it can no longer be used.')
); ]);
} }
return true; return true;
}()); }());
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// 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/widgets.dart'; import 'package:flutter/widgets.dart';
import 'material.dart'; import 'material.dart';
...@@ -24,45 +25,26 @@ import 'scaffold.dart' show Scaffold; ...@@ -24,45 +25,26 @@ import 'scaffold.dart' show Scaffold;
bool debugCheckHasMaterial(BuildContext context) { bool debugCheckHasMaterial(BuildContext context) {
assert(() { assert(() {
if (context.widget is! Material && context.ancestorWidgetOfExactType(Material) == null) { if (context.widget is! Material && context.ancestorWidgetOfExactType(Material) == null) {
final StringBuffer message = StringBuffer(); throw FlutterError.fromParts(<DiagnosticsNode>[
message.writeln('No Material widget found.'); ErrorSummary('No Material widget found.'),
message.writeln( ErrorDescription(
'${context.widget.runtimeType} widgets require a Material ' '${context.widget.runtimeType} widgets require a Material '
'widget ancestor.' 'widget ancestor.\n'
'In material design, most widgets are conceptually "printed" on '
'a sheet of material. In Flutter\'s material library, that '
'material is represented by the Material widget. It is the '
'Material widget that renders ink splashes, for instance. '
'Because of this, many material library widgets require that '
'there be a Material widget in the tree above them.'
),
ErrorHint(
'To introduce a Material widget, you can either directly '
'include one, or use a widget that contains Material itself, '
'such as a Card, Dialog, Drawer, or Scaffold.',
),
...context.describeMissingAncestor(expectedAncestorType: Material)
]
); );
message.writeln(
'In material design, most widgets are conceptually "printed" on '
'a sheet of material. In Flutter\'s material library, that '
'material is represented by the Material widget. It is the '
'Material widget that renders ink splashes, for instance. '
'Because of this, many material library widgets require that '
'there be a Material widget in the tree above them.'
);
message.writeln(
'To introduce a Material widget, you can either directly '
'include one, or use a widget that contains Material itself, '
'such as a Card, Dialog, Drawer, or Scaffold.'
);
message.writeln(
'The specific widget that could not find a Material ancestor was:'
);
message.writeln(' ${context.widget}');
final List<Widget> ancestors = <Widget>[];
context.visitAncestorElements((Element element) {
ancestors.add(element.widget);
return true;
});
if (ancestors.isNotEmpty) {
message.write('The ancestors of this widget were:');
for (Widget ancestor in ancestors)
message.write('\n $ancestor');
} else {
message.writeln(
'This widget is the root of the tree, so it has no '
'ancestors, let alone a "Material" ancestor.'
);
}
throw FlutterError(message.toString());
} }
return true; return true;
}()); }());
...@@ -87,42 +69,24 @@ bool debugCheckHasMaterial(BuildContext context) { ...@@ -87,42 +69,24 @@ bool debugCheckHasMaterial(BuildContext context) {
bool debugCheckHasMaterialLocalizations(BuildContext context) { bool debugCheckHasMaterialLocalizations(BuildContext context) {
assert(() { assert(() {
if (Localizations.of<MaterialLocalizations>(context, MaterialLocalizations) == null) { if (Localizations.of<MaterialLocalizations>(context, MaterialLocalizations) == null) {
final StringBuffer message = StringBuffer(); throw FlutterError.fromParts(<DiagnosticsNode>[
message.writeln('No MaterialLocalizations found.'); ErrorSummary('No MaterialLocalizations found.'),
message.writeln( ErrorDescription(
'${context.widget.runtimeType} widgets require MaterialLocalizations ' '${context.widget.runtimeType} widgets require MaterialLocalizations '
'to be provided by a Localizations widget ancestor.' 'to be provided by a Localizations widget ancestor.'
); ),
message.writeln( ErrorDescription(
'Localizations are used to generate many different messages, labels,' 'Localizations are used to generate many different messages, labels,'
'and abbreviations which are used by the material library. ' 'and abbreviations which are used by the material library.'
); ),
message.writeln( ErrorHint(
'To introduce a MaterialLocalizations, either use a ' 'To introduce a MaterialLocalizations, either use a '
' MaterialApp at the root of your application to include them ' 'MaterialApp at the root of your application to include them '
'automatically, or add a Localization widget with a ' 'automatically, or add a Localization widget with a '
'MaterialLocalizations delegate.' 'MaterialLocalizations delegate.'
); ),
message.writeln( ...context.describeMissingAncestor(expectedAncestorType: MaterialLocalizations)
'The specific widget that could not find a MaterialLocalizations ancestor was:' ]);
);
message.writeln(' ${context.widget}');
final List<Widget> ancestors = <Widget>[];
context.visitAncestorElements((Element element) {
ancestors.add(element.widget);
return true;
});
if (ancestors.isNotEmpty) {
message.write('The ancestors of this widget were:');
for (Widget ancestor in ancestors)
message.write('\n $ancestor');
} else {
message.writeln(
'This widget is the root of the tree, so it has no '
'ancestors, let alone a "Localizations" ancestor.'
);
}
throw FlutterError(message.toString());
} }
return true; return true;
}()); }());
...@@ -145,17 +109,15 @@ bool debugCheckHasMaterialLocalizations(BuildContext context) { ...@@ -145,17 +109,15 @@ bool debugCheckHasMaterialLocalizations(BuildContext context) {
bool debugCheckHasScaffold(BuildContext context) { bool debugCheckHasScaffold(BuildContext context) {
assert(() { assert(() {
if (context.widget is! Scaffold && context.ancestorWidgetOfExactType(Scaffold) == null) { if (context.widget is! Scaffold && context.ancestorWidgetOfExactType(Scaffold) == null) {
final Element element = context; throw FlutterError.fromParts(<DiagnosticsNode>[
throw FlutterError( ErrorSummary('No Scaffold widget found.'),
'No Scaffold widget found.\n' ErrorDescription('${context.widget.runtimeType} widgets require a Scaffold widget ancestor.'),
'${context.widget.runtimeType} widgets require a Scaffold widget ancestor.\n' ...context.describeMissingAncestor(expectedAncestorType: Scaffold),
'The Specific widget that could not find a Scaffold ancestor was:\n' ErrorHint(
' ${context.widget}\n'
'The ownership chain for the affected widget is:\n'
' ${element.debugGetCreatorChain(10)}\n'
'Typically, the Scaffold widget is introduced by the MaterialApp or ' 'Typically, the Scaffold widget is introduced by the MaterialApp or '
'WidgetsApp widget at the top of your application widget tree.' 'WidgetsApp widget at the top of your application widget tree.'
); )
]);
} }
return true; return true;
}()); }());
......
...@@ -87,10 +87,10 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -87,10 +87,10 @@ class MaterialPageRoute<T> extends PageRoute<T> {
final Widget result = builder(context); final Widget result = builder(context);
assert(() { assert(() {
if (result == null) { if (result == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The builder for route "${settings.name}" returned null.\n' ErrorSummary('The builder for route "${settings.name}" returned null.'),
'Route builders must never return null.' ErrorDescription('Route builders must never return null.')
); ]);
} }
return true; return true;
}()); }());
......
...@@ -1310,25 +1310,32 @@ class Scaffold extends StatefulWidget { ...@@ -1310,25 +1310,32 @@ class Scaffold extends StatefulWidget {
final ScaffoldState result = context.ancestorStateOfType(const TypeMatcher<ScaffoldState>()); final ScaffoldState result = context.ancestorStateOfType(const TypeMatcher<ScaffoldState>());
if (nullOk || result != null) if (nullOk || result != null)
return result; return result;
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Scaffold.of() called with a context that does not contain a Scaffold.\n' ErrorSummary(
'No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). ' 'Scaffold.of() called with a context that does not contain a Scaffold.'
'This usually happens when the context provided is from the same StatefulWidget as that ' ),
'whose build function actually creates the Scaffold widget being sought.\n' ErrorDescription(
'There are several ways to avoid this problem. The simplest is to use a Builder to get a ' 'No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). '
'context that is "under" the Scaffold. For an example of this, please see the ' 'This usually happens when the context provided is from the same StatefulWidget as that '
'documentation for Scaffold.of():\n' 'whose build function actually creates the Scaffold widget being sought.'
' https://api.flutter.dev/flutter/material/Scaffold/of.html\n' ),
'A more efficient solution is to split your build function into several widgets. This ' ErrorHint(
'introduces a new context from which you can obtain the Scaffold. In this solution, ' 'There are several ways to avoid this problem. The simplest is to use a Builder to get a '
'you would have an outer widget that creates the Scaffold populated by instances of ' 'context that is "under" the Scaffold. For an example of this, please see the '
'your new inner widgets, and then in these inner widgets you would use Scaffold.of().\n' 'documentation for Scaffold.of():\n'
'A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, ' ' https://api.flutter.dev/flutter/material/Scaffold/of.html'
'then use the key.currentState property to obtain the ScaffoldState rather than ' ),
'using the Scaffold.of() function.\n' ErrorHint(
'The context used was:\n' 'A more efficient solution is to split your build function into several widgets. This '
' $context' 'introduces a new context from which you can obtain the Scaffold. In this solution, '
); 'you would have an outer widget that creates the Scaffold populated by instances of '
'your new inner widgets, and then in these inner widgets you would use Scaffold.of().\n'
'A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, '
'then use the key.currentState property to obtain the ScaffoldState rather than '
'using the Scaffold.of() function.'
),
context.describeElement('The context used was')
]);
} }
/// Returns a [ValueListenable] for the [ScaffoldGeometry] for the closest /// Returns a [ValueListenable] for the [ScaffoldGeometry] for the closest
...@@ -1354,22 +1361,28 @@ class Scaffold extends StatefulWidget { ...@@ -1354,22 +1361,28 @@ class Scaffold extends StatefulWidget {
static ValueListenable<ScaffoldGeometry> geometryOf(BuildContext context) { static ValueListenable<ScaffoldGeometry> geometryOf(BuildContext context) {
final _ScaffoldScope scaffoldScope = context.inheritFromWidgetOfExactType(_ScaffoldScope); final _ScaffoldScope scaffoldScope = context.inheritFromWidgetOfExactType(_ScaffoldScope);
if (scaffoldScope == null) if (scaffoldScope == null)
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Scaffold.geometryOf() called with a context that does not contain a Scaffold.\n' ErrorSummary(
'This usually happens when the context provided is from the same StatefulWidget as that ' 'Scaffold.geometryOf() called with a context that does not contain a Scaffold.'
'whose build function actually creates the Scaffold widget being sought.\n' ),
'There are several ways to avoid this problem. The simplest is to use a Builder to get a ' ErrorDescription(
'context that is "under" the Scaffold. For an example of this, please see the ' 'This usually happens when the context provided is from the same StatefulWidget as that '
'documentation for Scaffold.of():\n' 'whose build function actually creates the Scaffold widget being sought.'
' https://api.flutter.dev/flutter/material/Scaffold/of.html\n' ),
'A more efficient solution is to split your build function into several widgets. This ' ErrorHint(
'introduces a new context from which you can obtain the Scaffold. In this solution, ' 'There are several ways to avoid this problem. The simplest is to use a Builder to get a '
'you would have an outer widget that creates the Scaffold populated by instances of ' 'context that is "under" the Scaffold. For an example of this, please see the '
'your new inner widgets, and then in these inner widgets you would use Scaffold.geometryOf().\n' 'documentation for Scaffold.of():\n'
'The context used was:\n' ' https://api.flutter.dev/flutter/material/Scaffold/of.html'
' $context' ),
); ErrorHint(
'A more efficient solution is to split your build function into several widgets. This '
'introduces a new context from which you can obtain the Scaffold. In this solution, '
'you would have an outer widget that creates the Scaffold populated by instances of '
'your new inner widgets, and then in these inner widgets you would use Scaffold.geometryOf().',
),
context.describeElement('The context used was')
]);
return scaffoldScope.geometryNotifier; return scaffoldScope.geometryNotifier;
} }
...@@ -1679,9 +1692,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1679,9 +1692,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
assert(() { assert(() {
if (widget.bottomSheet != null && isPersistent && _currentBottomSheet != null) { if (widget.bottomSheet != null && isPersistent && _currentBottomSheet != null) {
throw FlutterError( throw FlutterError(
'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed ' 'Scaffold.bottomSheet cannot be specified while a bottom sheet'
'with showBottomSheet() is still visible.\n Rebuild the Scaffold with a null ' 'displayed with showBottomSheet() is still visible.\n'
'bottomSheet before calling showBottomSheet().' 'Rebuild the Scaffold with a null bottomSheet before calling showBottomSheet().'
); );
} }
return true; return true;
...@@ -1818,9 +1831,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1818,9 +1831,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
assert(() { assert(() {
if (widget.bottomSheet != null) { if (widget.bottomSheet != null) {
throw FlutterError( throw FlutterError(
'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed ' 'Scaffold.bottomSheet cannot be specified while a bottom sheet'
'with showBottomSheet() is still visible.\n Rebuild the Scaffold with a null ' 'displayed with showBottomSheet() is still visible.\n'
'bottomSheet before calling showBottomSheet().' 'Rebuild the Scaffold with a null bottomSheet before calling showBottomSheet().'
); );
} }
return true; return true;
...@@ -1952,12 +1965,17 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1952,12 +1965,17 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
if (widget.bottomSheet != oldWidget.bottomSheet) { if (widget.bottomSheet != oldWidget.bottomSheet) {
assert(() { assert(() {
if (widget.bottomSheet != null && _currentBottomSheet?._isLocalHistoryEntry == true) { if (widget.bottomSheet != null && _currentBottomSheet?._isLocalHistoryEntry == true) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed ' ErrorSummary(
'with showBottomSheet() is still visible.\n Use the PersistentBottomSheetController ' 'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed '
'returned by showBottomSheet() to close the old bottom sheet before creating ' 'with showBottomSheet() is still visible.'
'a Scaffold with a (non null) bottomSheet.' ),
); ErrorHint(
'Use the PersistentBottomSheetController '
'returned by showBottomSheet() to close the old bottom sheet before creating '
'a Scaffold with a (non null) bottomSheet.'
),
]);
} }
return true; return true;
}()); }());
......
...@@ -672,8 +672,9 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin { ...@@ -672,8 +672,9 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
assert(() { assert(() {
if (context.ancestorWidgetOfExactType(Stepper) != null) if (context.ancestorWidgetOfExactType(Stepper) != null)
throw FlutterError( throw FlutterError(
'Steppers must not be nested. The material specification advises ' 'Steppers must not be nested.\n'
'that one should avoid embedding steppers within steppers. ' 'The material specification advises that one should avoid embedding '
'steppers within steppers. '
'https://material.io/archive/guidelines/components/steppers.html#steppers-usage' 'https://material.io/archive/guidelines/components/steppers.html#steppers-usage'
); );
return true; return true;
......
...@@ -152,14 +152,16 @@ abstract class BoxBorder extends ShapeBorder { ...@@ -152,14 +152,16 @@ abstract class BoxBorder extends ShapeBorder {
bottom: BorderSide.lerp(a.bottom, b.bottom, t), bottom: BorderSide.lerp(a.bottom, b.bottom, t),
); );
} }
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'BoxBorder.lerp can only interpolate Border and BorderDirectional classes.\n' ErrorSummary('BoxBorder.lerp can only interpolate Border and BorderDirectional classes.'),
'BoxBorder.lerp() was called with two objects of type ${a.runtimeType} and ${b.runtimeType}:\n' ErrorDescription(
' $a\n' 'BoxBorder.lerp() was called with two objects of type ${a.runtimeType} and ${b.runtimeType}:\n'
' $b\n' ' $a\n'
'However, only Border and BorderDirectional classes are supported by this method. ' ' $b\n'
'For a more general interpolation method, consider using ShapeBorder.lerp instead.' 'However, only Border and BorderDirectional classes are supported by this method.'
); ),
ErrorHint('For a more general interpolation method, consider using ShapeBorder.lerp instead.'),
]);
} }
@override @override
......
...@@ -221,15 +221,15 @@ class DecorationImagePainter { ...@@ -221,15 +221,15 @@ class DecorationImagePainter {
// We check this first so that the assert will fire immediately, not just // We check this first so that the assert will fire immediately, not just
// when the image is ready. // when the image is ready.
if (configuration.textDirection == null) { if (configuration.textDirection == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'ImageDecoration.matchTextDirection can only be used when a TextDirection is available.\n' ErrorSummary('ImageDecoration.matchTextDirection can only be used when a TextDirection is available.'),
'When DecorationImagePainter.paint() was called, there was no text direction provided ' ErrorDescription(
'in the ImageConfiguration object to match.\n' 'When DecorationImagePainter.paint() was called, there was no text direction provided '
'The DecorationImage was:\n' 'in the ImageConfiguration object to match.'
' $_details\n' ),
'The ImageConfiguration was:\n' DiagnosticsProperty<DecorationImage>('The DecorationImage was', _details, style: DiagnosticsTreeStyle.errorProperty),
' $configuration' DiagnosticsProperty<ImageConfiguration>('The ImageConfiguration was', configuration, style: DiagnosticsTreeStyle.errorProperty),
); ]);
} }
return true; return true;
}()); }());
......
...@@ -362,12 +362,15 @@ class TextSpan extends InlineSpan { ...@@ -362,12 +362,15 @@ class TextSpan extends InlineSpan {
assert(() { assert(() {
if (children != null) { if (children != null) {
for (InlineSpan child in children) { for (InlineSpan child in children) {
assert(child != null, if (child == null) {
'TextSpan contains a null child.\n...' throw FlutterError.fromParts(<DiagnosticsNode>[
'A TextSpan object with a non-null child list should not have any nulls in its child list.\n' ErrorSummary('TextSpan contains a null child.'),
'The full text in question was:\n' ErrorDescription(
'${toStringDeep(prefixLineOne: ' ')}' 'A TextSpan object with a non-null child list should not have any nulls in its child list.'),
); toDiagnosticsNode(name: 'The full text in question was',
style: DiagnosticsTreeStyle.errorProperty),
]);
}
assert(child.debugAssertIsValid()); assert(child.debugAssertIsValid());
} }
} }
......
...@@ -134,27 +134,29 @@ abstract class MultiChildLayoutDelegate { ...@@ -134,27 +134,29 @@ abstract class MultiChildLayoutDelegate {
final RenderBox child = _idToChild[childId]; final RenderBox child = _idToChild[childId];
assert(() { assert(() {
if (child == null) { if (child == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The $this custom multichild layout delegate tried to lay out a non-existent child.\n' ErrorSummary('The $this custom multichild layout delegate tried to lay out a non-existent child.'),
'There is no child with the id "$childId".' ErrorDescription('There is no child with the id "$childId".')
); ]);
} }
if (!_debugChildrenNeedingLayout.remove(child)) { if (!_debugChildrenNeedingLayout.remove(child)) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The $this custom multichild layout delegate tried to lay out the child with id "$childId" more than once.\n' ErrorSummary('The $this custom multichild layout delegate tried to lay out the child with id "$childId" more than once.'),
'Each child must be laid out exactly once.' ErrorDescription('Each child must be laid out exactly once.')
); ]);
} }
try { try {
assert(constraints.debugAssertIsValid(isAppliedConstraint: true)); assert(constraints.debugAssertIsValid(isAppliedConstraint: true));
} on AssertionError catch (exception) { } on AssertionError catch (exception) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".\n' ErrorSummary('The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".'),
'$exception\n' DiagnosticsProperty<AssertionError>('Exception', exception, showName: false),
'The minimum width and height must be greater than or equal to zero.\n' ErrorDescription(
'The maximum width must be greater than or equal to the minimum width.\n' 'The minimum width and height must be greater than or equal to zero.\n'
'The maximum height must be greater than or equal to the minimum height.' 'The maximum width must be greater than or equal to the minimum width.\n'
); 'The maximum height must be greater than or equal to the minimum height.'
)
]);
} }
return true; return true;
}()); }());
...@@ -172,15 +174,15 @@ abstract class MultiChildLayoutDelegate { ...@@ -172,15 +174,15 @@ abstract class MultiChildLayoutDelegate {
final RenderBox child = _idToChild[childId]; final RenderBox child = _idToChild[childId];
assert(() { assert(() {
if (child == null) { if (child == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The $this custom multichild layout delegate tried to position out a non-existent child:\n' ErrorSummary('The $this custom multichild layout delegate tried to position out a non-existent child:'),
'There is no child with the id "$childId".' ErrorDescription('There is no child with the id "$childId".')
); ]);
} }
if (offset == null) { if (offset == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The $this custom multichild layout delegate provided a null position for the child with id "$childId".' ErrorSummary('The $this custom multichild layout delegate provided a null position for the child with id "$childId".')
); ]);
} }
return true; return true;
}()); }());
...@@ -188,9 +190,9 @@ abstract class MultiChildLayoutDelegate { ...@@ -188,9 +190,9 @@ abstract class MultiChildLayoutDelegate {
childParentData.offset = offset; childParentData.offset = offset;
} }
String _debugDescribeChild(RenderBox child) { DiagnosticsNode _debugDescribeChild(RenderBox child) {
final MultiChildLayoutParentData childParentData = child.parentData; final MultiChildLayoutParentData childParentData = child.parentData;
return '${childParentData.id}: $child'; return DiagnosticsProperty<RenderBox>('${childParentData.id}', child);
} }
void _callPerformLayout(Size size, RenderBox firstChild) { void _callPerformLayout(Size size, RenderBox firstChild) {
...@@ -213,11 +215,10 @@ abstract class MultiChildLayoutDelegate { ...@@ -213,11 +215,10 @@ abstract class MultiChildLayoutDelegate {
final MultiChildLayoutParentData childParentData = child.parentData; final MultiChildLayoutParentData childParentData = child.parentData;
assert(() { assert(() {
if (childParentData.id == null) { if (childParentData.id == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The following child has no ID:\n' ErrorSummary('Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.'),
' $child\n' child.describeForError('The following child has no ID'),
'Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.' ]);
);
} }
return true; return true;
}()); }());
...@@ -231,19 +232,17 @@ abstract class MultiChildLayoutDelegate { ...@@ -231,19 +232,17 @@ abstract class MultiChildLayoutDelegate {
performLayout(size); performLayout(size);
assert(() { assert(() {
if (_debugChildrenNeedingLayout.isNotEmpty) { if (_debugChildrenNeedingLayout.isNotEmpty) {
if (_debugChildrenNeedingLayout.length > 1) { throw FlutterError.fromParts(<DiagnosticsNode>[
throw FlutterError( ErrorSummary('Each child must be laid out exactly once.'),
'The $this custom multichild layout delegate forgot to lay out the following children:\n' DiagnosticsBlock(
' ${_debugChildrenNeedingLayout.map<String>(_debugDescribeChild).join("\n ")}\n' name:
'Each child must be laid out exactly once.' 'The $this custom multichild layout delegate forgot '
); 'to lay out the following '
} else { '${_debugChildrenNeedingLayout.length > 1 ? 'children' : 'child'}',
throw FlutterError( properties: _debugChildrenNeedingLayout.map<DiagnosticsNode>(_debugDescribeChild).toList(),
'The $this custom multichild layout delegate forgot to lay out the following child:\n' style: DiagnosticsTreeStyle.whitespace,
' ${_debugDescribeChild(_debugChildrenNeedingLayout.single)}\n' ),
'Each child must be laid out exactly once.' ]);
);
}
} }
return true; return true;
}()); }());
......
...@@ -539,24 +539,27 @@ class RenderCustomPaint extends RenderProxyBox { ...@@ -539,24 +539,27 @@ class RenderCustomPaint extends RenderProxyBox {
// below that number. // below that number.
final int debugNewCanvasSaveCount = canvas.getSaveCount(); final int debugNewCanvasSaveCount = canvas.getSaveCount();
if (debugNewCanvasSaveCount > debugPreviousCanvasSaveCount) { if (debugNewCanvasSaveCount > debugPreviousCanvasSaveCount) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The $painter custom painter called canvas.save() or canvas.saveLayer() at least ' ErrorSummary(
'${debugNewCanvasSaveCount - debugPreviousCanvasSaveCount} more ' 'The $painter custom painter called canvas.save() or canvas.saveLayer() at least '
'time${debugNewCanvasSaveCount - debugPreviousCanvasSaveCount == 1 ? '' : 's' } ' '${debugNewCanvasSaveCount - debugPreviousCanvasSaveCount} more '
'than it called canvas.restore().\n' 'time${debugNewCanvasSaveCount - debugPreviousCanvasSaveCount == 1 ? '' : 's' } '
'This leaves the canvas in an inconsistent state and will probably result in a broken display.\n' 'than it called canvas.restore().'
'You must pair each call to save()/saveLayer() with a later matching call to restore().' ),
); ErrorDescription('This leaves the canvas in an inconsistent state and will probably result in a broken display.'),
ErrorHint('You must pair each call to save()/saveLayer() with a later matching call to restore().')
]);
} }
if (debugNewCanvasSaveCount < debugPreviousCanvasSaveCount) { if (debugNewCanvasSaveCount < debugPreviousCanvasSaveCount) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The $painter custom painter called canvas.restore() ' ErrorSummary('The $painter custom painter called canvas.restore() '
'${debugPreviousCanvasSaveCount - debugNewCanvasSaveCount} more ' '${debugPreviousCanvasSaveCount - debugNewCanvasSaveCount} more '
'time${debugPreviousCanvasSaveCount - debugNewCanvasSaveCount == 1 ? '' : 's' } ' 'time${debugPreviousCanvasSaveCount - debugNewCanvasSaveCount == 1 ? '' : 's' } '
'than it called canvas.save() or canvas.saveLayer().\n' 'than it called canvas.save() or canvas.saveLayer().'
'This leaves the canvas in an inconsistent state and will result in a broken display.\n' ),
'You should only call restore() if you first called save() or saveLayer().' ErrorDescription('This leaves the canvas in an inconsistent state and will result in a broken display.'),
); ErrorHint('You should only call restore() if you first called save() or saveLayer().')
]);
} }
return debugNewCanvasSaveCount == debugPreviousCanvasSaveCount; return debugNewCanvasSaveCount == debugPreviousCanvasSaveCount;
}()); }());
...@@ -611,10 +614,12 @@ class RenderCustomPaint extends RenderProxyBox { ...@@ -611,10 +614,12 @@ class RenderCustomPaint extends RenderProxyBox {
) { ) {
assert(() { assert(() {
if (child == null && children.isNotEmpty) { if (child == null && children.isNotEmpty) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'$runtimeType does not have a child widget but received a non-empty list of child SemanticsNode:\n' ErrorSummary(
'${children.join('\n')}' '$runtimeType does not have a child widget but received a non-empty list of child SemanticsNode:\n'
); '${children.join('\n')}'
)
]);
} }
return true; return true;
}()); }());
...@@ -677,24 +682,20 @@ class RenderCustomPaint extends RenderProxyBox { ...@@ -677,24 +682,20 @@ class RenderCustomPaint extends RenderProxyBox {
assert(() { assert(() {
final Map<Key, int> keys = HashMap<Key, int>(); final Map<Key, int> keys = HashMap<Key, int>();
final StringBuffer errors = StringBuffer(); final List<DiagnosticsNode> information = <DiagnosticsNode>[];
for (int i = 0; i < newChildSemantics.length; i += 1) { for (int i = 0; i < newChildSemantics.length; i += 1) {
final CustomPainterSemantics child = newChildSemantics[i]; final CustomPainterSemantics child = newChildSemantics[i];
if (child.key != null) { if (child.key != null) {
if (keys.containsKey(child.key)) { if (keys.containsKey(child.key)) {
errors.writeln( information.add(ErrorDescription('- duplicate key ${child.key} found at position $i'));
'- duplicate key ${child.key} found at position $i',
);
} }
keys[child.key] = i; keys[child.key] = i;
} }
} }
if (errors.isNotEmpty) { if (information.isNotEmpty) {
throw FlutterError( information.insert(0, ErrorSummary('Failed to update the list of CustomPainterSemantics:'));
'Failed to update the list of CustomPainterSemantics:\n' throw FlutterError.fromParts(information);
'$errors'
);
} }
return true; return true;
......
...@@ -319,11 +319,13 @@ class RenderFlow extends RenderBox ...@@ -319,11 +319,13 @@ class RenderFlow extends RenderBox
final FlowParentData childParentData = child.parentData; final FlowParentData childParentData = child.parentData;
assert(() { assert(() {
if (childParentData._transform != null) { if (childParentData._transform != null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Cannot call paintChild twice for the same child.\n' ErrorSummary('Cannot call paintChild twice for the same child.'),
'The flow delegate of type ${_delegate.runtimeType} attempted to ' ErrorDescription(
'paint child $i multiple times, which is not permitted.' 'The flow delegate of type ${_delegate.runtimeType} attempted to '
); 'paint child $i multiple times, which is not permitted.'
)
]);
} }
return true; return true;
}()); }());
......
...@@ -74,13 +74,18 @@ class RenderListBody extends RenderBox ...@@ -74,13 +74,18 @@ class RenderListBody extends RenderBox
return true; return true;
break; break;
} }
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'RenderListBody must have unlimited space along its main axis.\n' ErrorSummary('RenderListBody must have unlimited space along its main axis.'),
'RenderListBody does not clip or resize its children, so it must be ' ErrorDescription(
'placed in a parent that does not constrain the main ' 'RenderListBody does not clip or resize its children, so it must be '
'axis. You probably want to put the RenderListBody inside a ' 'placed in a parent that does not constrain the main '
'RenderViewport with a matching main axis.' 'axis.'
); ),
ErrorHint(
'You probably want to put the RenderListBody inside a '
'RenderViewport with a matching main axis.'
)
]);
}()); }());
assert(() { assert(() {
switch (mainAxis) { switch (mainAxis) {
...@@ -96,16 +101,23 @@ class RenderListBody extends RenderBox ...@@ -96,16 +101,23 @@ class RenderListBody extends RenderBox
// TODO(ianh): Detect if we're actually nested blocks and say something // TODO(ianh): Detect if we're actually nested blocks and say something
// more specific to the exact situation in that case, and don't mention // more specific to the exact situation in that case, and don't mention
// nesting blocks in the negative case. // nesting blocks in the negative case.
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'RenderListBody must have a bounded constraint for its cross axis.\n' ErrorSummary('RenderListBody must have a bounded constraint for its cross axis.'),
'RenderListBody forces its children to expand to fit the RenderListBody\'s container, ' ErrorDescription(
'so it must be placed in a parent that constrains the cross ' 'RenderListBody forces its children to expand to fit the RenderListBody\'s container, '
'axis to a finite dimension. If you are attempting to nest a RenderListBody with ' 'so it must be placed in a parent that constrains the cross '
'one direction inside one of another direction, you will want to ' 'axis to a finite dimension.'
'wrap the inner one inside a box that fixes the dimension in that direction, ' ),
'for example, a RenderIntrinsicWidth or RenderIntrinsicHeight object. ' // TODO(jacobr): this hint is a great candidate to promote to being an
'This is relatively expensive, however.' // (that's why we don't do it automatically) // automated quick fix in the future.
); ErrorHint(
'If you are attempting to nest a RenderListBody with '
'one direction inside one of another direction, you will want to '
'wrap the inner one inside a box that fixes the dimension in that direction, '
'for example, a RenderIntrinsicWidth or RenderIntrinsicHeight object. '
'This is relatively expensive, however.' // (that's why we don't do it automatically)
)
]);
}()); }());
double mainAxisExtent = 0.0; double mainAxisExtent = 0.0;
RenderBox child = firstChild; RenderBox child = firstChild;
......
...@@ -456,13 +456,15 @@ class RenderAspectRatio extends RenderProxyBox { ...@@ -456,13 +456,15 @@ class RenderAspectRatio extends RenderProxyBox {
assert(constraints.debugAssertIsValid()); assert(constraints.debugAssertIsValid());
assert(() { assert(() {
if (!constraints.hasBoundedWidth && !constraints.hasBoundedHeight) { if (!constraints.hasBoundedWidth && !constraints.hasBoundedHeight) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'$runtimeType has unbounded constraints.\n' ErrorSummary('$runtimeType has unbounded constraints.'),
'This $runtimeType was given an aspect ratio of $aspectRatio but was given ' ErrorDescription(
'both unbounded width and unbounded height constraints. Because both ' 'This $runtimeType was given an aspect ratio of $aspectRatio but was given '
'constraints were unbounded, this render object doesn\'t know how much ' 'both unbounded width and unbounded height constraints. Because both '
'size to consume.' 'constraints were unbounded, this render object doesn\'t know how much '
); 'size to consume.'
)
]);
} }
return true; return true;
}()); }());
...@@ -1976,16 +1978,16 @@ class RenderDecoratedBox extends RenderProxyBox { ...@@ -1976,16 +1978,16 @@ class RenderDecoratedBox extends RenderProxyBox {
_painter.paint(context.canvas, offset, filledConfiguration); _painter.paint(context.canvas, offset, filledConfiguration);
assert(() { assert(() {
if (debugSaveCount != context.canvas.getSaveCount()) { if (debugSaveCount != context.canvas.getSaveCount()) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'${_decoration.runtimeType} painter had mismatching save and restore calls.\n' ErrorSummary('${_decoration.runtimeType} painter had mismatching save and restore calls.'),
'Before painting the decoration, the canvas save count was $debugSaveCount. ' ErrorDescription(
'After painting it, the canvas save count was ${context.canvas.getSaveCount()}. ' 'Before painting the decoration, the canvas save count was $debugSaveCount. '
'Every call to save() or saveLayer() must be matched by a call to restore().\n' 'After painting it, the canvas save count was ${context.canvas.getSaveCount()}. '
'The decoration was:\n' 'Every call to save() or saveLayer() must be matched by a call to restore().'
' $decoration\n' ),
'The painter was:\n' DiagnosticsProperty<Decoration>('The decoration was', decoration, style: DiagnosticsTreeStyle.errorProperty),
' $_painter' DiagnosticsProperty<BoxPainter>('The painter was', _painter, style: DiagnosticsTreeStyle.errorProperty),
); ]);
} }
return true; return true;
}()); }());
......
...@@ -132,11 +132,11 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje ...@@ -132,11 +132,11 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
assert(() { assert(() {
if (minExtent <= maxExtent) if (minExtent <= maxExtent)
return true; return true;
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The maxExtent for this $runtimeType is less than its minExtent.\n' ErrorSummary('The maxExtent for this $runtimeType is less than its minExtent.'),
'The specified maxExtent was: ${maxExtent.toStringAsFixed(1)}\n' DoubleProperty('The specified maxExtent was', maxExtent),
'The specified minExtent was: ${minExtent.toStringAsFixed(1)}\n' DoubleProperty('The specified minExtent was', minExtent),
); ]);
}()); }());
child?.layout( child?.layout(
constraints.asBoxConstraints(maxExtent: math.max(minExtent, maxExtent - shrinkOffset)), constraints.asBoxConstraints(maxExtent: math.max(minExtent, maxExtent - shrinkOffset)),
......
...@@ -296,14 +296,18 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -296,14 +296,18 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
assert(() { assert(() {
if (!RenderObject.debugCheckingIntrinsics) { if (!RenderObject.debugCheckingIntrinsics) {
assert(this is! RenderShrinkWrappingViewport); // it has its own message assert(this is! RenderShrinkWrappingViewport); // it has its own message
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'$runtimeType does not support returning intrinsic dimensions.\n' ErrorSummary('$runtimeType does not support returning intrinsic dimensions.'),
'Calculating the intrinsic dimensions would require instantiating every child of ' ErrorDescription(
'the viewport, which defeats the point of viewports being lazy.\n' 'Calculating the intrinsic dimensions would require instantiating every child of '
'If you are merely trying to shrink-wrap the viewport in the main axis direction, ' 'the viewport, which defeats the point of viewports being lazy.',
'consider a RenderShrinkWrappingViewport render object (ShrinkWrappingViewport widget), ' ),
'which achieves that effect without implementing the intrinsic dimension API.' ErrorHint(
); 'If you are merely trying to shrink-wrap the viewport in the main axis direction, '
'consider a RenderShrinkWrappingViewport render object (ShrinkWrappingViewport widget), '
'which achieves that effect without implementing the intrinsic dimension API.'
),
]);
} }
return true; return true;
}()); }());
...@@ -1165,19 +1169,23 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat ...@@ -1165,19 +1169,23 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat
switch (axis) { switch (axis) {
case Axis.vertical: case Axis.vertical:
if (!constraints.hasBoundedHeight) { if (!constraints.hasBoundedHeight) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Vertical viewport was given unbounded height.\n' ErrorSummary('Vertical viewport was given unbounded height.'),
'Viewports expand in the scrolling direction to fill their container. ' ErrorDescription(
'In this case, a vertical viewport was given an unlimited amount of ' 'Viewports expand in the scrolling direction to fill their container. '
'vertical space in which to expand. This situation typically happens ' 'In this case, a vertical viewport was given an unlimited amount of '
'when a scrollable widget is nested inside another scrollable widget.\n' 'vertical space in which to expand. This situation typically happens '
'If this widget is always nested in a scrollable widget there ' 'when a scrollable widget is nested inside another scrollable widget.'
'is no need to use a viewport because there will always be enough ' ),
'vertical space for the children. In this case, consider using a ' ErrorHint(
'Column instead. Otherwise, consider using the "shrinkWrap" property ' 'If this widget is always nested in a scrollable widget there '
'(or a ShrinkWrappingViewport) to size the height of the viewport ' 'is no need to use a viewport because there will always be enough '
'to the sum of the heights of its children.' 'vertical space for the children. In this case, consider using a '
); 'Column instead. Otherwise, consider using the "shrinkWrap" property '
'(or a ShrinkWrappingViewport) to size the height of the viewport '
'to the sum of the heights of its children.'
)
]);
} }
if (!constraints.hasBoundedWidth) { if (!constraints.hasBoundedWidth) {
throw FlutterError( throw FlutterError(
...@@ -1191,19 +1199,23 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat ...@@ -1191,19 +1199,23 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat
break; break;
case Axis.horizontal: case Axis.horizontal:
if (!constraints.hasBoundedWidth) { if (!constraints.hasBoundedWidth) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Horizontal viewport was given unbounded width.\n' ErrorSummary('Horizontal viewport was given unbounded width.'),
'Viewports expand in the scrolling direction to fill their container.' ErrorDescription(
'In this case, a horizontal viewport was given an unlimited amount of ' 'Viewports expand in the scrolling direction to fill their container.'
'horizontal space in which to expand. This situation typically happens ' 'In this case, a horizontal viewport was given an unlimited amount of '
'when a scrollable widget is nested inside another scrollable widget.\n' 'horizontal space in which to expand. This situation typically happens '
'If this widget is always nested in a scrollable widget there ' 'when a scrollable widget is nested inside another scrollable widget.'
'is no need to use a viewport because there will always be enough ' ),
'horizontal space for the children. In this case, consider using a ' ErrorHint(
'Row instead. Otherwise, consider using the "shrinkWrap" property ' 'If this widget is always nested in a scrollable widget there '
'(or a ShrinkWrappingViewport) to size the width of the viewport ' 'is no need to use a viewport because there will always be enough '
'to the sum of the widths of its children.' 'horizontal space for the children. In this case, consider using a '
); 'Row instead. Otherwise, consider using the "shrinkWrap" property '
'(or a ShrinkWrappingViewport) to size the width of the viewport '
'to the sum of the widths of its children.'
)
]);
} }
if (!constraints.hasBoundedHeight) { if (!constraints.hasBoundedHeight) {
throw FlutterError( throw FlutterError(
...@@ -1585,14 +1597,18 @@ class RenderShrinkWrappingViewport extends RenderViewportBase<SliverLogicalConta ...@@ -1585,14 +1597,18 @@ class RenderShrinkWrappingViewport extends RenderViewportBase<SliverLogicalConta
bool debugThrowIfNotCheckingIntrinsics() { bool debugThrowIfNotCheckingIntrinsics() {
assert(() { assert(() {
if (!RenderObject.debugCheckingIntrinsics) { if (!RenderObject.debugCheckingIntrinsics) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'$runtimeType does not support returning intrinsic dimensions.\n' ErrorSummary('$runtimeType does not support returning intrinsic dimensions.'),
'Calculating the intrinsic dimensions would require instantiating every child of ' ErrorDescription(
'the viewport, which defeats the point of viewports being lazy.\n' 'Calculating the intrinsic dimensions would require instantiating every child of '
'If you are merely trying to shrink-wrap the viewport in the main axis direction, ' 'the viewport, which defeats the point of viewports being lazy.'
'you should be able to achieve that effect by just giving the viewport loose ' ),
'constraints, without needing to measure its intrinsic dimensions.' ErrorHint(
); 'If you are merely trying to shrink-wrap the viewport in the main axis direction, '
'you should be able to achieve that effect by just giving the viewport loose '
'constraints, without needing to measure its intrinsic dimensions.'
)
]);
} }
return true; return true;
}()); }());
......
...@@ -94,15 +94,19 @@ class _FrameCallbackEntry { ...@@ -94,15 +94,19 @@ class _FrameCallbackEntry {
if (rescheduling) { if (rescheduling) {
assert(() { assert(() {
if (debugCurrentCallbackStack == null) { if (debugCurrentCallbackStack == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'scheduleFrameCallback called with rescheduling true, but no callback is in scope.\n' ErrorSummary('scheduleFrameCallback called with rescheduling true, but no callback is in scope.'),
'The "rescheduling" argument should only be set to true if the ' ErrorDescription(
'callback is being reregistered from within the callback itself, ' 'The "rescheduling" argument should only be set to true if the '
'and only then if the callback itself is entirely synchronous. ' 'callback is being reregistered from within the callback itself, '
'If this is the initial registration of the callback, or if the ' 'and only then if the callback itself is entirely synchronous.'
'callback is asynchronous, then do not use the "rescheduling" ' ),
'argument.' ErrorHint(
); 'If this is the initial registration of the callback, or if the '
'callback is asynchronous, then do not use the "rescheduling" '
'argument.'
)
]);
} }
return true; return true;
}()); }());
......
...@@ -38,6 +38,8 @@ abstract class TickerProvider { ...@@ -38,6 +38,8 @@ abstract class TickerProvider {
Ticker createTicker(TickerCallback onTick); Ticker createTicker(TickerCallback onTick);
} }
// TODO(jacobr): make Ticker Diagnosticable to simplify reporting errors
// related to a ticker.
/// Calls its callback once per animation frame. /// Calls its callback once per animation frame.
/// ///
/// When created, a ticker is initially disabled. Call [start] to /// When created, a ticker is initially disabled. Call [start] to
...@@ -145,11 +147,11 @@ class Ticker { ...@@ -145,11 +147,11 @@ class Ticker {
TickerFuture start() { TickerFuture start() {
assert(() { assert(() {
if (isActive) { if (isActive) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'A ticker was started twice.\n' ErrorSummary('A ticker was started twice.'),
'A ticker that is already active cannot be started again without first stopping it.\n' ErrorDescription('A ticker that is already active cannot be started again without first stopping it.'),
'The affected ticker was: ${ toString(debugIncludeStack: true) }' describeForError('The affected ticker was'),
); ]);
} }
return true; return true;
}()); }());
...@@ -164,6 +166,13 @@ class Ticker { ...@@ -164,6 +166,13 @@ class Ticker {
return _future; return _future;
} }
/// Adds a debug representation of a [Ticker] optimized for including in error
/// messages.
DiagnosticsNode describeForError(String name) {
// TODO(jacobr): make this more structured.
return DiagnosticsProperty<Ticker>(name, this, description: toString(debugIncludeStack: true));
}
/// Stops calling this [Ticker]'s callback. /// Stops calling this [Ticker]'s callback.
/// ///
/// If called with the `canceled` argument set to false (the default), causes /// If called with the `canceled` argument set to false (the default), causes
......
...@@ -116,10 +116,10 @@ class NetworkAssetBundle extends AssetBundle { ...@@ -116,10 +116,10 @@ class NetworkAssetBundle extends AssetBundle {
final HttpClientRequest request = await _httpClient.getUrl(_urlFromKey(key)); final HttpClientRequest request = await _httpClient.getUrl(_urlFromKey(key));
final HttpClientResponse response = await request.close(); final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok) if (response.statusCode != HttpStatus.ok)
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Unable to load asset: $key\n' ErrorSummary('Unable to load asset: $key'),
'HTTP status code: ${response.statusCode}' IntProperty('HTTP status code', response.statusCode),
); ]);
final Uint8List bytes = await consolidateHttpClientResponseBytes(response); final Uint8List bytes = await consolidateHttpClientResponseBytes(response);
return bytes.buffer.asByteData(); return bytes.buffer.asByteData();
} }
......
...@@ -765,7 +765,7 @@ TextInputAction _toTextInputAction(String action) { ...@@ -765,7 +765,7 @@ TextInputAction _toTextInputAction(String action) {
case 'TextInputAction.newline': case 'TextInputAction.newline':
return TextInputAction.newline; return TextInputAction.newline;
} }
throw FlutterError('Unknown text input action: $action'); throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Unknown text input action: $action')]);
} }
FloatingCursorDragState _toTextCursorAction(String state) { FloatingCursorDragState _toTextCursorAction(String state) {
...@@ -777,7 +777,7 @@ FloatingCursorDragState _toTextCursorAction(String state) { ...@@ -777,7 +777,7 @@ FloatingCursorDragState _toTextCursorAction(String state) {
case 'FloatingCursorDragState.end': case 'FloatingCursorDragState.end':
return FloatingCursorDragState.End; return FloatingCursorDragState.End;
} }
throw FlutterError('Unknown text cursor action: $state'); throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Unknown text cursor action: $state')]);
} }
RawFloatingCursorPoint _toTextPoint(FloatingCursorDragState state, Map<String, dynamic> encoded) { RawFloatingCursorPoint _toTextPoint(FloatingCursorDragState state, Map<String, dynamic> encoded) {
......
...@@ -391,16 +391,17 @@ class AnimatedList extends StatefulWidget { ...@@ -391,16 +391,17 @@ class AnimatedList extends StatefulWidget {
final AnimatedListState result = context.ancestorStateOfType(const TypeMatcher<AnimatedListState>()); final AnimatedListState result = context.ancestorStateOfType(const TypeMatcher<AnimatedListState>());
if (nullOk || result != null) if (nullOk || result != null)
return result; return result;
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'AnimatedList.of() called with a context that does not contain an AnimatedList.\n' ErrorSummary('AnimatedList.of() called with a context that does not contain an AnimatedList.'),
'No AnimatedList ancestor could be found starting from the context that ' ErrorDescription('No AnimatedList ancestor could be found starting from the context that was passed to AnimatedList.of().'),
'was passed to AnimatedList.of(). This can happen when the context ' ErrorHint(
'provided is from the same StatefulWidget that built the AnimatedList. ' 'This can happen when the context provided is from the same StatefulWidget that '
'Please see the AnimatedList documentation for examples of how to refer to ' 'built the AnimatedList. Please see the AnimatedList documentation for examples '
'an AnimatedListState object: https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html \n' 'of how to refer to an AnimatedListState object:'
'The context used was:\n' ' https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html'
' $context' ),
); context.describeElement('The context used was')
]);
} }
@override @override
......
...@@ -783,12 +783,14 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -783,12 +783,14 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
final Route<dynamic> result = widget.onUnknownRoute(settings); final Route<dynamic> result = widget.onUnknownRoute(settings);
assert(() { assert(() {
if (result == null) { if (result == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The onUnknownRoute callback returned null.\n' ErrorSummary('The onUnknownRoute callback returned null.'),
'When the $runtimeType requested the route $settings from its ' ErrorDescription(
'onUnknownRoute callback, the callback returned null. Such callbacks ' 'When the $runtimeType requested the route $settings from its '
'must never return null.' 'onUnknownRoute callback, the callback returned null. Such callbacks '
); 'must never return null.'
)
]);
} }
return true; return true;
}()); }());
......
...@@ -645,23 +645,27 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -645,23 +645,27 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
// should not trigger a new frame. // should not trigger a new frame.
assert(() { assert(() {
if (debugBuildingDirtyElements) { if (debugBuildingDirtyElements) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Build scheduled during frame.\n' ErrorSummary('Build scheduled during frame.'),
'While the widget tree was being built, laid out, and painted, ' ErrorDescription(
'a new frame was scheduled to rebuild the widget tree. ' 'While the widget tree was being built, laid out, and painted, '
'This might be because setState() was called from a layout or ' 'a new frame was scheduled to rebuild the widget tree.'
'paint callback. ' ),
'If a change is needed to the widget tree, it should be applied ' ErrorHint(
'as the tree is being built. Scheduling a change for the subsequent ' 'This might be because setState() was called from a layout or '
'frame instead results in an interface that lags behind by one frame. ' 'paint callback. '
'If this was done to make your build dependent on a size measured at ' 'If a change is needed to the widget tree, it should be applied '
'layout time, consider using a LayoutBuilder, CustomSingleChildLayout, ' 'as the tree is being built. Scheduling a change for the subsequent '
'or CustomMultiChildLayout. If, on the other hand, the one frame delay ' 'frame instead results in an interface that lags behind by one frame. '
'is the desired effect, for example because this is an ' 'If this was done to make your build dependent on a size measured at '
'animation, consider scheduling the frame in a post-frame callback ' 'layout time, consider using a LayoutBuilder, CustomSingleChildLayout, '
'using SchedulerBinding.addPostFrameCallback or ' 'or CustomMultiChildLayout. If, on the other hand, the one frame delay '
'using an AnimationController to trigger the animation.' 'is the desired effect, for example because this is an '
); 'animation, consider scheduling the frame in a post-frame callback '
'using SchedulerBinding.addPostFrameCallback or '
'using an AnimationController to trigger the animation.',
)
]);
} }
return true; return true;
}()); }());
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:developer' show Timeline; // to disambiguate reference in dartdocs below import 'dart:developer' show Timeline; // to disambiguate reference in dartdocs below
import 'package:flutter/foundation.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'media_query.dart'; import 'media_query.dart';
...@@ -126,11 +128,13 @@ bool debugChildrenHaveDuplicateKeys(Widget parent, Iterable<Widget> children) { ...@@ -126,11 +128,13 @@ bool debugChildrenHaveDuplicateKeys(Widget parent, Iterable<Widget> children) {
assert(() { assert(() {
final Key nonUniqueKey = _firstNonUniqueKey(children); final Key nonUniqueKey = _firstNonUniqueKey(children);
if (nonUniqueKey != null) { if (nonUniqueKey != null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Duplicate keys found.\n' ErrorSummary('Duplicate keys found.'),
'If multiple keyed nodes exist as children of another node, they must have unique keys.\n' ErrorDescription(
'$parent has multiple children with key $nonUniqueKey.' 'If multiple keyed nodes exist as children of another node, they must have unique keys.\n'
); '$parent has multiple children with key $nonUniqueKey.'
),
]);
} }
return true; return true;
}()); }());
...@@ -153,7 +157,9 @@ bool debugItemsHaveDuplicateKeys(Iterable<Widget> items) { ...@@ -153,7 +157,9 @@ bool debugItemsHaveDuplicateKeys(Iterable<Widget> items) {
assert(() { assert(() {
final Key nonUniqueKey = _firstNonUniqueKey(items); final Key nonUniqueKey = _firstNonUniqueKey(items);
if (nonUniqueKey != null) if (nonUniqueKey != null)
throw FlutterError('Duplicate key found: $nonUniqueKey.'); throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Duplicate key found: $nonUniqueKey.'),
]);
return true; return true;
}()); }());
return false; return false;
...@@ -174,15 +180,12 @@ bool debugItemsHaveDuplicateKeys(Iterable<Widget> items) { ...@@ -174,15 +180,12 @@ bool debugItemsHaveDuplicateKeys(Iterable<Widget> items) {
bool debugCheckHasTable(BuildContext context) { bool debugCheckHasTable(BuildContext context) {
assert(() { assert(() {
if (context.widget is! Table && context.ancestorWidgetOfExactType(Table) == null) { if (context.widget is! Table && context.ancestorWidgetOfExactType(Table) == null) {
final Element element = context; throw FlutterError.fromParts(<DiagnosticsNode>[
throw FlutterError( ErrorSummary('No Table widget found.'),
'No Table widget found.\n' ErrorDescription('${context.widget.runtimeType} widgets require a Table widget ancestor.'),
'${context.widget.runtimeType} widgets require a Table widget ancestor.\n' context.describeWidget('The specific widget that could not find a Table ancestor was'),
'The specific widget that could not find a Table ancestor was:\n' context.describeOwnershipChain('The ownership chain for the affected widget is'),
' ${context.widget}\n' ]);
'The ownership chain for the affected widget is:\n'
' ${element.debugGetCreatorChain(10)}'
);
} }
return true; return true;
}()); }());
...@@ -205,17 +208,16 @@ bool debugCheckHasTable(BuildContext context) { ...@@ -205,17 +208,16 @@ bool debugCheckHasTable(BuildContext context) {
bool debugCheckHasMediaQuery(BuildContext context) { bool debugCheckHasMediaQuery(BuildContext context) {
assert(() { assert(() {
if (context.widget is! MediaQuery && context.ancestorWidgetOfExactType(MediaQuery) == null) { if (context.widget is! MediaQuery && context.ancestorWidgetOfExactType(MediaQuery) == null) {
final Element element = context; throw FlutterError.fromParts(<DiagnosticsNode>[
throw FlutterError( ErrorSummary('No MediaQuery widget found.'),
'No MediaQuery widget found.\n' ErrorDescription('${context.widget.runtimeType} widgets require a MediaQuery widget ancestor.'),
'${context.widget.runtimeType} widgets require a MediaQuery widget ancestor.\n' context.describeWidget('The specific widget that could not find a MediaQuery ancestor was'),
'The specific widget that could not find a MediaQuery ancestor was:\n' context.describeOwnershipChain('The ownership chain for the affected widget is'),
' ${context.widget}\n' ErrorHint(
'The ownership chain for the affected widget is:\n' 'Typically, the MediaQuery widget is introduced by the MaterialApp or '
' ${element.debugGetCreatorChain(10)}\n' 'WidgetsApp widget at the top of your application widget tree.'
'Typically, the MediaQuery widget is introduced by the MaterialApp or ' ),
'WidgetsApp widget at the top of your application widget tree.' ]);
);
} }
return true; return true;
}()); }());
...@@ -238,21 +240,20 @@ bool debugCheckHasMediaQuery(BuildContext context) { ...@@ -238,21 +240,20 @@ bool debugCheckHasMediaQuery(BuildContext context) {
bool debugCheckHasDirectionality(BuildContext context) { bool debugCheckHasDirectionality(BuildContext context) {
assert(() { assert(() {
if (context.widget is! Directionality && context.ancestorWidgetOfExactType(Directionality) == null) { if (context.widget is! Directionality && context.ancestorWidgetOfExactType(Directionality) == null) {
final Element element = context; throw FlutterError.fromParts(<DiagnosticsNode>[
throw FlutterError( ErrorSummary('No Directionality widget found.'),
'No Directionality widget found.\n' ErrorDescription('${context.widget.runtimeType} widgets require a Directionality widget ancestor.\n'),
'${context.widget.runtimeType} widgets require a Directionality widget ancestor.\n' context.describeWidget('The specific widget that could not find a Directionality ancestor was'),
'The specific widget that could not find a Directionality ancestor was:\n' context.describeOwnershipChain('The ownership chain for the affected widget is'),
' ${context.widget}\n' ErrorHint(
'The ownership chain for the affected widget is:\n' 'Typically, the Directionality widget is introduced by the MaterialApp '
' ${element.debugGetCreatorChain(10)}\n' 'or WidgetsApp widget at the top of your application widget tree. It '
'Typically, the Directionality widget is introduced by the MaterialApp ' 'determines the ambient reading direction and is used, for example, to '
'or WidgetsApp widget at the top of your application widget tree. It ' 'determine how to lay out text, how to interpret "start" and "end" '
'determines the ambient reading direction and is used, for example, to ' 'values, and to resolve EdgeInsetsDirectional, '
'determine how to lay out text, how to interpret "start" and "end" ' 'AlignmentDirectional, and other *Directional objects.'
'values, and to resolve EdgeInsetsDirectional, ' ),
'AlignmentDirectional, and other *Directional objects.' ]);
);
} }
return true; return true;
}()); }());
...@@ -268,21 +269,25 @@ bool debugCheckHasDirectionality(BuildContext context) { ...@@ -268,21 +269,25 @@ bool debugCheckHasDirectionality(BuildContext context) {
void debugWidgetBuilderValue(Widget widget, Widget built) { void debugWidgetBuilderValue(Widget widget, Widget built) {
assert(() { assert(() {
if (built == null) { if (built == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'A build function returned null.\n' ErrorSummary('A build function returned null.'),
'The offending widget is: $widget\n' DiagnosticsProperty<Widget>('The offending widget is', widget, style: DiagnosticsTreeStyle.errorProperty),
'Build functions must never return null. ' ErrorDescription('Build functions must never return null.'),
'To return an empty space that causes the building widget to fill available room, return "new Container()". ' ErrorHint(
'To return an empty space that takes as little room as possible, return "new Container(width: 0.0, height: 0.0)".' 'To return an empty space that causes the building widget to fill available room, return "Container()". '
); 'To return an empty space that takes as little room as possible, return "Container(width: 0.0, height: 0.0)".'
),
]);
} }
if (widget == built) { if (widget == built) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'A build function returned context.widget.\n' ErrorSummary('A build function returned context.widget.'),
'The offending widget is: $widget\n' DiagnosticsProperty<Widget>('The offending widget is', widget, style: DiagnosticsTreeStyle.errorProperty),
'Build functions must never return their BuildContext parameter\'s widget or a child that contains "context.widget". ' ErrorDescription(
'Doing so introduces a loop in the widget tree that can cause the app to crash.' 'Build functions must never return their BuildContext parameter\'s widget or a child that contains "context.widget". '
); 'Doing so introduces a loop in the widget tree that can cause the app to crash.'
),
]);
} }
return true; return true;
}()); }());
...@@ -302,7 +307,7 @@ bool debugAssertAllWidgetVarsUnset(String reason) { ...@@ -302,7 +307,7 @@ bool debugAssertAllWidgetVarsUnset(String reason) {
debugPrintGlobalKeyedWidgetLifecycle || debugPrintGlobalKeyedWidgetLifecycle ||
debugProfileBuildsEnabled || debugProfileBuildsEnabled ||
debugHighlightDeprecatedWidgets) { debugHighlightDeprecatedWidgets) {
throw FlutterError(reason); throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('$reason')]);
} }
return true; return true;
}()); }());
......
...@@ -523,11 +523,13 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin ...@@ -523,11 +523,13 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
assert(() { assert(() {
if (_resizeAnimation.status != AnimationStatus.forward) { if (_resizeAnimation.status != AnimationStatus.forward) {
assert(_resizeAnimation.status == AnimationStatus.completed); assert(_resizeAnimation.status == AnimationStatus.completed);
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'A dismissed Dismissible widget is still part of the tree.\n' ErrorSummary('A dismissed Dismissible widget is still part of the tree.'),
'Make sure to implement the onDismissed handler and to immediately remove the Dismissible\n' ErrorHint(
'widget from the application once that handler has fired.' 'Make sure to implement the onDismissed handler and to immediately remove the Dismissible '
); 'widget from the application once that handler has fired.'
)
]);
} }
return true; return true;
}()); }());
......
...@@ -190,7 +190,7 @@ class TextEditingController extends ValueNotifier<TextEditingValue> { ...@@ -190,7 +190,7 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
/// change the controller's [value]. /// change the controller's [value].
set selection(TextSelection newSelection) { set selection(TextSelection newSelection) {
if (newSelection.start > text.length || newSelection.end > text.length) if (newSelection.start > text.length || newSelection.end > text.length)
throw FlutterError('invalid text selection: $newSelection'); throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('invalid text selection: $newSelection')]);
value = value.copyWith(selection: newSelection, composing: TextRange.empty); value = value.copyWith(selection: newSelection, composing: TextRange.empty);
} }
......
...@@ -232,18 +232,23 @@ class GestureDetector extends StatelessWidget { ...@@ -232,18 +232,23 @@ class GestureDetector extends StatelessWidget {
final bool haveScale = onScaleStart != null || onScaleUpdate != null || onScaleEnd != null; final bool haveScale = onScaleStart != null || onScaleUpdate != null || onScaleEnd != null;
if (havePan || haveScale) { if (havePan || haveScale) {
if (havePan && haveScale) { if (havePan && haveScale) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Incorrect GestureDetector arguments.\n' ErrorSummary('Incorrect GestureDetector arguments.'),
'Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan. Just use the scale gesture recognizer.' ErrorDescription(
); 'Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan.'
),
ErrorHint('Just use the scale gesture recognizer.')
]);
} }
final String recognizer = havePan ? 'pan' : 'scale'; final String recognizer = havePan ? 'pan' : 'scale';
if (haveVerticalDrag && haveHorizontalDrag) { if (haveVerticalDrag && haveHorizontalDrag) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Incorrect GestureDetector arguments.\n' ErrorSummary('Incorrect GestureDetector arguments.'),
'Simultaneously having a vertical drag gesture recognizer, a horizontal drag gesture recognizer, and a $recognizer gesture recognizer ' ErrorDescription(
'will result in the $recognizer gesture recognizer being ignored, since the other two will catch all drags.' 'Simultaneously having a vertical drag gesture recognizer, a horizontal drag gesture recognizer, and a $recognizer gesture recognizer '
); 'will result in the $recognizer gesture recognizer being ignored, since the other two will catch all drags.'
)
]);
} }
} }
return true; return true;
...@@ -935,13 +940,15 @@ class RawGestureDetectorState extends State<RawGestureDetector> { ...@@ -935,13 +940,15 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
void replaceGestureRecognizers(Map<Type, GestureRecognizerFactory> gestures) { void replaceGestureRecognizers(Map<Type, GestureRecognizerFactory> gestures) {
assert(() { assert(() {
if (!context.findRenderObject().owner.debugDoingLayout) { if (!context.findRenderObject().owner.debugDoingLayout) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Unexpected call to replaceGestureRecognizers() method of RawGestureDetectorState.\n' ErrorSummary('Unexpected call to replaceGestureRecognizers() method of RawGestureDetectorState.'),
'The replaceGestureRecognizers() method can only be called during the layout phase. ' ErrorDescription('The replaceGestureRecognizers() method can only be called during the layout phase.'),
'To set the gesture recognizers at other times, trigger a new build using setState() ' ErrorHint(
'and provide the new gesture recognizers as constructor arguments to the corresponding ' 'To set the gesture recognizers at other times, trigger a new build using setState() '
'RawGestureDetector or GestureDetector object.' 'and provide the new gesture recognizers as constructor arguments to the corresponding '
); 'RawGestureDetector or GestureDetector object.'
)
]);
} }
return true; return true;
}()); }());
......
...@@ -262,14 +262,15 @@ class Hero extends StatefulWidget { ...@@ -262,14 +262,15 @@ class Hero extends StatefulWidget {
void inviteHero(StatefulElement hero, Object tag) { void inviteHero(StatefulElement hero, Object tag) {
assert(() { assert(() {
if (result.containsKey(tag)) { if (result.containsKey(tag)) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'There are multiple heroes that share the same tag within a subtree.\n' ErrorSummary('There are multiple heroes that share the same tag within a subtree.'),
'Within each subtree for which heroes are to be animated (i.e. a PageRoute subtree), ' ErrorDescription(
'each Hero must have a unique non-null tag.\n' 'Within each subtree for which heroes are to be animated (i.e. a PageRoute subtree), '
'In this case, multiple heroes had the following tag: $tag\n' 'each Hero must have a unique non-null tag.\n'
'Here is the subtree for one of the offending heroes:\n' 'In this case, multiple heroes had the following tag: $tag\n'
'${hero.toStringDeep(prefixLineOne: "# ")}' ),
); DiagnosticsProperty<StatefulElement>('Here is the subtree for one of the offending heroes', hero, linePrefix: '# ', style: DiagnosticsTreeStyle.dense),
]);
} }
return true; return true;
}()); }());
......
...@@ -795,15 +795,16 @@ class MediaQuery extends InheritedWidget { ...@@ -795,15 +795,16 @@ class MediaQuery extends InheritedWidget {
return query.data; return query.data;
if (nullOk) if (nullOk)
return null; return null;
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'MediaQuery.of() called with a context that does not contain a MediaQuery.\n' ErrorSummary('MediaQuery.of() called with a context that does not contain a MediaQuery.'),
'No MediaQuery ancestor could be found starting from the context that was passed ' ErrorDescription(
'to MediaQuery.of(). This can happen because you do not have a WidgetsApp or ' 'No MediaQuery ancestor could be found starting from the context that was passed '
'MaterialApp widget (those widgets introduce a MediaQuery), or it can happen ' 'to MediaQuery.of(). This can happen because you do not have a WidgetsApp or '
'if the context you use comes from a widget above those widgets.\n' 'MaterialApp widget (those widgets introduce a MediaQuery), or it can happen '
'The context used was:\n' 'if the context you use comes from a widget above those widgets.'
' $context' ),
); context.describeElement('The context used was')
]);
} }
/// Returns textScaleFactor for the nearest MediaQuery ancestor or 1.0, if /// Returns textScaleFactor for the nearest MediaQuery ancestor or 1.0, if
......
...@@ -1630,26 +1630,28 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1630,26 +1630,28 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
if (route == null && !allowNull) { if (route == null && !allowNull) {
assert(() { assert(() {
if (widget.onUnknownRoute == null) { if (widget.onUnknownRoute == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'If a Navigator has no onUnknownRoute, then its onGenerateRoute must never return null.\n' ErrorSummary('If a Navigator has no onUnknownRoute, then its onGenerateRoute must never return null.'),
'When trying to build the route "$name", onGenerateRoute returned null, but there was no ' ErrorDescription(
'onUnknownRoute callback specified.\n' 'When trying to build the route "$name", onGenerateRoute returned null, but there was no '
'The Navigator was:\n' 'onUnknownRoute callback specified.'
' $this' ),
); DiagnosticsProperty<NavigatorState>('The Navigator was', this, style: DiagnosticsTreeStyle.errorProperty),
]);
} }
return true; return true;
}()); }());
route = widget.onUnknownRoute(settings); route = widget.onUnknownRoute(settings);
assert(() { assert(() {
if (route == null) { if (route == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'A Navigator\'s onUnknownRoute returned null.\n' ErrorSummary('A Navigator\'s onUnknownRoute returned null.'),
'When trying to build the route "$name", both onGenerateRoute and onUnknownRoute returned ' ErrorDescription(
'null. The onUnknownRoute callback should never return null.\n' 'When trying to build the route "$name", both onGenerateRoute and onUnknownRoute returned '
'The Navigator was:\n' 'null. The onUnknownRoute callback should never return null.'
' $this' ),
); DiagnosticsProperty<NavigatorState>('The Navigator was', this, style: DiagnosticsTreeStyle.errorProperty),
]);
} }
return true; return true;
}()); }());
......
...@@ -238,17 +238,16 @@ class Overlay extends StatefulWidget { ...@@ -238,17 +238,16 @@ class Overlay extends StatefulWidget {
final OverlayState result = context.ancestorStateOfType(const TypeMatcher<OverlayState>()); final OverlayState result = context.ancestorStateOfType(const TypeMatcher<OverlayState>());
assert(() { assert(() {
if (debugRequiredFor != null && result == null) { if (debugRequiredFor != null && result == null) {
final String additional = context.widget != debugRequiredFor final List<DiagnosticsNode> information = <DiagnosticsNode>[
? '\nThe context from which that widget was searching for an overlay was:\n $context' ErrorSummary('No Overlay widget found.'),
: ''; ErrorDescription('${debugRequiredFor.runtimeType} widgets require an Overlay widget ancestor for correct operation.'),
throw FlutterError( ErrorHint('The most common way to add an Overlay to an application is to include a MaterialApp or Navigator widget in the runApp() call.'),
'No Overlay widget found.\n' DiagnosticsProperty<Widget>('The specific widget that failed to find an overlay was', debugRequiredFor, style: DiagnosticsTreeStyle.errorProperty),
'${debugRequiredFor.runtimeType} widgets require an Overlay widget ancestor for correct operation.\n' if (context.widget != debugRequiredFor)
'The most common way to add an Overlay to an application is to include a MaterialApp or Navigator widget in the runApp() call.\n' context.describeElement('The context from which that widget was searching for an overlay was')
'The specific widget that failed to find an overlay was:\n' ];
' $debugRequiredFor'
'$additional' throw FlutterError.fromParts(information);
);
} }
return true; return true;
}()); }());
......
...@@ -422,17 +422,17 @@ class ClampingScrollPhysics extends ScrollPhysics { ...@@ -422,17 +422,17 @@ class ClampingScrollPhysics extends ScrollPhysics {
double applyBoundaryConditions(ScrollMetrics position, double value) { double applyBoundaryConditions(ScrollMetrics position, double value) {
assert(() { assert(() {
if (value == position.pixels) { if (value == position.pixels) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'$runtimeType.applyBoundaryConditions() was called redundantly.\n' ErrorSummary('$runtimeType.applyBoundaryConditions() was called redundantly.'),
'The proposed new position, $value, is exactly equal to the current position of the ' ErrorDescription(
'given ${position.runtimeType}, ${position.pixels}.\n' 'The proposed new position, $value, is exactly equal to the current position of the '
'The applyBoundaryConditions method should only be called when the value is ' 'given ${position.runtimeType}, ${position.pixels}.\n'
'going to actually change the pixels, otherwise it is redundant.\n' 'The applyBoundaryConditions method should only be called when the value is '
'The physics object in question was:\n' 'going to actually change the pixels, otherwise it is redundant.'
' $this\n' ),
'The position object in question was:\n' DiagnosticsProperty<ScrollPhysics>('The physics object in question was', this, style: DiagnosticsTreeStyle.errorProperty),
' $position' DiagnosticsProperty<ScrollMetrics>('The position object in question was', position, style: DiagnosticsTreeStyle.errorProperty)
); ]);
} }
return true; return true;
}()); }());
......
...@@ -203,12 +203,14 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -203,12 +203,14 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
assert(() { assert(() {
final double delta = newPixels - pixels; final double delta = newPixels - pixels;
if (overscroll.abs() > delta.abs()) { if (overscroll.abs() > delta.abs()) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'$runtimeType.applyBoundaryConditions returned invalid overscroll value.\n' ErrorSummary('$runtimeType.applyBoundaryConditions returned invalid overscroll value.'),
'setPixels() was called to change the scroll offset from $pixels to $newPixels.\n' ErrorDescription(
'That is a delta of $delta units.\n' 'setPixels() was called to change the scroll offset from $pixels to $newPixels.\n'
'$runtimeType.applyBoundaryConditions reported an overscroll of $overscroll units.' 'That is a delta of $delta units.\n'
); '$runtimeType.applyBoundaryConditions reported an overscroll of $overscroll units.'
)
]);
} }
return true; return true;
}()); }());
...@@ -374,16 +376,18 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -374,16 +376,18 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
assert(() { assert(() {
final double delta = value - pixels; final double delta = value - pixels;
if (result.abs() > delta.abs()) { if (result.abs() > delta.abs()) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'${physics.runtimeType}.applyBoundaryConditions returned invalid overscroll value.\n' ErrorSummary('${physics.runtimeType}.applyBoundaryConditions returned invalid overscroll value.'),
'The method was called to consider a change from $pixels to $value, which is a ' ErrorDescription(
'delta of ${delta.toStringAsFixed(1)} units. However, it returned an overscroll of ' 'The method was called to consider a change from $pixels to $value, which is a '
'${result.toStringAsFixed(1)} units, which has a greater magnitude than the delta. ' 'delta of ${delta.toStringAsFixed(1)} units. However, it returned an overscroll of '
'The applyBoundaryConditions method is only supposed to reduce the possible range ' '${result.toStringAsFixed(1)} units, which has a greater magnitude than the delta. '
'of movement, not increase it.\n' 'The applyBoundaryConditions method is only supposed to reduce the possible range '
'The scroll extents are $minScrollExtent .. $maxScrollExtent, and the ' 'of movement, not increase it.\n'
'viewport dimension is $viewportDimension.' 'The scroll extents are $minScrollExtent .. $maxScrollExtent, and the '
); 'viewport dimension is $viewportDimension.'
)
]);
} }
return true; return true;
}()); }());
......
...@@ -82,13 +82,15 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> imple ...@@ -82,13 +82,15 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> imple
assert(() { assert(() {
if (_ticker == null) if (_ticker == null)
return true; return true;
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.\n' ErrorSummary('$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.'),
'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. If a ' ErrorDescription('A SingleTickerProviderStateMixin can only be used as a TickerProvider once.'),
'State is used for multiple AnimationController objects, or if it is passed to other ' ErrorHint(
'objects and those objects might use it more than one time in total, then instead of ' 'If a State is used for multiple AnimationController objects, or if it is passed to other '
'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.' 'objects and those objects might use it more than one time in total, then instead of '
); 'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.'
)
]);
}()); }());
_ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by $this' : null); _ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by $this' : null);
// We assume that this is called from initState, build, or some sort of // We assume that this is called from initState, build, or some sort of
...@@ -103,15 +105,20 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> imple ...@@ -103,15 +105,20 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> imple
assert(() { assert(() {
if (_ticker == null || !_ticker.isActive) if (_ticker == null || !_ticker.isActive)
return true; return true;
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'$this was disposed with an active Ticker.\n' ErrorSummary('$this was disposed with an active Ticker.'),
'$runtimeType created a Ticker via its SingleTickerProviderStateMixin, but at the time ' ErrorDescription(
'dispose() was called on the mixin, that Ticker was still active. The Ticker must ' '$runtimeType created a Ticker via its SingleTickerProviderStateMixin, but at the time '
'be disposed before calling super.dispose(). Tickers used by AnimationControllers ' 'dispose() was called on the mixin, that Ticker was still active. The Ticker must '
'should be disposed by calling dispose() on the AnimationController itself. ' 'be disposed before calling super.dispose().'
'Otherwise, the ticker will leak.\n' ),
'The offending ticker was: ${_ticker.toString(debugIncludeStack: true)}' ErrorHint(
); 'Tickers used by AnimationControllers '
'should be disposed by calling dispose() on the AnimationController itself. '
'Otherwise, the ticker will leak.'
),
_ticker.describeForError('The offending ticker was')
]);
}()); }());
super.dispose(); super.dispose();
} }
...@@ -175,15 +182,20 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements ...@@ -175,15 +182,20 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
if (_tickers != null) { if (_tickers != null) {
for (Ticker ticker in _tickers) { for (Ticker ticker in _tickers) {
if (ticker.isActive) { if (ticker.isActive) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'$this was disposed with an active Ticker.\n' ErrorSummary('$this was disposed with an active Ticker.'),
'$runtimeType created a Ticker via its TickerProviderStateMixin, but at the time ' ErrorDescription(
'dispose() was called on the mixin, that Ticker was still active. All Tickers must ' '$runtimeType created a Ticker via its TickerProviderStateMixin, but at the time '
'be disposed before calling super.dispose(). Tickers used by AnimationControllers ' 'dispose() was called on the mixin, that Ticker was still active. All Tickers must '
'should be disposed by calling dispose() on the AnimationController itself. ' 'be disposed before calling super.dispose().'
'Otherwise, the ticker will leak.\n' ),
'The offending ticker was: ${ticker.toString(debugIncludeStack: true)}' ErrorHint(
); 'Tickers used by AnimationControllers '
'should be disposed by calling dispose() on the AnimationController itself. '
'Otherwise, the ticker will leak.'
),
ticker.describeForError('The offending ticker was'),
]);
} }
} }
} }
......
...@@ -238,8 +238,26 @@ void main() { ...@@ -238,8 +238,26 @@ void main() {
vsync: const TestVSync(), vsync: const TestVSync(),
); );
final CurvedAnimation curved = CurvedAnimation(parent: controller, curve: BogusCurve()); final CurvedAnimation curved = CurvedAnimation(parent: controller, curve: BogusCurve());
FlutterError error;
expect(() { curved.value; }, throwsFlutterError); try {
curved.value;
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.toStringDeep(), matches(
// RegExp matcher is required here due to flutter web and flutter mobile generating
// slightly different floating point numbers
// in Flutter web 0.0 sometimes just appears as 0. or 0
RegExp(
r'''FlutterError
Invalid curve endpoint at \d+(\.\d*)?\.
Curves must map 0\.0 to near zero and 1\.0 to near one but
BogusCurve mapped \d+(\.\d*)? to \d+(\.\d*)?, which is near \d+(\.\d*)?\.
''',
multiLine: true
),
));
}); });
test('CurvedAnimation running with different forward and reverse durations.', () { test('CurvedAnimation running with different forward and reverse durations.', () {
......
...@@ -167,4 +167,68 @@ void main() { ...@@ -167,4 +167,68 @@ void main() {
expect(find.text('go to second page'), findsOneWidget); expect(find.text('go to second page'), findsOneWidget);
expect(find.text('second route'), findsNothing); expect(find.text('second route'), findsNothing);
}); });
testWidgets('Throws FlutterError when onUnknownRoute is null', (
WidgetTester tester) async {
final GlobalKey<NavigatorState> key = GlobalKey();
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTabView(
navigatorKey: key,
builder: (BuildContext context) => const Text('first route'),
onUnknownRoute: null,
),
),
);
FlutterError error;
try {
key.currentState.pushNamed('/2');
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' Could not find a generator for route RouteSettings("/2", null) in\n'
' the _CupertinoTabViewState.\n'
' Generators for routes are searched for in the following order:\n'
' 1. For the "/" route, the "builder" property, if non-null, is\n'
' used.\n'
' 2. Otherwise, the "routes" table is used, if it has an entry for\n'
' the route.\n'
' 3. Otherwise, onGenerateRoute is called. It should return a\n'
' non-null value for any valid route not handled by "builder" and\n'
' "routes".\n'
' 4. Finally if all else fails onUnknownRoute is called.\n'
' Unfortunately, onUnknownRoute was not set.\n'
));
});
testWidgets('Throws FlutterError when onUnknownRoute returns null', (
WidgetTester tester) async {
final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>();
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTabView(
navigatorKey: key,
builder: (BuildContext context) => const Text('first route'),
onUnknownRoute: (_) => null,
),
),
);
FlutterError error;
try {
key.currentState.pushNamed('/2');
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' The onUnknownRoute callback returned null.\n'
' When the _CupertinoTabViewState requested the route\n'
' RouteSettings("/2", null) from its onUnknownRoute callback, the\n'
' callback returned null. Such callbacks must never return null.\n'
));
});
} }
...@@ -315,4 +315,24 @@ void main() { ...@@ -315,4 +315,24 @@ void main() {
expect(b.result, isTrue); expect(b.result, isTrue);
expect(notifications, 1); expect(notifications, 1);
}); });
test('Throws FlutterError when disposed and called', () {
final TestNotifier testNotifier = TestNotifier();
testNotifier.dispose();
FlutterError error;
try {
testNotifier.dispose();
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error, isFlutterError);
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' A TestNotifier was used after being disposed.\n'
' Once you have called dispose() on a TestNotifier, it can no\n'
' longer be used.\n'
));
});
} }
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// 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/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -10,7 +11,170 @@ void main() { ...@@ -10,7 +11,170 @@ void main() {
await tester.pumpWidget(const ListTile()); await tester.pumpWidget(const ListTile());
final dynamic exception = tester.takeException(); final dynamic exception = tester.takeException();
expect(exception, isFlutterError); expect(exception, isFlutterError);
expect(exception.toString(), startsWith('No Material widget found.')); final FlutterError error = exception;
expect(exception.toString(), endsWith(':\n ListTile\nThe ancestors of this widget were:\n [root]')); expect(error.diagnostics.length, 5);
expect(error.diagnostics[2].level, DiagnosticLevel.hint);
expect(
error.diagnostics[2].toStringDeep(),
equalsIgnoringHashCodes(
'To introduce a Material widget, you can either directly include\n'
'one, or use a widget that contains Material itself, such as a\n'
'Card, Dialog, Drawer, or Scaffold.\n',
),
);
expect(error.diagnostics[3], isInstanceOf<DiagnosticsProperty<Element>>());
expect(error.diagnostics[4], isInstanceOf<DiagnosticsBlock>());
expect(error.toStringDeep(),
'FlutterError\n'
' No Material widget found.\n'
' ListTile widgets require a Material widget ancestor.\n'
' In material design, most widgets are conceptually "printed" on a\n'
' sheet of material. In Flutter\'s material library, that material\n'
' is represented by the Material widget. It is the Material widget\n'
' that renders ink splashes, for instance. Because of this, many\n'
' material library widgets require that there be a Material widget\n'
' in the tree above them.\n'
' To introduce a Material widget, you can either directly include\n'
' one, or use a widget that contains Material itself, such as a\n'
' Card, Dialog, Drawer, or Scaffold.\n'
' The specific widget that could not find a Material ancestor was:\n'
' ListTile\n'
' The ancestors of this widget were:\n'
' [root]\n'
);
});
testWidgets('debugCheckHasMaterialLocalizations control test', (
WidgetTester tester) async {
await tester.pumpWidget(const BackButton());
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
final FlutterError error = exception;
expect(error.diagnostics.length, 6);
expect(error.diagnostics[3].level, DiagnosticLevel.hint);
expect(
error.diagnostics[3].toStringDeep(),
equalsIgnoringHashCodes(
'To introduce a MaterialLocalizations, either use a MaterialApp at\n'
'the root of your application to include them automatically, or\n'
'add a Localization widget with a MaterialLocalizations delegate.\n',
),
);
expect(error.diagnostics[4], isInstanceOf<DiagnosticsProperty<Element>>());
expect(error.diagnostics[5], isInstanceOf<DiagnosticsBlock>());
expect(error.toStringDeep(),
'FlutterError\n'
' No MaterialLocalizations found.\n'
' BackButton widgets require MaterialLocalizations to be provided\n'
' by a Localizations widget ancestor.\n'
' Localizations are used to generate many different messages,\n'
' labels,and abbreviations which are used by the material library.\n'
' To introduce a MaterialLocalizations, either use a MaterialApp at\n'
' the root of your application to include them automatically, or\n'
' add a Localization widget with a MaterialLocalizations delegate.\n'
' The specific widget that could not find a MaterialLocalizations\n'
' ancestor was:\n'
' BackButton\n'
' The ancestors of this widget were:\n'
' [root]\n'
);
});
testWidgets(
'debugCheckHasScaffold control test', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (BuildContext context) {
showBottomSheet<void>(context: context,
builder: (BuildContext context) => Container());
return Container();
}
),
),
);
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
final FlutterError error = exception;
expect(error.diagnostics.length, 5);
expect(error.diagnostics[2], isInstanceOf<DiagnosticsProperty<Element>>());
expect(error.diagnostics[3], isInstanceOf<DiagnosticsBlock>());
expect(error.diagnostics[4].level, DiagnosticLevel.hint);
expect(
error.diagnostics[4].toStringDeep(),
equalsIgnoringHashCodes(
'Typically, the Scaffold widget is introduced by the MaterialApp\n'
'or WidgetsApp widget at the top of your application widget tree.\n',
),
);
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' No Scaffold widget found.\n'
' Builder widgets require a Scaffold widget ancestor.\n'
' The specific widget that could not find a Scaffold ancestor was:\n'
' Builder\n'
' The ancestors of this widget were:\n'
' Semantics\n'
' Builder\n'
' RepaintBoundary-[GlobalKey#2d465]\n'
' IgnorePointer\n'
' AnimatedBuilder\n'
' FadeTransition\n'
' FractionalTranslation\n'
' SlideTransition\n'
' _FadeUpwardsPageTransition\n'
' AnimatedBuilder\n'
' RepaintBoundary\n'
' _FocusMarker\n'
' Semantics\n'
' FocusScope\n'
' PageStorage\n'
' Offstage\n'
' _ModalScopeStatus\n'
' _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#969b7]\n'
' _OverlayEntry-[LabeledGlobalKey<_OverlayEntryState>#7a3ae]\n'
' Stack\n'
' _Theatre\n'
' Overlay-[LabeledGlobalKey<OverlayState>#31a52]\n'
' _FocusMarker\n'
' Semantics\n'
' FocusScope\n'
' AbsorbPointer\n'
' _PointerListener\n'
' Listener\n'
' Navigator-[GlobalObjectKey<NavigatorState> _WidgetsAppState#10579]\n'
' IconTheme\n'
' IconTheme\n'
' _InheritedCupertinoTheme\n'
' CupertinoTheme\n'
' _InheritedTheme\n'
' Theme\n'
' AnimatedTheme\n'
' Builder\n'
' DefaultTextStyle\n'
' CustomPaint\n'
' Banner\n'
' CheckedModeBanner\n'
' Title\n'
' Directionality\n'
' _LocalizationsScope-[GlobalKey#a51e3]\n'
' Semantics\n'
' Localizations\n'
' MediaQuery\n'
' _MediaQueryFromWindow\n'
' DefaultFocusTraversal\n'
' Actions\n'
' _ShortcutsMarker\n'
' Semantics\n'
' _FocusMarker\n'
' Focus\n'
' Shortcuts\n'
' WidgetsApp-[GlobalObjectKey _MaterialAppState#38e79]\n'
' ScrollConfiguration\n'
' MaterialApp\n'
' [root]\n'
' Typically, the Scaffold widget is introduced by the MaterialApp\n'
' or WidgetsApp widget at the top of your application widget tree.\n',
));
}); });
} }
...@@ -468,7 +468,13 @@ void main() { ...@@ -468,7 +468,13 @@ void main() {
)); ));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// An exception should've been thrown because the `builder` returned null. // An exception should've been thrown because the `builder` returned null.
expect(tester.takeException(), isInstanceOf<FlutterError>()); final dynamic exception = tester.takeException();
expect(exception, isInstanceOf<FlutterError>());
expect(exception.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' The builder for route "broken" returned null.\n'
' Route builders must never return null.\n'
));
}); });
testWidgets('test iOS edge swipe then drop back at starting point works', (WidgetTester tester) async { testWidgets('test iOS edge swipe then drop back at starting point works', (WidgetTester tester) async {
......
...@@ -529,14 +529,15 @@ void main() { ...@@ -529,14 +529,15 @@ void main() {
// which will change depending on where the test is run. // which will change depending on where the test is run.
expect(lines.length, greaterThan(7)); expect(lines.length, greaterThan(7));
expect( expect(
lines.take(7).join('\n'), lines.take(8).join('\n'),
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════\n' '══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════\n'
'The following assertion was thrown building Stepper(dirty,\n' 'The following assertion was thrown building Stepper(dirty,\n'
'dependencies: [_LocalizationsScope-[GlobalKey#00000]], state:\n' 'dependencies: [_LocalizationsScope-[GlobalKey#00000]], state:\n'
'_StepperState#00000):\n' '_StepperState#00000):\n'
'Steppers must not be nested. The material specification advises\n' 'Steppers must not be nested.\n'
'that one should avoid embedding steppers within steppers.\n' 'The material specification advises that one should avoid\n'
'embedding steppers within steppers.\n'
'https://material.io/archive/guidelines/components/steppers.html#steppers-usage' 'https://material.io/archive/guidelines/components/steppers.html#steppers-usage'
), ),
); );
......
...@@ -3259,9 +3259,24 @@ void main() { ...@@ -3259,9 +3259,24 @@ void main() {
expect(controller.selection.start, lessThanOrEqualTo(0)); expect(controller.selection.start, lessThanOrEqualTo(0));
expect(controller.selection.end, lessThanOrEqualTo(0)); expect(controller.selection.end, lessThanOrEqualTo(0));
expect(() { FlutterError error;
try {
controller.selection = const TextSelection.collapsed(offset: 10); controller.selection = const TextSelection.collapsed(offset: 10);
}, throwsFlutterError); } on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 1);
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' invalid text selection: TextSelection(baseOffset: 10,\n'
' extentOffset: 10, affinity: TextAffinity.downstream,\n'
' isDirectional: false)\n',
),
);
}
}); });
testWidgets('maxLength limits input.', (WidgetTester tester) async { testWidgets('maxLength limits input.', (WidgetTester tester) async {
......
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
// 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' show DiagnosticLevel, FlutterError;
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
import '../rendering/rendering_tester.dart';
class SillyBorder extends BoxBorder { class SillyBorder extends BoxBorder {
@override @override
...@@ -115,6 +117,39 @@ void main() { ...@@ -115,6 +117,39 @@ void main() {
expect(() => BoxBorder.lerp(SillyBorder(), const Border(), 2.0), throwsFlutterError); expect(() => BoxBorder.lerp(SillyBorder(), const Border(), 2.0), throwsFlutterError);
}); });
test('BoxBorder.lerp throws correct FlutterError message', () {
FlutterError error;
try {
BoxBorder.lerp(SillyBorder(), const Border(), 2.0);
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.diagnostics.length, 3);
expect(error.diagnostics[2].level, DiagnosticLevel.hint);
expect(
error.diagnostics[2].toStringDeep(),
equalsIgnoringHashCodes(
'For a more general interpolation method, consider using\n'
'ShapeBorder.lerp instead.\n',
),
);
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' BoxBorder.lerp can only interpolate Border and BorderDirectional\n'
' classes.\n'
' BoxBorder.lerp() was called with two objects of type SillyBorder\n'
' and Border:\n'
' SillyBorder()\n'
' Border.all(BorderSide(Color(0xff000000), 0.0,\n'
' BorderStyle.none))\n'
' However, only Border and BorderDirectional classes are supported\n'
' by this method.\n'
' For a more general interpolation method, consider using\n'
' ShapeBorder.lerp instead.\n'
));
});
test('BoxBorder.getInnerPath / BoxBorder.getOuterPath', () { test('BoxBorder.getInnerPath / BoxBorder.getOuterPath', () {
// for Border, BorderDirectional // for Border, BorderDirectional
const Border border = Border(top: BorderSide(width: 10.0), right: BorderSide(width: 20.0)); const Border border = Border(top: BorderSide(width: 10.0), right: BorderSide(width: 20.0));
......
...@@ -10,8 +10,8 @@ import 'dart:ui' as ui show Image, ImageByteFormat, ColorFilter; ...@@ -10,8 +10,8 @@ import 'dart:ui' as ui show Image, ImageByteFormat, ColorFilter;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:quiver/testing/async.dart'; import 'package:quiver/testing/async.dart';
import '../flutter_test_alternative.dart';
import '../flutter_test_alternative.dart';
import '../painting/mocks_for_image_cache.dart'; import '../painting/mocks_for_image_cache.dart';
import '../rendering/rendering_tester.dart'; import '../rendering/rendering_tester.dart';
...@@ -232,6 +232,43 @@ void main() { ...@@ -232,6 +232,43 @@ void main() {
expect(call.positionalArguments[3].filterQuality, FilterQuality.low); expect(call.positionalArguments[3].filterQuality, FilterQuality.low);
}); });
test(
'DecorationImage with null textDirection configuration should throw Error', () {
final DecorationImage backgroundImage = DecorationImage(
image: SynchronousTestImageProvider(),
matchTextDirection: true,
);
final BoxDecoration boxDecoration = BoxDecoration(
image: backgroundImage);
final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
assert(false);
});
final TestCanvas canvas = TestCanvas(<Invocation>[]);
FlutterError error;
try {
boxPainter.paint(canvas, Offset.zero, const ImageConfiguration(
size: Size(100.0, 100.0), textDirection: null));
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.diagnostics.length, 4);
expect(error.diagnostics[2], isInstanceOf<DiagnosticsProperty<DecorationImage>>());
expect(error.diagnostics[3], isInstanceOf<DiagnosticsProperty<ImageConfiguration>>());
expect(error.toStringDeep(),
'FlutterError\n'
' ImageDecoration.matchTextDirection can only be used when a\n'
' TextDirection is available.\n'
' When DecorationImagePainter.paint() was called, there was no text\n'
' direction provided in the ImageConfiguration object to match.\n'
' The DecorationImage was:\n'
' DecorationImage(SynchronousTestImageProvider(), center, match\n'
' text direction)\n'
' The ImageConfiguration was:\n'
' ImageConfiguration(size: Size(100.0, 100.0))\n'
);
});
test('BoxDecoration.lerp - shapes', () { test('BoxDecoration.lerp - shapes', () {
// We don't lerp the shape, we just switch from one to the other at t=0.5. // We don't lerp the shape, we just switch from one to the other at t=0.5.
// (Use a ShapeDecoration and ShapeBorder if you want to lerp the shapes...) // (Use a ShapeDecoration and ShapeBorder if you want to lerp the shapes...)
......
...@@ -235,4 +235,29 @@ void main() { ...@@ -235,4 +235,29 @@ void main() {
expect(textSpan.getSpanForPosition(const TextPosition(offset: 2)).runtimeType, WidgetSpan); expect(textSpan.getSpanForPosition(const TextPosition(offset: 2)).runtimeType, WidgetSpan);
expect(textSpan.getSpanForPosition(const TextPosition(offset: 3)).runtimeType, TextSpan); expect(textSpan.getSpanForPosition(const TextPosition(offset: 3)).runtimeType, TextSpan);
}); });
test('TextSpan with a null child should throw FlutterError', () {
const TextSpan text = TextSpan(
text: 'foo bar',
children: <InlineSpan>[
null,
],
);
FlutterError error;
try {
text.computeToPlainText(StringBuffer());
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.toStringDeep(),
'FlutterError\n'
' TextSpan contains a null child.\n'
' A TextSpan object with a non-null child list should not have any\n'
' nulls in its child list.\n'
' The full text in question was:\n'
' TextSpan("foo bar")\n'
);
});
} }
...@@ -108,14 +108,20 @@ void main() { ...@@ -108,14 +108,20 @@ void main() {
), ),
); );
final List<String> errorMessages = <String>[]; final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
layout(box, onErrors: () { layout(box, onErrors: () {
errorMessages.addAll( errors.addAll(renderer.takeAllFlutterErrorDetails());
renderer.takeAllFlutterErrorDetails().map((FlutterErrorDetails details) => '${details.exceptionAsString()}'),
);
}); });
expect(errorMessages, hasLength(2)); expect(errors, hasLength(2));
expect(errorMessages[0], contains('RenderAspectRatio has unbounded constraints.')); expect(errors.first.exception, isA<FlutterError>());
expect(errors.first.exception.toStringDeep(),
'FlutterError\n'
' RenderAspectRatio has unbounded constraints.\n'
' This RenderAspectRatio was given an aspect ratio of 0.5 but was\n'
' given both unbounded width and unbounded height constraints.\n'
' Because both constraints were unbounded, this render object\n'
' doesn\'t know how much size to consume.\n'
);
// The second error message is a generic message generated by the Dart VM. Not worth testing. // The second error message is a generic message generated by the Dart VM. Not worth testing.
}); });
......
...@@ -151,7 +151,7 @@ void main() { ...@@ -151,7 +151,7 @@ void main() {
ContainerLayer(), const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)), ContainerLayer(), const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)),
const Offset(0.0, 500), const Offset(0.0, 500),
); );
} catch(e) { } catch (e) {
error = e; error = e;
} }
expect(error, isNull); expect(error, isNull);
...@@ -184,7 +184,7 @@ void main() { ...@@ -184,7 +184,7 @@ void main() {
ContainerLayer(), const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)), ContainerLayer(), const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)),
const Offset(0.0, 500), const Offset(0.0, 500),
); );
} catch(e) { } catch (e) {
error = e; error = e;
} }
expect(error, isNull); expect(error, isNull);
......
...@@ -531,6 +531,11 @@ class _FakeTicker implements Ticker { ...@@ -531,6 +531,11 @@ class _FakeTicker implements Ticker {
@override @override
String toString({ bool debugIncludeStack = false }) => super.toString(); String toString({ bool debugIncludeStack = false }) => super.toString();
@override
DiagnosticsNode describeForError(String name) {
return DiagnosticsProperty<Ticker>(name, this, style: DiagnosticsTreeStyle.errorProperty);
}
} }
// Forces two frames and checks that: // Forces two frames and checks that:
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -979,4 +980,163 @@ void main() { ...@@ -979,4 +980,163 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.offset, 300.0); expect(controller.offset, 300.0);
}); });
group('unbounded constraints control test', () {
Widget buildNestedWidget([Axis a1 = Axis.vertical, Axis a2 = Axis.horizontal]) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Container(
child: ListView(
scrollDirection: a1,
children: List<Widget>.generate(10, (int y) {
return Container(
child: ListView(
scrollDirection: a2,
),
);
}),
),
),
),
);
}
Future<void> expectFlutterError({
Widget widget,
WidgetTester tester,
String message,
}) async {
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
try {
await tester.pumpWidget(widget);
} finally {
FlutterError.onError = oldHandler;
}
expect(errors, isNotEmpty);
expect(errors.first.exception, isFlutterError);
expect(errors.first.exception.toStringDeep(), message);
}
testWidgets('Horizontal viewport was given unbounded height', (WidgetTester tester) async {
await expectFlutterError(
widget: buildNestedWidget(),
tester: tester,
message:
'FlutterError\n'
' Horizontal viewport was given unbounded height.\n'
' Viewports expand in the cross axis to fill their container and\n'
' constrain their children to match their extent in the cross axis.\n'
' In this case, a horizontal viewport was given an unlimited amount\n'
' of vertical space in which to expand.\n',
);
});
testWidgets('Horizontal viewport was given unbounded width', (WidgetTester tester) async {
await expectFlutterError(
widget: buildNestedWidget(Axis.horizontal, Axis.horizontal),
tester: tester,
message:
'FlutterError\n'
' Horizontal viewport was given unbounded width.\n'
' Viewports expand in the scrolling direction to fill their\n'
' container.In this case, a horizontal viewport was given an\n'
' unlimited amount of horizontal space in which to expand. This\n'
' situation typically happens when a scrollable widget is nested\n'
' inside another scrollable widget.\n'
' If this widget is always nested in a scrollable widget there is\n'
' no need to use a viewport because there will always be enough\n'
' horizontal space for the children. In this case, consider using a\n'
' Row instead. Otherwise, consider using the "shrinkWrap" property\n'
' (or a ShrinkWrappingViewport) to size the width of the viewport\n'
' to the sum of the widths of its children.\n'
);
});
testWidgets('Vertical viewport was given unbounded width', (WidgetTester tester) async {
await expectFlutterError(
widget: buildNestedWidget(Axis.horizontal, Axis.vertical),
tester: tester,
message:
'FlutterError\n'
' Vertical viewport was given unbounded width.\n'
' Viewports expand in the cross axis to fill their container and\n'
' constrain their children to match their extent in the cross axis.\n'
' In this case, a vertical viewport was given an unlimited amount\n'
' of horizontal space in which to expand.\n'
);
});
testWidgets('Vertical viewport was given unbounded height', (WidgetTester tester) async {
await expectFlutterError(
widget: buildNestedWidget(Axis.vertical, Axis.vertical),
tester: tester,
message:
'FlutterError\n'
' Vertical viewport was given unbounded height.\n'
' Viewports expand in the scrolling direction to fill their\n'
' container. In this case, a vertical viewport was given an\n'
' unlimited amount of vertical space in which to expand. This\n'
' situation typically happens when a scrollable widget is nested\n'
' inside another scrollable widget.\n'
' If this widget is always nested in a scrollable widget there is\n'
' no need to use a viewport because there will always be enough\n'
' vertical space for the children. In this case, consider using a\n'
' Column instead. Otherwise, consider using the "shrinkWrap"\n'
' property (or a ShrinkWrappingViewport) to size the height of the\n'
' viewport to the sum of the heights of its children.\n'
);
});
});
test('Viewport debugThrowIfNotCheckingIntrinsics() control test', () {
final RenderViewport renderViewport = RenderViewport(
crossAxisDirection: AxisDirection.right, offset: ViewportOffset.zero()
);
FlutterError error;
try {
renderViewport.computeMinIntrinsicHeight(0);
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(
error.toStringDeep(),
'FlutterError\n'
' RenderViewport does not support returning intrinsic dimensions.\n'
' Calculating the intrinsic dimensions would require instantiating\n'
' every child of the viewport, which defeats the point of viewports\n'
' being lazy.\n'
' If you are merely trying to shrink-wrap the viewport in the main\n'
' axis direction, consider a RenderShrinkWrappingViewport render\n'
' object (ShrinkWrappingViewport widget), which achieves that\n'
' effect without implementing the intrinsic dimension API.\n',
);
final RenderShrinkWrappingViewport renderShrinkWrappingViewport = RenderShrinkWrappingViewport(
crossAxisDirection: AxisDirection.right, offset: ViewportOffset.zero()
);
error = null;
try {
renderShrinkWrappingViewport.computeMinIntrinsicHeight(0);
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(
error.toStringDeep(),
'FlutterError\n'
' RenderShrinkWrappingViewport does not support returning intrinsic\n'
' dimensions.\n'
' Calculating the intrinsic dimensions would require instantiating\n'
' every child of the viewport, which defeats the point of viewports\n'
' being lazy.\n'
' If you are merely trying to shrink-wrap the viewport in the main\n'
' axis direction, you should be able to achieve that effect by just\n'
' giving the viewport loose constraints, without needing to measure\n'
' its intrinsic dimensions.\n',
);
});
} }
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// 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/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -24,7 +25,26 @@ void main() { ...@@ -24,7 +25,26 @@ void main() {
expect(ticker.isActive, isTrue); expect(ticker.isActive, isTrue);
expect(tickCount, equals(0)); expect(tickCount, equals(0));
expect(ticker.start, throwsFlutterError); FlutterError error;
try {
ticker.start();
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.diagnostics.length, 3);
expect(error.diagnostics.last, isInstanceOf<DiagnosticsProperty<Ticker>>());
expect(
error.toStringDeep(),
startsWith(
'FlutterError\n'
' A ticker was started twice.\n'
' A ticker that is already active cannot be started again without\n'
' first stopping it.\n'
' The affected ticker was:\n'
' Ticker()\n',
),
);
await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10));
......
...@@ -56,4 +56,24 @@ void main() { ...@@ -56,4 +56,24 @@ void main() {
expect(key.name, 'one'); expect(key.name, 'one');
expect(key.scale, 1.0); expect(key.scale, 1.0);
}); });
test('NetworkAssetBundle control test', () async {
final Uri uri = Uri.http('example.org', '/path');
final NetworkAssetBundle bundle = NetworkAssetBundle(uri);
FlutterError error;
try {
await bundle.load('key');
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.diagnostics.length, 2);
expect(error.diagnostics.last, isInstanceOf<IntProperty>());
expect(
error.toStringDeep(),
'FlutterError\n'
' Unable to load asset: key\n'
' HTTP status code: 404\n',
);
}, skip: true);
} }
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/src/foundation/diagnostics.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
void main() { void main() {
...@@ -319,4 +320,47 @@ void main() { ...@@ -319,4 +320,47 @@ void main() {
expect(tester.getTopLeft(find.text('item 0')).dy, 200); expect(tester.getTopLeft(find.text('item 0')).dy, 200);
}); });
}); });
testWidgets('AnimatedList.of() called with a context that does not contain AnimatedList',
(WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(Container(key: key));
FlutterError error;
try {
AnimatedList.of(key.currentContext);
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.diagnostics.length, 4);
expect(error.diagnostics[2].level, DiagnosticLevel.hint);
expect(
error.diagnostics[2].toStringDeep(),
equalsIgnoringHashCodes(
'This can happen when the context provided is from the same\n'
'StatefulWidget that built the AnimatedList. Please see the\n'
'AnimatedList documentation for examples of how to refer to an\n'
'AnimatedListState object:\n'
'https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html\n'
),
);
expect(error.diagnostics[3], isInstanceOf<DiagnosticsProperty<Element>>());
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' AnimatedList.of() called with a context that does not contain an\n'
' AnimatedList.\n'
' No AnimatedList ancestor could be found starting from the context\n'
' that was passed to AnimatedList.of().\n'
' This can happen when the context provided is from the same\n'
' StatefulWidget that built the AnimatedList. Please see the\n'
' AnimatedList documentation for examples of how to refer to an\n'
' AnimatedListState object:\n'
' https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html\n'
' The context used was:\n'
' Container-[GlobalKey#32cc6]\n'
),
);
});
} }
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// 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/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -19,4 +20,72 @@ void main() { ...@@ -19,4 +20,72 @@ void main() {
); );
expect(find.byKey(key), findsOneWidget); expect(find.byKey(key), findsOneWidget);
}); });
group('error control test', () {
Future<void> expectFlutterError({
GlobalKey<NavigatorState> key,
Widget widget,
WidgetTester tester,
String errorMessage,
}) async {
await tester.pumpWidget(widget);
FlutterError error;
try {
key.currentState.pushNamed('/path');
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error, isFlutterError);
expect(error.toStringDeep(), errorMessage);
}
}
testWidgets('push unknown route when onUnknownRoute is null', (WidgetTester tester) async {
final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>();
expectFlutterError(
key: key,
tester: tester,
widget: MaterialApp(
navigatorKey: key,
home: Container(),
onGenerateRoute: (_) => null,
),
errorMessage:
'FlutterError\n'
' Could not find a generator for route RouteSettings("/path", null)\n'
' in the _WidgetsAppState.\n'
' Generators for routes are searched for in the following order:\n'
' 1. For the "/" route, the "home" property, if non-null, is used.\n'
' 2. Otherwise, the "routes" table is used, if it has an entry for\n'
' the route.\n'
' 3. Otherwise, onGenerateRoute is called. It should return a\n'
' non-null value for any valid route not handled by "home" and\n'
' "routes".\n'
' 4. Finally if all else fails onUnknownRoute is called.\n'
' Unfortunately, onUnknownRoute was not set.\n',
);
});
testWidgets('push unknown route when onUnknownRoute returns null', (WidgetTester tester) async {
final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>();
expectFlutterError(
key: key,
tester: tester,
widget: MaterialApp(
navigatorKey: key,
home: Container(),
onGenerateRoute: (_) => null,
onUnknownRoute: (_) => null,
),
errorMessage:
'FlutterError\n'
' The onUnknownRoute callback returned null.\n'
' When the _WidgetsAppState requested the route\n'
' RouteSettings("/path", null) from its onUnknownRoute callback,\n'
' the callback returned null. Such callbacks must never return\n'
' null.\n' ,
);
});
});
} }
...@@ -138,4 +138,36 @@ void main() { ...@@ -138,4 +138,36 @@ void main() {
expect(tester.binding.hasScheduledFrame, isFalse); expect(tester.binding.hasScheduledFrame, isFalse);
expect(frameCount, 1); expect(frameCount, 1);
}); });
testWidgets('scheduleFrameCallback error control test', (WidgetTester tester) async {
FlutterError error;
try {
tester.binding.scheduleFrameCallback(null, rescheduling: true);
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.diagnostics.length, 3);
expect(error.diagnostics.last.level, DiagnosticLevel.hint);
expect(
error.diagnostics.last.toStringDeep(),
equalsIgnoringHashCodes(
'If this is the initial registration of the callback, or if the\n'
'callback is asynchronous, then do not use the "rescheduling"\n'
'argument.\n'
),
);
expect(
error.toStringDeep(),
'FlutterError\n'
' scheduleFrameCallback called with rescheduling true, but no\n'
' callback is in scope.\n'
' The "rescheduling" argument should only be set to true if the\n'
' callback is being reregistered from within the callback itself,\n'
' and only then if the callback itself is entirely synchronous.\n'
' If this is the initial registration of the callback, or if the\n'
' callback is asynchronous, then do not use the "rescheduling"\n'
' argument.\n'
);
});
} }
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:mockito/mockito.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
...@@ -461,6 +462,32 @@ void main() { ...@@ -461,6 +462,32 @@ void main() {
' android)\n', ' android)\n',
), ),
); );
final RenderBox decoratedBox = tester.renderObject(find.byType(DecoratedBox).last);
final PaintingContext context = _MockPaintingContext();
final Canvas canvas = _MockCanvas();
int saveCount = 0;
when(canvas.getSaveCount()).thenAnswer((_) => saveCount++);
when(context.canvas).thenReturn(canvas);
FlutterError error;
try {
decoratedBox.paint(context, const Offset(0, 0));
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(
error.toStringDeep(),
'FlutterError\n'
' BoxDecoration painter had mismatching save and restore calls.\n'
' Before painting the decoration, the canvas save count was 0.\n'
' After painting it, the canvas save count was 2. Every call to\n'
' save() or saveLayer() must be matched by a call to restore().\n'
' The decoration was:\n'
' BoxDecoration(color: Color(0xffffff00))\n'
' The painter was:\n'
' BoxPainter for BoxDecoration(color: Color(0xffffff00))\n'
);
}); });
testWidgets('Can be placed in an infinite box', (WidgetTester tester) async { testWidgets('Can be placed in an infinite box', (WidgetTester tester) async {
...@@ -472,3 +499,6 @@ void main() { ...@@ -472,3 +499,6 @@ void main() {
); );
}); });
} }
class _MockPaintingContext extends Mock implements PaintingContext {}
class _MockCanvas extends Mock implements Canvas {}
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -90,6 +91,86 @@ class NotifierLayoutDelegate extends MultiChildLayoutDelegate { ...@@ -90,6 +91,86 @@ class NotifierLayoutDelegate extends MultiChildLayoutDelegate {
} }
} }
// LayoutDelegate that lays out child with id 0 and 1
// Used in the 'performLayout error control test' test case to trigger:
// - error when laying out a non existent child and a child that has not been laid out
class ZeroAndOneIdLayoutDelegate extends MultiChildLayoutDelegate {
@override
void performLayout(Size size) {
final BoxConstraints constraints = BoxConstraints.loose(size);
layoutChild(0, constraints);
layoutChild(1, constraints);
}
@override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true;
}
// Used in the 'performLayout error control test' test case
// to trigger an error when laying out child more than once
class DuplicateLayoutDelegate extends MultiChildLayoutDelegate {
@override
void performLayout(Size size) {
final BoxConstraints constraints = BoxConstraints.loose(size);
layoutChild(0, constraints);
layoutChild(0, constraints);
}
@override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true;
}
// Used in the 'performLayout error control test' test case
// to trigger an error when positioning non existent child
class NonExistentPositionDelegate extends MultiChildLayoutDelegate {
@override
void performLayout(Size size) {
positionChild(0, const Offset(0, 0));
positionChild(1, const Offset(0, 0));
}
@override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true;
}
// Used in the 'performLayout error control test' test case
// to trigger an error when positioning with null offset
class NullOffsetPositionDelegate extends MultiChildLayoutDelegate {
@override
void performLayout(Size size) {
positionChild(0, null);
}
@override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true;
}
// Used in the 'performLayout error control test' test case for triggering
// to layout child more than once
class InvalidConstraintsChildLayoutDelegate extends MultiChildLayoutDelegate {
@override
void performLayout(Size size) {
final BoxConstraints constraints = BoxConstraints.loose(
// Invalid because width and height must be greater than or equal to 0
const Size(-1, -1)
);
layoutChild(0, constraints);
}
@override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true;
}
class LayoutWithMissingId extends ParentDataWidget<CustomMultiChildLayout> {
const LayoutWithMissingId({
Key key,
@required Widget child,
}) : assert(child != null),
super(key: key, child: child);
@override
void applyParentData(RenderObject renderObject) {}
}
void main() { void main() {
testWidgets('Control test for CustomMultiChildLayout', (WidgetTester tester) async { testWidgets('Control test for CustomMultiChildLayout', (WidgetTester tester) async {
final TestMultiChildLayoutDelegate delegate = TestMultiChildLayoutDelegate(); final TestMultiChildLayoutDelegate delegate = TestMultiChildLayoutDelegate();
...@@ -199,4 +280,170 @@ void main() { ...@@ -199,4 +280,170 @@ void main() {
box = tester.renderObject(find.byType(CustomMultiChildLayout)); box = tester.renderObject(find.byType(CustomMultiChildLayout));
expect(box.size, equals(const Size(150.0, 240.0))); expect(box.size, equals(const Size(150.0, 240.0)));
}); });
group('performLayout error control test', () {
Widget buildSingleChildFrame(MultiChildLayoutDelegate delegate) {
return Center(
child: CustomMultiChildLayout(
children: <Widget>[LayoutId(id: 0, child: const SizedBox())],
delegate: delegate,
),
);
}
Future<void> expectFlutterErrorMessage({
Widget widget,
MultiChildLayoutDelegate delegate,
@required WidgetTester tester,
@required String message,
}) async {
final FlutterExceptionHandler oldHandler = FlutterError.onError;
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
try {
await tester.pumpWidget(widget ?? buildSingleChildFrame(delegate));
} finally {
FlutterError.onError = oldHandler;
}
expect(errors.length, isNonZero);
expect(errors.first, isNotNull);
expect(errors.first.exception, isFlutterError);
expect(errors.first.exception.toStringDeep(), equalsIgnoringHashCodes(message));
}
testWidgets('layoutChild on non existent child', (WidgetTester tester) async {
expectFlutterErrorMessage(
tester: tester,
delegate: ZeroAndOneIdLayoutDelegate(),
message:
'FlutterError\n'
' The ZeroAndOneIdLayoutDelegate custom multichild layout delegate\n'
' tried to lay out a non-existent child.\n'
' There is no child with the id "1".\n'
);
});
testWidgets('layoutChild more than once', (WidgetTester tester) async {
expectFlutterErrorMessage(
tester: tester,
delegate: DuplicateLayoutDelegate(),
message:
'FlutterError\n'
' The DuplicateLayoutDelegate custom multichild layout delegate\n'
' tried to lay out the child with id "0" more than once.\n'
' Each child must be laid out exactly once.\n'
);
});
testWidgets('layoutChild on invalid size constraint', (WidgetTester tester) async {
expectFlutterErrorMessage(
tester: tester,
delegate: InvalidConstraintsChildLayoutDelegate(),
message:
'FlutterError\n'
' The InvalidConstraintsChildLayoutDelegate custom multichild\n'
' layout delegate provided invalid box constraints for the child\n'
' with id "0".\n'
' FlutterError\n'
' The minimum width and height must be greater than or equal to\n'
' zero.\n'
' The maximum width must be greater than or equal to the minimum\n'
' width.\n'
' The maximum height must be greater than or equal to the minimum\n'
' height.\n'
);
});
testWidgets('positionChild on non existent child', (WidgetTester tester) async {
expectFlutterErrorMessage(
tester: tester,
delegate: NonExistentPositionDelegate(),
message:
'FlutterError\n'
' The NonExistentPositionDelegate custom multichild layout delegate\n'
' tried to position out a non-existent child:\n'
' There is no child with the id "1".\n'
);
});
testWidgets('positionChild on non existent child', (WidgetTester tester) async {
expectFlutterErrorMessage(
tester: tester,
delegate: NullOffsetPositionDelegate(),
message:
'FlutterError\n'
' The NullOffsetPositionDelegate custom multichild layout delegate\n'
' provided a null position for the child with id "0".\n',
);
});
testWidgets("_callPerformLayout on child that doesn't have id", (WidgetTester tester) async {
expectFlutterErrorMessage(
widget: Center(
child: CustomMultiChildLayout(
children: <Widget>[LayoutWithMissingId(child: Container(width: 100))],
delegate: PreferredSizeDelegate(preferredSize: const Size(10, 10)),
),
),
tester: tester,
message:
'FlutterError\n'
' Every child of a RenderCustomMultiChildLayoutBox must have an ID\n'
' in its parent data.\n'
' The following child has no ID: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT:\n'
' creator: ConstrainedBox ← Container ← LayoutWithMissingId ←\n'
' CustomMultiChildLayout ← Center ← [root]\n'
' parentData: offset=Offset(0.0, 0.0); id=null\n'
' constraints: MISSING\n'
' size: MISSING\n'
' additionalConstraints: BoxConstraints(w=100.0, 0.0<=h<=Infinity)\n'
);
});
testWidgets('performLayout did not layout a child', (WidgetTester tester) async {
expectFlutterErrorMessage(
widget: Center(
child: CustomMultiChildLayout(
children: <Widget>[
LayoutId(id: 0, child: Container(width: 100)),
LayoutId(id: 1, child: Container(width: 100)),
LayoutId(id: 2, child: Container(width: 100)),
],
delegate: ZeroAndOneIdLayoutDelegate(),
),
),
tester: tester,
message:
'FlutterError\n'
' Each child must be laid out exactly once.\n'
' The ZeroAndOneIdLayoutDelegate custom multichild layout delegate'
' forgot to lay out the following child:\n'
' 2: RenderConstrainedBox#62a34 NEEDS-LAYOUT NEEDS-PAINT\n'
);
});
testWidgets('performLayout did not layout multiple child', (WidgetTester tester) async {
expectFlutterErrorMessage(
widget: Center(
child: CustomMultiChildLayout(
children: <Widget>[
LayoutId(id: 0, child: Container(width: 100)),
LayoutId(id: 1, child: Container(width: 100)),
LayoutId(id: 2, child: Container(width: 100)),
LayoutId(id: 3, child: Container(width: 100)),
],
delegate: ZeroAndOneIdLayoutDelegate(),
),
),
tester: tester,
message:
'FlutterError\n'
' Each child must be laid out exactly once.\n'
' The ZeroAndOneIdLayoutDelegate custom multichild layout delegate'
' forgot to lay out the following children:\n'
' 2: RenderConstrainedBox#62a34 NEEDS-LAYOUT NEEDS-PAINT\n'
' 3: RenderConstrainedBox#62a34 NEEDS-LAYOUT NEEDS-PAINT\n'
);
});
});
} }
...@@ -2,9 +2,10 @@ ...@@ -2,9 +2,10 @@
// 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_test/flutter_test.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
class TestCustomPainter extends CustomPainter { class TestCustomPainter extends CustomPainter {
TestCustomPainter({ this.log, this.name }); TestCustomPainter({ this.log, this.name });
...@@ -21,6 +22,25 @@ class TestCustomPainter extends CustomPainter { ...@@ -21,6 +22,25 @@ class TestCustomPainter extends CustomPainter {
bool shouldRepaint(TestCustomPainter oldPainter) => true; bool shouldRepaint(TestCustomPainter oldPainter) => true;
} }
class TestCustomPainterWithCustomSemanticsBuilder extends TestCustomPainter {
TestCustomPainterWithCustomSemanticsBuilder() : super(log: <String>[]);
@override
SemanticsBuilderCallback get semanticsBuilder => (Size size) {
const Key key = Key('0');
const Rect rect = Rect.fromLTRB(0, 0, 0, 0);
const SemanticsProperties semanticsProperties = SemanticsProperties();
return <CustomPainterSemantics>[
const CustomPainterSemantics(key: key, rect: rect, properties: semanticsProperties),
const CustomPainterSemantics(key: key, rect: rect, properties: semanticsProperties),
];
};
}
class MockCanvas extends Mock implements Canvas {}
class MockPaintingContext extends Mock implements PaintingContext {}
void main() { void main() {
testWidgets('Control test for custom painting', (WidgetTester tester) async { testWidgets('Control test for custom painting', (WidgetTester tester) async {
final List<String> log = <String>[]; final List<String> log = <String>[];
...@@ -44,6 +64,108 @@ void main() { ...@@ -44,6 +64,108 @@ void main() {
expect(log, equals(<String>['background', 'child', 'foreground'])); expect(log, equals(<String>['background', 'child', 'foreground']));
}); });
testWidgets('Throws FlutterError on custom painter incorrect restore/save calls', (
WidgetTester tester) async {
final GlobalKey target = GlobalKey();
final List<String> log = <String>[];
await tester.pumpWidget(CustomPaint(
key: target,
isComplex: true,
painter: TestCustomPainter(log: log),
));
final RenderCustomPaint renderCustom = target.currentContext.findRenderObject();
final Canvas canvas = MockCanvas();
int saveCount = 0;
when(canvas.getSaveCount()).thenAnswer((_) => saveCount++);
final PaintingContext paintingContext = MockPaintingContext();
when(paintingContext.canvas).thenReturn(canvas);
FlutterError getError() {
FlutterError error;
try {
renderCustom.paint(paintingContext, const Offset(0, 0));
} on FlutterError catch (e) {
error = e;
}
return error;
}
FlutterError error = getError();
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' The TestCustomPainter#00000() custom painter called canvas.save()\n'
' or canvas.saveLayer() at least 1 more time than it called\n'
' canvas.restore().\n'
' This leaves the canvas in an inconsistent state and will probably\n'
' result in a broken display.\n'
' You must pair each call to save()/saveLayer() with a later\n'
' matching call to restore().\n'
));
when(canvas.getSaveCount()).thenAnswer((_) => saveCount--);
error = getError();
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' The TestCustomPainter#00000() custom painter called\n'
' canvas.restore() 1 more time than it called canvas.save() or\n'
' canvas.saveLayer().\n'
' This leaves the canvas in an inconsistent state and will result\n'
' in a broken display.\n'
' You should only call restore() if you first called save() or\n'
' saveLayer().\n'
));
when(canvas.getSaveCount()).thenAnswer((_) => saveCount += 2);
error = getError();
expect(error.toStringDeep(), contains('2 more times'));
when(canvas.getSaveCount()).thenAnswer((_) => saveCount -= 2);
error = getError();
expect(error.toStringDeep(), contains('2 more times'));
});
testWidgets('assembleSemanticsNode throws FlutterError', (WidgetTester tester) async {
final List<String> log = <String>[];
final GlobalKey target = GlobalKey();
await tester.pumpWidget(CustomPaint(
key: target,
isComplex: true,
painter: TestCustomPainter(log: log),
));
final RenderCustomPaint renderCustom = target.currentContext.findRenderObject();
FlutterError error;
try {
renderCustom.assembleSemanticsNode(
null,
null,
<SemanticsNode>[SemanticsNode()],
);
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' RenderCustomPaint does not have a child widget but received a\n'
' non-empty list of child SemanticsNode:\n'
' SemanticsNode#1(Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible)\n'
));
await tester.pumpWidget(CustomPaint(
key: target,
isComplex: true,
painter: TestCustomPainterWithCustomSemanticsBuilder(),
));
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
error = exception;
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' Failed to update the list of CustomPainterSemantics:\n'
' - duplicate key [<\'0\'>] found at position 1\n'
));
});
testWidgets('CustomPaint sizing', (WidgetTester tester) async { testWidgets('CustomPaint sizing', (WidgetTester tester) async {
final GlobalKey target = GlobalKey(); final GlobalKey target = GlobalKey();
......
// Copyright 2019 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';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('debugChildrenHaveDuplicateKeys control test', () {
const Key key = Key('key');
final List<Widget> children = <Widget>[
Container(key: key),
Container(key: key),
];
final Widget widget = Flex(
direction: Axis.vertical,
children: children
);
FlutterError error;
try {
debugChildrenHaveDuplicateKeys(widget, children);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' Duplicate keys found.\n'
' If multiple keyed nodes exist as children of another node, they\n'
' must have unique keys.\n'
' Flex(direction: vertical, mainAxisAlignment: start,\n'
' crossAxisAlignment: center) has multiple children with key\n'
' [<\'key\'>].\n',
),
);
}
});
test('debugItemsHaveDuplicateKeys control test', () {
const Key key = Key('key');
final List<Widget> items = <Widget>[
Container(key: key),
Container(key: key),
];
FlutterError error;
try {
debugItemsHaveDuplicateKeys(items);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' Duplicate key found: [<\'key\'>].\n'
),
);
}
});
testWidgets('debugCheckHasTable control test', (WidgetTester tester) async {
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
FlutterError error;
try {
debugCheckHasTable(context);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 4);
expect(error.diagnostics[2], isInstanceOf<DiagnosticsProperty<Element>>());
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' No Table widget found.\n'
' Builder widgets require a Table widget ancestor.\n'
' The specific widget that could not find a Table ancestor was:\n'
' Builder\n'
' The ownership chain for the affected widget is: "Builder ←\n'
' [root]"\n'
),
);
}
return Container();
}
),
);
});
testWidgets('debugCheckHasMediaQuery control test', (WidgetTester tester) async {
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
FlutterError error;
try {
debugCheckHasMediaQuery(context);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 5);
expect(error.diagnostics[2], isInstanceOf<DiagnosticsProperty<Element>>());
expect(error.diagnostics.last.level, DiagnosticLevel.hint);
expect(
error.diagnostics.last.toStringDeep(),
equalsIgnoringHashCodes(
'Typically, the MediaQuery widget is introduced by the MaterialApp\n'
'or WidgetsApp widget at the top of your application widget tree.\n'
),
);
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' No MediaQuery widget found.\n'
' Builder widgets require a MediaQuery widget ancestor.\n'
' The specific widget that could not find a MediaQuery ancestor\n'
' was:\n'
' Builder\n'
' The ownership chain for the affected widget is: "Builder ←\n'
' [root]"\n'
' Typically, the MediaQuery widget is introduced by the MaterialApp\n'
' or WidgetsApp widget at the top of your application widget tree.\n'
),
);
}
return Container();
}
),
);
});
test('debugWidgetBuilderValue control test', () {
final Widget widget = Container();
FlutterError error;
try {
debugWidgetBuilderValue(widget, null);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 4);
expect(error.diagnostics[1], isInstanceOf<DiagnosticsProperty<Widget>>());
expect(error.diagnostics[1].style, DiagnosticsTreeStyle.errorProperty);
expect(
error.diagnostics[1].toStringDeep(),
equalsIgnoringHashCodes(
'The offending widget is:\n'
' Container\n'
)
);
expect(error.diagnostics[2].level, DiagnosticLevel.info);
expect(error.diagnostics[3].level, DiagnosticLevel.hint);
expect(
error.diagnostics[3].toStringDeep(),
equalsIgnoringHashCodes(
'To return an empty space that causes the building widget to fill\n'
'available room, return "Container()". To return an empty space\n'
'that takes as little room as possible, return "Container(width:\n'
'0.0, height: 0.0)".\n',
)
);
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' A build function returned null.\n'
' The offending widget is:\n'
' Container\n'
' Build functions must never return null.\n'
' To return an empty space that causes the building widget to fill\n'
' available room, return "Container()". To return an empty space\n'
' that takes as little room as possible, return "Container(width:\n'
' 0.0, height: 0.0)".\n'
),
);
error = null;
}
try {
debugWidgetBuilderValue(widget, widget);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 3);
expect(error.diagnostics[1], isInstanceOf<DiagnosticsProperty<Widget>>());
expect(error.diagnostics[1].style, DiagnosticsTreeStyle.errorProperty);
expect(
error.diagnostics[1].toStringDeep(),
equalsIgnoringHashCodes(
'The offending widget is:\n'
' Container\n'
)
);
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' A build function returned context.widget.\n'
' The offending widget is:\n'
' Container\n'
' Build functions must never return their BuildContext parameter\'s\n'
' widget or a child that contains "context.widget". Doing so\n'
' introduces a loop in the widget tree that can cause the app to\n'
' crash.\n'
),
);
}
});
test('debugAssertAllWidgetVarsUnset', () {
debugHighlightDeprecatedWidgets = true;
FlutterError error;
try {
debugAssertAllWidgetVarsUnset('The value of a widget debug variable was changed by the test.');
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 1);
expect(
error.toStringDeep(),
'FlutterError\n'
' The value of a widget debug variable was changed by the test.\n',
);
}
});
}
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
// 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/gestures.dart' show DragStartBehavior;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
const double itemExtent = 100.0; const double itemExtent = 100.0;
Axis scrollDirection = Axis.vertical; Axis scrollDirection = Axis.vertical;
...@@ -732,4 +732,68 @@ void main() { ...@@ -732,4 +732,68 @@ void main() {
expect(dismissedItems, isEmpty); expect(dismissedItems, isEmpty);
expect(confirmDismissDirection, DismissDirection.endToStart); expect(confirmDismissDirection, DismissDirection.endToStart);
}); });
testWidgets('setState that does not remove the Dismissible from tree should throws Error', (WidgetTester tester) async {
scrollDirection = Axis.vertical;
dismissDirection = DismissDirection.horizontal;
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return ListView(
dragStartBehavior: DragStartBehavior.down,
scrollDirection: scrollDirection,
itemExtent: itemExtent,
children: <Widget>[
Dismissible(
dragStartBehavior: DragStartBehavior.down,
key: const ValueKey<int>(1),
direction: dismissDirection,
onDismissed: (DismissDirection direction) {
setState(() {
reportedDismissDirection = direction;
expect(dismissedItems.contains(1), isFalse);
dismissedItems.add(1);
});
},
background: background,
dismissThresholds: const <DismissDirection, double>{},
crossAxisEndOffset: crossAxisEndOffset,
child: Container(
width: itemExtent,
height: itemExtent,
child: Text(1.toString()),
),
),
]
);
},
),
));
expect(dismissedItems, isEmpty);
await dismissItem(tester, 1, gestureDirection: AxisDirection.right);
expect(dismissedItems, equals(<int>[1]));
final dynamic exception = tester.takeException();
expect(exception, isNotNull);
expect(exception, isFlutterError);
final FlutterError error = exception;
expect(error.diagnostics.last.level, DiagnosticLevel.hint);
expect(
error.diagnostics.last.toStringDeep(),
equalsIgnoringHashCodes(
'Make sure to implement the onDismissed handler and to immediately\n'
'remove the Dismissible widget from the application once that\n'
'handler has fired.\n',
),
);
expect(
error.toStringDeep(),
'FlutterError\n'
' A dismissed Dismissible widget is still part of the tree.\n'
' Make sure to implement the onDismissed handler and to immediately\n'
' remove the Dismissible widget from the application once that\n'
' handler has fired.\n',
);
});
} }
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
// 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_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
class TestFlowDelegate extends FlowDelegate { class TestFlowDelegate extends FlowDelegate {
TestFlowDelegate({this.startOffset}) : super(repaint: startOffset); TestFlowDelegate({this.startOffset}) : super(repaint: startOffset);
...@@ -45,6 +45,21 @@ class OpacityFlowDelegate extends FlowDelegate { ...@@ -45,6 +45,21 @@ class OpacityFlowDelegate extends FlowDelegate {
bool shouldRepaint(OpacityFlowDelegate oldDelegate) => opacity != oldDelegate.opacity; bool shouldRepaint(OpacityFlowDelegate oldDelegate) => opacity != oldDelegate.opacity;
} }
// OpacityFlowDelegate that paints one of its children twice
class DuplicatePainterOpacityFlowDelegate extends OpacityFlowDelegate {
DuplicatePainterOpacityFlowDelegate(double opacity) : super(opacity);
@override
void paintChildren(FlowPaintingContext context) {
for (int i = 0; i < context.childCount; ++i) {
context.paintChild(i, opacity: opacity);
}
if (context.childCount > 0) {
context.paintChild(0, opacity: opacity);
}
}
}
void main() { void main() {
testWidgets('Flow control test', (WidgetTester tester) async { testWidgets('Flow control test', (WidgetTester tester) async {
final AnimationController startOffset = AnimationController.unbounded( final AnimationController startOffset = AnimationController.unbounded(
...@@ -100,6 +115,28 @@ void main() { ...@@ -100,6 +115,28 @@ void main() {
expect(log, equals(<int>[0])); expect(log, equals(<int>[0]));
}); });
testWidgets('paintChild gets called twice', (WidgetTester tester) async {
await tester.pumpWidget(
Flow(
delegate: DuplicatePainterOpacityFlowDelegate(1.0),
children: <Widget>[
Container(width: 100.0, height: 100.0),
Container(width: 100.0, height: 100.0),
],
),
);
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
final FlutterError error = exception;
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' Cannot call paintChild twice for the same child.\n'
' The flow delegate of type DuplicatePainterOpacityFlowDelegate\n'
' attempted to paint child 0 multiple times, which is not\n'
' permitted.\n'
));
});
testWidgets('Flow opacity layer', (WidgetTester tester) async { testWidgets('Flow opacity layer', (WidgetTester tester) async {
const double opacity = 0.2; const double opacity = 0.2;
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -657,6 +657,59 @@ void main() { ...@@ -657,6 +657,59 @@ void main() {
); );
test.includeChild = false; test.includeChild = false;
}); });
testWidgets('scheduleBuild while debugBuildingDirtyElements is true', (WidgetTester tester) async {
/// ignore here is required for testing purpose because changing the flag properly is hard
// ignore: invalid_use_of_protected_member
tester.binding.debugBuildingDirtyElements = true;
FlutterError error;
try {
tester.binding.buildOwner.scheduleBuildFor(
DirtyElementWithCustomBuildOwner(tester.binding.buildOwner, Container()));
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 3);
expect(error.diagnostics.last.level, DiagnosticLevel.hint);
expect(
error.diagnostics.last.toStringDeep(),
equalsIgnoringHashCodes(
'This might be because setState() was called from a layout or\n'
'paint callback. If a change is needed to the widget tree, it\n'
'should be applied as the tree is being built. Scheduling a change\n'
'for the subsequent frame instead results in an interface that\n'
'lags behind by one frame. If this was done to make your build\n'
'dependent on a size measured at layout time, consider using a\n'
'LayoutBuilder, CustomSingleChildLayout, or\n'
'CustomMultiChildLayout. If, on the other hand, the one frame\n'
'delay is the desired effect, for example because this is an\n'
'animation, consider scheduling the frame in a post-frame callback\n'
'using SchedulerBinding.addPostFrameCallback or using an\n'
'AnimationController to trigger the animation.\n',
),
);
expect(
error.toStringDeep(),
'FlutterError\n'
' Build scheduled during frame.\n'
' While the widget tree was being built, laid out, and painted, a\n'
' new frame was scheduled to rebuild the widget tree.\n'
' This might be because setState() was called from a layout or\n'
' paint callback. If a change is needed to the widget tree, it\n'
' should be applied as the tree is being built. Scheduling a change\n'
' for the subsequent frame instead results in an interface that\n'
' lags behind by one frame. If this was done to make your build\n'
' dependent on a size measured at layout time, consider using a\n'
' LayoutBuilder, CustomSingleChildLayout, or\n'
' CustomMultiChildLayout. If, on the other hand, the one frame\n'
' delay is the desired effect, for example because this is an\n'
' animation, consider scheduling the frame in a post-frame callback\n'
' using SchedulerBinding.addPostFrameCallback or using an\n'
' AnimationController to trigger the animation.\n',
);
}
});
} }
class NullChildTest extends Widget { class NullChildTest extends Widget {
...@@ -681,3 +734,23 @@ class NullChildElement extends Element { ...@@ -681,3 +734,23 @@ class NullChildElement extends Element {
@override @override
void performRebuild() { } void performRebuild() { }
} }
class DirtyElementWithCustomBuildOwner extends Element {
DirtyElementWithCustomBuildOwner(BuildOwner buildOwner, Widget widget)
: _owner = buildOwner, super(widget);
final BuildOwner _owner;
@override
void forgetChild(Element child) {}
@override
void performRebuild() {}
@override
BuildOwner get owner => _owner;
@override
bool get dirty => true;
}
...@@ -601,6 +601,104 @@ void main() { ...@@ -601,6 +601,104 @@ void main() {
'excludeFromSemantics: true', 'excludeFromSemantics: true',
]); ]);
}); });
group('error control test', () {
test('constructor redundant pan and scale', () {
FlutterError error;
try {
GestureDetector(onScaleStart: (_) {}, onPanStart: (_) {},);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(
error.toStringDeep(),
'FlutterError\n'
' Incorrect GestureDetector arguments.\n'
' Having both a pan gesture recognizer and a scale gesture\n'
' recognizer is redundant; scale is a superset of pan.\n'
' Just use the scale gesture recognizer.\n',
);
expect(error.diagnostics.last.level, DiagnosticLevel.hint);
expect(
error.diagnostics.last.toStringDeep(),
equalsIgnoringHashCodes(
'Just use the scale gesture recognizer.\n',
)
);
}
});
test('constructur duplicate drag recognizer', () {
FlutterError error;
try {
GestureDetector(
onVerticalDragStart: (_) {},
onHorizontalDragStart: (_) {},
onPanStart: (_) {},
);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(
error.toStringDeep(),
'FlutterError\n'
' Incorrect GestureDetector arguments.\n'
' Simultaneously having a vertical drag gesture recognizer, a\n'
' horizontal drag gesture recognizer, and a pan gesture recognizer\n'
' will result in the pan gesture recognizer being ignored, since\n'
' the other two will catch all drags.\n',
);
}
});
testWidgets('replaceGestureRecognizers not during layout', (WidgetTester tester) async {
final GlobalKey<RawGestureDetectorState> key = GlobalKey<RawGestureDetectorState>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: RawGestureDetector(
key: key,
child: Container(
child: const Text('Text'),
),
),
),
);
FlutterError error;
try {
key.currentState.replaceGestureRecognizers(
<Type, GestureRecognizerFactory>{});
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.last.level, DiagnosticLevel.hint);
expect(
error.diagnostics.last.toStringDeep(),
equalsIgnoringHashCodes(
'To set the gesture recognizers at other times, trigger a new\n'
'build using setState() and provide the new gesture recognizers as\n'
'constructor arguments to the corresponding RawGestureDetector or\n'
'GestureDetector object.\n'
),
);
expect(
error.toStringDeep(),
'FlutterError\n'
' Unexpected call to replaceGestureRecognizers() method of\n'
' RawGestureDetectorState.\n'
' The replaceGestureRecognizers() method can only be called during\n'
' the layout phase.\n'
' To set the gesture recognizers at other times, trigger a new\n'
' build using setState() and provide the new gesture recognizers as\n'
' constructor arguments to the corresponding RawGestureDetector or\n'
' GestureDetector object.\n',
);
}
});
});
}); });
} }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
...@@ -546,7 +547,31 @@ Future<void> main() async { ...@@ -546,7 +547,31 @@ Future<void> main() async {
await tester.tap(find.text('push')); await tester.tap(find.text('push'));
await tester.pump(); await tester.pump();
expect(tester.takeException(), isFlutterError); final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
final FlutterError error = exception;
expect(error.diagnostics.length, 3);
final DiagnosticsNode last = error.diagnostics.last;
expect(last, isInstanceOf<DiagnosticsProperty<StatefulElement>>());
expect(
last.toStringDeep(),
equalsIgnoringHashCodes(
'# Here is the subtree for one of the offending heroes: Hero\n',
),
);
expect(last.style, DiagnosticsTreeStyle.dense);
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' There are multiple heroes that share the same tag within a\n'
' subtree.\n'
' Within each subtree for which heroes are to be animated (i.e. a\n'
' PageRoute subtree), each Hero must have a unique non-null tag.\n'
' In this case, multiple heroes had the following tag: a\n'
' ├# Here is the subtree for one of the offending heroes: Hero\n',
),
);
}); });
testWidgets('Hero push transition interrupted by a pop', (WidgetTester tester) async { testWidgets('Hero push transition interrupted by a pop', (WidgetTester tester) async {
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// 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/src/foundation/assertions.dart';
import 'package:flutter/src/painting/basic_types.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -106,4 +108,90 @@ void main() { ...@@ -106,4 +108,90 @@ void main() {
], ],
); );
}); });
testWidgets('Limited space along main axis error', (WidgetTester tester) async {
final FlutterExceptionHandler oldHandler = FlutterError.onError;
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
try {
await tester.pumpWidget(
SizedBox(
width: 100,
height: 100,
child: Directionality(
textDirection: TextDirection.rtl,
child: ListBody(
mainAxis: Axis.horizontal,
children: children,
),
),
),
);
} finally {
FlutterError.onError = oldHandler;
}
expect(errors, isNotEmpty);
expect(errors.first.exception, isFlutterError);
expect(errors.first.exception.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' RenderListBody must have unlimited space along its main axis.\n'
' RenderListBody does not clip or resize its children, so it must\n'
' be placed in a parent that does not constrain the main axis.\n'
' You probably want to put the RenderListBody inside a\n'
' RenderViewport with a matching main axis.\n'
));
});
testWidgets('Nested ListBody unbounded cross axis error', (WidgetTester tester) async {
final FlutterExceptionHandler oldHandler = FlutterError.onError;
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
try {
await tester.pumpWidget(
Flex(
textDirection: TextDirection.ltr,
direction: Axis.horizontal,
children: <Widget>[
Directionality(
textDirection: TextDirection.ltr,
child: ListBody(
mainAxis: Axis.horizontal,
children: <Widget>[
Flex(
textDirection: TextDirection.ltr,
direction: Axis.vertical,
children: <Widget>[
Directionality(
textDirection: TextDirection.ltr,
child: ListBody(
mainAxis: Axis.vertical,
children: children,
),
),
],
),
],
),
),
],
),
);
} finally {
FlutterError.onError = oldHandler;
}
expect(errors, isNotEmpty);
expect(errors.first.exception, isFlutterError);
expect(errors.first.exception.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' RenderListBody must have a bounded constraint for its cross axis.\n'
' RenderListBody forces its children to expand to fit the\n'
' RenderListBody\'s container, so it must be placed in a parent that\n'
' constrains the cross axis to a finite dimension.\n'
' If you are attempting to nest a RenderListBody with one direction\n'
' inside one of another direction, you will want to wrap the inner\n'
' one inside a box that fixes the dimension in that direction, for\n'
' example, a RenderIntrinsicWidth or RenderIntrinsicHeight object.\n'
' This is relatively expensive, however.\n'
));
});
} }
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show Brightness; import 'dart:ui' show Brightness;
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -19,7 +20,27 @@ void main() { ...@@ -19,7 +20,27 @@ void main() {
), ),
); );
expect(tested, isTrue); expect(tested, isTrue);
expect(tester.takeException(), isFlutterError); final dynamic exception = tester.takeException();
expect(exception, isNotNull);
expect(exception ,isFlutterError);
final FlutterError error = exception;
expect(error.diagnostics.length, 3);
expect(error.diagnostics.last, isInstanceOf<DiagnosticsProperty<Element>>());
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' MediaQuery.of() called with a context that does not contain a\n'
' MediaQuery.\n'
' No MediaQuery ancestor could be found starting from the context\n'
' that was passed to MediaQuery.of(). This can happen because you\n'
' do not have a WidgetsApp or MaterialApp widget (those widgets\n'
' introduce a MediaQuery), or it can happen if the context you use\n'
' comes from a widget above those widgets.\n'
' The context used was:\n'
' Builder\n',
),
);
}); });
testWidgets('MediaQuery defaults to null', (WidgetTester tester) async { testWidgets('MediaQuery defaults to null', (WidgetTester tester) async {
......
...@@ -1033,4 +1033,60 @@ void main() { ...@@ -1033,4 +1033,60 @@ void main() {
expect(find.byKey(keyA), findsNothing); expect(find.byKey(keyA), findsNothing);
expect(find.byKey(keyAB), findsNothing); expect(find.byKey(keyAB), findsNothing);
}); });
group('error control test', () {
testWidgets('onUnknownRoute null and onGenerateRoute returns null', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(Navigator(
key: navigatorKey,
onGenerateRoute: (_) => null,
));
final dynamic exception = tester.takeException();
expect(exception, isNotNull);
expect(exception, isFlutterError);
final FlutterError error = exception;
expect(error, isNotNull);
expect(error.diagnostics.last, isInstanceOf<DiagnosticsProperty<NavigatorState>>());
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' If a Navigator has no onUnknownRoute, then its onGenerateRoute\n'
' must never return null.\n'
' When trying to build the route "/", onGenerateRoute returned\n'
' null, but there was no onUnknownRoute callback specified.\n'
' The Navigator was:\n'
' NavigatorState#4d6bf(lifecycle state: created)\n',
),
);
});
testWidgets('onUnknownRoute null and onGenerateRoute returns null', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(Navigator(
key: navigatorKey,
onGenerateRoute: (_) => null,
onUnknownRoute: (_) => null,
));
final dynamic exception = tester.takeException();
expect(exception, isNotNull);
expect(exception, isFlutterError);
final FlutterError error = exception;
expect(error, isNotNull);
expect(error.diagnostics.last, isInstanceOf<DiagnosticsProperty<NavigatorState>>());
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' A Navigator\'s onUnknownRoute returned null.\n'
' When trying to build the route "/", both onGenerateRoute and\n'
' onUnknownRoute returned null. The onUnknownRoute callback should\n'
' never return null.\n'
' The Navigator was:\n'
' NavigatorState#38036(lifecycle state: created)\n',
),
);
});
});
} }
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// 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_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -612,4 +613,48 @@ void main() { ...@@ -612,4 +613,48 @@ void main() {
expect(buildOrder, <int>[3, 4, 1, 2, 0]); expect(buildOrder, <int>[3, 4, 1, 2, 0]);
}); });
testWidgets('OverlayState.of() called without Overlay being exist', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Builder(
builder: (BuildContext context) {
FlutterError error;
final Widget debugRequiredFor = Container();
try {
Overlay.of(context, debugRequiredFor: debugRequiredFor);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 5);
expect(error.diagnostics[2].level, DiagnosticLevel.hint);
expect(error.diagnostics[2].toStringDeep(), equalsIgnoringHashCodes(
'The most common way to add an Overlay to an application is to\n'
'include a MaterialApp or Navigator widget in the runApp() call.\n',
));
expect(error.diagnostics[3], isInstanceOf<DiagnosticsProperty<Widget>>());
expect(error.diagnostics[3].value, debugRequiredFor);
expect(error.diagnostics[4], isInstanceOf<DiagnosticsProperty<Element>>());
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' No Overlay widget found.\n'
' Container widgets require an Overlay widget ancestor for correct\n'
' operation.\n'
' The most common way to add an Overlay to an application is to\n'
' include a MaterialApp or Navigator widget in the runApp() call.\n'
' The specific widget that failed to find an overlay was:\n'
' Container\n'
' The context from which that widget was searching for an overlay\n'
' was:\n'
' Builder\n',
));
}
return Container();
}
),
),
);
});
} }
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// 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/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -191,4 +193,51 @@ void main() { ...@@ -191,4 +193,51 @@ void main() {
expect(smallListOverscrollApplied, lessThan(20.0)); expect(smallListOverscrollApplied, lessThan(20.0));
}); });
}); });
test('ClampingScrollPhysics assertion test', () {
const ClampingScrollPhysics physics = ClampingScrollPhysics();
const double pixels = 500;
final ScrollMetrics position = FixedScrollMetrics(
pixels: pixels,
minScrollExtent: 0,
maxScrollExtent: 1000,
viewportDimension: 0,
axisDirection: AxisDirection.down,
);
expect(position.pixels, pixels);
FlutterError error;
try {
physics.applyBoundaryConditions(position, pixels);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 4);
expect(error.diagnostics[2], isInstanceOf<DiagnosticsProperty<ScrollPhysics>>());
expect(error.diagnostics[2].style, DiagnosticsTreeStyle.errorProperty);
expect(error.diagnostics[2].value, physics);
expect(error.diagnostics[3], isInstanceOf<DiagnosticsProperty<ScrollMetrics>>());
expect(error.diagnostics[3].style, DiagnosticsTreeStyle.errorProperty);
expect(error.diagnostics[3].value, position);
// RegExp matcher is required here due to flutter web and flutter mobile generating
// slightly different floating point numbers
// in Flutter web 0.0 sometimes just appears as 0. or 0
expect(error.toStringDeep(), matches(RegExp(
r'''FlutterError
ClampingScrollPhysics\.applyBoundaryConditions\(\) was called
redundantly\.
The proposed new position\, 500(\.\d*)?, is exactly equal to the current
position of the given FixedScrollMetrics, 500(\.\d*)?\.
The applyBoundaryConditions method should only be called when the
value is going to actually change the pixels, otherwise it is
redundant\.
The physics object in question was\:
ClampingScrollPhysics
The position object in question was\:
FixedScrollMetrics\(500(\.\d*)?..\[0(\.\d*)?\]..500(\.\d*)?\)
''',
multiLine: true,
)));
}
});
} }
...@@ -636,13 +636,16 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -636,13 +636,16 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
if (_tickers != null) { if (_tickers != null) {
for (Ticker ticker in _tickers) { for (Ticker ticker in _tickers) {
if (ticker.isActive) { if (ticker.isActive) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'A Ticker was active $when.\n' ErrorSummary('A Ticker was active $when.'),
'All Tickers must be disposed. Tickers used by AnimationControllers ' ErrorDescription('All Tickers must be disposed.'),
'should be disposed by calling dispose() on the AnimationController itself. ' ErrorHint(
'Otherwise, the ticker will leak.\n' 'Tickers used by AnimationControllers '
'The offending ticker was: ${ticker.toString(debugIncludeStack: true)}' 'should be disposed by calling dispose() on the AnimationController itself. '
); 'Otherwise, the ticker will leak.'
),
ticker.describeForError('The offending ticker was')
]);
} }
} }
} }
...@@ -656,13 +659,20 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -656,13 +659,20 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
void _verifySemanticsHandlesWereDisposed() { void _verifySemanticsHandlesWereDisposed() {
assert(_lastRecordedSemanticsHandles != null); assert(_lastRecordedSemanticsHandles != null);
if (binding.pipelineOwner.debugOutstandingSemanticsHandles > _lastRecordedSemanticsHandles) { if (binding.pipelineOwner.debugOutstandingSemanticsHandles > _lastRecordedSemanticsHandles) {
throw FlutterError( // TODO(jacobr): The hint for this one causes a change in line breaks but
'A SemanticsHandle was active at the end of the test.\n' // I think it is for the best.
'All SemanticsHandle instances must be disposed by calling dispose() on ' throw FlutterError.fromParts(<DiagnosticsNode>[
'the SemanticsHandle. If your test uses SemanticsTester, it is ' ErrorSummary('A SemanticsHandle was active at the end of the test.'),
'sufficient to call dispose() on SemanticsTester. Otherwise, the ' ErrorDescription(
'existing handle will leak into another test and alter its behavior.' 'All SemanticsHandle instances must be disposed by calling dispose() on '
); 'the SemanticsHandle.'
),
ErrorHint(
'If your test uses SemanticsTester, it is '
'sufficient to call dispose() on SemanticsTester. Otherwise, the '
'existing handle will leak into another test and alter its behavior.'
)
]);
} }
_lastRecordedSemanticsHandles = null; _lastRecordedSemanticsHandles = null;
} }
......
...@@ -9,6 +9,7 @@ import 'dart:ui'; ...@@ -9,6 +9,7 @@ import 'dart:ui';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:test_api/test_api.dart' as test_package; import 'package:test_api/test_api.dart' as test_package;
import 'package:test_api/src/frontend/async_matcher.dart' show AsyncMatcher; import 'package:test_api/src/frontend/async_matcher.dart' show AsyncMatcher;
...@@ -662,6 +663,41 @@ void main() { ...@@ -662,6 +663,41 @@ void main() {
expect(find.text('Item 15', skipOffstage: true), findsOneWidget); expect(find.text('Item 15', skipOffstage: true), findsOneWidget);
}); });
}); });
testWidgets('verifyTickersWereDisposed control test', (WidgetTester tester) async {
FlutterError error;
final Ticker ticker = tester.createTicker((Duration duration) {});
ticker.start();
try {
tester.verifyTickersWereDisposed('');
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 4);
expect(error.diagnostics[2].level, DiagnosticLevel.hint);
expect(
error.diagnostics[2].toStringDeep(),
'Tickers used by AnimationControllers should be disposed by\n'
'calling dispose() on the AnimationController itself. Otherwise,\n'
'the ticker will leak.\n',
);
expect(error.diagnostics.last, isInstanceOf<DiagnosticsProperty<Ticker>>());
expect(error.diagnostics.last.value, ticker);
expect(error.toStringDeep(), startsWith(
'FlutterError\n'
' A Ticker was active .\n'
' All Tickers must be disposed.\n'
' Tickers used by AnimationControllers should be disposed by\n'
' calling dispose() on the AnimationController itself. Otherwise,\n'
' the ticker will leak.\n'
' The offending ticker was:\n'
' _TestTicker()\n',
));
}
ticker.stop();
});
} }
class FakeMatcher extends AsyncMatcher { class FakeMatcher extends AsyncMatcher {
...@@ -679,3 +715,28 @@ class FakeMatcher extends AsyncMatcher { ...@@ -679,3 +715,28 @@ class FakeMatcher extends AsyncMatcher {
@override @override
Description describe(Description description) => description.add('--fake--'); Description describe(Description description) => description.add('--fake--');
} }
class _SingleTickerTest extends StatefulWidget {
const _SingleTickerTest({Key key}) : super(key: key);
@override
_SingleTickerTestState createState() => _SingleTickerTestState();
}
class _SingleTickerTestState extends State<_SingleTickerTest> with SingleTickerProviderStateMixin {
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 100),
) ;
}
@override
Widget build(BuildContext context) {
return Container();
}
}
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