diff --git a/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart b/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart index e55c235ea45..99987a29800 100644 --- a/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart +++ b/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart @@ -118,7 +118,11 @@ class FlutterErrorFormatter { final bool allChildrenAreLeaf = node.children.isNotEmpty && !node.children.any((_ErrorNode child) => child.children.isNotEmpty); if (node.level == _DiagnosticsNodeLevel.summary || allChildrenAreLeaf) { - _writeNode(node, recursive: false); + // DiagnosticsBlock is a container, so recurse into its children if + // there's only a single level. The container may be + // "The relevant error-causing widget was" and the child may be + // the specific widget details. + _writeNode(node, recursive: node.type == _DiagnosticsNodeType.DiagnosticsBlock && allChildrenAreLeaf); } } } @@ -148,6 +152,10 @@ enum _DiagnosticsNodeStyle { flat, } +enum _DiagnosticsNodeType { + DiagnosticsBlock, +} + class _ErrorData extends _ErrorNode { _ErrorData(super.data); @@ -167,6 +175,7 @@ class _ErrorNode { List<_ErrorNode> get properties => asList('properties', _ErrorNode.new); bool get showName => data['showName'] != false; _DiagnosticsNodeStyle? get style => asEnum('style', _DiagnosticsNodeStyle.values); + _DiagnosticsNodeType? get type => asEnum('type', _DiagnosticsNodeType.values); String? asString(String field) { final Object? value = data[field]; diff --git a/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart b/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart index fbb225f4ea0..a5e0d09c110 100644 --- a/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart +++ b/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart @@ -3,12 +3,14 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:dds/dap.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/debug_adapters/error_formatter.dart'; import 'package:flutter_tools/src/debug_adapters/flutter_adapter.dart'; import 'package:flutter_tools/src/debug_adapters/flutter_adapter_args.dart'; import 'package:flutter_tools/src/globals.dart' as globals show fs, platform; @@ -797,6 +799,55 @@ void main() { expect(adapter.processArgs, contains('tool_args')); }); }); + + group('error formatter', () { + /// Helpers to build a string representation of the DAP OutputEvents for + /// the structured error [errorData]. + String getFormattedError(Map errorData) { + // Format the error and write into a buffer in a text format convenient + // for test expectations. + final StringBuffer buffer = StringBuffer(); + FlutterErrorFormatter() + ..formatError(errorData) + ..sendOutput((String category, String message, {bool? parseStackFrames, int? variablesReference}) { + buffer.writeln('${category.padRight(6)} ${jsonEncode(message)}'); + }); + return buffer.toString(); + } + + test('includes children of DiagnosticsBlock when writing a summary', () { + // Format a simulated error that nests the error-causing widget in a + // diagnostic block and will be displayed in summary mode (because it + // is not the first error since the last reload). + // https://github.com/Dart-Code/Dart-Code/issues/4743 + final String error = getFormattedError({ + 'errorsSinceReload': 1, // Force summary mode + 'description': 'Exception caught...', + 'properties': >[ + { + 'description': 'The following assertion was thrown...', + }, + { + 'description': '', + 'type': 'DiagnosticsBlock', + 'name': 'The relevant error-causing widget was', + 'children': >[ + { + 'description': 'MyWidget:file:///path/to/widget.dart:1:2', + } + ] + } + ], + }); + + expect(error, r''' +stdout "\n" +stderr "════════ Exception caught... ═══════════════════════════════════════════════════\n" +stdout "The relevant error-causing widget was:\n MyWidget:file:///path/to/widget.dart:1:2\n" +stderr "════════════════════════════════════════════════════════════════════════════════\n" +'''); + }); + }); }); }