Unverified Commit 38f84901 authored by Jacob Richman's avatar Jacob Richman Committed by GitHub

Add more structure to errors messages. (#34684)

Breaking change to extremely rarely used ParentDataWidget.debugDescribeInvalidAncestorChain api changing the return type of the method from String to DiagnosticsNode.
parent 1b176c5d
...@@ -728,12 +728,15 @@ class AnimationController extends Animation<double> ...@@ -728,12 +728,15 @@ class AnimationController extends Animation<double>
void dispose() { void dispose() {
assert(() { assert(() {
if (_ticker == null) { if (_ticker == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'AnimationController.dispose() called more than once.\n' ErrorSummary('AnimationController.dispose() called more than once.'),
'A given $runtimeType cannot be disposed more than once.\n' ErrorDescription('A given $runtimeType cannot be disposed more than once.\n'),
'The following $runtimeType object was disposed multiple times:\n' DiagnosticsProperty<AnimationController>(
' $this' 'The following $runtimeType object was disposed multiple times',
); this,
style: DiagnosticsTreeStyle.errorProperty,
),
]);
} }
return true; return true;
}()); }());
......
...@@ -176,6 +176,19 @@ class ErrorHint extends _ErrorDiagnostic { ...@@ -176,6 +176,19 @@ class ErrorHint extends _ErrorDiagnostic {
ErrorHint._fromParts(List<Object> messageParts) : super._fromParts(messageParts, level:DiagnosticLevel.hint); ErrorHint._fromParts(List<Object> messageParts) : super._fromParts(messageParts, level:DiagnosticLevel.hint);
} }
/// An [ErrorSpacer] creates an empty [DiagnosticsNode], that can be used to
/// tune the spacing between other [DiagnosticsNode] objects.
class ErrorSpacer extends DiagnosticsProperty<void> {
/// Creates an empty space to insert into a list of [DiagnosticNode] objects
/// typically within a [FlutterError] object.
ErrorSpacer() : super(
'',
null,
description: '',
showName: false,
);
}
/// Class for information provided to [FlutterExceptionHandler] callbacks. /// Class for information provided to [FlutterExceptionHandler] callbacks.
/// ///
/// See [FlutterError.onError]. /// See [FlutterError.onError].
...@@ -407,7 +420,7 @@ class FlutterErrorDetails extends Diagnosticable { ...@@ -407,7 +420,7 @@ class FlutterErrorDetails extends Diagnosticable {
} }
} }
if (ourFault) { if (ourFault) {
properties.add(DiagnosticsNode.message('')); properties.add(ErrorSpacer());
properties.add(ErrorHint( properties.add(ErrorHint(
'Either the assertion indicates an error in the framework itself, or we should ' 'Either the assertion indicates an error in the framework itself, or we should '
'provide substantially more information in this error message to help you determine ' 'provide substantially more information in this error message to help you determine '
...@@ -418,11 +431,11 @@ class FlutterErrorDetails extends Diagnosticable { ...@@ -418,11 +431,11 @@ class FlutterErrorDetails extends Diagnosticable {
} }
} }
if (stack != null) { if (stack != null) {
properties.add(DiagnosticsNode.message('')); properties.add(ErrorSpacer());
properties.add(DiagnosticsStackTrace('When the exception was thrown, this was the stack', stack, stackFilter: stackFilter)); properties.add(DiagnosticsStackTrace('When the exception was thrown, this was the stack', stack, stackFilter: stackFilter));
} }
if (informationCollector != null) { if (informationCollector != null) {
properties.add(DiagnosticsNode.message('')); properties.add(ErrorSpacer());
informationCollector().forEach(properties.add); informationCollector().forEach(properties.add);
} }
} }
......
...@@ -690,7 +690,7 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin { ...@@ -690,7 +690,7 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
throw FlutterError( throw FlutterError(
'Steppers must not be nested. The material specification advises ' 'Steppers must not be nested. The material specification advises '
'that one should avoid embedding steppers within steppers. ' 'that one should avoid embedding steppers within steppers. '
'https://material.io/archive/guidelines/components/steppers.html#steppers-usage\n' 'https://material.io/archive/guidelines/components/steppers.html#steppers-usage'
); );
return true; return true;
}()); }());
......
...@@ -1315,20 +1315,22 @@ abstract class RenderBox extends RenderObject { ...@@ -1315,20 +1315,22 @@ abstract class RenderBox extends RenderObject {
double getMinIntrinsicWidth(double height) { double getMinIntrinsicWidth(double height) {
assert(() { assert(() {
if (height == null) { if (height == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The height argument to getMinIntrinsicWidth was null.\n' ErrorSummary('The height argument to getMinIntrinsicWidth was null.'),
'The argument to getMinIntrinsicWidth must not be negative or null. ' ErrorDescription('The argument to getMinIntrinsicWidth must not be negative or null.'),
'If you do not have a specific height in mind, then pass double.infinity instead.' ErrorHint('If you do not have a specific height in mind, then pass double.infinity instead.')
); ]);
} }
if (height < 0.0) { if (height < 0.0) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The height argument to getMinIntrinsicWidth was negative.\n' ErrorSummary('The height argument to getMinIntrinsicWidth was negative.'),
'The argument to getMinIntrinsicWidth must not be negative or null. ' ErrorDescription('The argument to getMinIntrinsicWidth must not be negative or null.'),
'If you perform computations on another height before passing it to ' ErrorHint(
'getMinIntrinsicWidth, consider using math.max() or double.clamp() ' 'If you perform computations on another height before passing it to '
'to force the value into the valid range.' 'getMinIntrinsicWidth, consider using math.max() or double.clamp() '
); 'to force the value into the valid range.'
),
]);
} }
return true; return true;
}()); }());
...@@ -1454,20 +1456,22 @@ abstract class RenderBox extends RenderObject { ...@@ -1454,20 +1456,22 @@ abstract class RenderBox extends RenderObject {
double getMaxIntrinsicWidth(double height) { double getMaxIntrinsicWidth(double height) {
assert(() { assert(() {
if (height == null) { if (height == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The height argument to getMaxIntrinsicWidth was null.\n' ErrorSummary('The height argument to getMaxIntrinsicWidth was null.'),
'The argument to getMaxIntrinsicWidth must not be negative or null. ' ErrorDescription('The argument to getMaxIntrinsicWidth must not be negative or null.'),
'If you do not have a specific height in mind, then pass double.infinity instead.' ErrorHint('If you do not have a specific height in mind, then pass double.infinity instead.')
); ]);
} }
if (height < 0.0) { if (height < 0.0) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The height argument to getMaxIntrinsicWidth was negative.\n' ErrorSummary('The height argument to getMaxIntrinsicWidth was negative.'),
'The argument to getMaxIntrinsicWidth must not be negative or null. ' ErrorDescription('The argument to getMaxIntrinsicWidth must not be negative or null.'),
'If you perform computations on another height before passing it to ' ErrorHint(
'getMaxIntrinsicWidth, consider using math.max() or double.clamp() ' 'If you perform computations on another height before passing it to '
'to force the value into the valid range.' 'getMaxIntrinsicWidth, consider using math.max() or double.clamp() '
); 'to force the value into the valid range.'
)
]);
} }
return true; return true;
}()); }());
...@@ -1530,20 +1534,22 @@ abstract class RenderBox extends RenderObject { ...@@ -1530,20 +1534,22 @@ abstract class RenderBox extends RenderObject {
double getMinIntrinsicHeight(double width) { double getMinIntrinsicHeight(double width) {
assert(() { assert(() {
if (width == null) { if (width == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The width argument to getMinIntrinsicHeight was null.\n' ErrorSummary('The width argument to getMinIntrinsicHeight was null.'),
'The argument to getMinIntrinsicHeight must not be negative or null. ' ErrorDescription('The argument to getMinIntrinsicHeight must not be negative or null.'),
'If you do not have a specific width in mind, then pass double.infinity instead.' ErrorHint('If you do not have a specific width in mind, then pass double.infinity instead.')
); ]);
} }
if (width < 0.0) { if (width < 0.0) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The width argument to getMinIntrinsicHeight was negative.\n' ErrorSummary('The width argument to getMinIntrinsicHeight was negative.'),
'The argument to getMinIntrinsicHeight must not be negative or null. ' ErrorDescription('The argument to getMinIntrinsicHeight must not be negative or null.'),
'If you perform computations on another width before passing it to ' ErrorHint(
'getMinIntrinsicHeight, consider using math.max() or double.clamp() ' 'If you perform computations on another width before passing it to '
'to force the value into the valid range.' 'getMinIntrinsicHeight, consider using math.max() or double.clamp() '
); 'to force the value into the valid range.'
)
]);
} }
return true; return true;
}()); }());
...@@ -1603,20 +1609,22 @@ abstract class RenderBox extends RenderObject { ...@@ -1603,20 +1609,22 @@ abstract class RenderBox extends RenderObject {
double getMaxIntrinsicHeight(double width) { double getMaxIntrinsicHeight(double width) {
assert(() { assert(() {
if (width == null) { if (width == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The width argument to getMaxIntrinsicHeight was null.\n' ErrorSummary('The width argument to getMaxIntrinsicHeight was null.'),
'The argument to getMaxIntrinsicHeight must not be negative or null. ' ErrorDescription('The argument to getMaxIntrinsicHeight must not be negative or null.'),
'If you do not have a specific width in mind, then pass double.infinity instead.' ErrorHint('If you do not have a specific width in mind, then pass double.infinity instead.')
); ]);
} }
if (width < 0.0) { if (width < 0.0) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The width argument to getMaxIntrinsicHeight was negative.\n' ErrorSummary('The width argument to getMaxIntrinsicHeight was negative.'),
'The argument to getMaxIntrinsicHeight must not be negative or null. ' ErrorDescription('The argument to getMaxIntrinsicHeight must not be negative or null.'),
'If you perform computations on another width before passing it to ' ErrorHint(
'getMaxIntrinsicHeight, consider using math.max() or double.clamp() ' 'If you perform computations on another width before passing it to '
'to force the value into the valid range.' 'getMaxIntrinsicHeight, consider using math.max() or double.clamp() '
); 'to force the value into the valid range.'
)
]);
} }
return true; return true;
}()); }());
...@@ -1753,41 +1761,45 @@ abstract class RenderBox extends RenderObject { ...@@ -1753,41 +1761,45 @@ abstract class RenderBox extends RenderObject {
if (value is _DebugSize) { if (value is _DebugSize) {
if (value._owner != this) { if (value._owner != this) {
if (value._owner.parent != this) { if (value._owner.parent != this) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'The size property was assigned a size inappropriately.\n' ErrorSummary('The size property was assigned a size inappropriately.'),
'The following render object:\n' describeForError('The following render object'),
' $this\n' value._owner.describeForError('...was assigned a size obtained from'),
'...was assigned a size obtained from:\n' ErrorDescription(
' ${value._owner}\n' 'However, this second render object is not, or is no longer, a '
'However, this second render object is not, or is no longer, a ' 'child of the first, and it is therefore a violation of the '
'child of the first, and it is therefore a violation of the ' 'RenderBox layout protocol to use that size in the layout of the '
'RenderBox layout protocol to use that size in the layout of the ' 'first render object.'
'first render object.\n' ),
'If the size was obtained at a time where it was valid to read ' ErrorHint(
'the size (because the second render object above was a child ' 'If the size was obtained at a time where it was valid to read '
'of the first at the time), then it should be adopted using ' 'the size (because the second render object above was a child '
'debugAdoptSize at that time.\n' 'of the first at the time), then it should be adopted using '
'If the size comes from a grandchild or a render object from an ' 'debugAdoptSize at that time.'
'entirely different part of the render tree, then there is no ' ),
'way to be notified when the size changes and therefore attempts ' ErrorHint(
'to read that size are almost certainly a source of bugs. A different ' 'If the size comes from a grandchild or a render object from an '
'approach should be used.' 'entirely different part of the render tree, then there is no '
); 'way to be notified when the size changes and therefore attempts '
'to read that size are almost certainly a source of bugs. A different '
'approach should be used.'
),
]);
} }
if (!value._canBeUsedByParent) { if (!value._canBeUsedByParent) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'A child\'s size was used without setting parentUsesSize.\n' ErrorSummary('A child\'s size was used without setting parentUsesSize.'),
'The following render object:\n' describeForError('The following render object'),
' $this\n' value._owner.describeForError('...was assigned a size obtained from its child'),
'...was assigned a size obtained from its child:\n' ErrorDescription(
' ${value._owner}\n' 'However, when the child was laid out, the parentUsesSize argument '
'However, when the child was laid out, the parentUsesSize argument ' 'was not set or set to false. Subsequently this transpired to be '
'was not set or set to false. Subsequently this transpired to be ' 'inaccurate: the size was nonetheless used by the parent.\n'
'inaccurate: the size was nonetheless used by the parent.\n' 'It is important to tell the framework if the size will be used or not '
'It is important to tell the framework if the size will be used or not ' 'as several important performance optimizations can be made if the '
'as several important performance optimizations can be made if the ' 'size will not be used by the parent.'
'size will not be used by the parent.' )
); ]);
} }
} }
} }
...@@ -2047,12 +2059,14 @@ abstract class RenderBox extends RenderObject { ...@@ -2047,12 +2059,14 @@ abstract class RenderBox extends RenderObject {
void performLayout() { void performLayout() {
assert(() { assert(() {
if (!sizedByParent) { if (!sizedByParent) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'$runtimeType did not implement performLayout().\n' ErrorSummary('$runtimeType did not implement performLayout().'),
'RenderBox subclasses need to either override performLayout() to ' ErrorHint(
'set a size and lay out any children, or, set sizedByParent to true ' 'RenderBox subclasses need to either override performLayout() to '
'so that performResize() sizes the render object.' 'set a size and lay out any children, or, set sizedByParent to true '
); 'so that performResize() sizes the render object.'
)
]);
} }
return true; return true;
}()); }());
...@@ -2086,27 +2100,35 @@ abstract class RenderBox extends RenderObject { ...@@ -2086,27 +2100,35 @@ abstract class RenderBox extends RenderObject {
assert(() { assert(() {
if (!hasSize) { if (!hasSize) {
if (debugNeedsLayout) { if (debugNeedsLayout) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Cannot hit test a render box that has never been laid out.\n' ErrorSummary('Cannot hit test a render box that has never been laid out.'),
'The hitTest() method was called on this RenderBox:\n' describeForError('The hitTest() method was called on this RenderBox'),
' $this\n' ErrorDescription(
'Unfortunately, this object\'s geometry is not known at this time, ' 'Unfortunately, this object\'s geometry is not known at this time, '
'probably because it has never been laid out. ' 'probably because it has never been laid out. '
'This means it cannot be accurately hit-tested. If you are trying ' 'This means it cannot be accurately hit-tested.'
'to perform a hit test during the layout phase itself, make sure ' ),
'you only hit test nodes that have completed layout (e.g. the node\'s ' ErrorHint(
'children, after their layout() method has been called).' 'If you are trying '
); 'to perform a hit test during the layout phase itself, make sure '
'you only hit test nodes that have completed layout (e.g. the node\'s '
'children, after their layout() method has been called).'
)
]);
} }
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Cannot hit test a render box with no size.\n' ErrorSummary('Cannot hit test a render box with no size.'),
'The hitTest() method was called on this RenderBox:\n' describeForError('The hitTest() method was called on this RenderBox'),
' $this\n' ErrorDescription(
'Although this node is not marked as needing layout, ' 'Although this node is not marked as needing layout, '
'its size is not set. A RenderBox object must have an ' 'its size is not set.'
'explicit size before it can be hit-tested. Make sure ' ),
'that the RenderBox in question sets its size during layout.' ErrorHint(
); 'A RenderBox object must have an '
'explicit size before it can be hit-tested. Make sure '
'that the RenderBox in question sets its size during layout.'
),
]);
} }
return true; return true;
}()); }());
...@@ -2171,19 +2193,19 @@ abstract class RenderBox extends RenderObject { ...@@ -2171,19 +2193,19 @@ abstract class RenderBox extends RenderObject {
assert(child.parent == this); assert(child.parent == this);
assert(() { assert(() {
if (child.parentData is! BoxParentData) { if (child.parentData is! BoxParentData) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'$runtimeType does not implement applyPaintTransform.\n' ErrorSummary('$runtimeType does not implement applyPaintTransform.'),
'The following $runtimeType object:\n' describeForError('The following $runtimeType object'),
' ${toStringShallow()}\n' child.describeForError('...did not use a BoxParentData class for the parentData field of the following child'),
'...did not use a BoxParentData class for the parentData field of the following child:\n' ErrorDescription('The $runtimeType class inherits from RenderBox.'),
' ${child.toStringShallow()}\n' ErrorHint(
'The $runtimeType class inherits from RenderBox. ' 'The default applyPaintTransform implementation provided by RenderBox assumes that the '
'The default applyPaintTransform implementation provided by RenderBox assumes that the ' 'children all use BoxParentData objects for their parentData field. '
'children all use BoxParentData objects for their parentData field. ' 'Since $runtimeType does not in fact use that ParentData class for its children, it must '
'Since $runtimeType does not in fact use that ParentData class for its children, it must ' 'provide an implementation of applyPaintTransform that supports the specific ParentData '
'provide an implementation of applyPaintTransform that supports the specific ParentData ' 'subclass used by its children (which apparently is ${child.parentData.runtimeType}).'
'subclass used by its children (which apparently is ${child.parentData.runtimeType}).' )
); ]);
} }
return true; return true;
}()); }());
......
...@@ -1194,7 +1194,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1194,7 +1194,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
// displaying the truncated children is really useful for command line // displaying the truncated children is really useful for command line
// users. Inspector users can see the full tree by clicking on the // users. Inspector users can see the full tree by clicking on the
// render object so this may not be that useful. // render object so this may not be that useful.
yield describeForError('This RenderObject', style: DiagnosticsTreeStyle.truncateChildren); yield describeForError('RenderObject', style: DiagnosticsTreeStyle.truncateChildren);
} }
)); ));
} }
...@@ -2030,14 +2030,17 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2030,14 +2030,17 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
void _paintWithContext(PaintingContext context, Offset offset) { void _paintWithContext(PaintingContext context, Offset offset) {
assert(() { assert(() {
if (_debugDoingThisPaint) { if (_debugDoingThisPaint) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Tried to paint a RenderObject reentrantly.\n' ErrorSummary('Tried to paint a RenderObject reentrantly.'),
'The following RenderObject was already being painted when it was ' describeForError(
'painted again:\n' 'The following RenderObject was already being painted when it was '
' ${toStringShallow(joiner: "\n ")}\n' 'painted again'
'Since this typically indicates an infinite recursion, it is ' ),
'disallowed.' ErrorDescription(
); 'Since this typically indicates an infinite recursion, it is '
'disallowed.'
)
]);
} }
return true; return true;
}()); }());
...@@ -2052,17 +2055,24 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2052,17 +2055,24 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
return; return;
assert(() { assert(() {
if (_needsCompositingBitsUpdate) { if (_needsCompositingBitsUpdate) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Tried to paint a RenderObject before its compositing bits were ' ErrorSummary(
'updated.\n' 'Tried to paint a RenderObject before its compositing bits were '
'The following RenderObject was marked as having dirty compositing ' 'updated.'
'bits at the time that it was painted:\n' ),
' ${toStringShallow(joiner: "\n ")}\n' describeForError(
'A RenderObject that still has dirty compositing bits cannot be ' 'The following RenderObject was marked as having dirty compositing '
'painted because this indicates that the tree has not yet been ' 'bits at the time that it was painted',
'properly configured for creating the layer tree.\n' ),
'This usually indicates an error in the Flutter framework itself.' ErrorDescription(
); 'A RenderObject that still has dirty compositing bits cannot be '
'painted because this indicates that the tree has not yet been '
'properly configured for creating the layer tree.'
),
ErrorHint(
'This usually indicates an error in the Flutter framework itself.'
)
]);
} }
return true; return true;
}()); }());
...@@ -2720,21 +2730,31 @@ mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject ...@@ -2720,21 +2730,31 @@ mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject
bool debugValidateChild(RenderObject child) { bool debugValidateChild(RenderObject child) {
assert(() { assert(() {
if (child is! ChildType) { if (child is! ChildType) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'A $runtimeType expected a child of type $ChildType but received a ' ErrorSummary(
'child of type ${child.runtimeType}.\n' 'A $runtimeType expected a child of type $ChildType but received a '
'RenderObjects expect specific types of children because they ' 'child of type ${child.runtimeType}.'
'coordinate with their children during layout and paint. For ' ),
'example, a RenderSliver cannot be the child of a RenderBox because ' ErrorDescription(
'a RenderSliver does not understand the RenderBox layout protocol.\n' 'RenderObjects expect specific types of children because they '
'\n' 'coordinate with their children during layout and paint. For '
'The $runtimeType that expected a $ChildType child was created by:\n' 'example, a RenderSliver cannot be the child of a RenderBox because '
' $debugCreator\n' 'a RenderSliver does not understand the RenderBox layout protocol.',
'\n' ),
'The ${child.runtimeType} that did not match the expected child type ' ErrorSpacer(),
'was created by:\n' DiagnosticsProperty<dynamic>(
' ${child.debugCreator}\n' 'The $runtimeType that expected a $ChildType child was created by',
); debugCreator,
style: DiagnosticsTreeStyle.errorProperty,
),
ErrorSpacer(),
DiagnosticsProperty<dynamic>(
'The ${child.runtimeType} that did not match the expected child type '
'was created by',
child.debugCreator,
style: DiagnosticsTreeStyle.errorProperty,
)
]);
} }
return true; return true;
}()); }());
...@@ -2849,21 +2869,31 @@ mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType ...@@ -2849,21 +2869,31 @@ mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType
bool debugValidateChild(RenderObject child) { bool debugValidateChild(RenderObject child) {
assert(() { assert(() {
if (child is! ChildType) { if (child is! ChildType) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'A $runtimeType expected a child of type $ChildType but received a ' ErrorSummary(
'child of type ${child.runtimeType}.\n' 'A $runtimeType expected a child of type $ChildType but received a '
'RenderObjects expect specific types of children because they ' 'child of type ${child.runtimeType}.'
'coordinate with their children during layout and paint. For ' ),
'example, a RenderSliver cannot be the child of a RenderBox because ' ErrorDescription(
'a RenderSliver does not understand the RenderBox layout protocol.\n' 'RenderObjects expect specific types of children because they '
'\n' 'coordinate with their children during layout and paint. For '
'The $runtimeType that expected a $ChildType child was created by:\n' 'example, a RenderSliver cannot be the child of a RenderBox because '
' $debugCreator\n' 'a RenderSliver does not understand the RenderBox layout protocol.'
'\n' ),
'The ${child.runtimeType} that did not match the expected child type ' ErrorSpacer(),
'was created by:\n' DiagnosticsProperty<dynamic>(
' ${child.debugCreator}\n' 'The $runtimeType that expected a $ChildType child was created by',
); debugCreator,
style: DiagnosticsTreeStyle.errorProperty,
),
ErrorSpacer(),
DiagnosticsProperty<dynamic>(
'The ${child.runtimeType} that did not match the expected child type '
'was created by',
child.debugCreator,
style: DiagnosticsTreeStyle.errorProperty,
),
]);
} }
return true; return true;
}()); }());
......
...@@ -1282,30 +1282,31 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1282,30 +1282,31 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
assert(!newChildren.any((SemanticsNode child) => child == this)); assert(!newChildren.any((SemanticsNode child) => child == this));
assert(() { assert(() {
if (identical(newChildren, _children)) { if (identical(newChildren, _children)) {
final StringBuffer mutationErrors = StringBuffer(); final List<DiagnosticsNode> mutationErrors = <DiagnosticsNode>[];
if (newChildren.length != _debugPreviousSnapshot.length) { if (newChildren.length != _debugPreviousSnapshot.length) {
mutationErrors.writeln( mutationErrors.add(ErrorDescription(
'The list\'s length has changed from ${_debugPreviousSnapshot.length} ' 'The list\'s length has changed from ${_debugPreviousSnapshot.length} '
'to ${newChildren.length}.' 'to ${newChildren.length}.'
); ));
} else { } else {
for (int i = 0; i < newChildren.length; i++) { for (int i = 0; i < newChildren.length; i++) {
if (!identical(newChildren[i], _debugPreviousSnapshot[i])) { if (!identical(newChildren[i], _debugPreviousSnapshot[i])) {
mutationErrors.writeln( if (mutationErrors.isNotEmpty) {
'Child node at position $i was replaced:\n' mutationErrors.add(ErrorSpacer());
'Previous child: ${newChildren[i]}\n' }
'New child: ${_debugPreviousSnapshot[i]}\n' mutationErrors.add(ErrorDescription('Child node at position $i was replaced:'));
); mutationErrors.add(newChildren[i].toDiagnosticsNode(name: 'Previous child', style: DiagnosticsTreeStyle.singleLine));
mutationErrors.add(_debugPreviousSnapshot[i].toDiagnosticsNode(name: 'New child', style: DiagnosticsTreeStyle.singleLine));
} }
} }
} }
if (mutationErrors.isNotEmpty) { if (mutationErrors.isNotEmpty) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.\n' ErrorSummary('Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.'),
'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.\n' ErrorHint('Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.'),
'Error details:\n' ErrorDescription('Error details:'),
'$mutationErrors' ...mutationErrors
); ]);
} }
} }
assert(!newChildren.any((SemanticsNode node) => node.isMergedIntoParent) || isPartOfNodeMerging); assert(!newChildren.any((SemanticsNode node) => node.isMergedIntoParent) || isPartOfNodeMerging);
......
...@@ -164,21 +164,25 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key { ...@@ -164,21 +164,25 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
final String older = _debugReservations[this].toString(); final String older = _debugReservations[this].toString();
final String newer = parent.toString(); final String newer = parent.toString();
if (older != newer) { if (older != newer) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Multiple widgets used the same GlobalKey.\n' ErrorSummary('Multiple widgets used the same GlobalKey.'),
'The key $this was used by multiple widgets. The parents of those widgets were:\n' ErrorDescription(
'- $older\n' 'The key $this was used by multiple widgets. The parents of those widgets were:\n'
'- $newer\n' '- $older\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.' '- $newer\n'
); 'A GlobalKey can only be specified on one widget at a time in the widget tree.'
)
]);
} }
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Multiple widgets used the same GlobalKey.\n' ErrorSummary('Multiple widgets used the same GlobalKey.'),
'The key $this was used by multiple widgets. The parents of those widgets were ' ErrorDescription(
'different widgets that both had the following description:\n' 'The key $this was used by multiple widgets. The parents of those widgets were '
' $newer\n' 'different widgets that both had the following description:\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.' ' $parent\n'
); 'A GlobalKey can only be specified on one widget at a time in the widget tree.'
),
]);
} }
_debugReservations[this] = parent; _debugReservations[this] = parent;
return true; return true;
...@@ -1098,44 +1102,57 @@ abstract class State<T extends StatefulWidget> extends Diagnosticable { ...@@ -1098,44 +1102,57 @@ abstract class State<T extends StatefulWidget> extends Diagnosticable {
assert(fn != null); assert(fn != null);
assert(() { assert(() {
if (_debugLifecycleState == _StateLifecycle.defunct) { if (_debugLifecycleState == _StateLifecycle.defunct) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'setState() called after dispose(): $this\n' ErrorSummary('setState() called after dispose(): $this'),
'This error happens if you call setState() on a State object for a widget that ' ErrorDescription(
'no longer appears in the widget tree (e.g., whose parent widget no longer ' 'This error happens if you call setState() on a State object for a widget that '
'includes the widget in its build). This error can occur when code calls ' 'no longer appears in the widget tree (e.g., whose parent widget no longer '
'setState() from a timer or an animation callback. The preferred solution is ' 'includes the widget in its build). This error can occur when code calls '
'to cancel the timer or stop listening to the animation in the dispose() ' 'setState() from a timer or an animation callback.'
'callback. Another solution is to check the "mounted" property of this ' ),
'object before calling setState() to ensure the object is still in the ' ErrorHint(
'tree.\n' 'The preferred solution is '
'This error might indicate a memory leak if setState() is being called ' 'to cancel the timer or stop listening to the animation in the dispose() '
'because another object is retaining a reference to this State object ' 'callback. Another solution is to check the "mounted" property of this '
'after it has been removed from the tree. To avoid memory leaks, ' 'object before calling setState() to ensure the object is still in the '
'consider breaking the reference to this object during dispose().' 'tree.'
); ),
ErrorHint(
'This error might indicate a memory leak if setState() is being called '
'because another object is retaining a reference to this State object '
'after it has been removed from the tree. To avoid memory leaks, '
'consider breaking the reference to this object during dispose().'
),
]);
} }
if (_debugLifecycleState == _StateLifecycle.created && !mounted) { if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'setState() called in constructor: $this\n' ErrorSummary('setState() called in constructor: $this'),
'This happens when you call setState() on a State object for a widget that ' ErrorHint(
'hasn\'t been inserted into the widget tree yet. It is not necessary to call ' 'This happens when you call setState() on a State object for a widget that '
'setState() in the constructor, since the state is already assumed to be dirty ' 'hasn\'t been inserted into the widget tree yet. It is not necessary to call '
'when it is initially created.' 'setState() in the constructor, since the state is already assumed to be dirty '
); 'when it is initially created.'
)
]);
} }
return true; return true;
}()); }());
final dynamic result = fn() as dynamic; final dynamic result = fn() as dynamic;
assert(() { assert(() {
if (result is Future) { if (result is Future) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'setState() callback argument returned a Future.\n' ErrorSummary('setState() callback argument returned a Future.'),
'The setState() method on $this was called with a closure or method that ' ErrorDescription(
'returned a Future. Maybe it is marked as "async".\n' 'The setState() method on $this was called with a closure or method that '
'Instead of performing asynchronous work inside a call to setState(), first ' 'returned a Future. Maybe it is marked as "async".'
'execute the work (without updating the widget state), and then synchronously ' ),
'update the state inside a call to setState().' ErrorHint(
); 'Instead of performing asynchronous work inside a call to setState(), first '
'execute the work (without updating the widget state), and then synchronously '
'update the state inside a call to setState().'
)
]);
} }
// We ignore other types of return values so that you can do things like: // We ignore other types of return values so that you can do things like:
// setState(() => x = 3); // setState(() => x = 3);
...@@ -1433,28 +1450,30 @@ abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidge ...@@ -1433,28 +1450,30 @@ abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidge
/// ParentDataWidget subclass. It is called when debugIsValidAncestor() /// ParentDataWidget subclass. It is called when debugIsValidAncestor()
/// returned false for an ancestor, or when there are extraneous /// returned false for an ancestor, or when there are extraneous
/// [ParentDataWidget]s in the ancestor chain. /// [ParentDataWidget]s in the ancestor chain.
String debugDescribeInvalidAncestorChain({ String description, String ownershipChain, bool foundValidAncestor, Iterable<Widget> badAncestors }) { Iterable<DiagnosticsNode> debugDescribeInvalidAncestorChain({ String description, DiagnosticsNode ownershipChain, bool foundValidAncestor, Iterable<Widget> badAncestors }) sync* {
assert(T != dynamic); assert(T != dynamic);
assert(T != RenderObjectWidget); assert(T != RenderObjectWidget);
String result;
if (!foundValidAncestor) { if (!foundValidAncestor) {
result = '$runtimeType widgets must be placed inside $T widgets.\n' yield ErrorDescription(
'$description has no $T ancestor at all.\n'; '$runtimeType widgets must be placed inside $T widgets.\n'
'$description has no $T ancestor at all.'
);
} else { } else {
assert(badAncestors.isNotEmpty); assert(badAncestors.isNotEmpty);
result = '$runtimeType widgets must be placed directly inside $T widgets.\n' yield ErrorDescription(
'$description has a $T ancestor, but there are other widgets between them:\n'; '$runtimeType widgets must be placed directly inside $T widgets.\n'
'$description has a $T ancestor, but there are other widgets between them:'
);
for (Widget ancestor in badAncestors) { for (Widget ancestor in badAncestors) {
if (ancestor.runtimeType == runtimeType) { if (ancestor.runtimeType == runtimeType) {
result += '- $ancestor (this is a different $runtimeType than the one with the problem)\n'; yield ErrorDescription('- $ancestor (this is a different $runtimeType than the one with the problem)');
} else { } else {
result += '- $ancestor\n'; yield ErrorDescription('- $ancestor');
} }
} }
result += 'These widgets cannot come between a $runtimeType and its $T.\n'; yield ErrorDescription('These widgets cannot come between a $runtimeType and its $T.');
} }
result += 'The ownership chain for the parent of the offending $runtimeType was:\n $ownershipChain'; yield ErrorDescription('The ownership chain for the parent of the offending $runtimeType was:\n $ownershipChain');
return result;
} }
/// Write the data from this widget into the given render object's parent data. /// Write the data from this widget into the given render object's parent data.
...@@ -2167,16 +2186,18 @@ class BuildOwner { ...@@ -2167,16 +2186,18 @@ class BuildOwner {
if (debugPrintScheduleBuildForStacks) if (debugPrintScheduleBuildForStacks)
debugPrintStack(label: 'scheduleBuildFor() called for $element${_dirtyElements.contains(element) ? " (ALREADY IN LIST)" : ""}'); debugPrintStack(label: 'scheduleBuildFor() called for $element${_dirtyElements.contains(element) ? " (ALREADY IN LIST)" : ""}');
if (!element.dirty) { if (!element.dirty) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'scheduleBuildFor() called for a widget that is not marked as dirty.\n' ErrorSummary('scheduleBuildFor() called for a widget that is not marked as dirty.'),
'The method was called for the following element:\n' element.describeElement('The method was called for the following element'),
' $element\n' ErrorDescription(
'This element is not current marked as dirty. Make sure to set the dirty flag before ' 'This element is not current marked as dirty. Make sure to set the dirty flag before '
'calling scheduleBuildFor().\n' 'calling scheduleBuildFor().'),
'If you did not attempt to call scheduleBuildFor() yourself, then this probably ' ErrorHint(
'indicates a bug in the widgets framework. Please report it: ' 'If you did not attempt to call scheduleBuildFor() yourself, then this probably '
'https://github.com/flutter/flutter/issues/new?template=BUG.md' 'indicates a bug in the widgets framework. Please report it:\n'
); ' https://github.com/flutter/flutter/issues/new?template=BUG.md'
)
]);
} }
return true; return true;
}()); }());
...@@ -2185,11 +2206,13 @@ class BuildOwner { ...@@ -2185,11 +2206,13 @@ class BuildOwner {
if (debugPrintScheduleBuildForStacks) if (debugPrintScheduleBuildForStacks)
debugPrintStack(label: 'BuildOwner.scheduleBuildFor() called; _dirtyElementsNeedsResorting was $_dirtyElementsNeedsResorting (now true); dirty list is: $_dirtyElements'); debugPrintStack(label: 'BuildOwner.scheduleBuildFor() called; _dirtyElementsNeedsResorting was $_dirtyElementsNeedsResorting (now true); dirty list is: $_dirtyElements');
if (!_debugIsInBuildScope) { if (!_debugIsInBuildScope) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'BuildOwner.scheduleBuildFor() called inappropriately.\n' ErrorSummary('BuildOwner.scheduleBuildFor() called inappropriately.'),
'The BuildOwner.scheduleBuildFor() method should only be called while the ' ErrorHint(
'buildScope() method is actively rebuilding the widget tree.' 'The BuildOwner.scheduleBuildFor() method should only be called while the '
); 'buildScope() method is actively rebuilding the widget tree.'
)
]);
} }
return true; return true;
}()); }());
...@@ -2344,12 +2367,11 @@ class BuildOwner { ...@@ -2344,12 +2367,11 @@ class BuildOwner {
} }
assert(() { assert(() {
if (_dirtyElements.any((Element element) => element._active && element.dirty)) { if (_dirtyElements.any((Element element) => element._active && element.dirty)) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'buildScope missed some dirty elements.\n' ErrorSummary('buildScope missed some dirty elements.'),
'This probably indicates that the dirty list should have been resorted but was not.\n' ErrorHint('This probably indicates that the dirty list should have been resorted but was not.'),
'The list of dirty elements at the end of the buildScope call was:\n' Element.describeElements('The list of dirty elements at the end of the buildScope call was', _dirtyElements)
' $_dirtyElements' ]);
);
} }
return true; return true;
}()); }());
...@@ -2459,21 +2481,26 @@ class BuildOwner { ...@@ -2459,21 +2481,26 @@ class BuildOwner {
final String they = elementLabels.length == 1 ? 'it' : 'they'; final String they = elementLabels.length == 1 ? 'it' : 'they';
final String think = elementLabels.length == 1 ? 'thinks' : 'think'; final String think = elementLabels.length == 1 ? 'thinks' : 'think';
final String are = elementLabels.length == 1 ? 'is' : 'are'; final String are = elementLabels.length == 1 ? 'is' : 'are';
throw FlutterError( // TODO(jacobr): make this error more structured to better expose which widgets had problems.
'Duplicate GlobalKey$s detected in widget tree.\n' throw FlutterError.fromParts(<DiagnosticsNode>[
'The following GlobalKey$s $were specified multiple times in the widget tree. This will lead to ' ErrorSummary('Duplicate GlobalKey$s detected in widget tree.'),
'parts of the widget tree being truncated unexpectedly, because the second time a key is seen, ' // TODO(jacobr): refactor this code so the elements are clickable
'the previous instance is moved to the new location. The key$s $were:\n' // in GUI debug tools.
'- ${keyLabels.join("\n ")}\n' ErrorDescription(
'This was determined by noticing that after$the widget$s with the above global key$s $were moved ' 'The following GlobalKey$s $were specified multiple times in the widget tree. This will lead to '
'out of $their$respective previous parent$s2, $those2 previous parent$s2 never updated during this frame, meaning ' 'parts of the widget tree being truncated unexpectedly, because the second time a key is seen, '
'that $they either did not update at all or updated before the widget$s $were moved, in either case ' 'the previous instance is moved to the new location. The key$s $were:\n'
'implying that $they still $think that $they should have a child with $those global key$s.\n' '- ${keyLabels.join("\n ")}\n'
'The specific parent$s2 that did not update after having one or more children forcibly removed ' 'This was determined by noticing that after$the widget$s with the above global key$s $were moved '
'due to GlobalKey reparenting $are:\n' 'out of $their$respective previous parent$s2, $those2 previous parent$s2 never updated during this frame, meaning '
'- ${elementLabels.join("\n ")}\n' 'that $they either did not update at all or updated before the widget$s $were moved, in either case '
'A GlobalKey can only be specified on one widget at a time in the widget tree.' 'implying that $they still $think that $they should have a child with $those global key$s.\n'
); 'The specific parent$s2 that did not update after having one or more children forcibly removed '
'due to GlobalKey reparenting $are:\n'
'- ${elementLabels.join("\n ")}'
'\nA GlobalKey can only be specified on one widget at a time in the widget tree.'
)
]);
} }
} }
} finally { } finally {
...@@ -2780,13 +2807,15 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -2780,13 +2807,15 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
assert(() { assert(() {
if (owner == null || !owner._debugStateLocked) if (owner == null || !owner._debugStateLocked)
return true; return true;
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'visitChildElements() called during build.\n' ErrorSummary('visitChildElements() called during build.'),
'The BuildContext.visitChildElements() method can\'t be called during ' ErrorDescription(
'build because the child list is still being updated at that point, ' 'The BuildContext.visitChildElements() method can\'t be called during '
'so the children might not be constructed yet, or might be old children ' 'build because the child list is still being updated at that point, '
'that are going to be replaced.' 'so the children might not be constructed yet, or might be old children '
); 'that are going to be replaced.'
)
]);
}()); }());
visitChildren(visitor); visitChildren(visitor);
} }
...@@ -2998,14 +3027,14 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -2998,14 +3027,14 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
if (parent != null) { if (parent != null) {
assert(() { assert(() {
if (parent == this) { if (parent == this) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'A GlobalKey was used multiple times inside one widget\'s child list.\n' ErrorSummary('A GlobalKey was used multiple times inside one widget\'s child list.'),
'The offending GlobalKey was: $key\n' DiagnosticsProperty<GlobalKey>('The offending GlobalKey was', key),
'The parent of the widgets with that key was:\n $parent\n' parent.describeElement('The parent of the widgets with that key was'),
'The first child to get instantiated with that key became:\n $element\n' element.describeElement('The first child to get instantiated with that key became'),
'The second child that was to be instantiated with that key was:\n $widget\n' DiagnosticsProperty<Widget>('The second child that was to be instantiated with that key was', widget, style: DiagnosticsTreeStyle.errorProperty),
'A GlobalKey can only be specified on one widget at a time in the widget tree.' ErrorDescription('A GlobalKey can only be specified on one widget at a time in the widget tree.')
); ]);
} }
parent.owner._debugTrackElementThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans( parent.owner._debugTrackElementThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans(
parent, parent,
...@@ -3231,108 +3260,117 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3231,108 +3260,117 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
Size get size { Size get size {
assert(() { assert(() {
if (_debugLifecycleState != _ElementLifecycle.active) { if (_debugLifecycleState != _ElementLifecycle.active) {
throw FlutterError( // TODO(jacobr): is this a good separation into contract and violation?
'Cannot get size of inactive element.\n' // I have added a line of white space.
'In order for an element to have a valid size, the element must be ' throw FlutterError.fromParts(<DiagnosticsNode>[
'active, which means it is part of the tree. Instead, this element ' ErrorSummary('Cannot get size of inactive element.'),
'is in the $_debugLifecycleState state.\n' ErrorDescription(
'The size getter was called for the following element:\n' 'In order for an element to have a valid size, the element must be '
' $this\n' 'active, which means it is part of the tree.\n'
); 'Instead, this element is in the $_debugLifecycleState state.'
),
describeElement('The size getter was called for the following element')
]);
} }
if (owner._debugBuilding) { if (owner._debugBuilding) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Cannot get size during build.\n' ErrorSummary('Cannot get size during build.'),
'The size of this render object has not yet been determined because ' ErrorDescription(
'the framework is still in the process of building widgets, which ' 'The size of this render object has not yet been determined because '
'means the render tree for this frame has not yet been determined. ' 'the framework is still in the process of building widgets, which '
'The size getter should only be called from paint callbacks or ' 'means the render tree for this frame has not yet been determined. '
'interaction event handlers (e.g. gesture callbacks).\n' 'The size getter should only be called from paint callbacks or '
'\n' 'interaction event handlers (e.g. gesture callbacks).'
'If you need some sizing information during build to decide which ' ),
'widgets to build, consider using a LayoutBuilder widget, which can ' ErrorSpacer(),
'tell you the layout constraints at a given location in the tree. See ' ErrorHint(
'<https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html> ' 'If you need some sizing information during build to decide which '
'for more details.\n' 'widgets to build, consider using a LayoutBuilder widget, which can '
'\n' 'tell you the layout constraints at a given location in the tree. See '
'The size getter was called for the following element:\n' '<https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html> '
' $this\n' 'for more details.'
); ),
ErrorSpacer(),
describeElement('The size getter was called for the following element')
]);
} }
return true; return true;
}()); }());
final RenderObject renderObject = findRenderObject(); final RenderObject renderObject = findRenderObject();
assert(() { assert(() {
if (renderObject == null) { if (renderObject == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Cannot get size without a render object.\n' ErrorSummary('Cannot get size without a render object.'),
'In order for an element to have a valid size, the element must have ' ErrorHint(
'an associated render object. This element does not have an associated ' 'In order for an element to have a valid size, the element must have '
'render object, which typically means that the size getter was called ' 'an associated render object. This element does not have an associated '
'too early in the pipeline (e.g., during the build phase) before the ' 'render object, which typically means that the size getter was called '
'framework has created the render tree.\n' 'too early in the pipeline (e.g., during the build phase) before the '
'The size getter was called for the following element:\n' 'framework has created the render tree.'
' $this\n' ),
); describeElement('The size getter was called for the following element')
]);
} }
if (renderObject is RenderSliver) { if (renderObject is RenderSliver) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Cannot get size from a RenderSliver.\n' ErrorSummary('Cannot get size from a RenderSliver.'),
'The render object associated with this element is a ' ErrorHint(
'${renderObject.runtimeType}, which is a subtype of RenderSliver. ' 'The render object associated with this element is a '
'Slivers do not have a size per se. They have a more elaborate ' '${renderObject.runtimeType}, which is a subtype of RenderSliver. '
'geometry description, which can be accessed by calling ' 'Slivers do not have a size per se. They have a more elaborate '
'findRenderObject and then using the "geometry" getter on the ' 'geometry description, which can be accessed by calling '
'resulting object.\n' 'findRenderObject and then using the "geometry" getter on the '
'The size getter was called for the following element:\n' 'resulting object.'
' $this\n' ),
'The associated render sliver was:\n' describeElement('The size getter was called for the following element'),
' ${renderObject.toStringShallow(joiner: "\n ")}' renderObject.describeForError('The associated render sliver was'),
); ]);
} }
if (renderObject is! RenderBox) { if (renderObject is! RenderBox) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Cannot get size from a render object that is not a RenderBox.\n' ErrorSummary('Cannot get size from a render object that is not a RenderBox.'),
'Instead of being a subtype of RenderBox, the render object associated ' ErrorHint(
'with this element is a ${renderObject.runtimeType}. If this type of ' 'Instead of being a subtype of RenderBox, the render object associated '
'render object does have a size, consider calling findRenderObject ' 'with this element is a ${renderObject.runtimeType}. If this type of '
'and extracting its size manually.\n' 'render object does have a size, consider calling findRenderObject '
'The size getter was called for the following element:\n' 'and extracting its size manually.'
' $this\n' ),
'The associated render object was:\n' describeElement('The size getter was called for the following element'),
' ${renderObject.toStringShallow(joiner: "\n ")}' renderObject.describeForError('The associated render object was')
); ]);
} }
final RenderBox box = renderObject; final RenderBox box = renderObject;
if (!box.hasSize) { if (!box.hasSize) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Cannot get size from a render object that has not been through layout.\n' ErrorSummary('Cannot get size from a render object that has not been through layout.'),
'The size of this render object has not yet been determined because ' ErrorHint(
'this render object has not yet been through layout, which typically ' 'The size of this render object has not yet been determined because '
'means that the size getter was called too early in the pipeline ' 'this render object has not yet been through layout, which typically '
'(e.g., during the build phase) before the framework has determined ' 'means that the size getter was called too early in the pipeline '
'the size and position of the render objects during layout.\n' '(e.g., during the build phase) before the framework has determined '
'The size getter was called for the following element:\n' 'the size and position of the render objects during layout.'
' $this\n' ),
'The render object from which the size was to be obtained was:\n' describeElement('The size getter was called for the following element'),
' ${box.toStringShallow(joiner: "\n ")}' box.describeForError('The render object from which the size was to be obtained was')
); ]);
} }
if (box.debugNeedsLayout) { if (box.debugNeedsLayout) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Cannot get size from a render object that has been marked dirty for layout.\n' ErrorSummary('Cannot get size from a render object that has been marked dirty for layout.'),
'The size of this render object is ambiguous because this render object has ' ErrorHint(
'been modified since it was last laid out, which typically means that the size ' 'The size of this render object is ambiguous because this render object has '
'getter was called too early in the pipeline (e.g., during the build phase) ' 'been modified since it was last laid out, which typically means that the size '
'before the framework has determined the size and position of the render ' 'getter was called too early in the pipeline (e.g., during the build phase) '
'objects during layout.\n' 'before the framework has determined the size and position of the render '
'The size getter was called for the following element:\n' 'objects during layout.'
' $this\n' ),
'The render object from which the size was to be obtained was:\n' describeElement('The size getter was called for the following element'),
' ${box.toStringShallow(joiner: "\n ")}\n' box.describeForError('The render object from which the size was to be obtained was'),
'Consider using debugPrintMarkNeedsLayoutStacks to determine why the render ' ErrorHint(
'object in question is dirty, if you did not expect this.' 'Consider using debugPrintMarkNeedsLayoutStacks to determine why the render '
); 'object in question is dirty, if you did not expect this.'
),
]);
} }
return true; return true;
}()); }());
...@@ -3348,13 +3386,18 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3348,13 +3386,18 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
bool _debugCheckStateIsActiveForAncestorLookup() { bool _debugCheckStateIsActiveForAncestorLookup() {
assert(() { assert(() {
if (_debugLifecycleState != _ElementLifecycle.active) { if (_debugLifecycleState != _ElementLifecycle.active) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'Looking up a deactivated widget\'s ancestor is unsafe.\n' ErrorSummary('Looking up a deactivated widget\'s ancestor is unsafe.'),
'At this point the state of the widget\'s element tree is no longer ' ErrorDescription(
'stable. To safely refer to a widget\'s ancestor in its dispose() method, ' 'At this point the state of the widget\'s element tree is no longer '
'save a reference to the ancestor by calling inheritFromWidgetOfExactType() ' 'stable.'
'in the widget\'s didChangeDependencies() method.\n' ),
); ErrorHint(
'To safely refer to a widget\'s ancestor in its dispose() method, '
'save a reference to the ancestor by calling inheritFromWidgetOfExactType() '
'in the widget\'s didChangeDependencies() method.'
)
]);
} }
return true; return true;
}()); }());
...@@ -3468,17 +3511,21 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3468,17 +3511,21 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
bool _debugCheckOwnerBuildTargetExists(String methodName) { bool _debugCheckOwnerBuildTargetExists(String methodName) {
assert(() { assert(() {
if (owner._debugCurrentBuildTarget == null) { if (owner._debugCurrentBuildTarget == null) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'$methodName for ${widget.runtimeType} was called at an ' ErrorSummary(
'inappropriate time.\n' '$methodName for ${widget.runtimeType} was called at an '
'It may only be called while the widgets are being built. A possible ' 'inappropriate time.'
'cause of this error is when $methodName is called during ' ),
'one of:\n' ErrorDescription('It may only be called while the widgets are being built.'),
' * network I/O event\n' ErrorHint(
' * file I/O event\n' 'A possible cause of this error is when $methodName is called during '
' * timer\n' 'one of:\n'
' * microtask (caused by Future.then, async/await, scheduleMicrotask)' ' * network I/O event\n'
); ' * file I/O event\n'
' * timer\n'
' * microtask (caused by Future.then, async/await, scheduleMicrotask)'
)
]);
} }
return true; return true;
}()); }());
...@@ -3603,30 +3650,36 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3603,30 +3650,36 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
if (_debugIsInScope(owner._debugCurrentBuildTarget)) if (_debugIsInScope(owner._debugCurrentBuildTarget))
return true; return true;
if (!_debugAllowIgnoredCallsToMarkNeedsBuild) { if (!_debugAllowIgnoredCallsToMarkNeedsBuild) {
throw FlutterError( final List<DiagnosticsNode> information = <DiagnosticsNode>[
'setState() or markNeedsBuild() called during build.\n' ErrorSummary('setState() or markNeedsBuild() called during build.'),
'This ${widget.runtimeType} widget cannot be marked as needing to build because the framework ' ErrorDescription(
'is already in the process of building widgets. A widget can be marked as ' 'This ${widget.runtimeType} widget cannot be marked as needing to build because the framework '
'needing to be built during the build phase only if one of its ancestors ' 'is already in the process of building widgets. A widget can be marked as '
'is currently building. This exception is allowed because the framework ' 'needing to be built during the build phase only if one of its ancestors '
'builds parent widgets before children, which means a dirty descendant ' 'is currently building. This exception is allowed because the framework '
'will always be built. Otherwise, the framework might not visit this ' 'builds parent widgets before children, which means a dirty descendant '
'widget during this build phase.\n' 'will always be built. Otherwise, the framework might not visit this '
'The widget on which setState() or markNeedsBuild() was called was:\n' 'widget during this build phase.'
' $this\n' ),
'${owner._debugCurrentBuildTarget == null ? "" : "The widget which was currently being built when the offending call was made was:\n ${owner._debugCurrentBuildTarget}"}' describeElement(
); 'The widget on which setState() or markNeedsBuild() was called was',
)
];
if (owner._debugCurrentBuildTarget != null)
information.add(owner._debugCurrentBuildTarget.describeWidget('The widget which was currently being built when the offending call was made was'));
throw FlutterError.fromParts(information);
} }
assert(dirty); // can only get here if we're not in scope, but ignored calls are allowed, and our call would somehow be ignored (since we're already dirty) assert(dirty); // can only get here if we're not in scope, but ignored calls are allowed, and our call would somehow be ignored (since we're already dirty)
} else if (owner._debugStateLocked) { } else if (owner._debugStateLocked) {
assert(!_debugAllowIgnoredCallsToMarkNeedsBuild); assert(!_debugAllowIgnoredCallsToMarkNeedsBuild);
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'setState() or markNeedsBuild() called when widget tree was locked.\n' ErrorSummary('setState() or markNeedsBuild() called when widget tree was locked.'),
'This ${widget.runtimeType} widget cannot be marked as needing to build ' ErrorDescription(
'because the framework is locked.\n' 'This ${widget.runtimeType} widget cannot be marked as needing to build '
'The widget on which setState() or markNeedsBuild() was called was:\n' 'because the framework is locked.'
' $this\n' ),
); describeElement('The widget on which setState() or markNeedsBuild() was called was')
]);
} }
return true; return true;
}()); }());
...@@ -3937,12 +3990,14 @@ class StatefulElement extends ComponentElement { ...@@ -3937,12 +3990,14 @@ class StatefulElement extends ComponentElement {
super(widget) { super(widget) {
assert(() { assert(() {
if (!_state._debugTypesAreRight(widget)) { if (!_state._debugTypesAreRight(widget)) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'StatefulWidget.createState must return a subtype of State<${widget.runtimeType}>\n' ErrorSummary('StatefulWidget.createState must return a subtype of State<${widget.runtimeType}>'),
'The createState function for ${widget.runtimeType} returned a state ' ErrorDescription(
'of type ${_state.runtimeType}, which is not a subtype of ' 'The createState function for ${widget.runtimeType} returned a state '
'State<${widget.runtimeType}>, violating the contract for createState.' 'of type ${_state.runtimeType}, which is not a subtype of '
); 'State<${widget.runtimeType}>, violating the contract for createState.'
)
]);
} }
return true; return true;
}()); }());
...@@ -3978,12 +4033,14 @@ class StatefulElement extends ComponentElement { ...@@ -3978,12 +4033,14 @@ class StatefulElement extends ComponentElement {
final dynamic debugCheckForReturnedFuture = _state.initState() as dynamic; final dynamic debugCheckForReturnedFuture = _state.initState() as dynamic;
assert(() { assert(() {
if (debugCheckForReturnedFuture is Future) { if (debugCheckForReturnedFuture is Future) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'${_state.runtimeType}.initState() returned a Future.\n' ErrorSummary('${_state.runtimeType}.initState() returned a Future.'),
'State.initState() must be a void method without an `async` keyword.\n' ErrorDescription('State.initState() must be a void method without an `async` keyword.'),
'Rather than awaiting on asynchronous work directly inside of initState,\n' ErrorHint(
'call a separate method to do this work without awaiting it.' 'Rather than awaiting on asynchronous work directly inside of initState, '
); 'call a separate method to do this work without awaiting it.'
)
]);
} }
return true; return true;
}()); }());
...@@ -4011,12 +4068,14 @@ class StatefulElement extends ComponentElement { ...@@ -4011,12 +4068,14 @@ class StatefulElement extends ComponentElement {
final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic; final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;
assert(() { assert(() {
if (debugCheckForReturnedFuture is Future) { if (debugCheckForReturnedFuture is Future) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'${_state.runtimeType}.didUpdateWidget() returned a Future.\n' ErrorSummary('${_state.runtimeType}.didUpdateWidget() returned a Future.'),
'State.didUpdateWidget() must be a void method without an `async` keyword.\n' ErrorDescription( 'State.didUpdateWidget() must be a void method without an `async` keyword.'),
'Rather than awaiting on asynchronous work directly inside of didUpdateWidget,\n' ErrorHint(
'call a separate method to do this work without awaiting it.' 'Rather than awaiting on asynchronous work directly inside of didUpdateWidget, '
); 'call a separate method to do this work without awaiting it.'
)
]);
} }
return true; return true;
}()); }());
...@@ -4049,11 +4108,13 @@ class StatefulElement extends ComponentElement { ...@@ -4049,11 +4108,13 @@ class StatefulElement extends ComponentElement {
assert(() { assert(() {
if (_state._debugLifecycleState == _StateLifecycle.defunct) if (_state._debugLifecycleState == _StateLifecycle.defunct)
return true; return true;
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'${_state.runtimeType}.dispose failed to call super.dispose.\n' ErrorSummary('${_state.runtimeType}.dispose failed to call super.dispose.'),
'dispose() implementations must always call their superclass dispose() method, to ensure ' ErrorDescription(
'that all the resources used by the widget are fully released.' 'dispose() implementations must always call their superclass dispose() method, to ensure '
); 'that all the resources used by the widget are fully released.'
)
]);
}()); }());
_state._element = null; _state._element = null;
_state = null; _state = null;
...@@ -4065,37 +4126,47 @@ class StatefulElement extends ComponentElement { ...@@ -4065,37 +4126,47 @@ class StatefulElement extends ComponentElement {
assert(() { assert(() {
final Type targetType = ancestor.widget.runtimeType; final Type targetType = ancestor.widget.runtimeType;
if (state._debugLifecycleState == _StateLifecycle.created) { if (state._debugLifecycleState == _StateLifecycle.created) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'inheritFromWidgetOfExactType($targetType) or inheritFromElement() was called before ${_state.runtimeType}.initState() completed.\n' ErrorSummary('inheritFromWidgetOfExactType($targetType) or inheritFromElement() was called before ${_state.runtimeType}.initState() completed.'),
'When an inherited widget changes, for example if the value of Theme.of() changes, ' ErrorDescription(
'its dependent widgets are rebuilt. If the dependent widget\'s reference to ' 'When an inherited widget changes, for example if the value of Theme.of() changes, '
'the inherited widget is in a constructor or an initState() method, ' 'its dependent widgets are rebuilt. If the dependent widget\'s reference to '
'then the rebuilt dependent widget will not reflect the changes in the ' 'the inherited widget is in a constructor or an initState() method, '
'inherited widget.\n' 'then the rebuilt dependent widget will not reflect the changes in the '
'Typically references to inherited widgets should occur in widget build() methods. Alternatively, ' 'inherited widget.',
'initialization based on inherited widgets can be placed in the didChangeDependencies method, which ' ),
'is called after initState and whenever the dependencies change thereafter.' ErrorHint(
); 'Typically references to inherited widgets should occur in widget build() methods. Alternatively, '
'initialization based on inherited widgets can be placed in the didChangeDependencies method, which '
'is called after initState and whenever the dependencies change thereafter.'
)
]);
} }
if (state._debugLifecycleState == _StateLifecycle.defunct) { if (state._debugLifecycleState == _StateLifecycle.defunct) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'inheritFromWidgetOfExactType($targetType) or inheritFromElement() was called after dispose(): $this\n' ErrorSummary('inheritFromWidgetOfExactType($targetType) or inheritFromElement() was called after dispose(): $this'),
'This error happens if you call inheritFromWidgetOfExactType() on the ' ErrorDescription(
'BuildContext for a widget that no longer appears in the widget tree ' 'This error happens if you call inheritFromWidgetOfExactType() on the '
'(e.g., whose parent widget no longer includes the widget in its ' 'BuildContext for a widget that no longer appears in the widget tree '
'build). This error can occur when code calls ' '(e.g., whose parent widget no longer includes the widget in its '
'inheritFromWidgetOfExactType() from a timer or an animation callback. ' 'build). This error can occur when code calls '
'The preferred solution is to cancel the timer or stop listening to the ' 'inheritFromWidgetOfExactType() from a timer or an animation callback.'
'animation in the dispose() callback. Another solution is to check the ' ),
'"mounted" property of this object before calling ' ErrorHint(
'inheritFromWidgetOfExactType() to ensure the object is still in the ' 'The preferred solution is to cancel the timer or stop listening to the '
'tree.\n' 'animation in the dispose() callback. Another solution is to check the '
'This error might indicate a memory leak if ' '"mounted" property of this object before calling '
'inheritFromWidgetOfExactType() is being called because another object ' 'inheritFromWidgetOfExactType() to ensure the object is still in the '
'is retaining a reference to this State object after it has been ' 'tree.'
'removed from the tree. To avoid memory leaks, consider breaking the ' ),
'reference to this object during dispose().' ErrorHint(
); 'This error might indicate a memory leak if '
'inheritFromWidgetOfExactType() is being called because another object '
'is retaining a reference to this State object after it has been '
'removed from the tree. To avoid memory leaks, consider breaking the '
'reference to this object during dispose().'
),
]);
} }
return true; return true;
}()); }());
...@@ -4192,15 +4263,18 @@ class ParentDataElement<T extends RenderObjectWidget> extends ProxyElement { ...@@ -4192,15 +4263,18 @@ class ParentDataElement<T extends RenderObjectWidget> extends ProxyElement {
} }
if (ancestor != null && badAncestors.isEmpty) if (ancestor != null && badAncestors.isEmpty)
return true; return true;
throw FlutterError( // TODO(jacobr): switch to describing the invalid parent chain in terms
'Incorrect use of ParentDataWidget.\n' + // of DiagnosticsNode objects when possible.
widget.debugDescribeInvalidAncestorChain( throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Incorrect use of ParentDataWidget.'),
// TODO(jacobr): fix this constructor call to use FlutterErrorBuilder.
...widget.debugDescribeInvalidAncestorChain(
description: '$this', description: '$this',
ownershipChain: parent.debugGetCreatorChain(10), ownershipChain: ErrorDescription(parent.debugGetCreatorChain(10)),
foundValidAncestor: ancestor != null, foundValidAncestor: ancestor != null,
badAncestors: badAncestors, badAncestors: badAncestors,
) ),
); ]);
}()); }());
super.mount(parent, newSlot); super.mount(parent, newSlot);
} }
......
...@@ -431,7 +431,7 @@ class ClampingScrollPhysics extends ScrollPhysics { ...@@ -431,7 +431,7 @@ class ClampingScrollPhysics extends ScrollPhysics {
'The physics object in question was:\n' 'The physics object in question was:\n'
' $this\n' ' $this\n'
'The position object in question was:\n' 'The position object in question was:\n'
' $position\n' ' $position'
); );
} }
return true; return true;
......
...@@ -348,7 +348,29 @@ void main() { ...@@ -348,7 +348,29 @@ void main() {
expect(controller.repeat, throwsFlutterError); expect(controller.repeat, throwsFlutterError);
controller.dispose(); controller.dispose();
expect(controller.dispose, throwsFlutterError); FlutterError result;
try {
controller.dispose();
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' AnimationController.dispose() called more than once.\n'
' A given AnimationController cannot be disposed more than once.\n'
' The following AnimationController object was disposed multiple\n'
' times:\n'
' AnimationController#00000(⏮ 0.000; paused; DISPOSED)\n'
),
);
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
result.debugFillProperties(builder);
final DiagnosticsNode controllerProperty = builder.properties.last;
expect(controllerProperty.name, 'The following AnimationController object was disposed multiple times');
expect(controllerProperty.value, controller);
}); });
test('AnimationController repeat() throws if period is not specified', () { test('AnimationController repeat() throws if period is not specified', () {
......
...@@ -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';
...@@ -478,6 +479,72 @@ void main() { ...@@ -478,6 +479,72 @@ void main() {
expect(find.text('!'), findsOneWidget); expect(find.text('!'), findsOneWidget);
}); });
testWidgets('Nested stepper error test', (WidgetTester tester) async {
FlutterErrorDetails errorDetails;
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
errorDetails = details;
};
try {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Stepper(
type: StepperType.horizontal,
steps: <Step>[
Step(
title: const Text('Step 2'),
content: Stepper(
type: StepperType.vertical,
steps: const <Step>[
Step(
title: Text('Nested step 1'),
content: Text('A'),
),
Step(
title: Text('Nested step 2'),
content: Text('A'),
),
],
)
),
const Step(
title: Text('Step 1'),
content: Text('A'),
),
],
),
),
),
);
} finally {
FlutterError.onError = oldHandler;
}
expect(errorDetails, isNotNull);
expect(errorDetails.stack, isNotNull);
// Check the ErrorDetails without the stack trace
final List<String> lines = errorDetails.toString().split('\n');
// The lines in the middle of the error message contain the stack trace
// which will change depending on where the test is run.
expect(lines.length, greaterThan(9));
expect(
lines.take(9).join('\n'),
equalsIgnoringHashCodes(
'══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════\n'
'The following assertion was thrown building Stepper(dirty,\n'
'dependencies: [_LocalizationsScope-[GlobalKey#00000]], state:\n'
'_StepperState#00000):\n'
'Steppers must not be nested. The material specification advises\n'
'that one should avoid embedding steppers within steppers.\n'
'https://material.io/archive/guidelines/components/steppers.html#steppers-usage\n'
'\n'
'When the exception was thrown, this was the stack:'
),
);
});
///https://github.com/flutter/flutter/issues/16920 ///https://github.com/flutter/flutter/issues/16920
testWidgets('Stepper icons size test', (WidgetTester tester) async { testWidgets('Stepper icons size test', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -9,6 +9,27 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -9,6 +9,27 @@ import 'package:flutter_test/flutter_test.dart';
import 'rendering_tester.dart'; import 'rendering_tester.dart';
class MissingPerformLayoutRenderBox extends RenderBox {
void triggerExceptionSettingSizeOutsideOfLayout() {
size = const Size(200, 200);
}
// performLayout is left unimplemented to test the error reported if it is
// missing.
}
class FakeMissingSizeRenderBox extends RenderBox {
@override
void performLayout() {
size = constraints.biggest;
}
@override
bool get hasSize => fakeMissingSize ? false : super.hasSize;
bool fakeMissingSize = false;
}
void main() { void main() {
test('should size to render view', () { test('should size to render view', () {
final RenderBox root = RenderDecoratedBox( final RenderBox root = RenderDecoratedBox(
...@@ -27,6 +48,165 @@ void main() { ...@@ -27,6 +48,165 @@ void main() {
expect(root.size.height, equals(600.0)); expect(root.size.height, equals(600.0));
}); });
test('performLayout error message', () {
FlutterError result;
try {
MissingPerformLayoutRenderBox().performLayout();
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' MissingPerformLayoutRenderBox did not implement performLayout().\n'
' RenderBox subclasses need to either override performLayout() to\n'
' set a size and lay out any children, or, set sizedByParent to\n'
' true so that performResize() sizes the render object.\n'
)
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'RenderBox subclasses need to either override performLayout() to set a '
'size and lay out any children, or, set sizedByParent to true so that '
'performResize() sizes the render object.'
);
});
test('applyPaintTransform error message', () {
final RenderBox paddingBox = RenderPadding(
padding: const EdgeInsets.all(10.0),
);
final RenderBox root = RenderPadding(
padding: const EdgeInsets.all(10.0),
child: paddingBox,
);
layout(root);
// Trigger the error by overriding the parentData with data that isn't a
// BoxParentData.
paddingBox.parentData = ParentData();
FlutterError result;
try {
root.applyPaintTransform(paddingBox, Matrix4.identity());
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' RenderPadding does not implement applyPaintTransform.\n'
' The following RenderPadding object: RenderPadding#00000 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE:\n'
' parentData: <none>\n'
' constraints: BoxConstraints(w=800.0, h=600.0)\n'
' size: Size(800.0, 600.0)\n'
' padding: EdgeInsets.all(10.0)\n'
' ...did not use a BoxParentData class for the parentData field of the following child:\n'
' RenderPadding#00000 NEEDS-PAINT:\n'
' parentData: <none> (can use size)\n'
' constraints: BoxConstraints(w=780.0, h=580.0)\n'
' size: Size(780.0, 580.0)\n'
' padding: EdgeInsets.all(10.0)\n'
' The RenderPadding class inherits from RenderBox.\n'
' The default applyPaintTransform implementation provided by\n'
' RenderBox assumes that the children all use BoxParentData objects\n'
' for their parentData field. Since RenderPadding does not in fact\n'
' use that ParentData class for its children, it must provide an\n'
' implementation of applyPaintTransform that supports the specific\n'
' ParentData subclass used by its children (which apparently is\n'
' ParentData).\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'The default applyPaintTransform implementation provided by RenderBox '
'assumes that the children all use BoxParentData objects for their '
'parentData field. Since RenderPadding does not in fact use that '
'ParentData class for its children, it must provide an implementation '
'of applyPaintTransform that supports the specific ParentData subclass '
'used by its children (which apparently is ParentData).'
);
});
test('Set size error messages', () {
final RenderBox root = RenderDecoratedBox(
decoration: const BoxDecoration(
color: Color(0xFF00FF00),
),
);
layout(root);
final MissingPerformLayoutRenderBox testBox = MissingPerformLayoutRenderBox();
{
FlutterError result;
try {
testBox.triggerExceptionSettingSizeOutsideOfLayout();
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' RenderBox size setter called incorrectly.\n'
' The size setter was called from outside layout (neither\n'
' performResize() nor performLayout() were being run for this\n'
' object).\n'
' Because this RenderBox has sizedByParent set to false, it must\n'
' set its size in performLayout().\n'
)
);
expect(result.diagnostics.where((DiagnosticsNode node) => node.level == DiagnosticLevel.hint), isEmpty);
}
{
FlutterError result;
try {
testBox.debugAdoptSize(root.size);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The size property was assigned a size inappropriately.\n'
' The following render object: MissingPerformLayoutRenderBox#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED:\n'
' parentData: MISSING\n'
' constraints: MISSING\n'
' size: MISSING\n'
' ...was assigned a size obtained from: RenderDecoratedBox#00000 NEEDS-PAINT:\n'
' parentData: <none>\n'
' constraints: BoxConstraints(w=800.0, h=600.0)\n'
' size: Size(800.0, 600.0)\n'
' decoration: BoxDecoration:\n'
' color: Color(0xff00ff00)\n'
' configuration: ImageConfiguration()\n'
' However, this second render object is not, or is no longer, a\n'
' child of the first, and it is therefore a violation of the\n'
' RenderBox layout protocol to use that size in the layout of the\n'
' first render object.\n'
' If the size was obtained at a time where it was valid to read the\n'
' size (because the second render object above was a child of the\n'
' first at the time), then it should be adopted using\n'
' debugAdoptSize at that time.\n'
' If the size comes from a grandchild or a render object from an\n'
' entirely different part of the render tree, then there is no way\n'
' to be notified when the size changes and therefore attempts to\n'
' read that size are almost certainly a source of bugs. A different\n'
' approach should be used.\n'
)
);
expect(result.diagnostics.where((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).length, 2);
}
});
test('Flex and padding', () { test('Flex and padding', () {
final RenderBox size = RenderConstrainedBox( final RenderBox size = RenderConstrainedBox(
additionalConstraints: const BoxConstraints().tighten(height: 100.0), additionalConstraints: const BoxConstraints().tighten(height: 100.0),
...@@ -192,6 +372,230 @@ void main() { ...@@ -192,6 +372,230 @@ void main() {
expect(unconstrained.getMaxIntrinsicWidth(100.0), equals(200.0)); expect(unconstrained.getMaxIntrinsicWidth(100.0), equals(200.0));
}); });
test ('getMinInstrinsicWidth error handling', () {
final RenderUnconstrainedBox unconstrained = RenderUnconstrainedBox(
textDirection: TextDirection.ltr,
child: RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(width: 200.0),
),
alignment: Alignment.center,
);
const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
layout(unconstrained, constraints: viewport);
{
FlutterError result;
try {
unconstrained.getMinIntrinsicWidth(null);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The height argument to getMinIntrinsicWidth was null.\n'
' The argument to getMinIntrinsicWidth must not be negative or\n'
' null.\n'
' If you do not have a specific height in mind, then pass\n'
' double.infinity instead.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you do not have a specific height in mind, then pass double.infinity instead.'
);
}
{
FlutterError result;
try {
unconstrained.getMinIntrinsicWidth(-1);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The height argument to getMinIntrinsicWidth was negative.\n'
' The argument to getMinIntrinsicWidth must not be negative or\n'
' null.\n'
' If you perform computations on another height before passing it\n'
' to getMinIntrinsicWidth, consider using math.max() or\n'
' double.clamp() to force the value into the valid range.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you perform computations on another height before passing it to '
'getMinIntrinsicWidth, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
);
}
{
FlutterError result;
try {
unconstrained.getMinIntrinsicHeight(null);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The width argument to getMinIntrinsicHeight was null.\n'
' The argument to getMinIntrinsicHeight must not be negative or\n'
' null.\n'
' If you do not have a specific width in mind, then pass\n'
' double.infinity instead.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you do not have a specific width in mind, then pass double.infinity instead.'
);
}
{
FlutterError result;
try {
unconstrained.getMinIntrinsicHeight(-1);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The width argument to getMinIntrinsicHeight was negative.\n'
' The argument to getMinIntrinsicHeight must not be negative or\n'
' null.\n'
' If you perform computations on another width before passing it to\n'
' getMinIntrinsicHeight, consider using math.max() or\n'
' double.clamp() to force the value into the valid range.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you perform computations on another width before passing it to '
'getMinIntrinsicHeight, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
);
}
{
FlutterError result;
try {
unconstrained.getMaxIntrinsicWidth(null);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The height argument to getMaxIntrinsicWidth was null.\n'
' The argument to getMaxIntrinsicWidth must not be negative or\n'
' null.\n'
' If you do not have a specific height in mind, then pass\n'
' double.infinity instead.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you do not have a specific height in mind, then pass double.infinity instead.'
);
}
{
FlutterError result;
try {
unconstrained.getMaxIntrinsicWidth(-1);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The height argument to getMaxIntrinsicWidth was negative.\n'
' The argument to getMaxIntrinsicWidth must not be negative or\n'
' null.\n'
' If you perform computations on another height before passing it\n'
' to getMaxIntrinsicWidth, consider using math.max() or\n'
' double.clamp() to force the value into the valid range.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you perform computations on another height before passing it to '
'getMaxIntrinsicWidth, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
);
}
{
FlutterError result;
try {
unconstrained.getMaxIntrinsicHeight(null);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The width argument to getMaxIntrinsicHeight was null.\n'
' The argument to getMaxIntrinsicHeight must not be negative or\n'
' null.\n'
' If you do not have a specific width in mind, then pass\n'
' double.infinity instead.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you do not have a specific width in mind, then pass double.infinity instead.'
);
}
{
FlutterError result;
try {
unconstrained.getMaxIntrinsicHeight(-1);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The width argument to getMaxIntrinsicHeight was negative.\n'
' The argument to getMaxIntrinsicHeight must not be negative or\n'
' null.\n'
' If you perform computations on another width before passing it to\n'
' getMaxIntrinsicHeight, consider using math.max() or\n'
' double.clamp() to force the value into the valid range.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you perform computations on another width before passing it to '
'getMaxIntrinsicHeight, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
);
}
});
test('UnconstrainedBox.toStringDeep returns useful information', () { test('UnconstrainedBox.toStringDeep returns useful information', () {
final RenderUnconstrainedBox unconstrained = RenderUnconstrainedBox( final RenderUnconstrainedBox unconstrained = RenderUnconstrainedBox(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
...@@ -513,6 +917,84 @@ void main() { ...@@ -513,6 +917,84 @@ void main() {
expect(positions.single, position + const Offset(20, 30)); expect(positions.single, position + const Offset(20, 30));
positions.clear(); positions.clear();
}); });
test('error message', () {
{
final RenderBox renderObject = RenderConstrainedBox(
additionalConstraints: const BoxConstraints().tighten(height: 100.0),
);
FlutterError result;
try {
final BoxHitTestResult result = BoxHitTestResult();
renderObject.hitTest(result, position: Offset.zero);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' Cannot hit test a render box that has never been laid out.\n'
' The hitTest() method was called on this RenderBox: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED:\n'
' parentData: MISSING\n'
' constraints: MISSING\n'
' size: MISSING\n'
' additionalConstraints: BoxConstraints(0.0<=w<=Infinity, h=100.0)\n'
' Unfortunately, this object\'s geometry is not known at this time,\n'
' probably because it has never been laid out. This means it cannot\n'
' be accurately hit-tested.\n'
' If you are trying to perform a hit test during the layout phase\n'
' itself, make sure you only hit test nodes that have completed\n'
' layout (e.g. the node\'s children, after their layout() method has\n'
' been called).\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you are trying to perform a hit test during the layout phase '
'itself, make sure you only hit test nodes that have completed '
'layout (e.g. the node\'s children, after their layout() method has '
'been called).'
);
}
{
FlutterError result;
final FakeMissingSizeRenderBox renderObject = FakeMissingSizeRenderBox();
layout(renderObject);
renderObject.fakeMissingSize = true;
try {
final BoxHitTestResult result = BoxHitTestResult();
renderObject.hitTest(result, position: Offset.zero);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' Cannot hit test a render box with no size.\n'
' The hitTest() method was called on this RenderBox: FakeMissingSizeRenderBox#00000 NEEDS-PAINT:\n'
' parentData: <none>\n'
' constraints: BoxConstraints(w=800.0, h=600.0)\n'
' size: Size(800.0, 600.0)\n'
' Although this node is not marked as needing layout, its size is\n'
' not set.\n'
' A RenderBox object must have an explicit size before it can be\n'
' hit-tested. Make sure that the RenderBox in question sets its\n'
' size during layout.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'A RenderBox object must have an explicit size before it can be '
'hit-tested. Make sure that the RenderBox in question sets its '
'size during layout.'
);
}
});
}); });
} }
......
...@@ -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/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/src/binding.dart' show TestWidgetsFlutterBinding; import 'package:flutter_test/src/binding.dart' show TestWidgetsFlutterBinding;
...@@ -35,6 +36,51 @@ void main() { ...@@ -35,6 +36,51 @@ void main() {
renderObject.markNeedsSemanticsUpdate(); renderObject.markNeedsSemanticsUpdate();
expect(renderObject.describeSemanticsConfigurationCallCount, 0); expect(renderObject.describeSemanticsConfigurationCallCount, 0);
}); });
test('ensure errors processing render objects are well formatted', () {
FlutterErrorDetails errorDetails;
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
errorDetails = details;
};
final PipelineOwner owner = PipelineOwner();
final TestThrowingRenderObject renderObject = TestThrowingRenderObject();
try {
renderObject.attach(owner);
renderObject.layout(const BoxConstraints());
} finally {
FlutterError.onError = oldHandler;
}
expect(errorDetails, isNotNull);
expect(errorDetails.stack, isNotNull);
// Check the ErrorDetails without the stack trace
final List<String> lines = errorDetails.toString().split('\n');
// The lines in the middle of the error message contain the stack trace
// which will change depending on where the test is run.
expect(lines.length, greaterThan(8));
expect(
lines.take(4).join('\n'),
equalsIgnoringHashCodes(
'══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══════════════════════\n'
'The following assertion was thrown during performLayout():\n'
'TestThrowingRenderObject does not support performLayout.\n'
)
);
expect(
lines.getRange(lines.length - 8, lines.length).join('\n'),
equalsIgnoringHashCodes(
'\n'
'The following RenderObject was being processed when the exception was fired:\n'
' TestThrowingRenderObject#00000 NEEDS-PAINT:\n'
' parentData: MISSING\n'
' constraints: BoxConstraints(unconstrained)\n'
'This RenderObject has no descendants.\n'
'═════════════════════════════════════════════════════════════════\n'
),
);
});
} }
class TestRenderObject extends RenderObject { class TestRenderObject extends RenderObject {
...@@ -62,3 +108,22 @@ class TestRenderObject extends RenderObject { ...@@ -62,3 +108,22 @@ class TestRenderObject extends RenderObject {
describeSemanticsConfigurationCallCount++; describeSemanticsConfigurationCallCount++;
} }
} }
class TestThrowingRenderObject extends RenderObject {
@override
void performLayout() {
throw FlutterError('TestThrowingRenderObject does not support performLayout.');
}
@override
void debugAssertDoesMeetConstraints() { }
@override
Rect get paintBounds => null;
@override
void performResize() { }
@override
Rect get semanticBounds => null;
}
// 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 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'rendering_tester.dart';
void main() {
// This test has to be kept separate from object_test.dart because the way
// the rendering_test.dart dependency of this test uses the bindings in not
// compatible with existing tests in object_test.dart.
test('reentrant paint error', () {
FlutterErrorDetails errorDetails;
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
errorDetails = details;
};
final RenderBox root = TestReentrantPaintingErrorRenderBox();
try {
layout(root);
pumpFrame(phase: EnginePhase.paint);
} finally {
FlutterError.onError = oldHandler;
}
expect(errorDetails, isNotNull);
expect(errorDetails.stack, isNotNull);
// Check the ErrorDetails without the stack trace
final List<String> lines = errorDetails.toString().split('\n');
// The lines in the middle of the error message contain the stack trace
// which will change depending on where the test is run.
expect(lines.length, greaterThan(12));
expect(
lines.take(12).join('\n'),
equalsIgnoringHashCodes(
'══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══════════════════════\n'
'The following assertion was thrown during paint():\n'
'Tried to paint a RenderObject reentrantly.\n'
'The following RenderObject was already being painted when it was painted again:\n'
' TestReentrantPaintingErrorRenderBox#00000:\n'
' parentData: <none>\n'
' constraints: BoxConstraints(w=800.0, h=600.0)\n'
' size: Size(100.0, 100.0)\n'
'Since this typically indicates an infinite recursion, it is\n'
'disallowed.\n'
'\n'
'When the exception was thrown, this was the stack:'
),
);
expect(
lines.getRange(lines.length - 8, lines.length).join('\n'),
equalsIgnoringHashCodes(
'The following RenderObject was being processed when the exception was fired:\n'
' TestReentrantPaintingErrorRenderBox#00000:\n'
' parentData: <none>\n'
' constraints: BoxConstraints(w=800.0, h=600.0)\n'
' size: Size(100.0, 100.0)\n'
'This RenderObject has no descendants.\n'
'═════════════════════════════════════════════════════════════════\n'
),
);
});
test('needsCompositingBitsUpdate paint error', () {
FlutterError flutterError;
final RenderBox root = RenderRepaintBoundary(child: RenderSizedBox(const Size(100, 100)));
try {
layout(root);
PaintingContext.repaintCompositedChild(root, debugAlsoPaintedParent: true);
} on FlutterError catch (exception) {
flutterError = exception;
}
expect(flutterError, isNotNull);
// The lines in the middle of the error message contain the stack trace
// which will change depending on where the test is run.
expect(
flutterError.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' Tried to paint a RenderObject before its compositing bits were\n'
' updated.\n'
' The following RenderObject was marked as having dirty compositing bits at the time that it was painted:\n'
' RenderRepaintBoundary#00000 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE:\n'
' needs compositing\n'
' parentData: <none>\n'
' constraints: BoxConstraints(w=800.0, h=600.0)\n'
' layer: OffsetLayer#00000 DETACHED\n'
' size: Size(800.0, 600.0)\n'
' metrics: 0.0% useful (1 bad vs 0 good)\n'
' diagnosis: insufficient data to draw conclusion (less than five\n'
' repaints)\n'
' A RenderObject that still has dirty compositing bits cannot be\n'
' painted because this indicates that the tree has not yet been\n'
' properly configured for creating the layer tree.\n'
' This usually indicates an error in the Flutter framework itself.\n'
),
);
expect(
flutterError.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'This usually indicates an error in the Flutter framework itself.'
);
});
}
class TestReentrantPaintingErrorRenderBox extends RenderBox {
@override
void paint(PaintingContext context, Offset offset) {
// Cause a reentrant painting bug that would show up as a stack overflow if
// it was not for debugging checks in RenderObject.
context.paintChild(this, offset);
}
@override
void performLayout() {
size = const Size(100, 100);
}
}
...@@ -63,6 +63,107 @@ void main() { ...@@ -63,6 +63,107 @@ void main() {
expect(node.getSemanticsData().tags, tags); expect(node.getSemanticsData().tags, tags);
}); });
test('mutate existing semantic node list errors', () {
final SemanticsNode node = SemanticsNode()
..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
final SemanticsConfiguration config = SemanticsConfiguration()
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = true;
final List<SemanticsNode> children = <SemanticsNode>[
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0)
];
node.updateWith(
config: config,
childrenInInversePaintOrder: children
);
children.add( SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(42.0, 42.0, 10.0, 10.0)
);
{
FlutterError error;
try {
node.updateWith(
config: config,
childrenInInversePaintOrder: children
);
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.toString(), equalsIgnoringHashCodes(
'Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.\n'
'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.\n'
'Error details:\n'
'The list\'s length has changed from 1 to 2.'
));
expect(
error.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.'
);
}
{
FlutterError error;
final List<SemanticsNode> modifiedChildren = <SemanticsNode>[
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0),
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(10.0, 10.0, 20.0, 20.0)
];
node.updateWith(
config: config,
childrenInInversePaintOrder: modifiedChildren,
);
try {
modifiedChildren[0] = SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(0.0, 0.0, 20.0, 20.0);
modifiedChildren[1] = SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(40.0, 14.0, 20.0, 20.0);
node.updateWith(
config: config,
childrenInInversePaintOrder: modifiedChildren
);
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' Failed to replace child semantics nodes because the list of\n'
' `SemanticsNode`s was mutated.\n'
' Instead of mutating the existing list, create a new list\n'
' containing the desired `SemanticsNode`s.\n'
' Error details:\n'
' Child node at position 0 was replaced:\n'
' Previous child: SemanticsNode#6(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(0.0, 0.0, 20.0, 20.0))\n'
' New child: SemanticsNode#4(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(5.0, 5.0, 10.0, 10.0))\n'
'\n'
' Child node at position 1 was replaced:\n'
' Previous child: SemanticsNode#7(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(40.0, 14.0, 20.0, 20.0))\n'
' New child: SemanticsNode#5(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(10.0, 10.0, 20.0, 20.0))\n'
));
expect(
error.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.'
);
// Two previous children and two new children.
expect(error.diagnostics.where((DiagnosticsNode node) => node.value is SemanticsNode).length, 4);
}
});
test('after markNeedsSemanticsUpdate() all render objects between two semantic boundaries are asked for annotations', () { test('after markNeedsSemanticsUpdate() all render objects between two semantic boundaries are asked for annotations', () {
renderer.pipelineOwner.ensureSemantics(); renderer.pipelineOwner.ensureSemantics();
......
...@@ -76,7 +76,18 @@ void main() { ...@@ -76,7 +76,18 @@ void main() {
), ),
], ],
)); ));
expect(tester.takeException(), isFlutterError); final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Multiple widgets used the same GlobalKey.\n'
'The key [GlobalKey#00000 problematic] was used by multiple widgets. The parents of those widgets were:\n'
'- Container-[<1>]\n'
'- Container-[<2>]\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
),
);
}); });
testWidgets('GlobalKey duplication 2 - splitting and changing type', (WidgetTester tester) async { testWidgets('GlobalKey duplication 2 - splitting and changing type', (WidgetTester tester) async {
...@@ -111,7 +122,18 @@ void main() { ...@@ -111,7 +122,18 @@ void main() {
], ],
)); ));
expect(tester.takeException(), isFlutterError); final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Multiple widgets used the same GlobalKey.\n'
'The key [GlobalKey#00000 problematic] was used by multiple widgets. The parents of those widgets were:\n'
'- Container-[<1>]\n'
'- Container-[<2>]\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
)
);
}); });
testWidgets('GlobalKey duplication 3 - splitting and changing type', (WidgetTester tester) async { testWidgets('GlobalKey duplication 3 - splitting and changing type', (WidgetTester tester) async {
...@@ -129,7 +151,18 @@ void main() { ...@@ -129,7 +151,18 @@ void main() {
Placeholder(key: key), Placeholder(key: key),
], ],
)); ));
expect(tester.takeException(), isFlutterError); final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Multiple widgets used the same GlobalKey.\n'
'The key [GlobalKey#00000 problematic] was used by 2 widgets:\n'
' SizedBox-[GlobalKey#00000 problematic]\n'
' Placeholder-[GlobalKey#00000 problematic]\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
)
);
}); });
testWidgets('GlobalKey duplication 4 - splitting and half changing type', (WidgetTester tester) async { testWidgets('GlobalKey duplication 4 - splitting and half changing type', (WidgetTester tester) async {
...@@ -147,7 +180,18 @@ void main() { ...@@ -147,7 +180,18 @@ void main() {
Placeholder(key: key), Placeholder(key: key),
], ],
)); ));
expect(tester.takeException(), isFlutterError); final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Multiple widgets used the same GlobalKey.\n'
'The key [GlobalKey#00000 problematic] was used by 2 widgets:\n'
' Container-[GlobalKey#00000 problematic]\n'
' Placeholder-[GlobalKey#00000 problematic]\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
)
);
}); });
testWidgets('GlobalKey duplication 5 - splitting and half changing type', (WidgetTester tester) async { testWidgets('GlobalKey duplication 5 - splitting and half changing type', (WidgetTester tester) async {
...@@ -314,7 +358,16 @@ void main() { ...@@ -314,7 +358,16 @@ void main() {
Container(key: key3), Container(key: key3),
], ],
)); ));
expect(tester.takeException(), isFlutterError); final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Duplicate keys found.\n'
'If multiple keyed nodes exist as children of another node, they must have unique keys.\n'
'Stack(alignment: AlignmentDirectional.topStart, textDirection: ltr, fit: loose, overflow: clip) has multiple children with key [GlobalKey#00000 problematic].'
),
);
}); });
testWidgets('GlobalKey duplication 13 - all kinds of badness at once', (WidgetTester tester) async { testWidgets('GlobalKey duplication 13 - all kinds of badness at once', (WidgetTester tester) async {
......
...@@ -266,7 +266,20 @@ void main() { ...@@ -266,7 +266,20 @@ void main() {
], ],
), ),
); );
expect(tester.takeException(), isFlutterError); dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Incorrect use of ParentDataWidget.\n'
'Positioned widgets must be placed directly inside Stack widgets.\n'
'Positioned(no depth, left: 7.0, top: 6.0, dirty) has a Stack ancestor, but there are other widgets between them:\n'
'- Positioned(top: 5.0, bottom: 8.0) (this is a different Positioned than the one with the problem)\n'
'These widgets cannot come between a Positioned and its Stack.\n'
'The ownership chain for the parent of the offending Positioned was:\n'
' Positioned ← Stack ← [root]'
),
);
await tester.pumpWidget(Stack(textDirection: TextDirection.ltr)); await tester.pumpWidget(Stack(textDirection: TextDirection.ltr));
...@@ -285,7 +298,18 @@ void main() { ...@@ -285,7 +298,18 @@ void main() {
), ),
), ),
); );
expect(tester.takeException(), isFlutterError); exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Incorrect use of ParentDataWidget.\n'
'Positioned widgets must be placed inside Stack widgets.\n'
'Positioned(no depth, left: 7.0, top: 6.0, dirty) has no Stack ancestor at all.\n'
'The ownership chain for the parent of the offending Positioned was:\n'
' Row ← Container ← [root]'
)
);
await tester.pumpWidget( await tester.pumpWidget(
Stack(textDirection: TextDirection.ltr) Stack(textDirection: TextDirection.ltr)
...@@ -356,7 +380,7 @@ void main() { ...@@ -356,7 +380,7 @@ void main() {
await tester.pumpWidget(Row( await tester.pumpWidget(Row(
children: <Widget>[ children: <Widget>[
Stack( Stack(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: Container(), child: Container(),
...@@ -366,6 +390,19 @@ void main() { ...@@ -366,6 +390,19 @@ void main() {
], ],
)); ));
expect(tester.takeException(), isFlutterError); final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Incorrect use of ParentDataWidget.\n'
'Expanded widgets must be placed directly inside Flex widgets.\n'
'Expanded(no depth, flex: 1, dirty) has a Flex ancestor, but there are other widgets between them:\n'
'- Stack(alignment: AlignmentDirectional.topStart, textDirection: ltr, fit: loose, overflow: clip)\n'
'These widgets cannot come between a Expanded and its Flex.\n'
'The ownership chain for the parent of the offending Expanded was:\n'
' Stack ← Row ← [root]'
),
);
}); });
} }
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