diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index b9b911418d..130bc3eb79 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -2038,91 +2038,91 @@ mixin WidgetInspectorService { subtreeDepth: subtreeDepth, service: this, addAdditionalPropertiesCallback: (DiagnosticsNode node, InspectorSerializationDelegate delegate) { - final Map additionalJson = {}; final Object? value = node.value; - if (value is Element) { - final RenderObject? renderObject = value.renderObject; - if (renderObject != null) { - additionalJson['renderObject'] = - renderObject.toDiagnosticsNode().toJsonMap( + final RenderObject? renderObject = value is Element ? value.renderObject : null; + if (renderObject == null) { + return const {}; + } + + final DiagnosticsSerializationDelegate renderObjectSerializationDelegate = delegate.copyWith( + subtreeDepth: 0, + includeProperties: true, + expandPropertyValues: false, + ); + final Map additionalJson = { + 'renderObject': renderObject.toDiagnosticsNode().toJsonMap(renderObjectSerializationDelegate), + }; + + final AbstractNode? renderParent = renderObject.parent; + if (renderParent is RenderObject && subtreeDepth > 0) { + final Object? parentCreator = renderParent.debugCreator; + if (parentCreator is DebugCreator) { + additionalJson['parentRenderElement'] = + parentCreator.element.toDiagnosticsNode().toJsonMap( delegate.copyWith( subtreeDepth: 0, includeProperties: true, ), ); - - final AbstractNode? renderParent = renderObject.parent; - if (renderParent is RenderObject && subtreeDepth > 0) { - final Object? parentCreator = renderParent.debugCreator; - if (parentCreator is DebugCreator) { - additionalJson['parentRenderElement'] = - parentCreator.element.toDiagnosticsNode().toJsonMap( - delegate.copyWith( - subtreeDepth: 0, - includeProperties: true, - ), - ); - // TODO(jacobr): also describe the path back up the tree to - // the RenderParentElement from the current element. It - // could be a surprising distance up the tree if a lot of - // elements don't have their own RenderObjects. - } - } - - try { - if (!renderObject.debugNeedsLayout) { - // ignore: invalid_use_of_protected_member - final Constraints constraints = renderObject.constraints; - final MapconstraintsProperty = { - 'type': constraints.runtimeType.toString(), - 'description': constraints.toString(), - }; - if (constraints is BoxConstraints) { - constraintsProperty.addAll({ - 'minWidth': constraints.minWidth.toString(), - 'minHeight': constraints.minHeight.toString(), - 'maxWidth': constraints.maxWidth.toString(), - 'maxHeight': constraints.maxHeight.toString(), - }); - } - additionalJson['constraints'] = constraintsProperty; - } - } catch (e) { - // Constraints are sometimes unavailable even though - // debugNeedsLayout is false. - } - - try { - if (renderObject is RenderBox) { - additionalJson['isBox'] = true; - additionalJson['size'] = { - 'width': renderObject.size.width.toString(), - 'height': renderObject.size.height.toString(), - }; - - final ParentData? parentData = renderObject.parentData; - if (parentData is FlexParentData) { - additionalJson['flexFactor'] = parentData.flex!; - additionalJson['flexFit'] = - describeEnum(parentData.fit ?? FlexFit.tight); - } else if (parentData is BoxParentData) { - final Offset offset = parentData.offset; - additionalJson['parentData'] = { - 'offsetX': offset.dx.toString(), - 'offsetY': offset.dy.toString(), - }; - } - } else if (renderObject is RenderView) { - additionalJson['size'] = { - 'width': renderObject.size.width.toString(), - 'height': renderObject.size.height.toString(), - }; - } - } catch (e) { - // Not laid out yet. - } + // TODO(jacobr): also describe the path back up the tree to + // the RenderParentElement from the current element. It + // could be a surprising distance up the tree if a lot of + // elements don't have their own RenderObjects. } } + + try { + if (!renderObject.debugNeedsLayout) { + // ignore: invalid_use_of_protected_member + final Constraints constraints = renderObject.constraints; + final MapconstraintsProperty = { + 'type': constraints.runtimeType.toString(), + 'description': constraints.toString(), + }; + if (constraints is BoxConstraints) { + constraintsProperty.addAll({ + 'minWidth': constraints.minWidth.toString(), + 'minHeight': constraints.minHeight.toString(), + 'maxWidth': constraints.maxWidth.toString(), + 'maxHeight': constraints.maxHeight.toString(), + }); + } + additionalJson['constraints'] = constraintsProperty; + } + } catch (e) { + // Constraints are sometimes unavailable even though + // debugNeedsLayout is false. + } + + try { + if (renderObject is RenderBox) { + additionalJson['isBox'] = true; + additionalJson['size'] = { + 'width': renderObject.size.width.toString(), + 'height': renderObject.size.height.toString(), + }; + + final ParentData? parentData = renderObject.parentData; + if (parentData is FlexParentData) { + additionalJson['flexFactor'] = parentData.flex!; + additionalJson['flexFit'] = + describeEnum(parentData.fit ?? FlexFit.tight); + } else if (parentData is BoxParentData) { + final Offset offset = parentData.offset; + additionalJson['parentData'] = { + 'offsetX': offset.dx.toString(), + 'offsetY': offset.dy.toString(), + }; + } + } else if (renderObject is RenderView) { + additionalJson['size'] = { + 'width': renderObject.size.width.toString(), + 'height': renderObject.size.height.toString(), + }; + } + } catch (e) { + // Not laid out yet. + } return additionalJson; }, ), @@ -3633,12 +3633,12 @@ class InspectorSerializationDelegate implements DiagnosticsSerializationDelegate } @override - DiagnosticsSerializationDelegate copyWith({int? subtreeDepth, bool? includeProperties}) { + DiagnosticsSerializationDelegate copyWith({int? subtreeDepth, bool? includeProperties, bool? expandPropertyValues}) { return InspectorSerializationDelegate( groupName: groupName, summaryTree: summaryTree, maxDescendentsTruncatableNode: maxDescendentsTruncatableNode, - expandPropertyValues: expandPropertyValues, + expandPropertyValues: expandPropertyValues ?? this.expandPropertyValues, subtreeDepth: subtreeDepth ?? this.subtreeDepth, includeProperties: includeProperties ?? this.includeProperties, service: service, diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index fec382c7da..df876e4aeb 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -18,6 +18,7 @@ import 'dart:convert'; import 'dart:math'; import 'dart:ui' as ui; +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; @@ -3549,7 +3550,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { _CreationLocation location = knownLocations[id]!; expect(location.file, equals(file)); // ClockText widget. - expect(location.line, equals(59)); + expect(location.line, equals(60)); expect(location.column, equals(9)); expect(location.name, equals('ClockText')); expect(count, equals(1)); @@ -3559,7 +3560,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { location = knownLocations[id]!; expect(location.file, equals(file)); // Text widget in _ClockTextState build method. - expect(location.line, equals(97)); + expect(location.line, equals(98)); expect(location.column, equals(12)); expect(location.name, equals('Text')); expect(count, equals(1)); @@ -3586,7 +3587,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { location = knownLocations[id]!; expect(location.file, equals(file)); // ClockText widget. - expect(location.line, equals(59)); + expect(location.line, equals(60)); expect(location.column, equals(9)); expect(location.name, equals('ClockText')); expect(count, equals(3)); // 3 clock widget instances rebuilt. @@ -3596,7 +3597,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { location = knownLocations[id]!; expect(location.file, equals(file)); // Text widget in _ClockTextState build method. - expect(location.line, equals(97)); + expect(location.line, equals(98)); expect(location.column, equals(12)); expect(location.name, equals('Text')); expect(count, equals(3)); // 3 clock widget instances rebuilt. @@ -4437,6 +4438,36 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(mainAxisAlignment, equals('center')); expect(crossAxisAlignment, equals('start')); }); + + testWidgets('ext.flutter.inspector.getLayoutExplorerNode does not throw StackOverflowError',(WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/115228 + const Key leafKey = ValueKey('ColoredBox'); + await tester.pumpWidget( + CupertinoApp( + home: CupertinoPageScaffold( + child: Builder( + builder: (BuildContext context) => ColoredBox(key: leafKey, color: CupertinoTheme.of(context).primaryColor), + ), + ), + ), + ); + + final Element leaf = tester.element(find.byKey(leafKey)); + service.setSelection(leaf, group); + final DiagnosticsNode diagnostic = leaf.toDiagnosticsNode(); + final String id = service.toId(diagnostic, group)!; + + Object? error; + try { + await service.testExtension( + WidgetInspectorServiceExtensions.getLayoutExplorerNode.name, + {'id': id, 'groupName': group, 'subtreeDepth': '1'}, + ); + } catch (e) { + error = e; + } + expect(error, isNull); + }); }); test('ext.flutter.inspector.structuredErrors', () async {