[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)
This commit is contained in:
Elliott Brooks 2025-03-28 12:30:49 -07:00 committed by GitHub
parent 884e011d67
commit cf5212d4bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 98 additions and 69 deletions

View File

@ -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', <String, Object>{
'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) {

View File

@ -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: <Widget>[
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>(T object) {
// Ensure that developer.inspect was called on the widget.
final List<Object?> objectsInspected = service.inspectedObjects();
expect(objectsInspected, equals(<T>[object]));
}
setupDefaultPubRootDirectory(service);
void verifyNavigateEvent({
required String expectedFileEnding,
required int? expectedColumn,
}) {
// Ensure that a navigate event was sent for the element.
final List<Map<Object, Object?>> navigateEventsPosted = service.dispatchedEvents(
'navigate',
stream: 'ToolEvent',
);
expect(navigateEventsPosted.length, equals(1));
final Map<Object, Object?> 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<Object?> objectsInspected = service.inspectedObjects();
expect(objectsInspected, equals(<Element>[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<Map<Object, Object?>> navigateEventsPosted = service.dispatchedEvents(
'navigate',
stream: 'ToolEvent',
);
expect(navigateEventsPosted.length, equals(1));
final Map<Object, Object?> 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<Element>(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<Object?> objectsInspected = service.inspectedObjects();
expect(objectsInspected, equals(<RenderObject?>[elementA.renderObject]));
// Select the widget.
service.setSelection(richTextElement, 'my-group');
// ensure that a navigate event was sent for the renderObject
final List<Map<Object, Object?>> navigateEventsPosted = service.dispatchedEvents(
'navigate',
stream: 'ToolEvent',
);
expect(navigateEventsPosted.length, equals(1));
final Map<Object, Object?> 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<Element>(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<RenderObject>(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<RenderObject>(stackElement.renderObject!);
verifyNavigateEvent(
expectedFileEnding: 'widget_inspector_test.dart',
expectedColumn: 18,
);
});
});
group('Widget Tree APIs', () {