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.'),
describeForError(
'The following RenderObject was already being painted when it was ' 'The following RenderObject was already being painted when it was '
'painted again:\n' 'painted again'
' ${toStringShallow(joiner: "\n ")}\n' ),
ErrorDescription(
'Since this typically indicates an infinite recursion, it is ' 'Since this typically indicates an infinite recursion, it is '
'disallowed.' '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>[
ErrorSummary(
'Tried to paint a RenderObject before its compositing bits were ' 'Tried to paint a RenderObject before its compositing bits were '
'updated.\n' 'updated.'
),
describeForError(
'The following RenderObject was marked as having dirty compositing ' 'The following RenderObject was marked as having dirty compositing '
'bits at the time that it was painted:\n' 'bits at the time that it was painted',
' ${toStringShallow(joiner: "\n ")}\n' ),
ErrorDescription(
'A RenderObject that still has dirty compositing bits cannot be ' 'A RenderObject that still has dirty compositing bits cannot be '
'painted because this indicates that the tree has not yet been ' 'painted because this indicates that the tree has not yet been '
'properly configured for creating the layer tree.\n' 'properly configured for creating the layer tree.'
),
ErrorHint(
'This usually indicates an error in the Flutter framework itself.' '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>[
ErrorSummary(
'A $runtimeType expected a child of type $ChildType but received a ' 'A $runtimeType expected a child of type $ChildType but received a '
'child of type ${child.runtimeType}.\n' 'child of type ${child.runtimeType}.'
),
ErrorDescription(
'RenderObjects expect specific types of children because they ' 'RenderObjects expect specific types of children because they '
'coordinate with their children during layout and paint. For ' 'coordinate with their children during layout and paint. For '
'example, a RenderSliver cannot be the child of a RenderBox because ' 'example, a RenderSliver cannot be the child of a RenderBox because '
'a RenderSliver does not understand the RenderBox layout protocol.\n' 'a RenderSliver does not understand the RenderBox layout protocol.',
'\n' ),
'The $runtimeType that expected a $ChildType child was created by:\n' ErrorSpacer(),
' $debugCreator\n' DiagnosticsProperty<dynamic>(
'\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 ' 'The ${child.runtimeType} that did not match the expected child type '
'was created by:\n' 'was created by',
' ${child.debugCreator}\n' 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>[
ErrorSummary(
'A $runtimeType expected a child of type $ChildType but received a ' 'A $runtimeType expected a child of type $ChildType but received a '
'child of type ${child.runtimeType}.\n' 'child of type ${child.runtimeType}.'
),
ErrorDescription(
'RenderObjects expect specific types of children because they ' 'RenderObjects expect specific types of children because they '
'coordinate with their children during layout and paint. For ' 'coordinate with their children during layout and paint. For '
'example, a RenderSliver cannot be the child of a RenderBox because ' 'example, a RenderSliver cannot be the child of a RenderBox because '
'a RenderSliver does not understand the RenderBox layout protocol.\n' 'a RenderSliver does not understand the RenderBox layout protocol.'
'\n' ),
'The $runtimeType that expected a $ChildType child was created by:\n' ErrorSpacer(),
' $debugCreator\n' DiagnosticsProperty<dynamic>(
'\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 ' 'The ${child.runtimeType} that did not match the expected child type '
'was created by:\n' 'was created by',
' ${child.debugCreator}\n' 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)
...@@ -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