diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index 6135736216db..74fad805aacf 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -1500,13 +1500,13 @@ mixin WidgetInspectorService { return false; } selection.currentElement = object; - developer.inspect(selection.currentElement); + _sendInspectEvent(selection.currentElement); } else { if (object == selection.current) { return false; } selection.current = object! as RenderObject; - developer.inspect(selection.current); + _sendInspectEvent(selection.current); } if (selectionChangedCallback != null) { if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) { @@ -1525,6 +1525,25 @@ mixin WidgetInspectorService { return false; } + /// Notify attached tools to navigate to an object's source location. + void _sendInspectEvent(Object? object){ + inspect(object); + + final _Location? location = _getSelectedSummaryWidgetLocation(null); + if (location != null) { + postEvent( + 'navigate', + { + 'fileUri': location.file, // URI file path of the location. + 'line': location.line, // 1-based line number. + 'column': location.column, // 1-based column number. + 'source': 'flutter.inspector', + }, + stream: 'ToolEvent', + ); + } + } + /// Returns a DevTools uri linking to a specific element on the inspector page. String? _devToolsInspectorUriForElement(Element element) { if (activeDevToolsServerAddress != null && connectedVmServiceUri != null) { @@ -2214,9 +2233,16 @@ mixin WidgetInspectorService { } Map? _getSelectedWidget(String? previousSelectionId, String groupName) { + return _nodeToJson( + _getSelectedWidgetDiagnosticsNode(previousSelectionId), + InspectorSerializationDelegate(groupName: groupName, service: this), + ); + } + + DiagnosticsNode? _getSelectedWidgetDiagnosticsNode(String? previousSelectionId) { final DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?; final Element? current = selection.currentElement; - return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), InspectorSerializationDelegate(groupName: groupName, service: this)); + return current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(); } /// Returns a [DiagnosticsNode] representing the currently selected [Element] @@ -2231,9 +2257,13 @@ mixin WidgetInspectorService { return _safeJsonEncode(_getSelectedSummaryWidget(previousSelectionId, groupName)); } - Map? _getSelectedSummaryWidget(String? previousSelectionId, String groupName) { + _Location? _getSelectedSummaryWidgetLocation(String? previousSelectionId) { + return _getCreationLocation(_getSelectedSummaryDiagnosticsNode(previousSelectionId)?.value); + } + + DiagnosticsNode? _getSelectedSummaryDiagnosticsNode(String? previousSelectionId) { if (!isWidgetCreationTracked()) { - return _getSelectedWidget(previousSelectionId, groupName); + return _getSelectedWidgetDiagnosticsNode(previousSelectionId); } final DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?; Element? current = selection.currentElement; @@ -2247,7 +2277,11 @@ mixin WidgetInspectorService { } current = firstLocal; } - return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), InspectorSerializationDelegate(groupName: groupName, service: this)); + return current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(); + } + + Map? _getSelectedSummaryWidget(String? previousSelectionId, String groupName) { + return _nodeToJson(_getSelectedSummaryDiagnosticsNode(previousSelectionId), InspectorSerializationDelegate(groupName: groupName, service: this)); } /// Returns whether [Widget] creation locations are available. @@ -2281,12 +2315,27 @@ mixin WidgetInspectorService { } /// All events dispatched by a [WidgetInspectorService] use this method - /// instead of calling [developer.postEvent] directly so that tests for - /// [WidgetInspectorService] can track which events were dispatched by - /// overriding this method. + /// instead of calling [developer.postEvent] directly. + /// + /// This allows tests for [WidgetInspectorService] to track which events were + /// dispatched by overriding this method. + @protected + void postEvent( + String eventKind, + Map eventData, { + String stream = 'Extension', + }) { + developer.postEvent(eventKind, eventData, stream: stream); + } + + /// All events dispatched by a [WidgetInspectorService] use this method + /// instead of calling [developer.inspect]. + /// + /// This allows tests for [WidgetInspectorService] to track which events were + /// dispatched by overriding this method. @protected - void postEvent(String eventKind, Map eventData) { - developer.postEvent(eventKind, eventData); + void inspect(Object? object) { + developer.inspect(object); } final _ElementLocationStatsTracker _rebuildStats = _ElementLocationStatsTracker(); @@ -2743,9 +2792,7 @@ class _WidgetInspectorState extends State } if (_lastPointerLocation != null) { _inspectAt(_lastPointerLocation!); - - // Notify debuggers to open an inspector on the object. - developer.inspect(selection.current); + WidgetInspectorService.instance._sendInspectEvent(selection.current); } setState(() { // Only exit select mode if there is a button to return to select mode. diff --git a/packages/flutter/test/widgets/widget_inspector_structure_error_test.dart b/packages/flutter/test/widgets/widget_inspector_structure_error_test.dart index 49f8d62ae1f4..0c08631d2c59 100644 --- a/packages/flutter/test/widgets/widget_inspector_structure_error_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_structure_error_test.dart @@ -29,7 +29,7 @@ class StructureErrorTestWidgetInspectorService extends TestWidgetInspectorServic final FlutterExceptionHandler? oldHandler = FlutterError.onError; try { - expect(service.getEventsDispatched('Flutter.Error'), isEmpty); + expect(service.dispatchedEvents('Flutter.Error'), isEmpty); // Set callback that doesn't call presentError. bool onErrorCalled = false; @@ -49,7 +49,7 @@ class StructureErrorTestWidgetInspectorService extends TestWidgetInspectorServic // Verify structured errors are not shown. expect(onErrorCalled, true); - expect(service.getEventsDispatched('Flutter.Error'), isEmpty); + expect(service.dispatchedEvents('Flutter.Error'), isEmpty); // Set callback that calls presentError. onErrorCalled = false; @@ -64,9 +64,9 @@ class StructureErrorTestWidgetInspectorService extends TestWidgetInspectorServic expect(onErrorCalled, true); // Structured errors are not supported on web. if (!kIsWeb) { - expect(service.getEventsDispatched('Flutter.Error'), hasLength(1)); + expect(service.dispatchedEvents('Flutter.Error'), hasLength(1)); } else { - expect(service.getEventsDispatched('Flutter.Error'), isEmpty); + expect(service.dispatchedEvents('Flutter.Error'), isEmpty); } // Verify disabling structured errors sets the default FlutterError.presentError diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index 9246b3601335..173436c8e627 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -974,6 +974,147 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(columnA, equals(columnB)); }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag. + testWidgets('WidgetInspectorService setSelection notifiers for an Element', + (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: const [ + Text('a'), + Text('b', textDirection: TextDirection.ltr), + Text('c', textDirection: TextDirection.ltr), + ], + ), + ), + ); + final Element elementA = find.text('a').evaluate().first; + + service.disposeAllGroups(); + + setupDefaultPubRootDirectory(service); + + // Select the widget + service.setSelection(elementA, 'my-group'); + + // ensure that developer.inspect was called on the widget + final List objectsInspected = service.inspectedObjects(); + expect(objectsInspected, equals([elementA])); + + // ensure that a navigate event was sent for the element + final List> navigateEventsPosted + = service.dispatchedEvents('navigate', stream: 'ToolEvent',); + expect(navigateEventsPosted.length, equals(1)); + final Map event = navigateEventsPosted[0]; + final String file = event['fileUri']! as String; + final int line = event['line']! as int; + final int column = event['column']! as int; + expect(file, endsWith('widget_inspector_test.dart')); + // We don't hardcode the actual lines the widgets are created on as that + // would make this test fragile. + expect(line, isNotNull); + // Column numbers are more stable than line numbers. + expect(column, equals(15)); + }, + skip: !WidgetInspectorService.instance.isWidgetCreationTracked(), // [intended] Test requires --track-widget-creation flag. + ); + + testWidgets( + 'WidgetInspectorService setSelection notifiers for a RenderObject', + (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: const [ + Text('a'), + Text('b', textDirection: TextDirection.ltr), + Text('c', textDirection: TextDirection.ltr), + ], + ), + ), + ); + final Element elementA = find.text('a').evaluate().first; + + service.disposeAllGroups(); + + setupDefaultPubRootDirectory(service); + + // Select the render object for the widget. + service.setSelection(elementA.renderObject, 'my-group'); + + // ensure that developer.inspect was called on the widget + final List objectsInspected = service.inspectedObjects(); + expect(objectsInspected, equals([elementA.renderObject])); + + // ensure that a navigate event was sent for the renderObject + final List> navigateEventsPosted + = service.dispatchedEvents('navigate', stream: 'ToolEvent',); + expect(navigateEventsPosted.length, equals(1)); + final Map event = navigateEventsPosted[0]; + final String file = event['fileUri']! as String; + final int line = event['line']! as int; + final int column = event['column']! as int; + expect(file, endsWith('widget_inspector_test.dart')); + // We don't hardcode the actual lines the widgets are created on as that + // would make this test fragile. + expect(line, isNotNull); + // Column numbers are more stable than line numbers. + expect(column, equals(17)); + }, + skip: !WidgetInspectorService.instance.isWidgetCreationTracked(), // [intended] Test requires --track-widget-creation flag. + ); + + testWidgets( + 'WidgetInspector selectButton inspection for tap', + (WidgetTester tester) async { + final GlobalKey selectButtonKey = GlobalKey(); + final GlobalKey inspectorKey = GlobalKey(); + setupDefaultPubRootDirectory(service); + + Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) { + return Material(child: ElevatedButton(onPressed: onPressed, key: selectButtonKey, child: null)); + } + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WidgetInspector( + key: inspectorKey, + selectButtonBuilder: selectButtonBuilder, + child: const Text('Child 1'), + ), + ), + ); + final Finder child = find.text('Child 1'); + final Element childElement = child.evaluate().first; + + await tester.tap(child, warnIfMissed: false); + + await tester.pump(); + + // ensure that developer.inspect was called on the widget + final List objectsInspected = service.inspectedObjects(); + expect(objectsInspected, equals([childElement.renderObject])); + + // ensure that a navigate event was sent for the renderObject + final List> navigateEventsPosted + = service.dispatchedEvents('navigate', stream: 'ToolEvent',); + expect(navigateEventsPosted.length, equals(1)); + final Map event = navigateEventsPosted[0]; + final String file = event['fileUri']! as String; + final int line = event['line']! as int; + final int column = event['column']! as int; + expect(file, endsWith('widget_inspector_test.dart')); + // We don't hardcode the actual lines the widgets are created on as that + // would make this test fragile. + expect(line, isNotNull); + // Column numbers are more stable than line numbers. + expect(column, equals(28)); + }, + skip: !WidgetInspectorService.instance.isWidgetCreationTracked() // [intended] 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( @@ -3472,7 +3613,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ); final List> rebuildEvents = - service.getEventsDispatched('Flutter.RebuiltWidgets'); + service.dispatchedEvents('Flutter.RebuiltWidgets'); expect(rebuildEvents, isEmpty); expect(service.rebuildCount, equals(0)); @@ -3692,7 +3833,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ); final List> repaintEvents = - service.getEventsDispatched('Flutter.RepaintWidgets'); + service.dispatchedEvents('Flutter.RepaintWidgets'); expect(repaintEvents, isEmpty); expect(service.rebuildCount, equals(0)); @@ -4467,7 +4608,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { }); test('ext.flutter.inspector.structuredErrors', () async { - List> flutterErrorEvents = service.getEventsDispatched('Flutter.Error'); + List> flutterErrorEvents = service.dispatchedEvents('Flutter.Error'); expect(flutterErrorEvents, isEmpty); final FlutterExceptionHandler oldHandler = FlutterError.presentError; @@ -4490,7 +4631,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { )); // Validate that we received an error. - flutterErrorEvents = service.getEventsDispatched('Flutter.Error'); + flutterErrorEvents = service.dispatchedEvents('Flutter.Error'); expect(flutterErrorEvents, hasLength(1)); // Validate the error contents. @@ -4513,7 +4654,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { )); // Validate that the error count increased. - flutterErrorEvents = service.getEventsDispatched('Flutter.Error'); + flutterErrorEvents = service.dispatchedEvents('Flutter.Error'); expect(flutterErrorEvents, hasLength(2)); error = flutterErrorEvents.last; expect(error['errorsSinceReload'], 1); @@ -4541,7 +4682,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { )); // And, validate that the error count has been reset. - flutterErrorEvents = service.getEventsDispatched('Flutter.Error'); + flutterErrorEvents = service.dispatchedEvents('Flutter.Error'); expect(flutterErrorEvents, hasLength(3)); error = flutterErrorEvents.last; expect(error['errorsSinceReload'], 0); diff --git a/packages/flutter/test/widgets/widget_inspector_test_utils.dart b/packages/flutter/test/widgets/widget_inspector_test_utils.dart index 00e99ee09b78..0ac9091ed7de 100644 --- a/packages/flutter/test/widgets/widget_inspector_test_utils.dart +++ b/packages/flutter/test/widgets/widget_inspector_test_utils.dart @@ -9,10 +9,39 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +/// Tuple-like test class for storing a [stream] and [eventKind]. +/// +/// Used to store the [stream] and [eventKind] that a dispatched event would be +/// sent on. +@immutable +class DispatchedEventKey { + const DispatchedEventKey({required this.stream, required this.eventKind}); + + final String stream; + final String eventKind; + + @override + String toString() { + return '[DispatchedEventKey]($stream, $eventKind)'; + } + + @override + bool operator ==(Object other) { + return other is DispatchedEventKey && + stream == other.stream && + eventKind == other.eventKind; + } + + @override + int get hashCode => Object.hash(stream, eventKind); +} + class TestWidgetInspectorService extends Object with WidgetInspectorService { final Map extensions = {}; - final Map>> eventsDispatched = >>{}; + final Map>> eventsDispatched = + >>{}; + final List objectsInspected = []; @override void registerServiceExtension({ @@ -24,16 +53,35 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { } @override - void postEvent(String eventKind, Map eventData) { - getEventsDispatched(eventKind).add(eventData); + void postEvent( + String eventKind, + Map eventData, { + String stream = 'Extension', + }) { + dispatchedEvents(eventKind, stream: stream).add(eventData); + } + + @override + void inspect(Object? object) { + objectsInspected.add(object); + } + + List> dispatchedEvents( + String eventKind, { + String stream = 'Extension', + }) { + return eventsDispatched.putIfAbsent( + DispatchedEventKey(stream: stream, eventKind: eventKind), + () => >[], + ); } - List> getEventsDispatched(String eventKind) { - return eventsDispatched.putIfAbsent(eventKind, () => >[]); + List inspectedObjects(){ + return objectsInspected; } Iterable> getServiceExtensionStateChangedEvents(String extensionName) { - return getEventsDispatched('Flutter.ServiceExtensionStateChanged') + return dispatchedEvents('Flutter.ServiceExtensionStateChanged') .where((Map event) => event['extension'] == extensionName); } @@ -67,6 +115,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { void resetAllState() { super.resetAllState(); eventsDispatched.clear(); + objectsInspected.clear(); rebuildCount = 0; } }