Unverified Commit ab9ba3f9 authored by Jacob Richman's avatar Jacob Richman Committed by GitHub

Support exposing the InspectorService over the Flutterservice extension...

Support exposing the InspectorService over the Flutterservice extension protocol as well as the observatory protocol. (#15876)

* Support exposing the InspectorService over the Flutter
service extension protocol as well as the observatory protocol.

We will probably remove most of the observatory protocol support once a
couple versions of the Flutter IntelliJ plugin have shipped that use the
Flutter service extension protocol. The only reason to continue supporting
the observatory protocol is it will allow using the inspector when paused
at a breakpoint.
parent 7dd166fa
...@@ -16,6 +16,7 @@ import 'package:flutter/services.dart'; ...@@ -16,6 +16,7 @@ import 'package:flutter/services.dart';
import 'app.dart'; import 'app.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
import 'framework.dart'; import 'framework.dart';
import 'widget_inspector.dart';
export 'dart:ui' show AppLifecycleState, Locale; export 'dart:ui' show AppLifecycleState, Locale;
...@@ -285,6 +286,8 @@ abstract class WidgetsBinding extends BindingBase with SchedulerBinding, Gesture ...@@ -285,6 +286,8 @@ abstract class WidgetsBinding extends BindingBase with SchedulerBinding, Gesture
} }
); );
// This service extension is deprecated and will be removed by 7/1/2018.
// Use ext.flutter.inspector.show instead.
registerBoolServiceExtension( registerBoolServiceExtension(
name: 'debugWidgetInspector', name: 'debugWidgetInspector',
getter: () async => WidgetsApp.debugShowWidgetInspectorOverride, getter: () async => WidgetsApp.debugShowWidgetInspectorOverride,
...@@ -295,6 +298,8 @@ abstract class WidgetsBinding extends BindingBase with SchedulerBinding, Gesture ...@@ -295,6 +298,8 @@ abstract class WidgetsBinding extends BindingBase with SchedulerBinding, Gesture
return _forceRebuild(); return _forceRebuild();
} }
); );
WidgetInspectorService.instance.initServiceExtensions(registerServiceExtension);
} }
Future<Null> _forceRebuild() { Future<Null> _forceRebuild() {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer' as developer; import 'dart:developer' as developer;
...@@ -14,6 +15,7 @@ import 'package:flutter/painting.dart'; ...@@ -14,6 +15,7 @@ import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'app.dart';
import 'basic.dart'; import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
import 'framework.dart'; import 'framework.dart';
...@@ -23,6 +25,11 @@ import 'gesture_detector.dart'; ...@@ -23,6 +25,11 @@ import 'gesture_detector.dart';
/// [WidgetInspector.selectButtonBuilder]. /// [WidgetInspector.selectButtonBuilder].
typedef Widget InspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed); typedef Widget InspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed);
typedef void _RegisterServiceExtensionCallback({
@required String name,
@required ServiceExtensionCallback callback
});
/// A class describing a step along a path through a tree of [DiagnosticsNode] /// A class describing a step along a path through a tree of [DiagnosticsNode]
/// objects. /// objects.
/// ///
...@@ -96,6 +103,9 @@ class _InspectorReferenceData { ...@@ -96,6 +103,9 @@ class _InspectorReferenceData {
int count = 1; int count = 1;
} }
class _WidgetInspectorService extends Object with WidgetInspectorService {
}
/// Service used by GUI tools to interact with the [WidgetInspector]. /// Service used by GUI tools to interact with the [WidgetInspector].
/// ///
/// Calls to this object are typically made from GUI tools such as the [Flutter /// Calls to this object are typically made from GUI tools such as the [Flutter
...@@ -117,11 +127,19 @@ class _InspectorReferenceData { ...@@ -117,11 +127,19 @@ class _InspectorReferenceData {
/// ///
/// All methods returning String values return JSON. /// All methods returning String values return JSON.
class WidgetInspectorService { class WidgetInspectorService {
WidgetInspectorService._(); // This class is usable as a mixin for test purposes and as a singleton
// [instance] for production purposes.
factory WidgetInspectorService._() => new _WidgetInspectorService();
/// The current [WidgetInspectorService]. /// The current [WidgetInspectorService].
static WidgetInspectorService get instance => _instance; static WidgetInspectorService get instance => _instance;
static final WidgetInspectorService _instance = new WidgetInspectorService._(); static WidgetInspectorService _instance = new WidgetInspectorService._();
@protected
static set instance(WidgetInspectorService instance) {
_instance = instance;
}
static bool _debugServiceExtensionsRegistered = false;
/// Ground truth tracking what object(s) are currently selected used by both /// Ground truth tracking what object(s) are currently selected used by both
/// GUI tools such as the Flutter IntelliJ Plugin and the [WidgetInspector] /// GUI tools such as the Flutter IntelliJ Plugin and the [WidgetInspector]
...@@ -146,10 +164,232 @@ class WidgetInspectorService { ...@@ -146,10 +164,232 @@ class WidgetInspectorService {
List<String> _pubRootDirectories; List<String> _pubRootDirectories;
_RegisterServiceExtensionCallback _registerServiceExtensionCallback;
/// Registers a service extension method with the given name (full
/// name "ext.flutter.inspector.name").
///
/// The given callback is called when the extension method is called. The
/// callback must return a value that can be converted to JSON using
/// `json.encode()` (see [JsonEncoder]). The return value is stored as a
/// property named `result` in the JSON. In case of failure, the failure is
/// reported to the remote caller and is dumped to the logs.
@protected
void registerServiceExtension({
@required String name,
@required ServiceExtensionCallback callback,
}) {
_registerServiceExtensionCallback(
name: 'inspector.$name',
callback: callback,
);
}
/// Registers a service extension method with the given name (full
/// name "ext.flutter.inspector.name"), which takes no arguments.
void _registerSignalServiceExtension({
@required String name,
@required FutureOr<Object> callback(),
}) {
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
return <String, Object>{'result': await callback()};
},
);
}
/// Registers a service extension method with the given name (full
/// name "ext.flutter.inspector.name"), which takes a single required argument
/// "objectGroup" specifying what group is used to manage lifetimes of
/// object references in the returned JSON (see [disposeGroup]).
void _registerObjectGroupServiceExtension({
@required String name,
@required FutureOr<Object> callback(String objectGroup),
}) {
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
assert(parameters.containsKey('objectGroup'));
return <String, Object>{'result': await callback(parameters['objectGroup'])};
},
);
}
/// Registers a service extension method with the given name (full
/// name "ext.flutter.inspector.name"), which takes a single argument
/// "enabled" which can have the value "true" or the value "false"
/// or can be omitted to read the current value. (Any value other
/// than "true" is considered equivalent to "false". Other arguments
/// are ignored.)
///
/// Calls the `getter` callback to obtain the value when
/// responding to the service extension method being called.
///
/// Calls the `setter` callback with the new value when the
/// service extension method is called with a new value.
void _registerBoolServiceExtension({
@required String name,
@required AsyncValueGetter<bool> getter,
@required AsyncValueSetter<bool> setter
}) {
assert(name != null);
assert(getter != null);
assert(setter != null);
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
if (parameters.containsKey('enabled'))
await setter(parameters['enabled'] == 'true');
return <String, dynamic>{ 'enabled': await getter() ? 'true' : 'false' };
},
);
}
/// Registers a service extension method with the given name (full
/// name "ext.flutter.inspector.name") which takes an optional parameter named
/// "arg" and a required parameter named "objectGroup" used to control the
/// lifetimes of object references in the returned JSON (see [disposeGroup]).
void _registerServiceExtensionWithArg({
@required String name,
@required FutureOr<Object> callback(String objectId, String objectGroup),
}) {
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
assert(parameters.containsKey('objectGroup'));
return <String, Object>{
'result': await callback(parameters['arg'], parameters['objectGroup']),
};
},
);
}
/// Registers a service extension method with the given name (full
/// name "ext.flutter.inspector.name"), that takes arguments
/// "arg0", "arg1", "arg2", ..., "argn".
void _registerServiceExtensionVarArgs({
@required String name,
@required FutureOr<Object> callback(List<String> args),
}) {
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
const String argPrefix = 'arg';
final List<String> args = <String>[];
parameters.forEach((String name, String value) {
if (name.startsWith(argPrefix)) {
final int index = int.parse(name.substring(argPrefix.length));
if (index >= args.length) {
args.length = index + 1;
}
args[index] = value;
}
});
return <String, Object>{'result': await callback(args)};
},
);
}
@protected
Future<Null> forceRebuild() {
final WidgetsBinding binding = WidgetsBinding.instance;
if (binding.renderViewElement != null) {
binding.buildOwner.reassemble(binding.renderViewElement);
return binding.endOfFrame;
}
return new Future<Null>.value();
}
/// Called to register service extensions.
///
/// Service extensions are only exposed when the observatory is
/// included in the build, which should only happen in checked mode
/// and in profile mode.
///
/// See also:
///
/// * <https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#rpcs-requests-and-responses>
void initServiceExtensions(
_RegisterServiceExtensionCallback registerServiceExtensionCallback) {
_registerServiceExtensionCallback = registerServiceExtensionCallback;
assert(!_debugServiceExtensionsRegistered);
assert(() { _debugServiceExtensionsRegistered = true; return true; }());
_registerBoolServiceExtension(
name: 'show',
getter: () async => WidgetsApp.debugShowWidgetInspectorOverride,
setter: (bool value) {
if (WidgetsApp.debugShowWidgetInspectorOverride == value) {
return new Future<Null>.value();
}
WidgetsApp.debugShowWidgetInspectorOverride = value;
return forceRebuild();
},
);
_registerSignalServiceExtension(
name: 'disposeAllGroups',
callback: disposeAllGroups,
);
_registerObjectGroupServiceExtension(
name: 'disposeGroup',
callback: disposeGroup,
);
_registerSignalServiceExtension(
name: 'isWidgetTreeReady',
callback: isWidgetTreeReady,
);
_registerServiceExtensionWithArg(
name: 'disposeId',
callback: disposeId,
);
_registerServiceExtensionVarArgs(
name: 'setPubRootDirectories',
callback: setPubRootDirectories,
);
_registerServiceExtensionWithArg(
name: 'setSelectionById',
callback: setSelectionById,
);
_registerServiceExtensionWithArg(
name: 'getParentChain',
callback: _getParentChain,
);
_registerServiceExtensionWithArg(
name: 'getProperties',
callback: _getProperties,
);
_registerServiceExtensionWithArg(
name: 'getChildren',
callback: _getChildren,
);
_registerObjectGroupServiceExtension(
name: 'getRootWidget',
callback: _getRootWidget,
);
_registerObjectGroupServiceExtension(
name: 'getRootRenderObject',
callback: _getRootRenderObject,
);
_registerServiceExtensionWithArg(
name: 'getSelectedRenderObject',
callback: _getSelectedRenderObject,
);
_registerServiceExtensionWithArg(
name: 'getSelectedWidget',
callback: _getSelectedWidget,
);
_registerSignalServiceExtension(
name: 'isWidgetCreationTracked',
callback: isWidgetCreationTracked,
);
}
/// Clear all InspectorService object references. /// Clear all InspectorService object references.
/// ///
/// Use this method only for testing to ensure that object references from one /// Use this method only for testing to ensure that object references from one
/// test case do not impact other test cases. /// test case do not impact other test cases.
@protected
void disposeAllGroups() { void disposeAllGroups() {
_groups.clear(); _groups.clear();
_idToReferenceData.clear(); _idToReferenceData.clear();
...@@ -161,6 +401,7 @@ class WidgetInspectorService { ...@@ -161,6 +401,7 @@ class WidgetInspectorService {
/// ///
/// Objects and their associated ids in the group may be kept alive by /// Objects and their associated ids in the group may be kept alive by
/// references from a different group. /// references from a different group.
@protected
void disposeGroup(String name) { void disposeGroup(String name) {
final Set<_InspectorReferenceData> references = _groups.remove(name); final Set<_InspectorReferenceData> references = _groups.remove(name);
if (references == null) if (references == null)
...@@ -181,6 +422,7 @@ class WidgetInspectorService { ...@@ -181,6 +422,7 @@ class WidgetInspectorService {
/// Returns a unique id for [object] that will remain live at least until /// Returns a unique id for [object] that will remain live at least until
/// [disposeGroup] is called on [groupName] or [dispose] is called on the id /// [disposeGroup] is called on [groupName] or [dispose] is called on the id
/// returned by this method. /// returned by this method.
@protected
String toId(Object object, String groupName) { String toId(Object object, String groupName) {
if (object == null) if (object == null)
return null; return null;
...@@ -205,6 +447,7 @@ class WidgetInspectorService { ...@@ -205,6 +447,7 @@ class WidgetInspectorService {
/// Returns whether the application has rendered its first frame and it is /// Returns whether the application has rendered its first frame and it is
/// appropriate to display the Widget tree in the inspector. /// appropriate to display the Widget tree in the inspector.
@protected
bool isWidgetTreeReady([String groupName]) { bool isWidgetTreeReady([String groupName]) {
return WidgetsBinding.instance != null && return WidgetsBinding.instance != null &&
WidgetsBinding.instance.debugDidSendFirstFrameEvent; WidgetsBinding.instance.debugDidSendFirstFrameEvent;
...@@ -215,6 +458,7 @@ class WidgetInspectorService { ...@@ -215,6 +458,7 @@ class WidgetInspectorService {
/// The `groupName` parameter is not required by is added to regularize the /// The `groupName` parameter is not required by is added to regularize the
/// API surface of the methods in this class called from the Flutter IntelliJ /// API surface of the methods in this class called from the Flutter IntelliJ
/// Plugin. /// Plugin.
@protected
Object toObject(String id, [String groupName]) { Object toObject(String id, [String groupName]) {
if (id == null) if (id == null)
return null; return null;
...@@ -235,6 +479,7 @@ class WidgetInspectorService { ...@@ -235,6 +479,7 @@ class WidgetInspectorService {
/// ///
/// The `groupName` parameter is not required by is added to regularize the /// The `groupName` parameter is not required by is added to regularize the
/// API surface of methods called from the Flutter IntelliJ Plugin. /// API surface of methods called from the Flutter IntelliJ Plugin.
@protected
Object toObjectForSourceLocation(String id, [String groupName]) { Object toObjectForSourceLocation(String id, [String groupName]) {
final Object object = toObject(id); final Object object = toObject(id);
if (object is Element) { if (object is Element) {
...@@ -248,6 +493,7 @@ class WidgetInspectorService { ...@@ -248,6 +493,7 @@ class WidgetInspectorService {
/// ///
/// If the object exists in other groups it will remain alive and the object /// If the object exists in other groups it will remain alive and the object
/// id will remain valid. /// id will remain valid.
@protected
void disposeId(String id, String groupName) { void disposeId(String id, String groupName) {
if (id == null) if (id == null)
return; return;
...@@ -265,6 +511,7 @@ class WidgetInspectorService { ...@@ -265,6 +511,7 @@ class WidgetInspectorService {
/// ///
/// The local project directories are used to distinguish widgets created by /// The local project directories are used to distinguish widgets created by
/// the local project over widgets created from inside the framework. /// the local project over widgets created from inside the framework.
@protected
void setPubRootDirectories(List<Object> pubRootDirectories) { void setPubRootDirectories(List<Object> pubRootDirectories) {
_pubRootDirectories = pubRootDirectories.map<String>( _pubRootDirectories = pubRootDirectories.map<String>(
(Object directory) => Uri.parse(directory).path, (Object directory) => Uri.parse(directory).path,
...@@ -278,6 +525,7 @@ class WidgetInspectorService { ...@@ -278,6 +525,7 @@ class WidgetInspectorService {
/// ///
/// The `groupName` parameter is not required by is added to regularize the /// The `groupName` parameter is not required by is added to regularize the
/// API surface of methods called from the Flutter IntelliJ Plugin. /// API surface of methods called from the Flutter IntelliJ Plugin.
@protected
bool setSelectionById(String id, [String groupName]) { bool setSelectionById(String id, [String groupName]) {
return setSelection(toObject(id), groupName); return setSelection(toObject(id), groupName);
} }
...@@ -289,6 +537,7 @@ class WidgetInspectorService { ...@@ -289,6 +537,7 @@ class WidgetInspectorService {
/// ///
/// The `groupName` parameter is not needed but is specified to regularize the /// The `groupName` parameter is not needed but is specified to regularize the
/// API surface of methods called from the Flutter IntelliJ Plugin. /// API surface of methods called from the Flutter IntelliJ Plugin.
@protected
bool setSelection(Object object, [String groupName]) { bool setSelection(Object object, [String groupName]) {
if (object is Element || object is RenderObject) { if (object is Element || object is RenderObject) {
if (object is Element) { if (object is Element) {
...@@ -324,7 +573,12 @@ class WidgetInspectorService { ...@@ -324,7 +573,12 @@ class WidgetInspectorService {
/// ///
/// The JSON contains all information required to display a tree view with /// The JSON contains all information required to display a tree view with
/// all nodes other than nodes along the path collapsed. /// all nodes other than nodes along the path collapsed.
@protected
String getParentChain(String id, String groupName) { String getParentChain(String id, String groupName) {
return json.encode(_getParentChain(id, groupName));
}
List<Object> _getParentChain(String id, String groupName) {
final Object value = toObject(id); final Object value = toObject(id);
List<_DiagnosticsPathNode> path; List<_DiagnosticsPathNode> path;
if (value is RenderObject) if (value is RenderObject)
...@@ -334,7 +588,7 @@ class WidgetInspectorService { ...@@ -334,7 +588,7 @@ class WidgetInspectorService {
else else
throw new FlutterError('Cannot get parent chain for node of type ${value.runtimeType}'); throw new FlutterError('Cannot get parent chain for node of type ${value.runtimeType}');
return json.encode(path.map((_DiagnosticsPathNode node) => _pathNodeToJson(node, groupName)).toList()); return path.map((_DiagnosticsPathNode node) => _pathNodeToJson(node, groupName)).toList();
} }
Map<String, Object> _pathNodeToJson(_DiagnosticsPathNode pathNode, String groupName) { Map<String, Object> _pathNodeToJson(_DiagnosticsPathNode pathNode, String groupName) {
...@@ -392,8 +646,8 @@ class WidgetInspectorService { ...@@ -392,8 +646,8 @@ class WidgetInspectorService {
return false; return false;
} }
String _serialize(DiagnosticsNode node, String groupName) { Map<String, Object> _serializeToJson(DiagnosticsNode node, String groupName) {
return json.encode(_nodeToJson(node, groupName)); return _nodeToJson(node, groupName);
} }
List<Map<String, Object>> _nodesToJson(Iterable<DiagnosticsNode> nodes, String groupName) { List<Map<String, Object>> _nodesToJson(Iterable<DiagnosticsNode> nodes, String groupName) {
...@@ -404,28 +658,46 @@ class WidgetInspectorService { ...@@ -404,28 +658,46 @@ class WidgetInspectorService {
/// Returns a JSON representation of the properties of the [DiagnosticsNode] /// Returns a JSON representation of the properties of the [DiagnosticsNode]
/// object that `diagnosticsNodeId` references. /// object that `diagnosticsNodeId` references.
@protected
String getProperties(String diagnosticsNodeId, String groupName) { String getProperties(String diagnosticsNodeId, String groupName) {
return json.encode(_getProperties(diagnosticsNodeId, groupName));
}
List<Object> _getProperties(String diagnosticsNodeId, String groupName) {
final DiagnosticsNode node = toObject(diagnosticsNodeId); final DiagnosticsNode node = toObject(diagnosticsNodeId);
return json.encode(_nodesToJson(node == null ? const <DiagnosticsNode>[] : node.getProperties(), groupName)); return _nodesToJson(node == null ? const <DiagnosticsNode>[] : node.getProperties(), groupName);
} }
/// Returns a JSON representation of the children of the [DiagnosticsNode] /// Returns a JSON representation of the children of the [DiagnosticsNode]
/// object that `diagnosticsNodeId` references. /// object that `diagnosticsNodeId` references.
String getChildren(String diagnosticsNodeId, String groupName) { String getChildren(String diagnosticsNodeId, String groupName) {
return json.encode(_getChildren(diagnosticsNodeId, groupName));
}
List<Object> _getChildren(String diagnosticsNodeId, String groupName) {
final DiagnosticsNode node = toObject(diagnosticsNodeId); final DiagnosticsNode node = toObject(diagnosticsNodeId);
return json.encode(_nodesToJson(node == null ? const <DiagnosticsNode>[] : node.getChildren(), groupName)); return _nodesToJson(node == null ? const <DiagnosticsNode>[] : node.getChildren(), groupName);
} }
/// Returns a JSON representation of the [DiagnosticsNode] for the root /// Returns a JSON representation of the [DiagnosticsNode] for the root
/// [Element]. /// [Element].
String getRootWidget(String groupName) { String getRootWidget(String groupName) {
return _serialize(WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(), groupName); return json.encode(_getRootWidget(groupName));
}
Map<String, Object> _getRootWidget(String groupName) {
return _serializeToJson(WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(), groupName);
} }
/// Returns a JSON representation of the [DiagnosticsNode] for the root /// Returns a JSON representation of the [DiagnosticsNode] for the root
/// [RenderObject]. /// [RenderObject].
@protected
String getRootRenderObject(String groupName) { String getRootRenderObject(String groupName) {
return _serialize(RendererBinding.instance?.renderView?.toDiagnosticsNode(), groupName); return json.encode(_getRootRenderObject(groupName));
}
Map<String, Object> _getRootRenderObject(String groupName) {
return _serializeToJson(RendererBinding.instance?.renderView?.toDiagnosticsNode(), groupName);
} }
/// Returns a [DiagnosticsNode] representing the currently selected /// Returns a [DiagnosticsNode] representing the currently selected
...@@ -434,10 +706,15 @@ class WidgetInspectorService { ...@@ -434,10 +706,15 @@ class WidgetInspectorService {
/// If the currently selected [RenderObject] is identical to the /// If the currently selected [RenderObject] is identical to the
/// [RenderObject] referenced by `previousSelectionId` then the previous /// [RenderObject] referenced by `previousSelectionId` then the previous
/// [DiagnosticNode] is reused. /// [DiagnosticNode] is reused.
@protected
String getSelectedRenderObject(String previousSelectionId, String groupName) { String getSelectedRenderObject(String previousSelectionId, String groupName) {
return json.encode(_getSelectedRenderObject(previousSelectionId, groupName));
}
Map<String, Object> _getSelectedRenderObject(String previousSelectionId, String groupName) {
final DiagnosticsNode previousSelection = toObject(previousSelectionId); final DiagnosticsNode previousSelection = toObject(previousSelectionId);
final RenderObject current = selection?.current; final RenderObject current = selection?.current;
return _serialize(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), groupName); return _serializeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), groupName);
} }
/// Returns a [DiagnosticsNode] representing the currently selected [Element]. /// Returns a [DiagnosticsNode] representing the currently selected [Element].
...@@ -445,10 +722,15 @@ class WidgetInspectorService { ...@@ -445,10 +722,15 @@ class WidgetInspectorService {
/// If the currently selected [Element] is identical to the [Element] /// If the currently selected [Element] is identical to the [Element]
/// referenced by `previousSelectionId` then the previous [DiagnosticNode] is /// referenced by `previousSelectionId` then the previous [DiagnosticNode] is
/// reused. /// reused.
@protected
String getSelectedWidget(String previousSelectionId, String groupName) { String getSelectedWidget(String previousSelectionId, String groupName) {
return json.encode(_getSelectedWidget(previousSelectionId, groupName));
}
Map<String, Object> _getSelectedWidget(String previousSelectionId, String groupName) {
final DiagnosticsNode previousSelection = toObject(previousSelectionId); final DiagnosticsNode previousSelection = toObject(previousSelectionId);
final Element current = selection?.currentElement; final Element current = selection?.currentElement;
return _serialize(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), groupName); return _serializeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), groupName);
} }
/// Returns whether [Widget] creation locations are available. /// Returns whether [Widget] creation locations are available.
...@@ -457,6 +739,7 @@ class WidgetInspectorService { ...@@ -457,6 +739,7 @@ class WidgetInspectorService {
/// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 /// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0
/// is required as injecting creation locations requires a /// is required as injecting creation locations requires a
/// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). /// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation).
@protected
bool isWidgetCreationTracked() => new _WidgetForTypeTests() is _HasCreationLocation; bool isWidgetCreationTracked() => new _WidgetForTypeTests() is _HasCreationLocation;
} }
......
...@@ -506,9 +506,12 @@ void main() { ...@@ -506,9 +506,12 @@ void main() {
}); });
test('Service extensions - posttest', () async { test('Service extensions - posttest', () async {
// See widget_inspector_test.dart for tests of the 15 ext.flutter.inspector
// service extensions included in this count.
// If you add a service extension... TEST IT! :-) // If you add a service extension... TEST IT! :-)
// ...then increment this number. // ...then increment this number.
expect(binding.extensions.length, 17); expect(binding.extensions.length, 32);
expect(console, isEmpty); expect(console, isEmpty);
debugPrint = debugPrintThrottled; debugPrint = debugPrintThrottled;
......
...@@ -2,34 +2,66 @@ ...@@ -2,34 +2,66 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.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';
typedef FutureOr<Map<String, Object>> InspectorServiceExtensionCallback(Map<String, String> parameters);
void main() { void main() {
testWidgets('WidgetInspector smoke test', (WidgetTester tester) async { TestWidgetInspectorService.runTests();
// This is a smoke test to verify that adding the inspector doesn't crash. }
await tester.pumpWidget(
new Directionality( class TestWidgetInspectorService extends Object with WidgetInspectorService {
textDirection: TextDirection.ltr, final Map<String, InspectorServiceExtensionCallback> extensions = <String, InspectorServiceExtensionCallback>{};
child: new Stack(
children: const <Widget>[ @override
const Text('a', textDirection: TextDirection.ltr), void registerServiceExtension({
const Text('b', textDirection: TextDirection.ltr), @required String name,
const Text('c', textDirection: TextDirection.ltr), @required FutureOr<Map<String, Object>> callback(Map<String, String> parameters),
], }) {
), assert(!extensions.containsKey(name));
), extensions[name] = callback;
); }
await tester.pumpWidget( Future<Object> testExtension(String name, Map<String, String> arguments) async {
new Directionality( expect(extensions.containsKey(name), isTrue);
textDirection: TextDirection.ltr, // Encode and decode to JSON to match behavior using a real service
child: new WidgetInspector( // extension where only JSON is allowed.
selectButtonBuilder: null, return json.decode(json.encode(await extensions[name](arguments)))['result'];
}
Future<String> testBoolExtension(String name, Map<String, String> arguments) async {
expect(extensions.containsKey(name), isTrue);
// Encode and decode to JSON to match behavior using a real service
// extension where only JSON is allowed.
return json.decode(json.encode(await extensions[name](arguments)))['enabled'];
}
int rebuildCount = 0;
@override
Future<Null> forceRebuild() async {
rebuildCount++;
return null;
}
// These tests need access to protected members of WidgetInspectorService.
static void runTests() {
final TestWidgetInspectorService service = new TestWidgetInspectorService();
WidgetInspectorService.instance = service;
testWidgets('WidgetInspector smoke test', (WidgetTester tester) async {
// This is a smoke test to verify that adding the inspector doesn't crash.
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack( child: new Stack(
children: const <Widget>[ children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr), const Text('a', textDirection: TextDirection.ltr),
...@@ -38,93 +70,13 @@ void main() { ...@@ -38,93 +70,13 @@ void main() {
], ],
), ),
), ),
), );
);
await tester.pumpWidget(
expect(true, isTrue); // Expect that we reach here without crashing. new Directionality(
}); textDirection: TextDirection.ltr,
child: new WidgetInspector(
testWidgets('WidgetInspector interaction test', (WidgetTester tester) async { selectButtonBuilder: null,
final List<String> log = <String>[];
final GlobalKey selectButtonKey = new GlobalKey();
final GlobalKey inspectorKey = new GlobalKey();
final GlobalKey topButtonKey = new GlobalKey();
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return new Material(child: new RaisedButton(onPressed: onPressed, key: selectButtonKey));
}
// State type is private, hence using dynamic.
dynamic getInspectorState() => inspectorKey.currentState;
String paragraphText(RenderParagraph paragraph) => paragraph.text.text;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
key: inspectorKey,
selectButtonBuilder: selectButtonBuilder,
child: new Material(
child: new ListView(
children: <Widget>[
new RaisedButton(
key: topButtonKey,
onPressed: () {
log.add('top');
},
child: const Text('TOP'),
),
new RaisedButton(
onPressed: () {
log.add('bottom');
},
child: const Text('BOTTOM'),
),
],
),
),
),
),
);
expect(getInspectorState().selection.current, isNull);
await tester.tap(find.text('TOP'));
await tester.pump();
// Tap intercepted by the inspector
expect(log, equals(<String>[]));
final InspectorSelection selection = getInspectorState().selection;
expect(paragraphText(selection.current), equals('TOP'));
final RenderObject topButton = find.byKey(topButtonKey).evaluate().first.renderObject;
expect(selection.candidates.contains(topButton), isTrue);
await tester.tap(find.text('TOP'));
expect(log, equals(<String>['top']));
log.clear();
await tester.tap(find.text('BOTTOM'));
expect(log, equals(<String>['bottom']));
log.clear();
// Ensure the inspector selection has not changed to bottom.
expect(paragraphText(getInspectorState().selection.current), equals('TOP'));
await tester.tap(find.byKey(selectButtonKey));
await tester.pump();
// We are now back in select mode so tapping the bottom button will have
// not trigger a click but will cause it to be selected.
await tester.tap(find.text('BOTTOM'));
expect(log, equals(<String>[]));
log.clear();
expect(paragraphText(getInspectorState().selection.current), equals('BOTTOM'));
});
testWidgets('WidgetInspector non-invertible transform regression test', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
selectButtonBuilder: null,
child: new Transform(
transform: new Matrix4.identity()..scale(0.0),
child: new Stack( child: new Stack(
children: const <Widget>[ children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr), const Text('a', textDirection: TextDirection.ltr),
...@@ -134,536 +86,947 @@ void main() { ...@@ -134,536 +86,947 @@ void main() {
), ),
), ),
), ),
), );
);
expect(true, isTrue); // Expect that we reach here without crashing.
await tester.tap(find.byType(Transform)); });
expect(true, isTrue); // Expect that we reach here without crashing. testWidgets('WidgetInspector interaction test', (WidgetTester tester) async {
}); final List<String> log = <String>[];
final GlobalKey selectButtonKey = new GlobalKey();
testWidgets('WidgetInspector scroll test', (WidgetTester tester) async { final GlobalKey inspectorKey = new GlobalKey();
final Key childKey = new UniqueKey(); final GlobalKey topButtonKey = new GlobalKey();
final GlobalKey selectButtonKey = new GlobalKey();
final GlobalKey inspectorKey = new GlobalKey(); Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return new Material(child: new RaisedButton(onPressed: onPressed, key: selectButtonKey));
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) { }
return new Material(child: new RaisedButton(onPressed: onPressed, key: selectButtonKey)); // State type is private, hence using dynamic.
} dynamic getInspectorState() => inspectorKey.currentState;
// State type is private, hence using dynamic. String paragraphText(RenderParagraph paragraph) => paragraph.text.text;
dynamic getInspectorState() => inspectorKey.currentState;
await tester.pumpWidget(
await tester.pumpWidget( new Directionality(
new Directionality( textDirection: TextDirection.ltr,
textDirection: TextDirection.ltr, child: new WidgetInspector(
child: new WidgetInspector( key: inspectorKey,
key: inspectorKey, selectButtonBuilder: selectButtonBuilder,
selectButtonBuilder: selectButtonBuilder, child: new Material(
child: new ListView( child: new ListView(
children: <Widget>[ children: <Widget>[
new Container( new RaisedButton(
key: childKey, key: topButtonKey,
height: 5000.0, onPressed: () {
log.add('top');
},
child: const Text('TOP'),
),
new RaisedButton(
onPressed: () {
log.add('bottom');
},
child: const Text('BOTTOM'),
),
],
), ),
], ),
),
),
);
expect(getInspectorState().selection.current, isNull);
await tester.tap(find.text('TOP'));
await tester.pump();
// Tap intercepted by the inspector
expect(log, equals(<String>[]));
final InspectorSelection selection = getInspectorState().selection;
expect(paragraphText(selection.current), equals('TOP'));
final RenderObject topButton = find.byKey(topButtonKey).evaluate().first.renderObject;
expect(selection.candidates.contains(topButton), isTrue);
await tester.tap(find.text('TOP'));
expect(log, equals(<String>['top']));
log.clear();
await tester.tap(find.text('BOTTOM'));
expect(log, equals(<String>['bottom']));
log.clear();
// Ensure the inspector selection has not changed to bottom.
expect(paragraphText(getInspectorState().selection.current), equals('TOP'));
await tester.tap(find.byKey(selectButtonKey));
await tester.pump();
// We are now back in select mode so tapping the bottom button will have
// not trigger a click but will cause it to be selected.
await tester.tap(find.text('BOTTOM'));
expect(log, equals(<String>[]));
log.clear();
expect(paragraphText(getInspectorState().selection.current), equals('BOTTOM'));
});
testWidgets('WidgetInspector non-invertible transform regression test', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
selectButtonBuilder: null,
child: new Transform(
transform: new Matrix4.identity()..scale(0.0),
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
), ),
), ),
), );
);
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0)); await tester.tap(find.byType(Transform));
await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0); expect(true, isTrue); // Expect that we reach here without crashing.
await tester.pump(); });
// Fling does nothing as are in inspect mode. testWidgets('WidgetInspector scroll test', (WidgetTester tester) async {
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0)); final Key childKey = new UniqueKey();
final GlobalKey selectButtonKey = new GlobalKey();
final GlobalKey inspectorKey = new GlobalKey();
await tester.fling(find.byType(ListView), const Offset(200.0, 0.0), 200.0); Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
await tester.pump(); return new Material(child: new RaisedButton(onPressed: onPressed, key: selectButtonKey));
}
// State type is private, hence using dynamic.
dynamic getInspectorState() => inspectorKey.currentState;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
key: inspectorKey,
selectButtonBuilder: selectButtonBuilder,
child: new ListView(
children: <Widget>[
new Container(
key: childKey,
height: 5000.0,
),
],
),
),
),
);
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
// Fling still does nothing as are in inspect mode. await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0);
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0)); await tester.pump();
await tester.tap(find.byType(ListView)); // Fling does nothing as are in inspect mode.
await tester.pump(); expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
expect(getInspectorState().selection.current, isNotNull);
// Now out of inspect mode due to the click. await tester.fling(find.byType(ListView), const Offset(200.0, 0.0), 200.0);
await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0); await tester.pump();
await tester.pump();
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(-200.0)); // Fling still does nothing as are in inspect mode.
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
await tester.fling(find.byType(ListView), const Offset(0.0, 200.0), 200.0); await tester.tap(find.byType(ListView));
await tester.pump(); await tester.pump();
expect(getInspectorState().selection.current, isNotNull);
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0)); // Now out of inspect mode due to the click.
}); await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0);
await tester.pump();
testWidgets('WidgetInspector long press', (WidgetTester tester) async { expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(-200.0));
bool didLongPress = false;
await tester.pumpWidget( await tester.fling(find.byType(ListView), const Offset(0.0, 200.0), 200.0);
new Directionality( await tester.pump();
textDirection: TextDirection.ltr,
child: new WidgetInspector( expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
selectButtonBuilder: null, });
child: new GestureDetector(
onLongPress: () { testWidgets('WidgetInspector long press', (WidgetTester tester) async {
expect(didLongPress, isFalse); bool didLongPress = false;
didLongPress = true;
}, await tester.pumpWidget(
child: const Text('target', textDirection: TextDirection.ltr), new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
selectButtonBuilder: null,
child: new GestureDetector(
onLongPress: () {
expect(didLongPress, isFalse);
didLongPress = true;
},
child: const Text('target', textDirection: TextDirection.ltr),
),
), ),
), ),
), );
);
await tester.longPress(find.text('target'));
await tester.longPress(find.text('target')); // The inspector will swallow the long press.
// The inspector will swallow the long press. expect(didLongPress, isFalse);
expect(didLongPress, isFalse); });
});
testWidgets('WidgetInspector offstage', (WidgetTester tester) async {
testWidgets('WidgetInspector offstage', (WidgetTester tester) async { final GlobalKey inspectorKey = new GlobalKey();
final GlobalKey inspectorKey = new GlobalKey(); final GlobalKey clickTarget = new GlobalKey();
final GlobalKey clickTarget = new GlobalKey();
Widget createSubtree({ double width, Key key }) {
Widget createSubtree({ double width, Key key }) { return new Stack(
return new Stack( children: <Widget>[
children: <Widget>[ new Positioned(
new Positioned( key: key,
key: key, left: 0.0,
left: 0.0, top: 0.0,
top: 0.0, width: width,
width: width, height: 100.0,
height: 100.0, child: new Text(width.toString(), textDirection: TextDirection.ltr),
child: new Text(width.toString(), textDirection: TextDirection.ltr), ),
],
);
}
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
key: inspectorKey,
selectButtonBuilder: null,
child: new Overlay(
initialEntries: <OverlayEntry>[
new OverlayEntry(
opaque: false,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 94.0),
),
new OverlayEntry(
opaque: true,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 95.0),
),
new OverlayEntry(
opaque: false,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 96.0, key: clickTarget),
),
],
),
), ),
], ),
); );
}
await tester.pumpWidget( await tester.longPress(find.byKey(clickTarget));
new Directionality( // State type is private, hence using dynamic.
textDirection: TextDirection.ltr, final dynamic inspectorState = inspectorKey.currentState;
child: new WidgetInspector( // The object with width 95.0 wins over the object with width 94.0 because
key: inspectorKey, // the subtree with width 94.0 is offstage.
selectButtonBuilder: null, expect(inspectorState.selection.current.semanticBounds.width, equals(95.0));
child: new Overlay(
initialEntries: <OverlayEntry>[ // Exactly 2 out of the 3 text elements should be in the candidate list of
new OverlayEntry( // objects to select as only 2 are onstage.
opaque: false, expect(inspectorState.selection.candidates.where((RenderObject object) => object is RenderParagraph).length, equals(2));
maintainState: true, });
builder: (BuildContext _) => createSubtree(width: 94.0),
), test('WidgetInspectorService null id', () {
new OverlayEntry( service.disposeAllGroups();
opaque: true, expect(service.toObject(null), isNull);
maintainState: true, expect(service.toId(null, 'test-group'), isNull);
builder: (BuildContext _) => createSubtree(width: 95.0), });
),
new OverlayEntry( test('WidgetInspectorService dispose group', () {
opaque: false, service.disposeAllGroups();
maintainState: true, final Object a = new Object();
builder: (BuildContext _) => createSubtree(width: 96.0, key: clickTarget), const String group1 = 'group-1';
), const String group2 = 'group-2';
const String group3 = 'group-3';
final String aId = service.toId(a, group1);
expect(service.toId(a, group2), equals(aId));
expect(service.toId(a, group3), equals(aId));
service.disposeGroup(group1);
service.disposeGroup(group2);
expect(service.toObject(aId), equals(a));
service.disposeGroup(group3);
expect(() => service.toObject(aId), throwsFlutterError);
});
test('WidgetInspectorService dispose id', () {
service.disposeAllGroups();
final Object a = new Object();
final Object b = new Object();
const String group1 = 'group-1';
const String group2 = 'group-2';
final String aId = service.toId(a, group1);
final String bId = service.toId(b, group1);
expect(service.toId(a, group2), equals(aId));
service.disposeId(bId, group1);
expect(() => service.toObject(bId), throwsFlutterError);
service.disposeId(aId, group1);
expect(service.toObject(aId), equals(a));
service.disposeId(aId, group2);
expect(() => service.toObject(aId), throwsFlutterError);
});
test('WidgetInspectorService toObjectForSourceLocation', () {
const String group = 'test-group';
const Text widget = const Text('a', textDirection: TextDirection.ltr);
service.disposeAllGroups();
final String id = service.toId(widget, group);
expect(service.toObjectForSourceLocation(id), equals(widget));
final Element element = widget.createElement();
final String elementId = service.toId(element, group);
expect(service.toObjectForSourceLocation(elementId), equals(widget));
expect(element, isNot(equals(widget)));
service.disposeGroup(group);
expect(() => service.toObjectForSourceLocation(elementId), throwsFlutterError);
});
test('WidgetInspectorService object id test', () {
const Text a = const Text('a', textDirection: TextDirection.ltr);
const Text b = const Text('b', textDirection: TextDirection.ltr);
const Text c = const Text('c', textDirection: TextDirection.ltr);
const Text d = const Text('d', textDirection: TextDirection.ltr);
const String group1 = 'group-1';
const String group2 = 'group-2';
const String group3 = 'group-3';
service.disposeAllGroups();
final String aId = service.toId(a, group1);
final String bId = service.toId(b, group2);
final String cId = service.toId(c, group3);
final String dId = service.toId(d, group1);
// Make sure we get a consistent id if we add the object to a group multiple
// times.
expect(aId, equals(service.toId(a, group1)));
expect(service.toObject(aId), equals(a));
expect(service.toObject(aId), isNot(equals(b)));
expect(service.toObject(bId), equals(b));
expect(service.toObject(cId), equals(c));
expect(service.toObject(dId), equals(d));
// Make sure we get a consistent id even if we add the object to a different
// group.
expect(aId, equals(service.toId(a, group3)));
expect(aId, isNot(equals(bId)));
expect(aId, isNot(equals(cId)));
service.disposeGroup(group3);
});
testWidgets('WidgetInspectorService maybeSetSelection', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
], ],
), ),
), ),
), );
); final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
await tester.longPress(find.byKey(clickTarget));
// State type is private, hence using dynamic. service.disposeAllGroups();
final dynamic inspectorState = inspectorKey.currentState; service.selection.clear();
// The object with width 95.0 wins over the object with width 94.0 because int selectionChangedCount = 0;
// the subtree with width 94.0 is offstage. service.selectionChangedCallback = () => selectionChangedCount++;
expect(inspectorState.selection.current.semanticBounds.width, equals(95.0)); service.setSelection('invalid selection');
expect(selectionChangedCount, equals(0));
// Exactly 2 out of the 3 text elements should be in the candidate list of expect(service.selection.currentElement, isNull);
// objects to select as only 2 are onstage. service.setSelection(elementA);
expect(inspectorState.selection.candidates.where((RenderObject object) => object is RenderParagraph).length, equals(2)); expect(selectionChangedCount, equals(1));
}); expect(service.selection.currentElement, equals(elementA));
expect(service.selection.current, equals(elementA.renderObject));
test('WidgetInspectorService null id', () {
final WidgetInspectorService service = WidgetInspectorService.instance; service.setSelection(elementB.renderObject);
service.disposeAllGroups(); expect(selectionChangedCount, equals(2));
expect(service.toObject(null), isNull); expect(service.selection.current, equals(elementB.renderObject));
expect(service.toId(null, 'test-group'), isNull); expect(service.selection.currentElement, equals(elementB.renderObject.debugCreator.element));
});
service.setSelection('invalid selection');
test('WidgetInspectorService dispose group', () { expect(selectionChangedCount, equals(2));
final WidgetInspectorService service = WidgetInspectorService.instance; expect(service.selection.current, equals(elementB.renderObject));
service.disposeAllGroups();
final Object a = new Object(); service.setSelectionById(service.toId(elementA, 'my-group'));
const String group1 = 'group-1'; expect(selectionChangedCount, equals(3));
const String group2 = 'group-2'; expect(service.selection.currentElement, equals(elementA));
const String group3 = 'group-3'; expect(service.selection.current, equals(elementA.renderObject));
final String aId = service.toId(a, group1);
expect(service.toId(a, group2), equals(aId)); service.setSelectionById(service.toId(elementA, 'my-group'));
expect(service.toId(a, group3), equals(aId)); expect(selectionChangedCount, equals(3));
service.disposeGroup(group1); expect(service.selection.currentElement, equals(elementA));
service.disposeGroup(group2); });
expect(service.toObject(aId), equals(a));
service.disposeGroup(group3); testWidgets('WidgetInspectorService getParentChain', (WidgetTester tester) async {
expect(() => service.toObject(aId), throwsFlutterError); const String group = 'test-group';
});
await tester.pumpWidget(
test('WidgetInspectorService dispose id', () { new Directionality(
final WidgetInspectorService service = WidgetInspectorService.instance; textDirection: TextDirection.ltr,
service.disposeAllGroups(); child: new Stack(
final Object a = new Object(); children: const <Widget>[
final Object b = new Object(); const Text('a', textDirection: TextDirection.ltr),
const String group1 = 'group-1'; const Text('b', textDirection: TextDirection.ltr),
const String group2 = 'group-2'; const Text('c', textDirection: TextDirection.ltr),
final String aId = service.toId(a, group1); ],
final String bId = service.toId(b, group1); ),
expect(service.toId(a, group2), equals(aId));
service.disposeId(bId, group1);
expect(() => service.toObject(bId), throwsFlutterError);
service.disposeId(aId, group1);
expect(service.toObject(aId), equals(a));
service.disposeId(aId, group2);
expect(() => service.toObject(aId), throwsFlutterError);
});
test('WidgetInspectorService toObjectForSourceLocation', () {
const String group = 'test-group';
const Text widget = const Text('a', textDirection: TextDirection.ltr);
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
final String id = service.toId(widget, group);
expect(service.toObjectForSourceLocation(id), equals(widget));
final Element element = widget.createElement();
final String elementId = service.toId(element, group);
expect(service.toObjectForSourceLocation(elementId), equals(widget));
expect(element, isNot(equals(widget)));
service.disposeGroup(group);
expect(() => service.toObjectForSourceLocation(elementId), throwsFlutterError);
});
test('WidgetInspectorService object id test', () {
const Text a = const Text('a', textDirection: TextDirection.ltr);
const Text b = const Text('b', textDirection: TextDirection.ltr);
const Text c = const Text('c', textDirection: TextDirection.ltr);
const Text d = const Text('d', textDirection: TextDirection.ltr);
const String group1 = 'group-1';
const String group2 = 'group-2';
const String group3 = 'group-3';
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
final String aId = service.toId(a, group1);
final String bId = service.toId(b, group2);
final String cId = service.toId(c, group3);
final String dId = service.toId(d, group1);
// Make sure we get a consistent id if we add the object to a group multiple
// times.
expect(aId, equals(service.toId(a, group1)));
expect(service.toObject(aId), equals(a));
expect(service.toObject(aId), isNot(equals(b)));
expect(service.toObject(bId), equals(b));
expect(service.toObject(cId), equals(c));
expect(service.toObject(dId), equals(d));
// Make sure we get a consistent id even if we add the object to a different
// group.
expect(aId, equals(service.toId(a, group3)));
expect(aId, isNot(equals(bId)));
expect(aId, isNot(equals(cId)));
service.disposeGroup(group3);
});
testWidgets('WidgetInspectorService maybeSetSelection', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
), ),
), );
);
final Element elementA = find.text('a').evaluate().first; service.disposeAllGroups();
final Element elementB = find.text('b').evaluate().first; final Element elementB = find.text('b').evaluate().first;
final String bId = service.toId(elementB, group);
final WidgetInspectorService service = WidgetInspectorService.instance; final Object jsonList = json.decode(service.getParentChain(bId, group));
service.disposeAllGroups(); expect(jsonList, isList);
service.selection.clear(); final List<Object> chainElements = jsonList;
int selectionChangedCount = 0; final List<Element> expectedChain = elementB.debugGetDiagnosticChain()?.reversed?.toList();
service.selectionChangedCallback = () => selectionChangedCount++; // Sanity check that the chain goes back to the root.
service.setSelection('invalid selection'); expect(expectedChain.first, tester.binding.renderViewElement);
expect(selectionChangedCount, equals(0));
expect(service.selection.currentElement, isNull); expect(chainElements.length, equals(expectedChain.length));
service.setSelection(elementA); for (int i = 0; i < expectedChain.length; i += 1) {
expect(selectionChangedCount, equals(1)); expect(chainElements[i], isMap);
expect(service.selection.currentElement, equals(elementA)); final Map<String, Object> chainNode = chainElements[i];
expect(service.selection.current, equals(elementA.renderObject)); final Element element = expectedChain[i];
expect(chainNode['node'], isMap);
service.setSelection(elementB.renderObject); final Map<String, Object> jsonNode = chainNode['node'];
expect(selectionChangedCount, equals(2)); expect(service.toObject(jsonNode['valueId']), equals(element));
expect(service.selection.current, equals(elementB.renderObject)); expect(service.toObject(jsonNode['objectId']), const isInstanceOf<DiagnosticsNode>());
expect(service.selection.currentElement, equals(elementB.renderObject.debugCreator.element));
expect(chainNode['children'], isList);
service.setSelection('invalid selection'); final List<Object> jsonChildren = chainNode['children'];
expect(selectionChangedCount, equals(2)); final List<Element> childrenElements = <Element>[];
expect(service.selection.current, equals(elementB.renderObject)); element.visitChildren(childrenElements.add);
expect(jsonChildren.length, equals(childrenElements.length));
service.setSelectionById(service.toId(elementA, 'my-group')); if (i + 1 == expectedChain.length) {
expect(selectionChangedCount, equals(3)); expect(chainNode['childIndex'], isNull);
expect(service.selection.currentElement, equals(elementA)); } else {
expect(service.selection.current, equals(elementA.renderObject)); expect(chainNode['childIndex'], equals(childrenElements.indexOf(expectedChain[i+1])));
}
service.setSelectionById(service.toId(elementA, 'my-group')); for (int j = 0; j < childrenElements.length; j += 1) {
expect(selectionChangedCount, equals(3)); expect(jsonChildren[j], isMap);
expect(service.selection.currentElement, equals(elementA)); final Map<String, Object> childJson = jsonChildren[j];
}); expect(service.toObject(childJson['valueId']), equals(childrenElements[j]));
expect(service.toObject(childJson['objectId']), const isInstanceOf<DiagnosticsNode>());
testWidgets('WidgetInspectorService getParentChain', (WidgetTester tester) async { }
const String group = 'test-group'; }
});
await tester.pumpWidget(
new Directionality( test('WidgetInspectorService getProperties', () {
textDirection: TextDirection.ltr, final DiagnosticsNode diagnostic = const Text('a', textDirection: TextDirection.ltr).toDiagnosticsNode();
child: new Stack( const String group = 'group';
children: const <Widget>[ service.disposeAllGroups();
const Text('a', textDirection: TextDirection.ltr), final String id = service.toId(diagnostic, group);
const Text('b', textDirection: TextDirection.ltr), final List<Object> propertiesJson = json.decode(service.getProperties(id, group));
const Text('c', textDirection: TextDirection.ltr), final List<DiagnosticsNode> properties = diagnostic.getProperties();
], expect(properties, isNotEmpty);
expect(propertiesJson.length, equals(properties.length));
for (int i = 0; i < propertiesJson.length; ++i) {
final Map<String, Object> propertyJson = propertiesJson[i];
expect(service.toObject(propertyJson['valueId']), equals(properties[i].value));
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
});
testWidgets('WidgetInspectorService getChildren', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final DiagnosticsNode diagnostic = find.byType(Stack).evaluate().first.toDiagnosticsNode();
service.disposeAllGroups();
final String id = service.toId(diagnostic, group);
final List<Object> propertiesJson = json.decode(service.getChildren(id, group));
final List<DiagnosticsNode> children = diagnostic.getChildren();
expect(children.length, equals(3));
expect(propertiesJson.length, equals(children.length));
for (int i = 0; i < propertiesJson.length; ++i) {
final Map<String, Object> propertyJson = propertiesJson[i];
expect(service.toObject(propertyJson['valueId']), equals(children[i].value));
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
});
testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a'),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
service.disposeAllGroups();
service.setPubRootDirectories(<Object>[]);
service.setSelection(elementA, 'my-group');
final Map<String, Object> jsonA = json.decode(service.getSelectedWidget(null, 'my-group'));
final Map<String, Object> creationLocationA = jsonA['creationLocation'];
expect(creationLocationA, isNotNull);
final String fileA = creationLocationA['file'];
final int lineA = creationLocationA['line'];
final int columnA = creationLocationA['column'];
final List<Object> parameterLocationsA = creationLocationA['parameterLocations'];
service.setSelection(elementB, 'my-group');
final Map<String, Object> jsonB = json.decode(service.getSelectedWidget(null, 'my-group'));
final Map<String, Object> creationLocationB = jsonB['creationLocation'];
expect(creationLocationB, isNotNull);
final String fileB = creationLocationB['file'];
final int lineB = creationLocationB['line'];
final int columnB = creationLocationB['column'];
final List<Object> parameterLocationsB = creationLocationB['parameterLocations'];
expect(fileA, endsWith('widget_inspector_test.dart'));
expect(fileA, equals(fileB));
// We don't hardcode the actual lines the widgets are created on as that
// would make this test fragile.
expect(lineA + 1, equals(lineB));
// Column numbers are more stable than line numbers.
expect(columnA, equals(21));
expect(columnA, equals(columnB));
expect(parameterLocationsA.length, equals(1));
final Map<String, Object> paramA = parameterLocationsA[0];
expect(paramA['name'], equals('data'));
expect(paramA['line'], equals(lineA));
expect(paramA['column'], equals(26));
expect(parameterLocationsB.length, equals(2));
final Map<String, Object> paramB1 = parameterLocationsB[0];
expect(paramB1['name'], equals('data'));
expect(paramB1['line'], equals(lineB));
expect(paramB1['column'], equals(26));
final Map<String, Object> paramB2 = parameterLocationsB[1];
expect(paramB2['name'], equals('textDirection'));
expect(paramB2['line'], equals(lineB));
expect(paramB2['column'], equals(31));
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
testWidgets('WidgetInspectorService setPubRootDirectories', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a'),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
service.disposeAllGroups();
service.setPubRootDirectories(<Object>[]);
service.setSelection(elementA, 'my-group');
Map<String, Object> jsonObject = json.decode(service.getSelectedWidget(null, 'my-group'));
Map<String, Object> creationLocation = jsonObject['creationLocation'];
expect(creationLocation, isNotNull);
final String fileA = creationLocation['file'];
expect(fileA, endsWith('widget_inspector_test.dart'));
expect(jsonObject, isNot(contains('createdByLocalProject')));
final List<String> segments = Uri.parse(fileA).pathSegments;
// Strip a couple subdirectories away to generate a plausible pub root
// directory.
final String pubRootTest = '/' + segments.take(segments.length - 2).join('/');
service.setPubRootDirectories(<Object>[pubRootTest]);
service.setSelection(elementA, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setPubRootDirectories(<Object>['/invalid/$pubRootTest']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>['file://$pubRootTest']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setPubRootDirectories(<Object>['$pubRootTest/different']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>[
'/invalid/$pubRootTest',
pubRootTest,
]);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
// The RichText child of the Text widget is created by the core framework
// not the current package.
final Element richText = find.descendant(
of: find.text('a'),
matching: find.byType(RichText),
).evaluate().first;
service.setSelection(richText, 'my-group');
service.setPubRootDirectories(<Object>[pubRootTest]);
jsonObject = json.decode(service.getSelectedWidget(null, 'my-group'));
expect(jsonObject, isNot(contains('createdByLocalProject')));
creationLocation = jsonObject['creationLocation'];
expect(creationLocation, isNotNull);
// This RichText widget is created by the build method of the Text widget
// thus the creation location is in text.dart not basic.dart
final List<String> pathSegmentsFramework = Uri.parse(creationLocation['file']).pathSegments;
expect(pathSegmentsFramework.join('/'), endsWith('/packages/flutter/lib/src/widgets/text.dart'));
// Strip off /src/widgets/text.dart.
final String pubRootFramework = '/' + pathSegmentsFramework.take(pathSegmentsFramework.length - 3).join('/');
service.setPubRootDirectories(<Object>[pubRootFramework]);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setSelection(elementA, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>[pubRootFramework, pubRootTest]);
service.setSelection(elementA, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setSelection(richText, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
test('ext.flutter.inspector.disposeGroup', () async {
final Object a = new Object();
const String group1 = 'group-1';
const String group2 = 'group-2';
const String group3 = 'group-3';
final String aId = service.toId(a, group1);
expect(service.toId(a, group2), equals(aId));
expect(service.toId(a, group3), equals(aId));
await service.testExtension('disposeGroup', <String, String>{'objectGroup': group1});
await service.testExtension('disposeGroup', <String, String>{'objectGroup': group2});
expect(service.toObject(aId), equals(a));
await service.testExtension('disposeGroup', <String, String>{'objectGroup': group3});
expect(() => service.toObject(aId), throwsFlutterError);
});
test('ext.flutter.inspector.disposeId', () async {
final Object a = new Object();
final Object b = new Object();
const String group1 = 'group-1';
const String group2 = 'group-2';
final String aId = service.toId(a, group1);
final String bId = service.toId(b, group1);
expect(service.toId(a, group2), equals(aId));
await service.testExtension('disposeId', <String, String>{'arg': bId, 'objectGroup': group1});
expect(() => service.toObject(bId), throwsFlutterError);
await service.testExtension('disposeId', <String, String>{'arg': aId, 'objectGroup': group1});
expect(service.toObject(aId), equals(a));
await service.testExtension('disposeId', <String, String>{'arg': aId, 'objectGroup': group2});
expect(() => service.toObject(aId), throwsFlutterError);
});
testWidgets('ext.flutter.inspector.setSelection', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
service.disposeAllGroups();
service.selection.clear();
int selectionChangedCount = 0;
service.selectionChangedCallback = () => selectionChangedCount++;
service.setSelection('invalid selection');
expect(selectionChangedCount, equals(0));
expect(service.selection.currentElement, isNull);
service.setSelection(elementA);
expect(selectionChangedCount, equals(1));
expect(service.selection.currentElement, equals(elementA));
expect(service.selection.current, equals(elementA.renderObject));
service.setSelection(elementB.renderObject);
expect(selectionChangedCount, equals(2));
expect(service.selection.current, equals(elementB.renderObject));
expect(service.selection.currentElement, equals(elementB.renderObject.debugCreator.element));
service.setSelection('invalid selection');
expect(selectionChangedCount, equals(2));
expect(service.selection.current, equals(elementB.renderObject));
await service.testExtension('setSelectionById', <String, String>{'arg' : service.toId(elementA, 'my-group'), 'objectGroup': 'my-group'});
expect(selectionChangedCount, equals(3));
expect(service.selection.currentElement, equals(elementA));
expect(service.selection.current, equals(elementA.renderObject));
service.setSelectionById(service.toId(elementA, 'my-group'));
expect(selectionChangedCount, equals(3));
expect(service.selection.currentElement, equals(elementA));
});
testWidgets('ext.flutter.inspector.getParentChain', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
), ),
), );
);
final Element elementB = find.text('b').evaluate().first;
final WidgetInspectorService service = WidgetInspectorService.instance; final String bId = service.toId(elementB, group);
service.disposeAllGroups(); final Object jsonList = await service.testExtension('getParentChain', <String, String>{'arg': bId, 'objectGroup': group});
final Element elementB = find.text('b').evaluate().first; expect(jsonList, isList);
final String bId = service.toId(elementB, group); final List<Object> chainElements = jsonList;
final Object jsonList = json.decode(service.getParentChain(bId, group)); final List<Element> expectedChain = elementB.debugGetDiagnosticChain()?.reversed?.toList();
expect(jsonList, isList); // Sanity check that the chain goes back to the root.
final List<Object> chainElements = jsonList; expect(expectedChain.first, tester.binding.renderViewElement);
final List<Element> expectedChain = elementB.debugGetDiagnosticChain()?.reversed?.toList();
// Sanity check that the chain goes back to the root. expect(chainElements.length, equals(expectedChain.length));
expect(expectedChain.first, tester.binding.renderViewElement); for (int i = 0; i < expectedChain.length; i += 1) {
expect(chainElements[i], isMap);
expect(chainElements.length, equals(expectedChain.length)); final Map<String, Object> chainNode = chainElements[i];
for (int i = 0; i < expectedChain.length; i += 1) { final Element element = expectedChain[i];
expect(chainElements[i], isMap); expect(chainNode['node'], isMap);
final Map<String, Object> chainNode = chainElements[i]; final Map<String, Object> jsonNode = chainNode['node'];
final Element element = expectedChain[i]; expect(service.toObject(jsonNode['valueId']), equals(element));
expect(chainNode['node'], isMap); expect(service.toObject(jsonNode['objectId']), const isInstanceOf<DiagnosticsNode>());
final Map<String, Object> jsonNode = chainNode['node'];
expect(service.toObject(jsonNode['valueId']), equals(element)); expect(chainNode['children'], isList);
expect(service.toObject(jsonNode['objectId']), const isInstanceOf<DiagnosticsNode>()); final List<Object> jsonChildren = chainNode['children'];
final List<Element> childrenElements = <Element>[];
expect(chainNode['children'], isList); element.visitChildren(childrenElements.add);
final List<Object> jsonChildren = chainNode['children']; expect(jsonChildren.length, equals(childrenElements.length));
final List<Element> childrenElements = <Element>[]; if (i + 1 == expectedChain.length) {
element.visitChildren(childrenElements.add); expect(chainNode['childIndex'], isNull);
expect(jsonChildren.length, equals(childrenElements.length)); } else {
if (i + 1 == expectedChain.length) { expect(chainNode['childIndex'], equals(childrenElements.indexOf(expectedChain[i+1])));
expect(chainNode['childIndex'], isNull); }
} else { for (int j = 0; j < childrenElements.length; j += 1) {
expect(chainNode['childIndex'], equals(childrenElements.indexOf(expectedChain[i+1]))); expect(jsonChildren[j], isMap);
final Map<String, Object> childJson = jsonChildren[j];
expect(service.toObject(childJson['valueId']), equals(childrenElements[j]));
expect(service.toObject(childJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
} }
for (int j = 0; j < childrenElements.length; j += 1) { });
expect(jsonChildren[j], isMap);
final Map<String, Object> childJson = jsonChildren[j]; test('ext.flutter.inspector.getProperties', () async {
expect(service.toObject(childJson['valueId']), equals(childrenElements[j])); final DiagnosticsNode diagnostic = const Text('a', textDirection: TextDirection.ltr).toDiagnosticsNode();
expect(service.toObject(childJson['objectId']), const isInstanceOf<DiagnosticsNode>()); const String group = 'group';
final String id = service.toId(diagnostic, group);
final List<Object> propertiesJson = await service.testExtension('getProperties', <String, String>{'arg': id, 'objectGroup': group});
final List<DiagnosticsNode> properties = diagnostic.getProperties();
expect(properties, isNotEmpty);
expect(propertiesJson.length, equals(properties.length));
for (int i = 0; i < propertiesJson.length; ++i) {
final Map<String, Object> propertyJson = propertiesJson[i];
expect(service.toObject(propertyJson['valueId']), equals(properties[i].value));
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
} }
} });
});
testWidgets('ext.flutter.inspector.getChildren', (WidgetTester tester) async {
test('WidgetInspectorService getProperties', () { const String group = 'test-group';
final DiagnosticsNode diagnostic = const Text('a', textDirection: TextDirection.ltr).toDiagnosticsNode();
const String group = 'group'; await tester.pumpWidget(
final WidgetInspectorService service = WidgetInspectorService.instance; new Directionality(
service.disposeAllGroups(); textDirection: TextDirection.ltr,
final String id = service.toId(diagnostic, group); child: new Stack(
final List<Object> propertiesJson = json.decode(service.getProperties(id, group)); children: const <Widget>[
final List<DiagnosticsNode> properties = diagnostic.getProperties(); const Text('a', textDirection: TextDirection.ltr),
expect(properties, isNotEmpty); const Text('b', textDirection: TextDirection.ltr),
expect(propertiesJson.length, equals(properties.length)); const Text('c', textDirection: TextDirection.ltr),
for (int i = 0; i < propertiesJson.length; ++i) { ],
final Map<String, Object> propertyJson = propertiesJson[i]; ),
expect(service.toObject(propertyJson['valueId']), equals(properties[i].value));
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
});
testWidgets('WidgetInspectorService getChildren', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
), ),
), );
); final DiagnosticsNode diagnostic = find.byType(Stack).evaluate().first.toDiagnosticsNode();
final DiagnosticsNode diagnostic = find.byType(Stack).evaluate().first.toDiagnosticsNode(); final String id = service.toId(diagnostic, group);
final WidgetInspectorService service = WidgetInspectorService.instance; final List<Object> propertiesJson = await service.testExtension('getChildren', <String, String>{'arg': id, 'objectGroup': group});
service.disposeAllGroups(); final List<DiagnosticsNode> children = diagnostic.getChildren();
final String id = service.toId(diagnostic, group); expect(children.length, equals(3));
final List<Object> propertiesJson = json.decode(service.getChildren(id, group)); expect(propertiesJson.length, equals(children.length));
final List<DiagnosticsNode> children = diagnostic.getChildren(); for (int i = 0; i < propertiesJson.length; ++i) {
expect(children.length, equals(3)); final Map<String, Object> propertyJson = propertiesJson[i];
expect(propertiesJson.length, equals(children.length)); expect(service.toObject(propertyJson['valueId']), equals(children[i].value));
for (int i = 0; i < propertiesJson.length; ++i) { expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
final Map<String, Object> propertyJson = propertiesJson[i]; }
expect(service.toObject(propertyJson['valueId']), equals(children[i].value)); });
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
} testWidgets('ext.flutter.inspector creationLocation', (WidgetTester tester) async {
}); await tester.pumpWidget(
new Directionality(
testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async { textDirection: TextDirection.ltr,
final WidgetInspectorService service = WidgetInspectorService.instance; child: new Stack(
children: const <Widget>[
await tester.pumpWidget( const Text('a'),
new Directionality( const Text('b', textDirection: TextDirection.ltr),
textDirection: TextDirection.ltr, const Text('c', textDirection: TextDirection.ltr),
child: new Stack( ],
children: const <Widget>[ ),
const Text('a'),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
), ),
), );
); final Element elementA = find.text('a').evaluate().first;
final Element elementA = find.text('a').evaluate().first; final Element elementB = find.text('b').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
service.disposeAllGroups();
service.disposeAllGroups(); await service.testExtension('setPubRootDirectories', <String, String>{});
service.setPubRootDirectories(<Object>[]); service.setSelection(elementA, 'my-group');
service.setSelection(elementA, 'my-group'); final Map<String, Object> jsonA = await service.testExtension('getSelectedWidget', <String, String>{'arg': null, 'objectGroup': 'my-group'});
final Map<String, Object> jsonA = json.decode(service.getSelectedWidget(null, 'my-group')); final Map<String, Object> creationLocationA = jsonA['creationLocation'];
final Map<String, Object> creationLocationA = jsonA['creationLocation']; expect(creationLocationA, isNotNull);
expect(creationLocationA, isNotNull); final String fileA = creationLocationA['file'];
final String fileA = creationLocationA['file']; final int lineA = creationLocationA['line'];
final int lineA = creationLocationA['line']; final int columnA = creationLocationA['column'];
final int columnA = creationLocationA['column']; final List<Object> parameterLocationsA = creationLocationA['parameterLocations'];
final List<Object> parameterLocationsA = creationLocationA['parameterLocations'];
service.setSelection(elementB, 'my-group');
service.setSelection(elementB, 'my-group'); final Map<String, Object> jsonB = await service.testExtension('getSelectedWidget', <String, String>{'arg': null, 'objectGroup': 'my-group'});
final Map<String, Object> jsonB = json.decode(service.getSelectedWidget(null, 'my-group')); final Map<String, Object> creationLocationB = jsonB['creationLocation'];
final Map<String, Object> creationLocationB = jsonB['creationLocation']; expect(creationLocationB, isNotNull);
expect(creationLocationB, isNotNull); final String fileB = creationLocationB['file'];
final String fileB = creationLocationB['file']; final int lineB = creationLocationB['line'];
final int lineB = creationLocationB['line']; final int columnB = creationLocationB['column'];
final int columnB = creationLocationB['column']; final List<Object> parameterLocationsB = creationLocationB['parameterLocations'];
final List<Object> parameterLocationsB = creationLocationB['parameterLocations']; expect(fileA, endsWith('widget_inspector_test.dart'));
expect(fileA, endsWith('widget_inspector_test.dart')); expect(fileA, equals(fileB));
expect(fileA, equals(fileB)); // We don't hardcode the actual lines the widgets are created on as that
// We don't hardcode the actual lines the widgets are created on as that // would make this test fragile.
// would make this test fragile. expect(lineA + 1, equals(lineB));
expect(lineA + 1, equals(lineB)); // Column numbers are more stable than line numbers.
// Column numbers are more stable than line numbers. expect(columnA, equals(21));
expect(columnA, equals(19)); expect(columnA, equals(columnB));
expect(columnA, equals(columnB)); expect(parameterLocationsA.length, equals(1));
expect(parameterLocationsA.length, equals(1)); final Map<String, Object> paramA = parameterLocationsA[0];
final Map<String, Object> paramA = parameterLocationsA[0]; expect(paramA['name'], equals('data'));
expect(paramA['name'], equals('data')); expect(paramA['line'], equals(lineA));
expect(paramA['line'], equals(lineA)); expect(paramA['column'], equals(26));
expect(paramA['column'], equals(24));
expect(parameterLocationsB.length, equals(2));
expect(parameterLocationsB.length, equals(2)); final Map<String, Object> paramB1 = parameterLocationsB[0];
final Map<String, Object> paramB1 = parameterLocationsB[0]; expect(paramB1['name'], equals('data'));
expect(paramB1['name'], equals('data')); expect(paramB1['line'], equals(lineB));
expect(paramB1['line'], equals(lineB)); expect(paramB1['column'], equals(26));
expect(paramB1['column'], equals(24)); final Map<String, Object> paramB2 = parameterLocationsB[1];
final Map<String, Object> paramB2 = parameterLocationsB[1]; expect(paramB2['name'], equals('textDirection'));
expect(paramB2['name'], equals('textDirection')); expect(paramB2['line'], equals(lineB));
expect(paramB2['line'], equals(lineB)); expect(paramB2['column'], equals(31));
expect(paramB2['column'], equals(29)); }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
testWidgets('ext.flutter.inspector.setPubRootDirectories', (WidgetTester tester) async {
testWidgets('WidgetInspectorService setPubRootDirectories', (WidgetTester tester) async { await tester.pumpWidget(
final WidgetInspectorService service = WidgetInspectorService.instance; new Directionality(
textDirection: TextDirection.ltr,
await tester.pumpWidget( child: new Stack(
new Directionality( children: const <Widget>[
textDirection: TextDirection.ltr, const Text('a'),
child: new Stack( const Text('b', textDirection: TextDirection.ltr),
children: const <Widget>[ const Text('c', textDirection: TextDirection.ltr),
const Text('a'), ],
const Text('b', textDirection: TextDirection.ltr), ),
const Text('c', textDirection: TextDirection.ltr),
],
), ),
), );
); final Element elementA = find.text('a').evaluate().first;
final Element elementA = find.text('a').evaluate().first;
await service.testExtension('setPubRootDirectories', <String, String>{});
service.disposeAllGroups(); service.setSelection(elementA, 'my-group');
service.setPubRootDirectories(<Object>[]); Map<String, Object> jsonObject = await service.testExtension('getSelectedWidget', <String, String>{'arg': null, 'objectGroup': 'my-group'});
service.setSelection(elementA, 'my-group'); Map<String, Object> creationLocation = jsonObject['creationLocation'];
Map<String, Object> jsonObject = json.decode(service.getSelectedWidget(null, 'my-group')); expect(creationLocation, isNotNull);
Map<String, Object> creationLocation = jsonObject['creationLocation']; final String fileA = creationLocation['file'];
expect(creationLocation, isNotNull); expect(fileA, endsWith('widget_inspector_test.dart'));
final String fileA = creationLocation['file']; expect(jsonObject, isNot(contains('createdByLocalProject')));
expect(fileA, endsWith('widget_inspector_test.dart')); final List<String> segments = Uri.parse(fileA).pathSegments;
expect(jsonObject, isNot(contains('createdByLocalProject'))); // Strip a couple subdirectories away to generate a plausible pub root
final List<String> segments = Uri.parse(fileA).pathSegments; // directory.
// Strip a couple subdirectories away to generate a plausible pub root final String pubRootTest = '/' + segments.take(segments.length - 2).join('/');
// directory. await service.testExtension('setPubRootDirectories', <String, String>{'arg0': pubRootTest});
final String pubRootTest = '/' + segments.take(segments.length - 2).join('/');
service.setPubRootDirectories(<Object>[pubRootTest]); service.setSelection(elementA, 'my-group');
expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
service.setSelection(elementA, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject')); await service.testExtension('setPubRootDirectories', <String, String>{'arg0': '/invalid/$pubRootTest'});
expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>['/invalid/$pubRootTest']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject'))); await service.testExtension('setPubRootDirectories', <String, String>{'arg0': 'file://$pubRootTest'});
expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
service.setPubRootDirectories(<Object>['file://$pubRootTest']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject')); await service.testExtension('setPubRootDirectories', <String, String>{'arg0': '$pubRootTest/different'});
expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>['$pubRootTest/different']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject'))); await service.testExtension('setPubRootDirectories', <String, String>{
'arg0': '/unrelated/$pubRootTest',
service.setPubRootDirectories(<Object>[ 'arg1': 'file://$pubRootTest',
'/invalid/$pubRootTest', });
pubRootTest,
]); expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
// The RichText child of the Text widget is created by the core framework
// The RichText child of the Text widget is created by the core framework // not the current package.
// not the current package. final Element richText = find.descendant(
final Element richText = find.descendant( of: find.text('a'),
of: find.text('a'), matching: find.byType(RichText),
matching: find.byType(RichText), ).evaluate().first;
).evaluate().first; service.setSelection(richText, 'my-group');
service.setSelection(richText, 'my-group'); service.setPubRootDirectories(<Object>[pubRootTest]);
service.setPubRootDirectories(<Object>[pubRootTest]); jsonObject = json.decode(service.getSelectedWidget(null, 'my-group'));
jsonObject = json.decode(service.getSelectedWidget(null, 'my-group')); expect(jsonObject, isNot(contains('createdByLocalProject')));
expect(jsonObject, isNot(contains('createdByLocalProject'))); creationLocation = jsonObject['creationLocation'];
creationLocation = jsonObject['creationLocation']; expect(creationLocation, isNotNull);
expect(creationLocation, isNotNull); // This RichText widget is created by the build method of the Text widget
// This RichText widget is created by the build method of the Text widget // thus the creation location is in text.dart not basic.dart
// thus the creation location is in text.dart not basic.dart final List<String> pathSegmentsFramework = Uri.parse(creationLocation['file']).pathSegments;
final List<String> pathSegmentsFramework = Uri.parse(creationLocation['file']).pathSegments; expect(pathSegmentsFramework.join('/'), endsWith('/packages/flutter/lib/src/widgets/text.dart'));
expect(pathSegmentsFramework.join('/'), endsWith('/packages/flutter/lib/src/widgets/text.dart'));
// Strip off /src/widgets/text.dart.
// Strip off /src/widgets/text.dart. final String pubRootFramework = '/' + pathSegmentsFramework.take(pathSegmentsFramework.length - 3).join('/');
final String pubRootFramework = '/' + pathSegmentsFramework.take(pathSegmentsFramework.length - 3).join('/'); await service.testExtension('setPubRootDirectories', <String, String>{'arg0': pubRootFramework});
service.setPubRootDirectories(<Object>[pubRootFramework]); expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject')); service.setSelection(elementA, 'my-group');
service.setSelection(elementA, 'my-group'); expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), isNot(contains('createdByLocalProject')));
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
await service.testExtension('setPubRootDirectories', <String, String>{'arg0': pubRootFramework, 'arg1': pubRootTest});
service.setPubRootDirectories(<Object>[pubRootFramework, pubRootTest]); service.setSelection(elementA, 'my-group');
service.setSelection(elementA, 'my-group'); expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject')); service.setSelection(richText, 'my-group');
service.setSelection(richText, 'my-group'); expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject')); }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
testWidgets('ext.flutter.inspector.show', (WidgetTester tester) async {
service.rebuildCount = 0;
expect(await service.testBoolExtension('show', <String, String>{'enabled': 'true'}), equals('true'));
expect(service.rebuildCount, equals(1));
expect(await service.testBoolExtension('show', <String, String>{}), equals('true'));
expect(WidgetsApp.debugShowWidgetInspectorOverride, isTrue);
expect(await service.testBoolExtension('show', <String, String>{'enabled': 'true'}), equals('true'));
expect(service.rebuildCount, equals(1));
expect(await service.testBoolExtension('show', <String, String>{'enabled': 'false'}), equals('false'));
expect(await service.testBoolExtension('show', <String, String>{}), equals('false'));
expect(service.rebuildCount, equals(2));
expect(WidgetsApp.debugShowWidgetInspectorOverride, isFalse);
});
}
} }
...@@ -1183,7 +1183,7 @@ class Isolate extends ServiceObjectOwner { ...@@ -1183,7 +1183,7 @@ class Isolate extends ServiceObjectOwner {
Future<Map<String, dynamic>> flutterTogglePerformanceOverlayOverride() => _flutterToggle('showPerformanceOverlay'); Future<Map<String, dynamic>> flutterTogglePerformanceOverlayOverride() => _flutterToggle('showPerformanceOverlay');
Future<Map<String, dynamic>> flutterToggleWidgetInspector() => _flutterToggle('debugWidgetInspector'); Future<Map<String, dynamic>> flutterToggleWidgetInspector() => _flutterToggle('inspector.show');
Future<Null> flutterDebugAllowBanner(bool show) async { Future<Null> flutterDebugAllowBanner(bool show) async {
await invokeFlutterExtensionRpcRaw( await invokeFlutterExtensionRpcRaw(
......
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