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>
void dispose() {
assert(() {
if (_ticker == null) {
throw FlutterError(
'AnimationController.dispose() called more than once.\n'
'A given $runtimeType cannot be disposed more than once.\n'
'The following $runtimeType object was disposed multiple times:\n'
' $this'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('AnimationController.dispose() called more than once.'),
ErrorDescription('A given $runtimeType cannot be disposed more than once.\n'),
DiagnosticsProperty<AnimationController>(
'The following $runtimeType object was disposed multiple times',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
]);
}
return true;
}());
......
......@@ -176,6 +176,19 @@ class ErrorHint extends _ErrorDiagnostic {
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.
///
/// See [FlutterError.onError].
......@@ -407,7 +420,7 @@ class FlutterErrorDetails extends Diagnosticable {
}
}
if (ourFault) {
properties.add(DiagnosticsNode.message(''));
properties.add(ErrorSpacer());
properties.add(ErrorHint(
'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 '
......@@ -418,11 +431,11 @@ class FlutterErrorDetails extends Diagnosticable {
}
}
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));
}
if (informationCollector != null) {
properties.add(DiagnosticsNode.message(''));
properties.add(ErrorSpacer());
informationCollector().forEach(properties.add);
}
}
......
......@@ -690,7 +690,7 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
throw FlutterError(
'Steppers must not be nested. The material specification advises '
'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;
}());
......
This diff is collapsed.
......@@ -1194,7 +1194,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
// displaying the truncated children is really useful for command line
// users. Inspector users can see the full tree by clicking on the
// 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
void _paintWithContext(PaintingContext context, Offset offset) {
assert(() {
if (_debugDoingThisPaint) {
throw FlutterError(
'Tried to paint a RenderObject reentrantly.\n'
'The following RenderObject was already being painted when it was '
'painted again:\n'
' ${toStringShallow(joiner: "\n ")}\n'
'Since this typically indicates an infinite recursion, it is '
'disallowed.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Tried to paint a RenderObject reentrantly.'),
describeForError(
'The following RenderObject was already being painted when it was '
'painted again'
),
ErrorDescription(
'Since this typically indicates an infinite recursion, it is '
'disallowed.'
)
]);
}
return true;
}());
......@@ -2052,17 +2055,24 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
return;
assert(() {
if (_needsCompositingBitsUpdate) {
throw FlutterError(
'Tried to paint a RenderObject before its compositing bits were '
'updated.\n'
'The following RenderObject was marked as having dirty compositing '
'bits at the time that it was painted:\n'
' ${toStringShallow(joiner: "\n ")}\n'
'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.\n'
'This usually indicates an error in the Flutter framework itself.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'Tried to paint a RenderObject before its compositing bits were '
'updated.'
),
describeForError(
'The following RenderObject was marked as having dirty compositing '
'bits at the time that it was painted',
),
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;
}());
......@@ -2720,21 +2730,31 @@ mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject
bool debugValidateChild(RenderObject child) {
assert(() {
if (child is! ChildType) {
throw FlutterError(
'A $runtimeType expected a child of type $ChildType but received a '
'child of type ${child.runtimeType}.\n'
'RenderObjects expect specific types of children because they '
'coordinate with their children during layout and paint. For '
'example, a RenderSliver cannot be the child of a RenderBox because '
'a RenderSliver does not understand the RenderBox layout protocol.\n'
'\n'
'The $runtimeType that expected a $ChildType child was created by:\n'
' $debugCreator\n'
'\n'
'The ${child.runtimeType} that did not match the expected child type '
'was created by:\n'
' ${child.debugCreator}\n'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'A $runtimeType expected a child of type $ChildType but received a '
'child of type ${child.runtimeType}.'
),
ErrorDescription(
'RenderObjects expect specific types of children because they '
'coordinate with their children during layout and paint. For '
'example, a RenderSliver cannot be the child of a RenderBox because '
'a RenderSliver does not understand the RenderBox layout protocol.',
),
ErrorSpacer(),
DiagnosticsProperty<dynamic>(
'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;
}());
......@@ -2849,21 +2869,31 @@ mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType
bool debugValidateChild(RenderObject child) {
assert(() {
if (child is! ChildType) {
throw FlutterError(
'A $runtimeType expected a child of type $ChildType but received a '
'child of type ${child.runtimeType}.\n'
'RenderObjects expect specific types of children because they '
'coordinate with their children during layout and paint. For '
'example, a RenderSliver cannot be the child of a RenderBox because '
'a RenderSliver does not understand the RenderBox layout protocol.\n'
'\n'
'The $runtimeType that expected a $ChildType child was created by:\n'
' $debugCreator\n'
'\n'
'The ${child.runtimeType} that did not match the expected child type '
'was created by:\n'
' ${child.debugCreator}\n'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'A $runtimeType expected a child of type $ChildType but received a '
'child of type ${child.runtimeType}.'
),
ErrorDescription(
'RenderObjects expect specific types of children because they '
'coordinate with their children during layout and paint. For '
'example, a RenderSliver cannot be the child of a RenderBox because '
'a RenderSliver does not understand the RenderBox layout protocol.'
),
ErrorSpacer(),
DiagnosticsProperty<dynamic>(
'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;
}());
......
......@@ -1282,30 +1282,31 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
assert(!newChildren.any((SemanticsNode child) => child == this));
assert(() {
if (identical(newChildren, _children)) {
final StringBuffer mutationErrors = StringBuffer();
final List<DiagnosticsNode> mutationErrors = <DiagnosticsNode>[];
if (newChildren.length != _debugPreviousSnapshot.length) {
mutationErrors.writeln(
mutationErrors.add(ErrorDescription(
'The list\'s length has changed from ${_debugPreviousSnapshot.length} '
'to ${newChildren.length}.'
);
));
} else {
for (int i = 0; i < newChildren.length; i++) {
if (!identical(newChildren[i], _debugPreviousSnapshot[i])) {
mutationErrors.writeln(
'Child node at position $i was replaced:\n'
'Previous child: ${newChildren[i]}\n'
'New child: ${_debugPreviousSnapshot[i]}\n'
);
if (mutationErrors.isNotEmpty) {
mutationErrors.add(ErrorSpacer());
}
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) {
throw FlutterError(
'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'
'$mutationErrors'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.'),
ErrorHint('Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.'),
ErrorDescription('Error details:'),
...mutationErrors
]);
}
}
assert(!newChildren.any((SemanticsNode node) => node.isMergedIntoParent) || isPartOfNodeMerging);
......
......@@ -431,7 +431,7 @@ class ClampingScrollPhysics extends ScrollPhysics {
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n'
' $position'
);
}
return true;
......
......@@ -348,7 +348,29 @@ void main() {
expect(controller.repeat, throwsFlutterError);
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', () {
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -478,6 +479,72 @@ void main() {
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
testWidgets('Stepper icons size test', (WidgetTester tester) async {
await tester.pumpWidget(
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/src/binding.dart' show TestWidgetsFlutterBinding;
......@@ -35,6 +36,51 @@ void main() {
renderObject.markNeedsSemanticsUpdate();
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 {
......@@ -62,3 +108,22 @@ class TestRenderObject extends RenderObject {
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() {
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', () {
renderer.pipelineOwner.ensureSemantics();
......
......@@ -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 {
......@@ -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 {
......@@ -129,7 +151,18 @@ void main() {
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 {
......@@ -147,7 +180,18 @@ void main() {
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 {
......@@ -314,7 +358,16 @@ void main() {
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 {
......
......@@ -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));
......@@ -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(
Stack(textDirection: TextDirection.ltr)
......@@ -356,7 +380,7 @@ void main() {
await tester.pumpWidget(Row(
children: <Widget>[
Stack(
textDirection: TextDirection.ltr,
textDirection: TextDirection.ltr,
children: <Widget>[
Expanded(
child: Container(),
......@@ -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