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;
}()); }());
......
This diff is collapsed.
...@@ -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);
......
...@@ -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(
......
...@@ -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