Rendering errors with root causes in the widget layer should have a reference to the widget (#32511)
This commit is contained in:
parent
97b2c98642
commit
a76e39f984
@ -0,0 +1,26 @@
|
||||
<<skip until matching line>>
|
||||
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
|
||||
The following assertion was thrown building
|
||||
RawGestureDetector-\[LabeledGlobalKey<RawGestureDetectorState>#.+\]\(state:
|
||||
RawGestureDetectorState#.+\(gestures: <none>, behavior: opaque\)\):
|
||||
'package:flutter\/src\/painting\/basic_types\.dart': Failed assertion: line 223 pos 10: 'textDirection
|
||||
!= null': is not true\.
|
||||
|
||||
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 and fix the underlying cause\.
|
||||
In either case, please report this assertion by filing a bug on GitHub:
|
||||
https:\/\/github\.com\/flutter\/flutter\/issues\/new\?template=BUG\.md
|
||||
|
||||
User-created ancestor of the error-causing widget was:
|
||||
CustomScrollView
|
||||
file:\/\/\/.+print_user_created_ancestor_test\.dart:[0-9]+:7
|
||||
|
||||
When the exception was thrown, this was the stack:
|
||||
<<skip until matching line>>
|
||||
\(elided [0-9]+ frames from .+\)
|
||||
════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||
.*..:.. \+0 -1: Rendering Error *
|
||||
Test failed\. See exception logs above\.
|
||||
The test description was: Rendering Error
|
||||
*
|
||||
.*..:.. \+0 -1: Some tests failed\. *
|
@ -0,0 +1,25 @@
|
||||
<<skip until matching line>>
|
||||
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
|
||||
The following assertion was thrown building
|
||||
RawGestureDetector-\[LabeledGlobalKey<RawGestureDetectorState>#.+\]\(state:
|
||||
RawGestureDetectorState#.+\(gestures: <none>, behavior: opaque\)\):
|
||||
'package:flutter\/src\/painting\/basic_types\.dart': Failed assertion: line 223 pos 10: 'textDirection
|
||||
!= null': is not true\.
|
||||
|
||||
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 and fix the underlying cause\.
|
||||
In either case, please report this assertion by filing a bug on GitHub:
|
||||
https:\/\/github\.com\/flutter\/flutter\/issues\/new\?template=BUG\.md
|
||||
|
||||
Widget creation tracking is currently disabled. Enabling it enables improved error messages\. It can
|
||||
be enabled by passing `--track-widget-creation` to `flutter run` or `flutter test`\.
|
||||
|
||||
When the exception was thrown, this was the stack:
|
||||
<<skip until matching line>>
|
||||
\(elided [0-9]+ frames from .+\)
|
||||
════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||
.*..:.. \+0 -1: Rendering Error *
|
||||
Test failed\. See exception logs above\.
|
||||
The test description was: Rendering Error
|
||||
*
|
||||
.*..:.. \+0 -1: Some tests failed\. *
|
@ -0,0 +1,19 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Rendering Error', (WidgetTester tester) async {
|
||||
// this should fail
|
||||
await tester.pumpWidget(
|
||||
CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverToBoxAdapter(child: Container()),
|
||||
]
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Rendering Error', (WidgetTester tester) async {
|
||||
// this should fail
|
||||
await tester.pumpWidget(
|
||||
CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverToBoxAdapter(child: Container()),
|
||||
]
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
@ -12,6 +12,9 @@ import 'print.dart';
|
||||
/// Signature for [FlutterError.onError] handler.
|
||||
typedef FlutterExceptionHandler = void Function(FlutterErrorDetails details);
|
||||
|
||||
/// Signature for [DiagnosticPropertiesBuilder] transformer.
|
||||
typedef DiagnosticPropertiesTransformer = Iterable<DiagnosticsNode> Function(Iterable<DiagnosticsNode> properties);
|
||||
|
||||
/// Signature for [FlutterErrorDetails.informationCollector] callback
|
||||
/// and other callbacks that collect information describing an error.
|
||||
typedef InformationCollector = Iterable<DiagnosticsNode> Function();
|
||||
@ -212,6 +215,20 @@ class FlutterErrorDetails extends Diagnosticable {
|
||||
this.silent = false,
|
||||
});
|
||||
|
||||
/// Transformers to transform [DiagnosticsNode] in [DiagnosticPropertiesBuilder]
|
||||
/// into a more descriptive form.
|
||||
///
|
||||
/// There are layers that attach certain [DiagnosticsNode] into
|
||||
/// [FlutterErrorDetails] that require knowledge from other layers to parse.
|
||||
/// To correctly interpret those [DiagnosticsNode], register transformers in
|
||||
/// the layers that possess the knowledge.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [WidgetsBinding.initInstances], which registers its transformer.
|
||||
static final List<DiagnosticPropertiesTransformer> propertiesTransformers =
|
||||
<DiagnosticPropertiesTransformer>[];
|
||||
|
||||
/// The exception. Often this will be an [AssertionError], maybe specifically
|
||||
/// a [FlutterError]. However, this could be any value at all.
|
||||
final dynamic exception;
|
||||
@ -449,6 +466,15 @@ class FlutterErrorDetails extends Diagnosticable {
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.debug}) {
|
||||
return toDiagnosticsNode(style: DiagnosticsTreeStyle.error).toStringDeep(minLevel: minLevel);
|
||||
}
|
||||
|
||||
@override
|
||||
DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style }) {
|
||||
return _FlutterErrorDetailsNode(
|
||||
name: name,
|
||||
value: this,
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Error class used to report Flutter-specific assertion failures and
|
||||
@ -777,3 +803,28 @@ class DiagnosticsStackTrace extends DiagnosticsBlock {
|
||||
return DiagnosticsNode.message(frame, allowWrap: false);
|
||||
}
|
||||
}
|
||||
|
||||
class _FlutterErrorDetailsNode extends DiagnosticableNode<FlutterErrorDetails> {
|
||||
_FlutterErrorDetailsNode({
|
||||
String name,
|
||||
@required FlutterErrorDetails value,
|
||||
@required DiagnosticsTreeStyle style,
|
||||
}) : super(
|
||||
name: name,
|
||||
value: value,
|
||||
style: style,
|
||||
);
|
||||
|
||||
@override
|
||||
DiagnosticPropertiesBuilder get builder {
|
||||
final DiagnosticPropertiesBuilder builder = super.builder;
|
||||
if (builder == null){
|
||||
return null;
|
||||
}
|
||||
Iterable<DiagnosticsNode> properties = builder.properties;
|
||||
for (DiagnosticPropertiesTransformer transformer in FlutterErrorDetails.propertiesTransformers) {
|
||||
properties = transformer(properties);
|
||||
}
|
||||
return DiagnosticPropertiesBuilder.fromProperties(properties.toList());
|
||||
}
|
||||
}
|
||||
|
@ -2774,7 +2774,10 @@ class DiagnosticableNode<T extends Diagnosticable> extends DiagnosticsNode {
|
||||
|
||||
DiagnosticPropertiesBuilder _cachedBuilder;
|
||||
|
||||
DiagnosticPropertiesBuilder get _builder {
|
||||
/// Retrieve the [DiagnosticPropertiesBuilder] of current node.
|
||||
///
|
||||
/// It will cache the result to prevent duplicate operation.
|
||||
DiagnosticPropertiesBuilder get builder {
|
||||
if (kReleaseMode)
|
||||
return null;
|
||||
if (_cachedBuilder == null) {
|
||||
@ -2786,14 +2789,14 @@ class DiagnosticableNode<T extends Diagnosticable> extends DiagnosticsNode {
|
||||
|
||||
@override
|
||||
DiagnosticsTreeStyle get style {
|
||||
return kReleaseMode ? DiagnosticsTreeStyle.none : super.style ?? _builder.defaultDiagnosticsTreeStyle;
|
||||
return kReleaseMode ? DiagnosticsTreeStyle.none : super.style ?? builder.defaultDiagnosticsTreeStyle;
|
||||
}
|
||||
|
||||
@override
|
||||
String get emptyBodyDescription => kReleaseMode ? '' : _builder.emptyBodyDescription;
|
||||
String get emptyBodyDescription => kReleaseMode ? '' : builder.emptyBodyDescription;
|
||||
|
||||
@override
|
||||
List<DiagnosticsNode> getProperties() => kReleaseMode ? const <DiagnosticsNode>[] : _builder.properties;
|
||||
List<DiagnosticsNode> getProperties() => kReleaseMode ? const <DiagnosticsNode>[] : builder.properties;
|
||||
|
||||
@override
|
||||
List<DiagnosticsNode> getChildren() {
|
||||
@ -2875,6 +2878,13 @@ String describeEnum(Object enumEntry) {
|
||||
/// Builder to accumulate properties and configuration used to assemble a
|
||||
/// [DiagnosticsNode] from a [Diagnosticable] object.
|
||||
class DiagnosticPropertiesBuilder {
|
||||
/// Creates a [DiagnosticPropertiesBuilder] with [properties] initialize to
|
||||
/// an empty array.
|
||||
DiagnosticPropertiesBuilder() : properties = <DiagnosticsNode>[];
|
||||
|
||||
/// Creates a [DiagnosticPropertiesBuilder] with a given [properties].
|
||||
DiagnosticPropertiesBuilder.fromProperties(this.properties);
|
||||
|
||||
/// Add a property to the list of properties.
|
||||
void add(DiagnosticsNode property) {
|
||||
if (!kReleaseMode) {
|
||||
@ -2883,7 +2893,7 @@ class DiagnosticPropertiesBuilder {
|
||||
}
|
||||
|
||||
/// List of properties accumulated so far.
|
||||
final List<DiagnosticsNode> properties = <DiagnosticsNode>[];
|
||||
final List<DiagnosticsNode> properties;
|
||||
|
||||
/// Default style to use for the [DiagnosticsNode] if no style is specified.
|
||||
DiagnosticsTreeStyle defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.sparse;
|
||||
|
@ -6,6 +6,7 @@ import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'object.dart';
|
||||
import 'stack.dart';
|
||||
@ -248,6 +249,8 @@ mixin DebugOverflowIndicatorMixin on RenderObject {
|
||||
context: ErrorDescription('during layout'),
|
||||
renderObject: this,
|
||||
informationCollector: () sync* {
|
||||
if (debugCreator != null)
|
||||
yield DiagnosticsDebugCreator(debugCreator);
|
||||
yield* overflowHints;
|
||||
yield describeForError('The specific $runtimeType in question is');
|
||||
// TODO(jacobr): this line is ascii art that it would be nice to
|
||||
|
@ -1189,6 +1189,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
context: ErrorDescription('during $method()'),
|
||||
renderObject: this,
|
||||
informationCollector: () sync* {
|
||||
if (debugCreator != null)
|
||||
yield DiagnosticsDebugCreator(debugCreator);
|
||||
yield describeForError('The following RenderObject was being processed when the exception was fired');
|
||||
// TODO(jacobr): this error message has a code smell. Consider whether
|
||||
// displaying the truncated children is really useful for command line
|
||||
@ -3675,3 +3677,20 @@ class _SemanticsGeometry {
|
||||
bool get markAsHidden => _markAsHidden;
|
||||
bool _markAsHidden = false;
|
||||
}
|
||||
|
||||
/// A class that creates [DiagnosticsNode] by wrapping [RenderObject.debugCreator].
|
||||
///
|
||||
/// Attach a [DiagnosticsDebugCreator] into [FlutterErrorDetails.informationCollector]
|
||||
/// when a [RenderObject.debugCreator] is available. This will lead to improved
|
||||
/// error message.
|
||||
class DiagnosticsDebugCreator extends DiagnosticsProperty<Object> {
|
||||
/// Create a [DiagnosticsProperty] with its [value] initialized to input
|
||||
/// [RenderObject.debugCreator].
|
||||
DiagnosticsDebugCreator(Object value):
|
||||
assert(value != null),
|
||||
super(
|
||||
'debugCreator',
|
||||
value,
|
||||
level: DiagnosticLevel.hidden
|
||||
);
|
||||
}
|
||||
|
@ -256,6 +256,7 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
|
||||
window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
|
||||
SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
|
||||
SystemChannels.system.setMessageHandler(_handleSystemMessage);
|
||||
FlutterErrorDetails.propertiesTransformers.add(transformDebugCreator);
|
||||
}
|
||||
|
||||
/// The current [WidgetsBinding], if one has been created.
|
||||
|
@ -2344,6 +2344,7 @@ class BuildOwner {
|
||||
e,
|
||||
stack,
|
||||
informationCollector: () sync* {
|
||||
yield DiagnosticsDebugCreator(DebugCreator(_dirtyElements[index]));
|
||||
yield _dirtyElements[index].describeElement('The element being rebuilt at the time was index $index of $dirtyCount');
|
||||
},
|
||||
);
|
||||
@ -2771,7 +2772,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
|
||||
return StringProperty(name, debugGetCreatorChain(10));
|
||||
}
|
||||
|
||||
|
||||
// This is used to verify that Element objects move through life in an
|
||||
// orderly fashion.
|
||||
_ElementLifecycle _debugLifecycleState = _ElementLifecycle.initial;
|
||||
@ -3933,7 +3933,16 @@ abstract class ComponentElement extends Element {
|
||||
built = build();
|
||||
debugWidgetBuilderValue(widget, built);
|
||||
} catch (e, stack) {
|
||||
built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
|
||||
built = ErrorWidget.builder(
|
||||
_debugReportException(
|
||||
ErrorDescription('building $this'),
|
||||
e,
|
||||
stack,
|
||||
informationCollector: () sync* {
|
||||
yield DiagnosticsDebugCreator(DebugCreator(this));
|
||||
},
|
||||
)
|
||||
);
|
||||
} finally {
|
||||
// We delay marking the element as clean until after calling build() so
|
||||
// that attempts to markNeedsBuild() during build() will be ignored.
|
||||
@ -3944,7 +3953,16 @@ abstract class ComponentElement extends Element {
|
||||
_child = updateChild(_child, built, slot);
|
||||
assert(_child != null);
|
||||
} catch (e, stack) {
|
||||
built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
|
||||
built = ErrorWidget.builder(
|
||||
_debugReportException(
|
||||
ErrorDescription('building $this'),
|
||||
e,
|
||||
stack,
|
||||
informationCollector: () sync* {
|
||||
yield DiagnosticsDebugCreator(DebugCreator(this));
|
||||
},
|
||||
)
|
||||
);
|
||||
_child = updateChild(null, built, slot);
|
||||
}
|
||||
|
||||
@ -4738,7 +4756,7 @@ abstract class RenderObjectElement extends Element {
|
||||
|
||||
void _debugUpdateRenderObjectOwner() {
|
||||
assert(() {
|
||||
_renderObject.debugCreator = _DebugCreator(this);
|
||||
_renderObject.debugCreator = DebugCreator(this);
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
@ -5219,9 +5237,17 @@ class MultiChildRenderObjectElement extends RenderObjectElement {
|
||||
}
|
||||
}
|
||||
|
||||
class _DebugCreator {
|
||||
_DebugCreator(this.element);
|
||||
final RenderObjectElement element;
|
||||
/// A wrapper class for the [Element] that is the creator of a [RenderObject].
|
||||
///
|
||||
/// Attaching a [DebugCreator] attach the [RenderObject] will lead to better error
|
||||
/// message.
|
||||
class DebugCreator {
|
||||
/// Create a [DebugCreator] instance with input [Element].
|
||||
DebugCreator(this.element);
|
||||
|
||||
/// The creator of the [RenderObject].
|
||||
final Element element;
|
||||
|
||||
@override
|
||||
String toString() => element.debugGetCreatorChain(12);
|
||||
}
|
||||
|
@ -113,14 +113,32 @@ class _LayoutBuilderElement extends RenderObjectElement {
|
||||
built = widget.builder(this, constraints);
|
||||
debugWidgetBuilderValue(widget, built);
|
||||
} catch (e, stack) {
|
||||
built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $widget'), e, stack));
|
||||
built = ErrorWidget.builder(
|
||||
_debugReportException(
|
||||
ErrorDescription('building $widget'),
|
||||
e,
|
||||
stack,
|
||||
informationCollector: () sync* {
|
||||
yield DiagnosticsDebugCreator(DebugCreator(this));
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
try {
|
||||
_child = updateChild(_child, built, null);
|
||||
assert(_child != null);
|
||||
} catch (e, stack) {
|
||||
built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $widget'), e, stack));
|
||||
built = ErrorWidget.builder(
|
||||
_debugReportException(
|
||||
ErrorDescription('building $widget'),
|
||||
e,
|
||||
stack,
|
||||
informationCollector: () sync* {
|
||||
yield DiagnosticsDebugCreator(DebugCreator(this));
|
||||
},
|
||||
)
|
||||
);
|
||||
_child = updateChild(null, built, slot);
|
||||
}
|
||||
});
|
||||
@ -228,13 +246,15 @@ class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin<Ren
|
||||
FlutterErrorDetails _debugReportException(
|
||||
DiagnosticsNode context,
|
||||
dynamic exception,
|
||||
StackTrace stack,
|
||||
) {
|
||||
StackTrace stack, {
|
||||
InformationCollector informationCollector,
|
||||
}) {
|
||||
final FlutterErrorDetails details = FlutterErrorDetails(
|
||||
exception: exception,
|
||||
stack: stack,
|
||||
library: 'widgets library',
|
||||
context: context,
|
||||
informationCollector: informationCollector,
|
||||
);
|
||||
FlutterError.reportError(details);
|
||||
return details;
|
||||
|
@ -1397,11 +1397,17 @@ mixin WidgetInspectorService {
|
||||
}
|
||||
|
||||
bool _isLocalCreationLocation(_Location location) {
|
||||
if (_pubRootDirectories == null || location == null || location.file == null) {
|
||||
if (location == null || location.file == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final String file = Uri.parse(location.file).path;
|
||||
|
||||
// By default check whether the creation location was within package:flutter.
|
||||
if (_pubRootDirectories == null) {
|
||||
// TODO(chunhtai): Make it more robust once
|
||||
// https://github.com/flutter/flutter/issues/32660 is fixed.
|
||||
return !file.contains('packages/flutter/');
|
||||
}
|
||||
for (String directory in _pubRootDirectories) {
|
||||
if (file.startsWith(directory)) {
|
||||
return true;
|
||||
@ -2705,6 +2711,105 @@ class _Location {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isDebugCreator(DiagnosticsNode node) => node is DiagnosticsDebugCreator;
|
||||
|
||||
/// Transformer to parse and gather information about [DiagnosticsDebugCreator].
|
||||
///
|
||||
/// This function will be registered to [FlutterErrorDetails.propertiesTransformers]
|
||||
/// in [WidgetsBinding.initInstances].
|
||||
Iterable<DiagnosticsNode> transformDebugCreator(Iterable<DiagnosticsNode> properties) sync* {
|
||||
final List<DiagnosticsNode> pending = <DiagnosticsNode>[];
|
||||
bool foundStackTrace = false;
|
||||
for (DiagnosticsNode node in properties) {
|
||||
if (!foundStackTrace && node is DiagnosticsStackTrace)
|
||||
foundStackTrace = true;
|
||||
if (_isDebugCreator(node)) {
|
||||
yield* _parseDiagnosticsNode(node);
|
||||
} else {
|
||||
if (foundStackTrace) {
|
||||
pending.add(node);
|
||||
} else {
|
||||
yield node;
|
||||
}
|
||||
}
|
||||
}
|
||||
yield* pending;
|
||||
}
|
||||
|
||||
/// Transform the input [DiagnosticsNode].
|
||||
///
|
||||
/// Return null if input [DiagnosticsNode] is not applicable.
|
||||
Iterable<DiagnosticsNode> _parseDiagnosticsNode(DiagnosticsNode node) {
|
||||
if (!_isDebugCreator(node))
|
||||
return null;
|
||||
final DebugCreator debugCreator = node.value;
|
||||
final Element element = debugCreator.element;
|
||||
return _describeRelevantUserCode(element);
|
||||
}
|
||||
|
||||
Iterable<DiagnosticsNode> _describeRelevantUserCode(Element element) {
|
||||
if (!WidgetInspectorService.instance.isWidgetCreationTracked()) {
|
||||
return <DiagnosticsNode>[
|
||||
ErrorDescription(
|
||||
'Widget creation tracking is currently disabled. Enabling '
|
||||
'it enables improved error messages. It can be enabled by passing '
|
||||
'`--track-widget-creation` to `flutter run` or `flutter test`.',
|
||||
),
|
||||
ErrorSpacer(),
|
||||
];
|
||||
}
|
||||
final List<DiagnosticsNode> nodes = <DiagnosticsNode>[];
|
||||
element.visitAncestorElements((Element ancestor) {
|
||||
// TODO(chunhtai): should print out all the widgets that are about to cross
|
||||
// package boundaries.
|
||||
if (_isLocalCreationLocation(ancestor)) {
|
||||
nodes.add(
|
||||
DiagnosticsBlock(
|
||||
name: 'User-created ancestor of the error-causing widget was',
|
||||
children: <DiagnosticsNode>[
|
||||
ErrorDescription('${ancestor.widget.toStringShort()} ${_describeCreationLocation(ancestor)}'),
|
||||
],
|
||||
)
|
||||
);
|
||||
nodes.add(ErrorSpacer());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/// Returns if an object is user created.
|
||||
///
|
||||
/// This function will only work in debug mode builds when
|
||||
/// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 is
|
||||
/// required as injecting creation locations requires a
|
||||
/// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation).
|
||||
///
|
||||
/// Currently is local creation locations are only available for
|
||||
/// [Widget] and [Element].
|
||||
bool _isLocalCreationLocation(Object object) {
|
||||
final _Location location = _getCreationLocation(object);
|
||||
if (location == null)
|
||||
return false;
|
||||
return WidgetInspectorService.instance._isLocalCreationLocation(location);
|
||||
}
|
||||
|
||||
/// Returns the creation location of an object in String format if one is available.
|
||||
///
|
||||
/// ex: "file:///path/to/main.dart:4:3"
|
||||
///
|
||||
/// Creation locations are only available for debug mode builds when
|
||||
/// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 is
|
||||
/// required as injecting creation locations requires a
|
||||
/// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation).
|
||||
///
|
||||
/// Currently creation locations are only available for [Widget] and [Element].
|
||||
String _describeCreationLocation(Object object) {
|
||||
final _Location location = _getCreationLocation(object);
|
||||
return location?.toString();
|
||||
}
|
||||
|
||||
/// Returns the creation location of an object if one is available.
|
||||
///
|
||||
/// Creation locations are only available for debug mode builds when
|
||||
@ -2712,7 +2817,7 @@ class _Location {
|
||||
/// required as injecting creation locations requires a
|
||||
/// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation).
|
||||
///
|
||||
/// Currently creation locations are only available for [Widget] and [Element]
|
||||
/// Currently creation locations are only available for [Widget] and [Element].
|
||||
_Location _getCreationLocation(Object object) {
|
||||
final Object candidate = object is Element ? object.widget : object;
|
||||
return candidate is _HasCreationLocation ? candidate._location : null;
|
||||
|
@ -527,9 +527,9 @@ void main() {
|
||||
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.length, greaterThan(7));
|
||||
expect(
|
||||
lines.take(9).join('\n'),
|
||||
lines.take(7).join('\n'),
|
||||
equalsIgnoringHashCodes(
|
||||
'══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════\n'
|
||||
'The following assertion was thrown building Stepper(dirty,\n'
|
||||
@ -537,12 +537,9 @@ void main() {
|
||||
'_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://material.io/archive/guidelines/components/steppers.html#steppers-usage'
|
||||
),
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
///https://github.com/flutter/flutter/issues/16920
|
||||
|
@ -853,6 +853,132 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
||||
expect(paramB2['column'], equals(25));
|
||||
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
|
||||
|
||||
testWidgets('test transformDebugCreator will re-order if after stack trace', (WidgetTester tester) async {
|
||||
final bool widgetTracked = WidgetInspectorService.instance.isWidgetCreationTracked();
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Stack(
|
||||
children: const <Widget>[
|
||||
Text('a'),
|
||||
Text('b', textDirection: TextDirection.ltr),
|
||||
Text('c', textDirection: TextDirection.ltr),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
final Element elementA = find.text('a').evaluate().first;
|
||||
String pubRootTest;
|
||||
if (widgetTracked) {
|
||||
final Map<String, Object> jsonObject = json.decode(
|
||||
service.getSelectedWidget(null, 'my-group'));
|
||||
final Map<String,
|
||||
Object> creationLocation = jsonObject['creationLocation'];
|
||||
expect(creationLocation, isNotNull);
|
||||
final String fileA = creationLocation['file'];
|
||||
expect(fileA, endsWith('widget_inspector_test.dart'));
|
||||
expect(jsonObject, isNot(contains('createdByLocalProject')));
|
||||
final List<String> segments = Uri
|
||||
.parse(fileA)
|
||||
.pathSegments;
|
||||
// Strip a couple subdirectories away to generate a plausible pub root
|
||||
// directory.
|
||||
pubRootTest = '/' +
|
||||
segments.take(segments.length - 2).join('/');
|
||||
service.setPubRootDirectories(<Object>[pubRootTest]);
|
||||
}
|
||||
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||
builder.add(StringProperty('dummy1', 'value'));
|
||||
builder.add(StringProperty('dummy2', 'value'));
|
||||
builder.add(DiagnosticsStackTrace('When the exception was thrown, this was the stack', null));
|
||||
builder.add(DiagnosticsDebugCreator(DebugCreator(elementA)));
|
||||
|
||||
final List<DiagnosticsNode> nodes = List<DiagnosticsNode>.from(transformDebugCreator(builder.properties));
|
||||
expect(nodes.length, 5);
|
||||
expect(nodes[0].runtimeType, StringProperty);
|
||||
expect(nodes[0].name, 'dummy1');
|
||||
expect(nodes[1].runtimeType, StringProperty);
|
||||
expect(nodes[1].name, 'dummy2');
|
||||
// transformed node should come in front of stack trace.
|
||||
if (widgetTracked) {
|
||||
expect(nodes[2].runtimeType, DiagnosticsBlock);
|
||||
final DiagnosticsBlock node = nodes[2];
|
||||
final List<DiagnosticsNode> children = node.getChildren();
|
||||
expect(children.length, 1);
|
||||
final ErrorDescription child = children[0];
|
||||
expect(child.valueToString().contains(Uri.parse(pubRootTest).path), true);
|
||||
} else {
|
||||
expect(nodes[2].runtimeType, ErrorDescription);
|
||||
final ErrorDescription node = nodes[2];
|
||||
expect(node.valueToString().startsWith('Widget creation tracking is currently disabled.'), true);
|
||||
}
|
||||
expect(nodes[3].runtimeType, ErrorSpacer);
|
||||
expect(nodes[4].runtimeType, DiagnosticsStackTrace);
|
||||
});
|
||||
|
||||
testWidgets('test transformDebugCreator will not re-order if before stack trace', (WidgetTester tester) async {
|
||||
final bool widgetTracked = WidgetInspectorService.instance.isWidgetCreationTracked();
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Stack(
|
||||
children: const <Widget>[
|
||||
Text('a'),
|
||||
Text('b', textDirection: TextDirection.ltr),
|
||||
Text('c', textDirection: TextDirection.ltr),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
final Element elementA = find.text('a').evaluate().first;
|
||||
String pubRootTest;
|
||||
if (widgetTracked) {
|
||||
final Map<String, Object> jsonObject = json.decode(
|
||||
service.getSelectedWidget(null, 'my-group'));
|
||||
final Map<String,
|
||||
Object> creationLocation = jsonObject['creationLocation'];
|
||||
expect(creationLocation, isNotNull);
|
||||
final String fileA = creationLocation['file'];
|
||||
expect(fileA, endsWith('widget_inspector_test.dart'));
|
||||
expect(jsonObject, isNot(contains('createdByLocalProject')));
|
||||
final List<String> segments = Uri
|
||||
.parse(fileA)
|
||||
.pathSegments;
|
||||
// Strip a couple subdirectories away to generate a plausible pub root
|
||||
// directory.
|
||||
pubRootTest = '/' +
|
||||
segments.take(segments.length - 2).join('/');
|
||||
service.setPubRootDirectories(<Object>[pubRootTest]);
|
||||
}
|
||||
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||
builder.add(StringProperty('dummy1', 'value'));
|
||||
builder.add(DiagnosticsDebugCreator(DebugCreator(elementA)));
|
||||
builder.add(StringProperty('dummy2', 'value'));
|
||||
builder.add(DiagnosticsStackTrace('When the exception was thrown, this was the stack', null));
|
||||
|
||||
final List<DiagnosticsNode> nodes = List<DiagnosticsNode>.from(transformDebugCreator(builder.properties));
|
||||
expect(nodes.length, 5);
|
||||
expect(nodes[0].runtimeType, StringProperty);
|
||||
expect(nodes[0].name, 'dummy1');
|
||||
// transformed node stays at original place.
|
||||
if (widgetTracked) {
|
||||
expect(nodes[1].runtimeType, DiagnosticsBlock);
|
||||
final DiagnosticsBlock node = nodes[1];
|
||||
final List<DiagnosticsNode> children = node.getChildren();
|
||||
expect(children.length, 1);
|
||||
final ErrorDescription child = children[0];
|
||||
expect(child.valueToString().contains(Uri.parse(pubRootTest).path), true);
|
||||
} else {
|
||||
expect(nodes[1].runtimeType, ErrorDescription);
|
||||
final ErrorDescription node = nodes[1];
|
||||
expect(node.valueToString().startsWith('Widget creation tracking is currently disabled.'), true);
|
||||
}
|
||||
expect(nodes[2].runtimeType, ErrorSpacer);
|
||||
expect(nodes[3].runtimeType, StringProperty);
|
||||
expect(nodes[3].name, 'dummy2');
|
||||
expect(nodes[4].runtimeType, DiagnosticsStackTrace);
|
||||
}, skip: WidgetInspectorService.instance.isWidgetCreationTracked());
|
||||
|
||||
testWidgets('WidgetInspectorService setPubRootDirectories', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
|
@ -31,33 +31,44 @@ void main() {
|
||||
testUsingContext('report nice errors for exceptions thrown within testWidgets()', () async {
|
||||
Cache.flutterRoot = '../..';
|
||||
return _testFile('exception_handling', automatedTestsDirectory, flutterTestDirectory);
|
||||
}, skip: io.Platform.isWindows); // Dart on Windows has trouble with unicode characters in output
|
||||
}, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
|
||||
|
||||
testUsingContext('report a nice error when a guarded function was called without await', () async {
|
||||
Cache.flutterRoot = '../..';
|
||||
return _testFile('test_async_utils_guarded', automatedTestsDirectory, flutterTestDirectory);
|
||||
}, skip: io.Platform.isWindows); // Dart on Windows has trouble with unicode characters in output
|
||||
}, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
|
||||
|
||||
testUsingContext('report a nice error when an async function was called without await', () async {
|
||||
Cache.flutterRoot = '../..';
|
||||
return _testFile('test_async_utils_unguarded', automatedTestsDirectory, flutterTestDirectory);
|
||||
}, skip: io.Platform.isWindows); // Dart on Windows has trouble with unicode characters in output
|
||||
}, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
|
||||
|
||||
testUsingContext('report a nice error when a Ticker is left running', () async {
|
||||
Cache.flutterRoot = '../..';
|
||||
return _testFile('ticker', automatedTestsDirectory, flutterTestDirectory);
|
||||
}, skip: io.Platform.isWindows); // Dart on Windows has trouble with unicode characters in output
|
||||
}, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
|
||||
|
||||
testUsingContext('report a nice error when a pubspec.yaml is missing a flutter_test dependency', () async {
|
||||
final String missingDependencyTests = fs.path.join('..', '..', 'dev', 'missing_dependency_tests');
|
||||
Cache.flutterRoot = '../..';
|
||||
return _testFile('trivial', missingDependencyTests, missingDependencyTests);
|
||||
}, skip: io.Platform.isWindows); // Dart on Windows has trouble with unicode characters in output
|
||||
}, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
|
||||
|
||||
testUsingContext('report which user created widget caused the error', () async {
|
||||
Cache.flutterRoot = '../..';
|
||||
return _testFile('print_user_created_ancestor', automatedTestsDirectory, flutterTestDirectory,
|
||||
extraArguments: const <String>['--track-widget-creation']);
|
||||
}, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
|
||||
|
||||
testUsingContext('report which user created widget caused the error - no flag', () async {
|
||||
Cache.flutterRoot = '../..';
|
||||
return _testFile('print_user_created_ancestor_no_flag', automatedTestsDirectory, flutterTestDirectory);
|
||||
}, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
|
||||
|
||||
testUsingContext('run a test when its name matches a regexp', () async {
|
||||
Cache.flutterRoot = '../..';
|
||||
final ProcessResult result = await _runFlutterTest('filtering', automatedTestsDirectory, flutterTestDirectory,
|
||||
extraArgs: const <String>['--name', 'inc.*de']);
|
||||
extraArguments: const <String>['--name', 'inc.*de']);
|
||||
if (!result.stdout.contains('+1: All tests passed'))
|
||||
fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
|
||||
expect(result.exitCode, 0);
|
||||
@ -66,7 +77,7 @@ void main() {
|
||||
testUsingContext('run a test when its name contains a string', () async {
|
||||
Cache.flutterRoot = '../..';
|
||||
final ProcessResult result = await _runFlutterTest('filtering', automatedTestsDirectory, flutterTestDirectory,
|
||||
extraArgs: const <String>['--plain-name', 'include']);
|
||||
extraArguments: const <String>['--plain-name', 'include']);
|
||||
if (!result.stdout.contains('+1: All tests passed'))
|
||||
fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
|
||||
expect(result.exitCode, 0);
|
||||
@ -75,7 +86,7 @@ void main() {
|
||||
testUsingContext('test runs to completion', () async {
|
||||
Cache.flutterRoot = '../..';
|
||||
final ProcessResult result = await _runFlutterTest('trivial', automatedTestsDirectory, flutterTestDirectory,
|
||||
extraArgs: const <String>['--verbose']);
|
||||
extraArguments: const <String>['--verbose']);
|
||||
if ((!result.stdout.contains('+1: All tests passed')) ||
|
||||
(!result.stdout.contains('test 0: starting shell process')) ||
|
||||
(!result.stdout.contains('test 0: deleting temporary directory')) ||
|
||||
@ -90,7 +101,13 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _testFile(String testName, String workingDirectory, String testDirectory, { Matcher exitCode }) async {
|
||||
Future<void> _testFile(
|
||||
String testName,
|
||||
String workingDirectory,
|
||||
String testDirectory, {
|
||||
Matcher exitCode,
|
||||
List<String> extraArguments = const <String>[],
|
||||
}) async {
|
||||
exitCode ??= isNonZero;
|
||||
final String fullTestExpectation = fs.path.join(testDirectory, '${testName}_expectation.txt');
|
||||
final File expectationFile = fs.file(fullTestExpectation);
|
||||
@ -100,7 +117,12 @@ Future<void> _testFile(String testName, String workingDirectory, String testDire
|
||||
while (_testExclusionLock != null)
|
||||
await _testExclusionLock;
|
||||
|
||||
final ProcessResult exec = await _runFlutterTest(testName, workingDirectory, testDirectory);
|
||||
final ProcessResult exec = await _runFlutterTest(
|
||||
testName,
|
||||
workingDirectory,
|
||||
testDirectory,
|
||||
extraArguments: extraArguments,
|
||||
);
|
||||
|
||||
expect(exec.exitCode, exitCode);
|
||||
final List<String> output = exec.stdout.split('\n');
|
||||
@ -164,7 +186,7 @@ Future<ProcessResult> _runFlutterTest(
|
||||
String testName,
|
||||
String workingDirectory,
|
||||
String testDirectory, {
|
||||
List<String> extraArgs = const <String>[],
|
||||
List<String> extraArguments = const <String>[],
|
||||
}) async {
|
||||
|
||||
final String testFilePath = fs.path.join(testDirectory, '${testName}_test.dart');
|
||||
@ -177,7 +199,7 @@ Future<ProcessResult> _runFlutterTest(
|
||||
fs.path.absolute(fs.path.join('bin', 'flutter_tools.dart')),
|
||||
'test',
|
||||
'--no-color',
|
||||
...extraArgs,
|
||||
...extraArguments,
|
||||
testFilePath
|
||||
];
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user