Unverified Commit cc7845e7 authored by Daniel Chevalier's avatar Daniel Chevalier Committed by GitHub

Post a ToolEvent when selecting widget for inspection (#118098)

Changes our inspection behaviour so that it also sends a postEvent on the ToolEvent stream.
parent f22280a0
...@@ -1500,13 +1500,13 @@ mixin WidgetInspectorService { ...@@ -1500,13 +1500,13 @@ mixin WidgetInspectorService {
return false; return false;
} }
selection.currentElement = object; selection.currentElement = object;
developer.inspect(selection.currentElement); _sendInspectEvent(selection.currentElement);
} else { } else {
if (object == selection.current) { if (object == selection.current) {
return false; return false;
} }
selection.current = object! as RenderObject; selection.current = object! as RenderObject;
developer.inspect(selection.current); _sendInspectEvent(selection.current);
} }
if (selectionChangedCallback != null) { if (selectionChangedCallback != null) {
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) { if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) {
...@@ -1525,6 +1525,25 @@ mixin WidgetInspectorService { ...@@ -1525,6 +1525,25 @@ mixin WidgetInspectorService {
return false; 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',
<String, Object>{
'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. /// Returns a DevTools uri linking to a specific element on the inspector page.
String? _devToolsInspectorUriForElement(Element element) { String? _devToolsInspectorUriForElement(Element element) {
if (activeDevToolsServerAddress != null && connectedVmServiceUri != null) { if (activeDevToolsServerAddress != null && connectedVmServiceUri != null) {
...@@ -2214,9 +2233,16 @@ mixin WidgetInspectorService { ...@@ -2214,9 +2233,16 @@ mixin WidgetInspectorService {
} }
Map<String, Object?>? _getSelectedWidget(String? previousSelectionId, String groupName) { Map<String, Object?>? _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 DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?;
final Element? current = selection.currentElement; 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] /// Returns a [DiagnosticsNode] representing the currently selected [Element]
...@@ -2231,9 +2257,13 @@ mixin WidgetInspectorService { ...@@ -2231,9 +2257,13 @@ mixin WidgetInspectorService {
return _safeJsonEncode(_getSelectedSummaryWidget(previousSelectionId, groupName)); return _safeJsonEncode(_getSelectedSummaryWidget(previousSelectionId, groupName));
} }
Map<String, Object?>? _getSelectedSummaryWidget(String? previousSelectionId, String groupName) { _Location? _getSelectedSummaryWidgetLocation(String? previousSelectionId) {
return _getCreationLocation(_getSelectedSummaryDiagnosticsNode(previousSelectionId)?.value);
}
DiagnosticsNode? _getSelectedSummaryDiagnosticsNode(String? previousSelectionId) {
if (!isWidgetCreationTracked()) { if (!isWidgetCreationTracked()) {
return _getSelectedWidget(previousSelectionId, groupName); return _getSelectedWidgetDiagnosticsNode(previousSelectionId);
} }
final DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?; final DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?;
Element? current = selection.currentElement; Element? current = selection.currentElement;
...@@ -2247,7 +2277,11 @@ mixin WidgetInspectorService { ...@@ -2247,7 +2277,11 @@ mixin WidgetInspectorService {
} }
current = firstLocal; current = firstLocal;
} }
return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), InspectorSerializationDelegate(groupName: groupName, service: this)); return current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode();
}
Map<String, Object?>? _getSelectedSummaryWidget(String? previousSelectionId, String groupName) {
return _nodeToJson(_getSelectedSummaryDiagnosticsNode(previousSelectionId), InspectorSerializationDelegate(groupName: groupName, service: this));
} }
/// Returns whether [Widget] creation locations are available. /// Returns whether [Widget] creation locations are available.
...@@ -2281,12 +2315,27 @@ mixin WidgetInspectorService { ...@@ -2281,12 +2315,27 @@ mixin WidgetInspectorService {
} }
/// All events dispatched by a [WidgetInspectorService] use this method /// All events dispatched by a [WidgetInspectorService] use this method
/// instead of calling [developer.postEvent] directly so that tests for /// instead of calling [developer.postEvent] directly.
/// [WidgetInspectorService] can track which events were dispatched by ///
/// overriding this method. /// This allows tests for [WidgetInspectorService] to track which events were
/// dispatched by overriding this method.
@protected
void postEvent(
String eventKind,
Map<Object, Object?> 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 @protected
void postEvent(String eventKind, Map<Object, Object?> eventData) { void inspect(Object? object) {
developer.postEvent(eventKind, eventData); developer.inspect(object);
} }
final _ElementLocationStatsTracker _rebuildStats = _ElementLocationStatsTracker(); final _ElementLocationStatsTracker _rebuildStats = _ElementLocationStatsTracker();
...@@ -2743,9 +2792,7 @@ class _WidgetInspectorState extends State<WidgetInspector> ...@@ -2743,9 +2792,7 @@ class _WidgetInspectorState extends State<WidgetInspector>
} }
if (_lastPointerLocation != null) { if (_lastPointerLocation != null) {
_inspectAt(_lastPointerLocation!); _inspectAt(_lastPointerLocation!);
WidgetInspectorService.instance._sendInspectEvent(selection.current);
// Notify debuggers to open an inspector on the object.
developer.inspect(selection.current);
} }
setState(() { setState(() {
// Only exit select mode if there is a button to return to select mode. // Only exit select mode if there is a button to return to select mode.
......
...@@ -29,7 +29,7 @@ class StructureErrorTestWidgetInspectorService extends TestWidgetInspectorServic ...@@ -29,7 +29,7 @@ class StructureErrorTestWidgetInspectorService extends TestWidgetInspectorServic
final FlutterExceptionHandler? oldHandler = FlutterError.onError; final FlutterExceptionHandler? oldHandler = FlutterError.onError;
try { try {
expect(service.getEventsDispatched('Flutter.Error'), isEmpty); expect(service.dispatchedEvents('Flutter.Error'), isEmpty);
// Set callback that doesn't call presentError. // Set callback that doesn't call presentError.
bool onErrorCalled = false; bool onErrorCalled = false;
...@@ -49,7 +49,7 @@ class StructureErrorTestWidgetInspectorService extends TestWidgetInspectorServic ...@@ -49,7 +49,7 @@ class StructureErrorTestWidgetInspectorService extends TestWidgetInspectorServic
// Verify structured errors are not shown. // Verify structured errors are not shown.
expect(onErrorCalled, true); expect(onErrorCalled, true);
expect(service.getEventsDispatched('Flutter.Error'), isEmpty); expect(service.dispatchedEvents('Flutter.Error'), isEmpty);
// Set callback that calls presentError. // Set callback that calls presentError.
onErrorCalled = false; onErrorCalled = false;
...@@ -64,9 +64,9 @@ class StructureErrorTestWidgetInspectorService extends TestWidgetInspectorServic ...@@ -64,9 +64,9 @@ class StructureErrorTestWidgetInspectorService extends TestWidgetInspectorServic
expect(onErrorCalled, true); expect(onErrorCalled, true);
// Structured errors are not supported on web. // Structured errors are not supported on web.
if (!kIsWeb) { if (!kIsWeb) {
expect(service.getEventsDispatched('Flutter.Error'), hasLength(1)); expect(service.dispatchedEvents('Flutter.Error'), hasLength(1));
} else { } else {
expect(service.getEventsDispatched('Flutter.Error'), isEmpty); expect(service.dispatchedEvents('Flutter.Error'), isEmpty);
} }
// Verify disabling structured errors sets the default FlutterError.presentError // Verify disabling structured errors sets the default FlutterError.presentError
......
...@@ -974,6 +974,147 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -974,6 +974,147 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
expect(columnA, equals(columnB)); expect(columnA, equals(columnB));
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag. }, 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 <Widget>[
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<Object?> objectsInspected = service.inspectedObjects();
expect(objectsInspected, equals(<Element>[elementA]));
// 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(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 <Widget>[
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<Object?> objectsInspected = service.inspectedObjects();
expect(objectsInspected, equals(<RenderObject?>[elementA.renderObject]));
// 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(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<Object?> objectsInspected = service.inspectedObjects();
expect(objectsInspected, equals(<RenderObject?>[childElement.renderObject]));
// 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(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 { testWidgets('test transformDebugCreator will re-order if after stack trace', (WidgetTester tester) async {
final bool widgetTracked = WidgetInspectorService.instance.isWidgetCreationTracked(); final bool widgetTracked = WidgetInspectorService.instance.isWidgetCreationTracked();
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3472,7 +3613,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -3472,7 +3613,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
); );
final List<Map<Object, Object?>> rebuildEvents = final List<Map<Object, Object?>> rebuildEvents =
service.getEventsDispatched('Flutter.RebuiltWidgets'); service.dispatchedEvents('Flutter.RebuiltWidgets');
expect(rebuildEvents, isEmpty); expect(rebuildEvents, isEmpty);
expect(service.rebuildCount, equals(0)); expect(service.rebuildCount, equals(0));
...@@ -3692,7 +3833,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -3692,7 +3833,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
); );
final List<Map<Object, Object?>> repaintEvents = final List<Map<Object, Object?>> repaintEvents =
service.getEventsDispatched('Flutter.RepaintWidgets'); service.dispatchedEvents('Flutter.RepaintWidgets');
expect(repaintEvents, isEmpty); expect(repaintEvents, isEmpty);
expect(service.rebuildCount, equals(0)); expect(service.rebuildCount, equals(0));
...@@ -4467,7 +4608,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -4467,7 +4608,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
}); });
test('ext.flutter.inspector.structuredErrors', () async { test('ext.flutter.inspector.structuredErrors', () async {
List<Map<Object, Object?>> flutterErrorEvents = service.getEventsDispatched('Flutter.Error'); List<Map<Object, Object?>> flutterErrorEvents = service.dispatchedEvents('Flutter.Error');
expect(flutterErrorEvents, isEmpty); expect(flutterErrorEvents, isEmpty);
final FlutterExceptionHandler oldHandler = FlutterError.presentError; final FlutterExceptionHandler oldHandler = FlutterError.presentError;
...@@ -4490,7 +4631,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -4490,7 +4631,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
)); ));
// Validate that we received an error. // Validate that we received an error.
flutterErrorEvents = service.getEventsDispatched('Flutter.Error'); flutterErrorEvents = service.dispatchedEvents('Flutter.Error');
expect(flutterErrorEvents, hasLength(1)); expect(flutterErrorEvents, hasLength(1));
// Validate the error contents. // Validate the error contents.
...@@ -4513,7 +4654,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -4513,7 +4654,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
)); ));
// Validate that the error count increased. // Validate that the error count increased.
flutterErrorEvents = service.getEventsDispatched('Flutter.Error'); flutterErrorEvents = service.dispatchedEvents('Flutter.Error');
expect(flutterErrorEvents, hasLength(2)); expect(flutterErrorEvents, hasLength(2));
error = flutterErrorEvents.last; error = flutterErrorEvents.last;
expect(error['errorsSinceReload'], 1); expect(error['errorsSinceReload'], 1);
...@@ -4541,7 +4682,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -4541,7 +4682,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
)); ));
// And, validate that the error count has been reset. // And, validate that the error count has been reset.
flutterErrorEvents = service.getEventsDispatched('Flutter.Error'); flutterErrorEvents = service.dispatchedEvents('Flutter.Error');
expect(flutterErrorEvents, hasLength(3)); expect(flutterErrorEvents, hasLength(3));
error = flutterErrorEvents.last; error = flutterErrorEvents.last;
expect(error['errorsSinceReload'], 0); expect(error['errorsSinceReload'], 0);
......
...@@ -9,10 +9,39 @@ import 'package:flutter/material.dart'; ...@@ -9,10 +9,39 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.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 { class TestWidgetInspectorService extends Object with WidgetInspectorService {
final Map<String, ServiceExtensionCallback> extensions = <String, ServiceExtensionCallback>{}; final Map<String, ServiceExtensionCallback> extensions = <String, ServiceExtensionCallback>{};
final Map<String, List<Map<Object, Object?>>> eventsDispatched = <String, List<Map<Object, Object?>>>{}; final Map<DispatchedEventKey, List<Map<Object, Object?>>> eventsDispatched =
<DispatchedEventKey, List<Map<Object, Object?>>>{};
final List<Object?> objectsInspected = <Object?>[];
@override @override
void registerServiceExtension({ void registerServiceExtension({
...@@ -24,16 +53,35 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -24,16 +53,35 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
} }
@override @override
void postEvent(String eventKind, Map<Object, Object?> eventData) { void postEvent(
getEventsDispatched(eventKind).add(eventData); String eventKind,
Map<Object, Object?> eventData, {
String stream = 'Extension',
}) {
dispatchedEvents(eventKind, stream: stream).add(eventData);
}
@override
void inspect(Object? object) {
objectsInspected.add(object);
}
List<Map<Object, Object?>> dispatchedEvents(
String eventKind, {
String stream = 'Extension',
}) {
return eventsDispatched.putIfAbsent(
DispatchedEventKey(stream: stream, eventKind: eventKind),
() => <Map<Object, Object?>>[],
);
} }
List<Map<Object, Object?>> getEventsDispatched(String eventKind) { List<Object?> inspectedObjects(){
return eventsDispatched.putIfAbsent(eventKind, () => <Map<Object, Object?>>[]); return objectsInspected;
} }
Iterable<Map<Object, Object?>> getServiceExtensionStateChangedEvents(String extensionName) { Iterable<Map<Object, Object?>> getServiceExtensionStateChangedEvents(String extensionName) {
return getEventsDispatched('Flutter.ServiceExtensionStateChanged') return dispatchedEvents('Flutter.ServiceExtensionStateChanged')
.where((Map<Object, Object?> event) => event['extension'] == extensionName); .where((Map<Object, Object?> event) => event['extension'] == extensionName);
} }
...@@ -67,6 +115,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -67,6 +115,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
void resetAllState() { void resetAllState() {
super.resetAllState(); super.resetAllState();
eventsDispatched.clear(); eventsDispatched.clear();
objectsInspected.clear();
rebuildCount = 0; rebuildCount = 0;
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment