Post a ToolEvent when selecting widget for inspection (#118098)
Changes our inspection behaviour so that it also sends a postEvent on the ToolEvent stream.
This commit is contained in:
@@ -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) {
|
||||
developer.postEvent(eventKind, eventData);
|
||||
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 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);
|
||||
}
|
||||
|
||||
List<Map<Object, Object?>> getEventsDispatched(String eventKind) {
|
||||
return eventsDispatched.putIfAbsent(eventKind, () => <Map<Object, Object?>>[]);
|
||||
@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<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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user