Skip to content

Commit cc7845e

Browse files
authored
Post a ToolEvent when selecting widget for inspection (#118098)
Changes our inspection behaviour so that it also sends a postEvent on the ToolEvent stream.
1 parent f22280a commit cc7845e

File tree

4 files changed

+267
-30
lines changed

4 files changed

+267
-30
lines changed

packages/flutter/lib/src/widgets/widget_inspector.dart

+61-14
Original file line numberDiff line numberDiff line change
@@ -1500,13 +1500,13 @@ mixin WidgetInspectorService {
15001500
return false;
15011501
}
15021502
selection.currentElement = object;
1503-
developer.inspect(selection.currentElement);
1503+
_sendInspectEvent(selection.currentElement);
15041504
} else {
15051505
if (object == selection.current) {
15061506
return false;
15071507
}
15081508
selection.current = object! as RenderObject;
1509-
developer.inspect(selection.current);
1509+
_sendInspectEvent(selection.current);
15101510
}
15111511
if (selectionChangedCallback != null) {
15121512
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) {
@@ -1525,6 +1525,25 @@ mixin WidgetInspectorService {
15251525
return false;
15261526
}
15271527

1528+
/// Notify attached tools to navigate to an object's source location.
1529+
void _sendInspectEvent(Object? object){
1530+
inspect(object);
1531+
1532+
final _Location? location = _getSelectedSummaryWidgetLocation(null);
1533+
if (location != null) {
1534+
postEvent(
1535+
'navigate',
1536+
<String, Object>{
1537+
'fileUri': location.file, // URI file path of the location.
1538+
'line': location.line, // 1-based line number.
1539+
'column': location.column, // 1-based column number.
1540+
'source': 'flutter.inspector',
1541+
},
1542+
stream: 'ToolEvent',
1543+
);
1544+
}
1545+
}
1546+
15281547
/// Returns a DevTools uri linking to a specific element on the inspector page.
15291548
String? _devToolsInspectorUriForElement(Element element) {
15301549
if (activeDevToolsServerAddress != null && connectedVmServiceUri != null) {
@@ -2214,9 +2233,16 @@ mixin WidgetInspectorService {
22142233
}
22152234

22162235
Map<String, Object?>? _getSelectedWidget(String? previousSelectionId, String groupName) {
2236+
return _nodeToJson(
2237+
_getSelectedWidgetDiagnosticsNode(previousSelectionId),
2238+
InspectorSerializationDelegate(groupName: groupName, service: this),
2239+
);
2240+
}
2241+
2242+
DiagnosticsNode? _getSelectedWidgetDiagnosticsNode(String? previousSelectionId) {
22172243
final DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?;
22182244
final Element? current = selection.currentElement;
2219-
return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), InspectorSerializationDelegate(groupName: groupName, service: this));
2245+
return current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode();
22202246
}
22212247

22222248
/// Returns a [DiagnosticsNode] representing the currently selected [Element]
@@ -2231,9 +2257,13 @@ mixin WidgetInspectorService {
22312257
return _safeJsonEncode(_getSelectedSummaryWidget(previousSelectionId, groupName));
22322258
}
22332259

2234-
Map<String, Object?>? _getSelectedSummaryWidget(String? previousSelectionId, String groupName) {
2260+
_Location? _getSelectedSummaryWidgetLocation(String? previousSelectionId) {
2261+
return _getCreationLocation(_getSelectedSummaryDiagnosticsNode(previousSelectionId)?.value);
2262+
}
2263+
2264+
DiagnosticsNode? _getSelectedSummaryDiagnosticsNode(String? previousSelectionId) {
22352265
if (!isWidgetCreationTracked()) {
2236-
return _getSelectedWidget(previousSelectionId, groupName);
2266+
return _getSelectedWidgetDiagnosticsNode(previousSelectionId);
22372267
}
22382268
final DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?;
22392269
Element? current = selection.currentElement;
@@ -2247,7 +2277,11 @@ mixin WidgetInspectorService {
22472277
}
22482278
current = firstLocal;
22492279
}
2250-
return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), InspectorSerializationDelegate(groupName: groupName, service: this));
2280+
return current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode();
2281+
}
2282+
2283+
Map<String, Object?>? _getSelectedSummaryWidget(String? previousSelectionId, String groupName) {
2284+
return _nodeToJson(_getSelectedSummaryDiagnosticsNode(previousSelectionId), InspectorSerializationDelegate(groupName: groupName, service: this));
22512285
}
22522286

