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 {
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',
<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.
String? _devToolsInspectorUriForElement(Element element) {
if (activeDevToolsServerAddress != null && connectedVmServiceUri != null) {
......@@ -2214,9 +2233,16 @@ mixin WidgetInspectorService {
}
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 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<String, Object?>? _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<String, Object?>? _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<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
void postEvent(String eventKind, Map<Object, Object?> eventData) {
developer.postEvent(eventKind, eventData);
void inspect(Object? object) {
developer.inspect(object);
}
final _ElementLocationStatsTracker _rebuildStats = _ElementLocationStatsTracker();
......@@ -2743,9 +2792,7 @@ class _WidgetInspectorState extends State<WidgetInspector>
}
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.
......
......@@ -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
......
......@@ -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 <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 {
final bool widgetTracked = WidgetInspectorService.instance.isWidgetCreationTracked();
await tester.pumpWidget(
......@@ -3472,7 +3613,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
);
final List<Map<Object, Object?>> 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<Map<Object, Object?>> 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<Map<Object, Object?>> flutterErrorEvents = service.getEventsDispatched('Flutter.Error');
List<Map<Object, Object?>> 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);
......
......@@ -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<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
void registerServiceExtension({
......@@ -24,16 +53,35 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
}
@override
void postEvent(String eventKind, Map<Object, Object?> eventData) {
getEventsDispatched(eventKind).add(eventData);
void postEvent(
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) {
return eventsDispatched.putIfAbsent(eventKind, () => <Map<Object, Object?>>[]);
List<Object?> inspectedObjects(){
return objectsInspected;
}
Iterable<Map<Object, Object?>> getServiceExtensionStateChangedEvents(String extensionName) {
return getEventsDispatched('Flutter.ServiceExtensionStateChanged')
return dispatchedEvents('Flutter.ServiceExtensionStateChanged')
.where((Map<Object, Object?> event) => event['extension'] == extensionName);
}
......@@ -67,6 +115,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
void resetAllState() {
super.resetAllState();
eventsDispatched.clear();
objectsInspected.clear();
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