From cf5212d4bb69b064d36308159cdb7fffdfcfd7c1 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:30:49 -0700 Subject: [PATCH] [Widget Inspector] Jump to source code of implementation widgets from Flutter Inspector (#165924) Fixes https://github.com/flutter/devtools/issues/9063 ![implementation_widgets_source_code](https://github.com/user-attachments/assets/f82a8123-3391-413e-ba76-23d6f45f13d7) --- .../lib/src/widgets/widget_inspector.dart | 6 +- .../test/widgets/widget_inspector_test.dart | 161 +++++++++++------- 2 files changed, 98 insertions(+), 69 deletions(-) diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index 6832e38b89a..e862bac44d1 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -1611,7 +1611,7 @@ mixin WidgetInspectorService { void _sendInspectEvent(Object? object) { inspect(object); - final _Location? location = _getSelectedSummaryWidgetLocation(null); + final _Location? location = _getSelectedWidgetLocation(); if (location != null) { postEvent('navigate', { 'fileUri': location.file, // URI file path of the location. @@ -2407,8 +2407,8 @@ mixin WidgetInspectorService { return _safeJsonEncode(_getSelectedSummaryWidget(null, groupName)); } - _Location? _getSelectedSummaryWidgetLocation(String? previousSelectionId) { - return _getCreationLocation(_getSelectedSummaryDiagnosticsNode(previousSelectionId)?.value); + _Location? _getSelectedWidgetLocation() { + return _getCreationLocation(_getSelectedWidgetDiagnosticsNode(null)?.value); } DiagnosticsNode? _getSelectedSummaryDiagnosticsNode(String? previousSelectionId) { diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index adfe355760e..e6f03740b69 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -895,12 +895,12 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { 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')); + expect(file, endsWith('text.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)); + expect(column, equals(16)); }, // [intended] Test requires --track-widget-creation flag. skip: !WidgetInspectorService.instance.isWidgetCreationTracked(), @@ -1988,81 +1988,110 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(columnC, equals(25)); }); - testWidgets('setSelection notifiers for an Element', (WidgetTester tester) async { - await tester.pumpWidget( - const Directionality( - textDirection: TextDirection.ltr, - child: Stack( - children: [ - Text('a'), - Text('b', textDirection: TextDirection.ltr), - Text('c', textDirection: TextDirection.ltr), - ], - ), - ), - ); - final Element elementA = find.text('a').evaluate().first; + group('setSelection notifiers', () { + setUp(() { + service.disposeAllGroups(); + setupDefaultPubRootDirectory(service); + }); - service.disposeAllGroups(); + void verifyDeveloperInspectCalled(T object) { + // Ensure that developer.inspect was called on the widget. + final List objectsInspected = service.inspectedObjects(); + expect(objectsInspected, equals([object])); + } - setupDefaultPubRootDirectory(service); + void verifyNavigateEvent({ + required String expectedFileEnding, + required int? expectedColumn, + }) { + // 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]; - // Select the widget - service.setSelection(elementA, 'my-group'); + // Verify the file URI. + final String file = event['fileUri']! as String; + expect(file, endsWith(expectedFileEnding)); - // ensure that developer.inspect was called on the widget - final List objectsInspected = service.inspectedObjects(); - expect(objectsInspected, equals([elementA])); + // Verify the column number. + final int column = event['column']! as int; + expect(column, expectedColumn == null ? isNotNull : equals(expectedColumn)); - // 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(21)); - }); + // Verify the line number is not null. Note: We don't hardcode the + // actual lines the widgets are created on as that would make this + // test fragile. + final int line = event['line']! as int; + expect(line, isNotNull); + } - testWidgets('setSelection notifiers for a RenderObject', (WidgetTester tester) async { - await pumpWidgetTreeWithABC(tester); - final Element elementA = findElementABC('a'); + testWidgets('for an Element in the local project', (WidgetTester tester) async { + await pumpWidgetTreeWithABC(tester); + final Element elementA = find.text('a').evaluate().first; - service.disposeAllGroups(); + // Select the widget. + service.setSelection(elementA, 'my-group'); - setupDefaultPubRootDirectory(service); + // Verify the correct events were dispatched in response. + verifyDeveloperInspectCalled(elementA); + verifyNavigateEvent( + expectedFileEnding: 'widget_inspector_test.dart', + expectedColumn: 15, + ); + }); - // Select the render object for the widget. - service.setSelection(elementA.renderObject, 'my-group'); + testWidgets('for an Element outside the local project', (WidgetTester tester) async { + await pumpWidgetTreeWithABC(tester); + // Note: RichText is an implementation widget of Text. + final Element richTextElement = find.byType(RichText).first.evaluate().first; - // ensure that developer.inspect was called on the widget - final List objectsInspected = service.inspectedObjects(); - expect(objectsInspected, equals([elementA.renderObject])); + // Select the widget. + service.setSelection(richTextElement, 'my-group'); - // 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(15)); + // Verify the correct events were dispatched in response. + verifyDeveloperInspectCalled(richTextElement); + verifyNavigateEvent( + expectedFileEnding: 'text.dart', + expectedColumn: null, // Including column is too fragile. + ); + }); + + testWidgets('for a Render Object outside the local project', ( + WidgetTester tester, + ) async { + await pumpWidgetTreeWithABC(tester); + final Element elementA = find.text('a').evaluate().first; + + // Select the render object for the widget. + service.setSelection(elementA.renderObject, 'my-group'); + + // Verify the correct events were dispatched in response. + verifyDeveloperInspectCalled(elementA.renderObject!); + verifyNavigateEvent( + // The Text widget does not have a render object, the backing + // render object is provided by RichText which is defined in + // text.dart. + expectedFileEnding: 'text.dart', + expectedColumn: null, // Including column is too fragile. + ); + }); + + testWidgets('for a RenderObject in the local project', (WidgetTester tester) async { + await pumpWidgetTreeWithABC(tester); + final Element stackElement = find.byType(Stack).evaluate().first; + + // Select the render object for the widget. + service.setSelection(stackElement.renderObject, 'my-group'); + + // Verify the correct events were dispatched in response. + verifyDeveloperInspectCalled(stackElement.renderObject!); + verifyNavigateEvent( + expectedFileEnding: 'widget_inspector_test.dart', + expectedColumn: 18, + ); + }); }); group('Widget Tree APIs', () {