22532287
/// Returns whether [Widget] creation locations are available.
@@ -2281,12 +2315,27 @@ mixin WidgetInspectorService {
22812315
}
22822316

22832317
/// All events dispatched by a [WidgetInspectorService] use this method
2284-
/// instead of calling [developer.postEvent] directly so that tests for
2285-
/// [WidgetInspectorService] can track which events were dispatched by
2286-
/// overriding this method.
2318+
/// instead of calling [developer.postEvent] directly.
2319+
///
2320+
/// This allows tests for [WidgetInspectorService] to track which events were
2321+
/// dispatched by overriding this method.
2322+
@protected
2323+
void postEvent(
2324+
String eventKind,
2325+
Map<Object, Object?> eventData, {
2326+
String stream = 'Extension',
2327+
}) {
2328+
developer.postEvent(eventKind, eventData, stream: stream);
2329+
}
2330+
2331+
/// All events dispatched by a [WidgetInspectorService] use this method
2332+
/// instead of calling [developer.inspect].
2333+
///
2334+
/// This allows tests for [WidgetInspectorService] to track which events were
2335+
/// dispatched by overriding this method.
22872336
@protected
2288-
void postEvent(String eventKind, Map<Object, Object?> eventData) {
2289-
developer.postEvent(eventKind, eventData);
2337+
void inspect(Object? object) {
2338+
developer.inspect(object);
22902339
}
22912340

22922341
final _ElementLocationStatsTracker _rebuildStats = _ElementLocationStatsTracker();
@@ -2743,9 +2792,7 @@ class _WidgetInspectorState extends State<WidgetInspector>
27432792
}
27442793
if (_lastPointerLocation != null) {
27452794
_inspectAt(_lastPointerLocation!);
2746-
2747-
// Notify debuggers to open an inspector on the object.
2748-
developer.inspect(selection.current);
2795+
WidgetInspectorService.instance._sendInspectEvent(selection.current);
27492796
}
27502797
setState(() {
27512798
// Only exit select mode if there is a button to return to select mode.

packages/flutter/test/widgets/widget_inspector_structure_error_test.dart

+4-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class StructureErrorTestWidgetInspectorService extends TestWidgetInspectorServic
2929
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
3030

3131
try {
32-
expect(service.getEventsDispatched('Flutter.Error'), isEmpty);
32+
expect(service.dispatchedEvents('Flutter.Error'), isEmpty);
3333

3434
// Set callback that doesn't call presentError.
3535
bool onErrorCalled = false;
@@ -49,7 +49,7 @@ class StructureErrorTestWidgetInspectorService extends TestWidgetInspectorServic
4949

5050
// Verify structured errors are not shown.
5151
expect(onErrorCalled, true);
52-
expect(service.getEventsDispatched('Flutter.Error'), isEmpty);
52+
expect(service.dispatchedEvents('Flutter.Error'), isEmpty);
5353

5454
// Set callback that calls presentError.
5555
onErrorCalled = false;
@@ -64,9 +64,9 @@ class StructureErrorTestWidgetInspectorService extends TestWidgetInspectorServic
6464
expect(onErrorCalled, true);
6565
// Structured errors are not supported on web.
6666
if (!kIsWeb) {
67-
expect(service.getEventsDispatched('Flutter.Error'), hasLength(1));
67+
expect(service.dispatchedEvents('Flutter.Error'), hasLength(1));
6868
} else {
69-
expect(service.getEventsDispatched('Flutter.Error'), isEmpty);
69+
expect(service.dispatchedEvents('Flutter.Error'), isEmpty);
7070
}
7171

7272
// Verify disabling structured errors sets the default FlutterError.presentError

packages/flutter/test/widgets/widget_inspector_test.dart

+147-6
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,147 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
974974
expect(columnA, equals(columnB));
975975
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag.
976976

977+
testWidgets('WidgetInspectorService setSelection notifiers for an Element',
978+
(WidgetTester tester) async {
979+
await tester.pumpWidget(
980+
Directionality(
981+
textDirection: TextDirection.ltr,
982+
child: Stack(
983+
children: const <Widget>[
984+
Text('a'),
985+
Text('b', textDirection: TextDirection.ltr),
986+
Text('c', textDirection: TextDirection.ltr),
987+
],
988+
),
989+
),
990+
);
991+
final Element elementA = find.text('a').evaluate().first;
992+
993+
service.disposeAllGroups();
994+
995+
setupDefaultPubRootDirectory(service);
996+
997+
// Select the widget
998+
service.setSelection(elementA, 'my-group');
999+
1000+
// ensure that developer.inspect was called on the widget
1001+
final List<Object?> objectsInspected = service.inspectedObjects();
1002+
expect(objectsInspected, equals(<Element>[elementA]));
1003+
1004+
// ensure that a navigate event was sent for the element
1005+
final List<Map<Object, Object?>> navigateEventsPosted
1006+
= service.dispatchedEvents('navigate', stream: 'ToolEvent',);
1007+
expect(navigateEventsPosted.length, equals(1));
1008+
final Map<Object,Object?> event = navigateEventsPosted[0];
1009+
final String file = event['fileUri']! as String;
1010+
final int line = event['line']! as int;
1011+
final int column = event['column']! as int;
1012+
expect(file, endsWith('widget_inspector_test.dart'));
1013+
// We don't hardcode the actual lines the widgets are created on as that
1014+
// would make this test fragile.
1015+
expect(line, isNotNull);
1016+
// Column numbers are more stable than line numbers.
1017+
expect(column, equals(15));
1018+
},
1019+
skip: !WidgetInspectorService.instance.isWidgetCreationTracked(), // [intended] Test requires --track-widget-creation flag.
1020+
);
1021+
1022+
testWidgets(
1023+
'WidgetInspectorService setSelection notifiers for a RenderObject',
1024+
(WidgetTester tester) async {
1025+
await tester.pumpWidget(
1026+
Directionality(
1027+
textDirection: TextDirection.ltr,
1028+
child: Stack(
1029+
children: const <Widget>[
1030+
Text('a'),
1031+
Text('b', textDirection: TextDirection.ltr),
1032+
Text('c', textDirection: TextDirection.ltr),
1033+
],
1034+
),
1035+
),
1036+
);
1037+
final Element elementA = find.text('a').evaluate().first;
1038+
1039+
service.disposeAllGroups();
1040+
1041+
setupDefaultPubRootDirectory(service);
1042+
1043+
// Select the render object for the widget.
1044+
service.setSelection(elementA.renderObject, 'my-group');
1045+
1046+
// ensure that developer.inspect was called on the widget
1047+
final List<Object?> objectsInspected = service.inspectedObjects();
1048+
expect(objectsInspected, equals(<RenderObject?>[elementA.renderObject]));
1049+
1050+
// ensure that a navigate event was sent for the renderObject
1051+
final List<Map<Object, Object?>> navigateEventsPosted
1052+
= service.dispatchedEvents('navigate', stream: 'ToolEvent',);
1053+
expect(navigateEventsPosted.length, equals(1));
1054+
final Map<Object,Object?> event = navigateEventsPosted[0];
1055+
final String file = event['fileUri']! as String;
1056+
final int line = event['line']! as int;
1057+
final int column = event['column']! as int;
1058+
expect(file, endsWith('widget_inspector_test.dart'));
1059+
// We don't hardcode the actual lines the widgets are created on as that
1060+
// would make this test fragile.
1061+
expect(line, isNotNull);
1062+
// Column numbers are more stable than line numbers.
1063+
expect(column, equals(17));
1064+
},
1065+
skip: !WidgetInspectorService.instance.isWidgetCreationTracked(), // [intended] Test requires --track-widget-creation flag.
1066+
);
1067+
1068+
testWidgets(
1069+
'WidgetInspector selectButton inspection for tap',
1070+
(WidgetTester tester) async {
1071+
final GlobalKey selectButtonKey = GlobalKey();
1072+
final GlobalKey inspectorKey = GlobalKey();
1073+
setupDefaultPubRootDirectory(service);
1074+
1075+
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
1076+
return Material(child: ElevatedButton(onPressed: onPressed, key: selectButtonKey, child: null));
1077+
}
1078+
1079+
await tester.pumpWidget(
1080+
Directionality(
1081+
textDirection: TextDirection.ltr,
1082+
child: WidgetInspector(
1083+
key: inspectorKey,
1084+
selectButtonBuilder: selectButtonBuilder,
1085+
child: const Text('Child 1'),
1086+
),
1087+
),
1088+
);
1089+
final Finder child = find.text('Child 1');
1090+
final Element childElement = child.evaluate().first;
1091+
1092+
await tester.tap(child, warnIfMissed: false);
1093+
1094+
await tester.pump();
1095+
1096+
// ensure that developer.inspect was called on the widget
1097+
final List<Object?> objectsInspected = service.inspectedObjects();
1098+
expect(objectsInspected, equals(<RenderObject?>[childElement.renderObject]));
1099+
1100+
// ensure that a navigate event was sent for the renderObject
1101+
final List<Map<Object, Object?>> navigateEventsPosted
1102+
= service.dispatchedEvents('navigate', stream: 'ToolEvent',);
1103+
expect(navigateEventsPosted.length, equals(1));
1104+
final Map<Object,Object?> event = navigateEventsPosted[0];
1105+
final String file = event['fileUri']! as String;
1106+
final int line = event['line']! as int;
1107+
final int column = event['column']! as int;
1108+
expect(file, endsWith('widget_inspector_test.dart'));
1109+
// We don't hardcode the actual lines the widgets are created on as that
1110+
// would make this test fragile.
1111+
expect(line, isNotNull);
1112+
// Column numbers are more stable than line numbers.
1113+
expect(column, equals(28));
1114+
},
1115+
skip: !WidgetInspectorService.instance.isWidgetCreationTracked() // [intended] Test requires --track-widget-creation flag.
1116+
);
1117+
9771118
testWidgets('test transformDebugCreator will re-order if after stack trace', (WidgetTester tester) async {
9781119
final bool widgetTracked = WidgetInspectorService.instance.isWidgetCreationTracked();
9791120
await tester.pumpWidget(
@@ -3472,7 +3613,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
34723613
);
34733614

34743615
final List<Map<Object, Object?>> rebuildEvents =
3475-
service.getEventsDispatched('Flutter.RebuiltWidgets');
3616+
service.dispatchedEvents('Flutter.RebuiltWidgets');
34763617
expect(rebuildEvents, isEmpty);
34773618

34783619
expect(service.rebuildCount, equals(0));
@@ -3692,7 +3833,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
36923833
);
36933834

36943835
final List<Map<Object, Object?>> repaintEvents =
3695-
service.getEventsDispatched('Flutter.RepaintWidgets');
3836+
service.dispatchedEvents('Flutter.RepaintWidgets');
36963837
expect(repaintEvents, isEmpty);
36973838

36983839
expect(service.rebuildCount, equals(0));
@@ -4467,7 +4608,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
44674608
});
44684609

44694610
test('ext.flutter.inspector.structuredErrors', () async {
4470-
List<Map<Object, Object?>> flutterErrorEvents = service.getEventsDispatched('Flutter.Error');
4611+
List<Map<Object, Object?>> flutterErrorEvents = service.dispatchedEvents('Flutter.Error');
44714612
expect(flutterErrorEvents, isEmpty);
44724613

44734614
final FlutterExceptionHandler oldHandler = FlutterError.presentError;
@@ -4490,7 +4631,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
44904631
));
44914632

44924633
// Validate that we received an error.
4493-
flutterErrorEvents = service.getEventsDispatched('Flutter.Error');
4634+
flutterErrorEvents = service.dispatchedEvents('Flutter.Error');
44944635
expect(flutterErrorEvents, hasLength(1));
44954636

44964637
// Validate the error contents.
@@ -4513,7 +4654,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
45134654
));
45144655

45154656
// Validate that the error count increased.
4516-
flutterErrorEvents = service.getEventsDispatched('Flutter.Error');
4657+
flutterErrorEvents = service.dispatchedEvents('Flutter.Error');
45174658
expect(flutterErrorEvents, hasLength(2));
45184659
error = flutterErrorEvents.last;
45194660
expect(error['errorsSinceReload'], 1);
@@ -4541,7 +4682,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
45414682
));
45424683

45434684
// And, validate that the error count has been reset.
4544-
flutterErrorEvents = service.getEventsDispatched('Flutter.Error');
4685+
flutterErrorEvents = service.dispatchedEvents('Flutter.Error');
45454686
expect(flutterErrorEvents, hasLength(3));
45464687
error = flutterErrorEvents.last;
45474688
expect(error['errorsSinceReload'], 0);

0 commit comments

Comments
 (0)