diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 6daeac7e3d1..262270e5b2a 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -16,6 +16,7 @@ import 'package:flutter/services.dart'; import 'app.dart'; import 'focus_manager.dart'; import 'framework.dart'; +import 'widget_inspector.dart'; export 'dart:ui' show AppLifecycleState, Locale; @@ -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( name: 'debugWidgetInspector', getter: () async => WidgetsApp.debugShowWidgetInspectorOverride, @@ -295,6 +298,8 @@ abstract class WidgetsBinding extends BindingBase with SchedulerBinding, Gesture return _forceRebuild(); } ); + + WidgetInspectorService.instance.initServiceExtensions(registerServiceExtension); } Future _forceRebuild() { diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index ec8f5c18b2b..797d3ebeeb3 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:developer' as developer; @@ -14,6 +15,7 @@ import 'package:flutter/painting.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; +import 'app.dart'; import 'basic.dart'; import 'binding.dart'; import 'framework.dart'; @@ -23,6 +25,11 @@ import 'gesture_detector.dart'; /// [WidgetInspector.selectButtonBuilder]. 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] /// objects. /// @@ -96,6 +103,9 @@ class _InspectorReferenceData { int count = 1; } +class _WidgetInspectorService extends Object with WidgetInspectorService { +} + /// Service used by GUI tools to interact with the [WidgetInspector]. /// /// Calls to this object are typically made from GUI tools such as the [Flutter @@ -117,11 +127,19 @@ class _InspectorReferenceData { /// /// All methods returning String values return JSON. 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]. 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 /// GUI tools such as the Flutter IntelliJ Plugin and the [WidgetInspector] @@ -146,10 +164,232 @@ class WidgetInspectorService { List _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 callback(), + }) { + registerServiceExtension( + name: name, + callback: (Map parameters) async { + return {'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 callback(String objectGroup), + }) { + registerServiceExtension( + name: name, + callback: (Map parameters) async { + assert(parameters.containsKey('objectGroup')); + return {'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 getter, + @required AsyncValueSetter setter + }) { + assert(name != null); + assert(getter != null); + assert(setter != null); + registerServiceExtension( + name: name, + callback: (Map parameters) async { + if (parameters.containsKey('enabled')) + await setter(parameters['enabled'] == 'true'); + return { '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 callback(String objectId, String objectGroup), + }) { + registerServiceExtension( + name: name, + callback: (Map parameters) async { + assert(parameters.containsKey('objectGroup')); + return { + '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 callback(List args), + }) { + registerServiceExtension( + name: name, + callback: (Map parameters) async { + const String argPrefix = 'arg'; + final List args = []; + 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 {'result': await callback(args)}; + }, + ); + } + + @protected + Future forceRebuild() { + final WidgetsBinding binding = WidgetsBinding.instance; + if (binding.renderViewElement != null) { + binding.buildOwner.reassemble(binding.renderViewElement); + return binding.endOfFrame; + } + return new Future.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: + /// + /// * + 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.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. /// /// Use this method only for testing to ensure that object references from one /// test case do not impact other test cases. + @protected void disposeAllGroups() { _groups.clear(); _idToReferenceData.clear(); @@ -161,6 +401,7 @@ class WidgetInspectorService { /// /// Objects and their associated ids in the group may be kept alive by /// references from a different group. + @protected void disposeGroup(String name) { final Set<_InspectorReferenceData> references = _groups.remove(name); if (references == null) @@ -181,6 +422,7 @@ class WidgetInspectorService { /// 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 /// returned by this method. + @protected String toId(Object object, String groupName) { if (object == null) return null; @@ -205,6 +447,7 @@ class WidgetInspectorService { /// Returns whether the application has rendered its first frame and it is /// appropriate to display the Widget tree in the inspector. + @protected bool isWidgetTreeReady([String groupName]) { return WidgetsBinding.instance != null && WidgetsBinding.instance.debugDidSendFirstFrameEvent; @@ -215,6 +458,7 @@ class WidgetInspectorService { /// 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 /// Plugin. + @protected Object toObject(String id, [String groupName]) { if (id == null) return null; @@ -235,6 +479,7 @@ class WidgetInspectorService { /// /// The `groupName` parameter is not required by is added to regularize the /// API surface of methods called from the Flutter IntelliJ Plugin. + @protected Object toObjectForSourceLocation(String id, [String groupName]) { final Object object = toObject(id); if (object is Element) { @@ -248,6 +493,7 @@ class WidgetInspectorService { /// /// If the object exists in other groups it will remain alive and the object /// id will remain valid. + @protected void disposeId(String id, String groupName) { if (id == null) return; @@ -265,6 +511,7 @@ class WidgetInspectorService { /// /// The local project directories are used to distinguish widgets created by /// the local project over widgets created from inside the framework. + @protected void setPubRootDirectories(List pubRootDirectories) { _pubRootDirectories = pubRootDirectories.map( (Object directory) => Uri.parse(directory).path, @@ -278,6 +525,7 @@ class WidgetInspectorService { /// /// The `groupName` parameter is not required by is added to regularize the /// API surface of methods called from the Flutter IntelliJ Plugin. + @protected bool setSelectionById(String id, [String groupName]) { return setSelection(toObject(id), groupName); } @@ -289,6 +537,7 @@ class WidgetInspectorService { /// /// The `groupName` parameter is not needed but is specified to regularize the /// API surface of methods called from the Flutter IntelliJ Plugin. + @protected bool setSelection(Object object, [String groupName]) { if (object is Element || object is RenderObject) { if (object is Element) { @@ -324,7 +573,12 @@ class WidgetInspectorService { /// /// The JSON contains all information required to display a tree view with /// all nodes other than nodes along the path collapsed. + @protected String getParentChain(String id, String groupName) { + return json.encode(_getParentChain(id, groupName)); + } + + List _getParentChain(String id, String groupName) { final Object value = toObject(id); List<_DiagnosticsPathNode> path; if (value is RenderObject) @@ -334,7 +588,7 @@ class WidgetInspectorService { else 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 _pathNodeToJson(_DiagnosticsPathNode pathNode, String groupName) { @@ -392,8 +646,8 @@ class WidgetInspectorService { return false; } - String _serialize(DiagnosticsNode node, String groupName) { - return json.encode(_nodeToJson(node, groupName)); + Map _serializeToJson(DiagnosticsNode node, String groupName) { + return _nodeToJson(node, groupName); } List> _nodesToJson(Iterable nodes, String groupName) { @@ -404,28 +658,46 @@ class WidgetInspectorService { /// Returns a JSON representation of the properties of the [DiagnosticsNode] /// object that `diagnosticsNodeId` references. + @protected String getProperties(String diagnosticsNodeId, String groupName) { + return json.encode(_getProperties(diagnosticsNodeId, groupName)); + } + + List _getProperties(String diagnosticsNodeId, String groupName) { final DiagnosticsNode node = toObject(diagnosticsNodeId); - return json.encode(_nodesToJson(node == null ? const [] : node.getProperties(), groupName)); + return _nodesToJson(node == null ? const [] : node.getProperties(), groupName); } /// Returns a JSON representation of the children of the [DiagnosticsNode] /// object that `diagnosticsNodeId` references. String getChildren(String diagnosticsNodeId, String groupName) { + return json.encode(_getChildren(diagnosticsNodeId, groupName)); + } + + List _getChildren(String diagnosticsNodeId, String groupName) { final DiagnosticsNode node = toObject(diagnosticsNodeId); - return json.encode(_nodesToJson(node == null ? const [] : node.getChildren(), groupName)); + return _nodesToJson(node == null ? const [] : node.getChildren(), groupName); } /// Returns a JSON representation of the [DiagnosticsNode] for the root /// [Element]. String getRootWidget(String groupName) { - return _serialize(WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(), groupName); + return json.encode(_getRootWidget(groupName)); + } + + Map _getRootWidget(String groupName) { + return _serializeToJson(WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(), groupName); } /// Returns a JSON representation of the [DiagnosticsNode] for the root /// [RenderObject]. + @protected String getRootRenderObject(String groupName) { - return _serialize(RendererBinding.instance?.renderView?.toDiagnosticsNode(), groupName); + return json.encode(_getRootRenderObject(groupName)); + } + + Map _getRootRenderObject(String groupName) { + return _serializeToJson(RendererBinding.instance?.renderView?.toDiagnosticsNode(), groupName); } /// Returns a [DiagnosticsNode] representing the currently selected @@ -434,10 +706,15 @@ class WidgetInspectorService { /// If the currently selected [RenderObject] is identical to the /// [RenderObject] referenced by `previousSelectionId` then the previous /// [DiagnosticNode] is reused. + @protected String getSelectedRenderObject(String previousSelectionId, String groupName) { + return json.encode(_getSelectedRenderObject(previousSelectionId, groupName)); + } + + Map _getSelectedRenderObject(String previousSelectionId, String groupName) { final DiagnosticsNode previousSelection = toObject(previousSelectionId); 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]. @@ -445,10 +722,15 @@ class WidgetInspectorService { /// If the currently selected [Element] is identical to the [Element] /// referenced by `previousSelectionId` then the previous [DiagnosticNode] is /// reused. + @protected String getSelectedWidget(String previousSelectionId, String groupName) { + return json.encode(_getSelectedWidget(previousSelectionId, groupName)); + } + + Map _getSelectedWidget(String previousSelectionId, String groupName) { final DiagnosticsNode previousSelection = toObject(previousSelectionId); 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. @@ -457,6 +739,7 @@ class WidgetInspectorService { /// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 /// is required as injecting creation locations requires a /// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). + @protected bool isWidgetCreationTracked() => new _WidgetForTypeTests() is _HasCreationLocation; } diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart index 88335217651..b824cbfb6ae 100644 --- a/packages/flutter/test/foundation/service_extensions_test.dart +++ b/packages/flutter/test/foundation/service_extensions_test.dart @@ -506,9 +506,12 @@ void main() { }); 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! :-) // ...then increment this number. - expect(binding.extensions.length, 17); + expect(binding.extensions.length, 32); expect(console, isEmpty); debugPrint = debugPrintThrottled; diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index 0b380ce88dc..14643e74368 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -2,34 +2,66 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -void main() { - 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( - children: const [ - const Text('a', textDirection: TextDirection.ltr), - const Text('b', textDirection: TextDirection.ltr), - const Text('c', textDirection: TextDirection.ltr), - ], - ), - ), - ); +typedef FutureOr> InspectorServiceExtensionCallback(Map parameters); - await tester.pumpWidget( - new Directionality( - textDirection: TextDirection.ltr, - child: new WidgetInspector( - selectButtonBuilder: null, +void main() { + TestWidgetInspectorService.runTests(); +} + +class TestWidgetInspectorService extends Object with WidgetInspectorService { + final Map extensions = {}; + + @override + void registerServiceExtension({ + @required String name, + @required FutureOr> callback(Map parameters), + }) { + assert(!extensions.containsKey(name)); + extensions[name] = callback; + } + + Future testExtension(String name, Map 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)))['result']; + } + + Future testBoolExtension(String name, Map 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 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( children: const [ const Text('a', textDirection: TextDirection.ltr), @@ -38,93 +70,13 @@ void main() { ], ), ), - ), - ); + ); - expect(true, isTrue); // Expect that we reach here without crashing. - }); - - testWidgets('WidgetInspector interaction test', (WidgetTester tester) async { - final List log = []; - 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: [ - 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([])); - 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(['top'])); - log.clear(); - - await tester.tap(find.text('BOTTOM')); - expect(log, equals(['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([])); - 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), + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new WidgetInspector( + selectButtonBuilder: null, child: new Stack( children: const [ const Text('a', textDirection: TextDirection.ltr), @@ -134,536 +86,947 @@ void main() { ), ), ), - ), - ); - - await tester.tap(find.byType(Transform)); - - expect(true, isTrue); // Expect that we reach here without crashing. - }); - - testWidgets('WidgetInspector scroll test', (WidgetTester tester) async { - final Key childKey = new UniqueKey(); - 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)); - } - // 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: [ - new Container( - key: childKey, - height: 5000.0, - ), - ], - ), - ), - ), - ); - 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.pump(); - - // Fling 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(200.0, 0.0), 200.0); - await tester.pump(); - - // Fling still does nothing as are in inspect mode. - expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0)); - - await tester.tap(find.byType(ListView)); - await tester.pump(); - expect(getInspectorState().selection.current, isNotNull); - - // 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(); - - expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(-200.0)); - - await tester.fling(find.byType(ListView), const Offset(0.0, 200.0), 200.0); - await tester.pump(); - - expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0)); - }); - - testWidgets('WidgetInspector long press', (WidgetTester tester) async { - bool didLongPress = false; - - await tester.pumpWidget( - 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')); - // The inspector will swallow the long press. - expect(didLongPress, isFalse); - }); - - testWidgets('WidgetInspector offstage', (WidgetTester tester) async { - final GlobalKey inspectorKey = new GlobalKey(); - final GlobalKey clickTarget = new GlobalKey(); - - Widget createSubtree({ double width, Key key }) { - return new Stack( - children: [ - new Positioned( - key: key, - left: 0.0, - top: 0.0, - width: width, - height: 100.0, - 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: [ - new OverlayEntry( - opaque: false, - maintainState: true, - builder: (BuildContext _) => createSubtree(width: 94.0), + + expect(true, isTrue); // Expect that we reach here without crashing. + }); + + testWidgets('WidgetInspector interaction test', (WidgetTester tester) async { + final List log = []; + 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: [ + new RaisedButton( + key: topButtonKey, + onPressed: () { + log.add('top'); + }, + child: const Text('TOP'), + ), + new RaisedButton( + onPressed: () { + log.add('bottom'); + }, + child: const Text('BOTTOM'), + ), + ], ), - 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), + ), + ), + ), + ); + + expect(getInspectorState().selection.current, isNull); + await tester.tap(find.text('TOP')); + await tester.pump(); + // Tap intercepted by the inspector + expect(log, equals([])); + 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(['top'])); + log.clear(); + + await tester.tap(find.text('BOTTOM')); + expect(log, equals(['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([])); + 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 [ + const Text('a', textDirection: TextDirection.ltr), + const Text('b', textDirection: TextDirection.ltr), + const Text('c', textDirection: TextDirection.ltr), + ], ), + ), + ), + ), + ); + + await tester.tap(find.byType(Transform)); + + expect(true, isTrue); // Expect that we reach here without crashing. + }); + + testWidgets('WidgetInspector scroll test', (WidgetTester tester) async { + final Key childKey = new UniqueKey(); + 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)); + } + // 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: [ + new Container( + key: childKey, + height: 5000.0, + ), + ], + ), + ), + ), + ); + 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.pump(); + + // Fling 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(200.0, 0.0), 200.0); + await tester.pump(); + + // Fling still does nothing as are in inspect mode. + expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0)); + + await tester.tap(find.byType(ListView)); + await tester.pump(); + expect(getInspectorState().selection.current, isNotNull); + + // 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(); + + expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(-200.0)); + + await tester.fling(find.byType(ListView), const Offset(0.0, 200.0), 200.0); + await tester.pump(); + + expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0)); + }); + + testWidgets('WidgetInspector long press', (WidgetTester tester) async { + bool didLongPress = false; + + await tester.pumpWidget( + 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')); + // The inspector will swallow the long press. + expect(didLongPress, isFalse); + }); + + testWidgets('WidgetInspector offstage', (WidgetTester tester) async { + final GlobalKey inspectorKey = new GlobalKey(); + final GlobalKey clickTarget = new GlobalKey(); + + Widget createSubtree({ double width, Key key }) { + return new Stack( + children: [ + new Positioned( + key: key, + left: 0.0, + top: 0.0, + width: width, + height: 100.0, + 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: [ + 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.longPress(find.byKey(clickTarget)); + // State type is private, hence using dynamic. + final dynamic inspectorState = inspectorKey.currentState; + // The object with width 95.0 wins over the object with width 94.0 because + // the subtree with width 94.0 is offstage. + expect(inspectorState.selection.current.semanticBounds.width, equals(95.0)); + + // Exactly 2 out of the 3 text elements should be in the candidate list of + // objects to select as only 2 are onstage. + expect(inspectorState.selection.candidates.where((RenderObject object) => object is RenderParagraph).length, equals(2)); + }); + + test('WidgetInspectorService null id', () { + service.disposeAllGroups(); + expect(service.toObject(null), isNull); + expect(service.toId(null, 'test-group'), isNull); + }); + + test('WidgetInspectorService dispose group', () { + service.disposeAllGroups(); + 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)); + 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 [ + 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. - final dynamic inspectorState = inspectorKey.currentState; - // The object with width 95.0 wins over the object with width 94.0 because - // the subtree with width 94.0 is offstage. - expect(inspectorState.selection.current.semanticBounds.width, equals(95.0)); + 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)); - // Exactly 2 out of the 3 text elements should be in the candidate list of - // objects to select as only 2 are onstage. - expect(inspectorState.selection.candidates.where((RenderObject object) => object is RenderParagraph).length, equals(2)); - }); + service.setSelection(elementB.renderObject); + expect(selectionChangedCount, equals(2)); + expect(service.selection.current, equals(elementB.renderObject)); + expect(service.selection.currentElement, equals(elementB.renderObject.debugCreator.element)); - test('WidgetInspectorService null id', () { - final WidgetInspectorService service = WidgetInspectorService.instance; - service.disposeAllGroups(); - expect(service.toObject(null), isNull); - expect(service.toId(null, 'test-group'), isNull); - }); + service.setSelection('invalid selection'); + expect(selectionChangedCount, equals(2)); + expect(service.selection.current, equals(elementB.renderObject)); - test('WidgetInspectorService dispose group', () { - final WidgetInspectorService service = WidgetInspectorService.instance; - service.disposeAllGroups(); - 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)); - service.disposeGroup(group1); - service.disposeGroup(group2); - expect(service.toObject(aId), equals(a)); - service.disposeGroup(group3); - expect(() => service.toObject(aId), throwsFlutterError); - }); + service.setSelectionById(service.toId(elementA, 'my-group')); + expect(selectionChangedCount, equals(3)); + expect(service.selection.currentElement, equals(elementA)); + expect(service.selection.current, equals(elementA.renderObject)); - test('WidgetInspectorService dispose id', () { - final WidgetInspectorService service = WidgetInspectorService.instance; - 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); - }); + service.setSelectionById(service.toId(elementA, 'my-group')); + expect(selectionChangedCount, equals(3)); + expect(service.selection.currentElement, equals(elementA)); + }); - 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); - }); + testWidgets('WidgetInspectorService getParentChain', (WidgetTester tester) async { + const String group = 'test-group'; - 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 [ - const Text('a', textDirection: TextDirection.ltr), - const Text('b', textDirection: TextDirection.ltr), - const Text('c', textDirection: TextDirection.ltr), - ], + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: const [ + 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; + ); - final WidgetInspectorService service = WidgetInspectorService.instance; - 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.disposeAllGroups(); + final Element elementB = find.text('b').evaluate().first; + final String bId = service.toId(elementB, group); + final Object jsonList = json.decode(service.getParentChain(bId, group)); + expect(jsonList, isList); + final List chainElements = jsonList; + final List expectedChain = elementB.debugGetDiagnosticChain()?.reversed?.toList(); + // Sanity check that the chain goes back to the root. + expect(expectedChain.first, tester.binding.renderViewElement); - service.setSelection(elementB.renderObject); - expect(selectionChangedCount, equals(2)); - expect(service.selection.current, equals(elementB.renderObject)); - expect(service.selection.currentElement, equals(elementB.renderObject.debugCreator.element)); + expect(chainElements.length, equals(expectedChain.length)); + for (int i = 0; i < expectedChain.length; i += 1) { + expect(chainElements[i], isMap); + final Map chainNode = chainElements[i]; + final Element element = expectedChain[i]; + expect(chainNode['node'], isMap); + final Map jsonNode = chainNode['node']; + expect(service.toObject(jsonNode['valueId']), equals(element)); + expect(service.toObject(jsonNode['objectId']), const isInstanceOf()); - service.setSelection('invalid selection'); - expect(selectionChangedCount, equals(2)); - expect(service.selection.current, equals(elementB.renderObject)); - - service.setSelectionById(service.toId(elementA, '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('WidgetInspectorService getParentChain', (WidgetTester tester) async { - const String group = 'test-group'; - - await tester.pumpWidget( - new Directionality( - textDirection: TextDirection.ltr, - child: new Stack( - children: const [ - const Text('a', textDirection: TextDirection.ltr), - const Text('b', textDirection: TextDirection.ltr), - const Text('c', textDirection: TextDirection.ltr), - ], - ), - ), - ); - - final WidgetInspectorService service = WidgetInspectorService.instance; - service.disposeAllGroups(); - final Element elementB = find.text('b').evaluate().first; - final String bId = service.toId(elementB, group); - final Object jsonList = json.decode(service.getParentChain(bId, group)); - expect(jsonList, isList); - final List chainElements = jsonList; - final List expectedChain = elementB.debugGetDiagnosticChain()?.reversed?.toList(); - // Sanity check that the chain goes back to the root. - expect(expectedChain.first, tester.binding.renderViewElement); - - expect(chainElements.length, equals(expectedChain.length)); - for (int i = 0; i < expectedChain.length; i += 1) { - expect(chainElements[i], isMap); - final Map chainNode = chainElements[i]; - final Element element = expectedChain[i]; - expect(chainNode['node'], isMap); - final Map jsonNode = chainNode['node']; - expect(service.toObject(jsonNode['valueId']), equals(element)); - expect(service.toObject(jsonNode['objectId']), const isInstanceOf()); - - expect(chainNode['children'], isList); - final List jsonChildren = chainNode['children']; - final List childrenElements = []; - element.visitChildren(childrenElements.add); - expect(jsonChildren.length, equals(childrenElements.length)); - if (i + 1 == expectedChain.length) { - expect(chainNode['childIndex'], isNull); - } else { - expect(chainNode['childIndex'], equals(childrenElements.indexOf(expectedChain[i+1]))); + expect(chainNode['children'], isList); + final List jsonChildren = chainNode['children']; + final List childrenElements = []; + element.visitChildren(childrenElements.add); + expect(jsonChildren.length, equals(childrenElements.length)); + if (i + 1 == expectedChain.length) { + expect(chainNode['childIndex'], isNull); + } else { + expect(chainNode['childIndex'], equals(childrenElements.indexOf(expectedChain[i+1]))); + } + for (int j = 0; j < childrenElements.length; j += 1) { + expect(jsonChildren[j], isMap); + final Map childJson = jsonChildren[j]; + expect(service.toObject(childJson['valueId']), equals(childrenElements[j])); + expect(service.toObject(childJson['objectId']), const isInstanceOf()); + } } - for (int j = 0; j < childrenElements.length; j += 1) { - expect(jsonChildren[j], isMap); - final Map childJson = jsonChildren[j]; - expect(service.toObject(childJson['valueId']), equals(childrenElements[j])); - expect(service.toObject(childJson['objectId']), const isInstanceOf()); + }); + + test('WidgetInspectorService getProperties', () { + final DiagnosticsNode diagnostic = const Text('a', textDirection: TextDirection.ltr).toDiagnosticsNode(); + const String group = 'group'; + service.disposeAllGroups(); + final String id = service.toId(diagnostic, group); + final List propertiesJson = json.decode(service.getProperties(id, group)); + final List properties = diagnostic.getProperties(); + expect(properties, isNotEmpty); + expect(propertiesJson.length, equals(properties.length)); + for (int i = 0; i < propertiesJson.length; ++i) { + final Map propertyJson = propertiesJson[i]; + expect(service.toObject(propertyJson['valueId']), equals(properties[i].value)); + expect(service.toObject(propertyJson['objectId']), const isInstanceOf()); } - } - }); + }); - test('WidgetInspectorService getProperties', () { - final DiagnosticsNode diagnostic = const Text('a', textDirection: TextDirection.ltr).toDiagnosticsNode(); - const String group = 'group'; - final WidgetInspectorService service = WidgetInspectorService.instance; - service.disposeAllGroups(); - final String id = service.toId(diagnostic, group); - final List propertiesJson = json.decode(service.getProperties(id, group)); - final List properties = diagnostic.getProperties(); - expect(properties, isNotEmpty); - expect(propertiesJson.length, equals(properties.length)); - for (int i = 0; i < propertiesJson.length; ++i) { - final Map propertyJson = propertiesJson[i]; - expect(service.toObject(propertyJson['valueId']), equals(properties[i].value)); - expect(service.toObject(propertyJson['objectId']), const isInstanceOf()); - } - }); + testWidgets('WidgetInspectorService getChildren', (WidgetTester tester) async { + const String group = 'test-group'; - testWidgets('WidgetInspectorService getChildren', (WidgetTester tester) async { - const String group = 'test-group'; - - await tester.pumpWidget( - new Directionality( - textDirection: TextDirection.ltr, - child: new Stack( - children: const [ - const Text('a', textDirection: TextDirection.ltr), - const Text('b', textDirection: TextDirection.ltr), - const Text('c', textDirection: TextDirection.ltr), - ], + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: const [ + 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 WidgetInspectorService service = WidgetInspectorService.instance; - service.disposeAllGroups(); - final String id = service.toId(diagnostic, group); - final List propertiesJson = json.decode(service.getChildren(id, group)); - final List children = diagnostic.getChildren(); - expect(children.length, equals(3)); - expect(propertiesJson.length, equals(children.length)); - for (int i = 0; i < propertiesJson.length; ++i) { - final Map propertyJson = propertiesJson[i]; - expect(service.toObject(propertyJson['valueId']), equals(children[i].value)); - expect(service.toObject(propertyJson['objectId']), const isInstanceOf()); - } - }); + ); + final DiagnosticsNode diagnostic = find.byType(Stack).evaluate().first.toDiagnosticsNode(); + service.disposeAllGroups(); + final String id = service.toId(diagnostic, group); + final List propertiesJson = json.decode(service.getChildren(id, group)); + final List children = diagnostic.getChildren(); + expect(children.length, equals(3)); + expect(propertiesJson.length, equals(children.length)); + for (int i = 0; i < propertiesJson.length; ++i) { + final Map propertyJson = propertiesJson[i]; + expect(service.toObject(propertyJson['valueId']), equals(children[i].value)); + expect(service.toObject(propertyJson['objectId']), const isInstanceOf()); + } + }); - testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async { - final WidgetInspectorService service = WidgetInspectorService.instance; - - await tester.pumpWidget( - new Directionality( - textDirection: TextDirection.ltr, - child: new Stack( - children: const [ - const Text('a'), - const Text('b', textDirection: TextDirection.ltr), - const Text('c', textDirection: TextDirection.ltr), - ], + testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: const [ + 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; + ); + final Element elementA = find.text('a').evaluate().first; + final Element elementB = find.text('b').evaluate().first; - service.disposeAllGroups(); - service.setPubRootDirectories([]); - service.setSelection(elementA, 'my-group'); - final Map jsonA = json.decode(service.getSelectedWidget(null, 'my-group')); - final Map creationLocationA = jsonA['creationLocation']; - expect(creationLocationA, isNotNull); - final String fileA = creationLocationA['file']; - final int lineA = creationLocationA['line']; - final int columnA = creationLocationA['column']; - final List parameterLocationsA = creationLocationA['parameterLocations']; + service.disposeAllGroups(); + service.setPubRootDirectories([]); + service.setSelection(elementA, 'my-group'); + final Map jsonA = json.decode(service.getSelectedWidget(null, 'my-group')); + final Map creationLocationA = jsonA['creationLocation']; + expect(creationLocationA, isNotNull); + final String fileA = creationLocationA['file']; + final int lineA = creationLocationA['line']; + final int columnA = creationLocationA['column']; + final List parameterLocationsA = creationLocationA['parameterLocations']; - service.setSelection(elementB, 'my-group'); - final Map jsonB = json.decode(service.getSelectedWidget(null, 'my-group')); - final Map creationLocationB = jsonB['creationLocation']; - expect(creationLocationB, isNotNull); - final String fileB = creationLocationB['file']; - final int lineB = creationLocationB['line']; - final int columnB = creationLocationB['column']; - final List 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(19)); - expect(columnA, equals(columnB)); - expect(parameterLocationsA.length, equals(1)); - final Map paramA = parameterLocationsA[0]; - expect(paramA['name'], equals('data')); - expect(paramA['line'], equals(lineA)); - expect(paramA['column'], equals(24)); + service.setSelection(elementB, 'my-group'); + final Map jsonB = json.decode(service.getSelectedWidget(null, 'my-group')); + final Map creationLocationB = jsonB['creationLocation']; + expect(creationLocationB, isNotNull); + final String fileB = creationLocationB['file']; + final int lineB = creationLocationB['line']; + final int columnB = creationLocationB['column']; + final List 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 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 paramB1 = parameterLocationsB[0]; - expect(paramB1['name'], equals('data')); - expect(paramB1['line'], equals(lineB)); - expect(paramB1['column'], equals(24)); - final Map paramB2 = parameterLocationsB[1]; - expect(paramB2['name'], equals('textDirection')); - expect(paramB2['line'], equals(lineB)); - expect(paramB2['column'], equals(29)); - }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag. + expect(parameterLocationsB.length, equals(2)); + final Map paramB1 = parameterLocationsB[0]; + expect(paramB1['name'], equals('data')); + expect(paramB1['line'], equals(lineB)); + expect(paramB1['column'], equals(26)); + final Map 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 { - final WidgetInspectorService service = WidgetInspectorService.instance; - - await tester.pumpWidget( - new Directionality( - textDirection: TextDirection.ltr, - child: new Stack( - children: const [ - const Text('a'), - const Text('b', textDirection: TextDirection.ltr), - const Text('c', textDirection: TextDirection.ltr), - ], + testWidgets('WidgetInspectorService setPubRootDirectories', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: const [ + 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; - service.disposeAllGroups(); - service.setPubRootDirectories([]); - service.setSelection(elementA, 'my-group'); - Map jsonObject = json.decode(service.getSelectedWidget(null, 'my-group')); - Map 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 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([pubRootTest]); + service.disposeAllGroups(); + service.setPubRootDirectories([]); + service.setSelection(elementA, 'my-group'); + Map jsonObject = json.decode(service.getSelectedWidget(null, 'my-group')); + Map 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 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([pubRootTest]); - service.setSelection(elementA, 'my-group'); - expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject')); + service.setSelection(elementA, 'my-group'); + expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject')); - service.setPubRootDirectories(['/invalid/$pubRootTest']); - expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject'))); + service.setPubRootDirectories(['/invalid/$pubRootTest']); + expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject'))); - service.setPubRootDirectories(['file://$pubRootTest']); - expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject')); + service.setPubRootDirectories(['file://$pubRootTest']); + expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject')); - service.setPubRootDirectories(['$pubRootTest/different']); - expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject'))); + service.setPubRootDirectories(['$pubRootTest/different']); + expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject'))); - service.setPubRootDirectories([ - '/invalid/$pubRootTest', - pubRootTest, - ]); - expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject')); + service.setPubRootDirectories([ + '/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([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 pathSegmentsFramework = Uri.parse(creationLocation['file']).pathSegments; - expect(pathSegmentsFramework.join('/'), endsWith('/packages/flutter/lib/src/widgets/text.dart')); + // 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([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 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([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'))); + // Strip off /src/widgets/text.dart. + final String pubRootFramework = '/' + pathSegmentsFramework.take(pathSegmentsFramework.length - 3).join('/'); + service.setPubRootDirectories([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([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. + service.setPubRootDirectories([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', {'objectGroup': group1}); + await service.testExtension('disposeGroup', {'objectGroup': group2}); + expect(service.toObject(aId), equals(a)); + await service.testExtension('disposeGroup', {'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', {'arg': bId, 'objectGroup': group1}); + expect(() => service.toObject(bId), throwsFlutterError); + await service.testExtension('disposeId', {'arg': aId, 'objectGroup': group1}); + expect(service.toObject(aId), equals(a)); + await service.testExtension('disposeId', {'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 [ + 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', {'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 [ + 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 String bId = service.toId(elementB, group); + final Object jsonList = await service.testExtension('getParentChain', {'arg': bId, 'objectGroup': group}); + expect(jsonList, isList); + final List chainElements = jsonList; + final List expectedChain = elementB.debugGetDiagnosticChain()?.reversed?.toList(); + // Sanity check that the chain goes back to the root. + expect(expectedChain.first, tester.binding.renderViewElement); + + expect(chainElements.length, equals(expectedChain.length)); + for (int i = 0; i < expectedChain.length; i += 1) { + expect(chainElements[i], isMap); + final Map chainNode = chainElements[i]; + final Element element = expectedChain[i]; + expect(chainNode['node'], isMap); + final Map jsonNode = chainNode['node']; + expect(service.toObject(jsonNode['valueId']), equals(element)); + expect(service.toObject(jsonNode['objectId']), const isInstanceOf()); + + expect(chainNode['children'], isList); + final List jsonChildren = chainNode['children']; + final List childrenElements = []; + element.visitChildren(childrenElements.add); + expect(jsonChildren.length, equals(childrenElements.length)); + if (i + 1 == expectedChain.length) { + expect(chainNode['childIndex'], isNull); + } else { + expect(chainNode['childIndex'], equals(childrenElements.indexOf(expectedChain[i+1]))); + } + for (int j = 0; j < childrenElements.length; j += 1) { + expect(jsonChildren[j], isMap); + final Map childJson = jsonChildren[j]; + expect(service.toObject(childJson['valueId']), equals(childrenElements[j])); + expect(service.toObject(childJson['objectId']), const isInstanceOf()); + } + } + }); + + test('ext.flutter.inspector.getProperties', () async { + final DiagnosticsNode diagnostic = const Text('a', textDirection: TextDirection.ltr).toDiagnosticsNode(); + const String group = 'group'; + final String id = service.toId(diagnostic, group); + final List propertiesJson = await service.testExtension('getProperties', {'arg': id, 'objectGroup': group}); + final List properties = diagnostic.getProperties(); + expect(properties, isNotEmpty); + expect(propertiesJson.length, equals(properties.length)); + for (int i = 0; i < propertiesJson.length; ++i) { + final Map propertyJson = propertiesJson[i]; + expect(service.toObject(propertyJson['valueId']), equals(properties[i].value)); + expect(service.toObject(propertyJson['objectId']), const isInstanceOf()); + } + }); + + testWidgets('ext.flutter.inspector.getChildren', (WidgetTester tester) async { + const String group = 'test-group'; + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: const [ + 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 String id = service.toId(diagnostic, group); + final List propertiesJson = await service.testExtension('getChildren', {'arg': id, 'objectGroup': group}); + final List children = diagnostic.getChildren(); + expect(children.length, equals(3)); + expect(propertiesJson.length, equals(children.length)); + for (int i = 0; i < propertiesJson.length; ++i) { + final Map propertyJson = propertiesJson[i]; + expect(service.toObject(propertyJson['valueId']), equals(children[i].value)); + expect(service.toObject(propertyJson['objectId']), const isInstanceOf()); + } + }); + + testWidgets('ext.flutter.inspector creationLocation', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: const [ + 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(); + await service.testExtension('setPubRootDirectories', {}); + service.setSelection(elementA, 'my-group'); + final Map jsonA = await service.testExtension('getSelectedWidget', {'arg': null, 'objectGroup': 'my-group'}); + final Map creationLocationA = jsonA['creationLocation']; + expect(creationLocationA, isNotNull); + final String fileA = creationLocationA['file']; + final int lineA = creationLocationA['line']; + final int columnA = creationLocationA['column']; + final List parameterLocationsA = creationLocationA['parameterLocations']; + + service.setSelection(elementB, 'my-group'); + final Map jsonB = await service.testExtension('getSelectedWidget', {'arg': null, 'objectGroup': 'my-group'}); + final Map creationLocationB = jsonB['creationLocation']; + expect(creationLocationB, isNotNull); + final String fileB = creationLocationB['file']; + final int lineB = creationLocationB['line']; + final int columnB = creationLocationB['column']; + final List 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 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 paramB1 = parameterLocationsB[0]; + expect(paramB1['name'], equals('data')); + expect(paramB1['line'], equals(lineB)); + expect(paramB1['column'], equals(26)); + final Map 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('ext.flutter.inspector.setPubRootDirectories', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: const [ + const Text('a'), + const Text('b', textDirection: TextDirection.ltr), + const Text('c', textDirection: TextDirection.ltr), + ], + ), + ), + ); + final Element elementA = find.text('a').evaluate().first; + + await service.testExtension('setPubRootDirectories', {}); + service.setSelection(elementA, 'my-group'); + Map jsonObject = await service.testExtension('getSelectedWidget', {'arg': null, 'objectGroup': 'my-group'}); + Map 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 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('/'); + await service.testExtension('setPubRootDirectories', {'arg0': pubRootTest}); + + service.setSelection(elementA, 'my-group'); + expect(await service.testExtension('getSelectedWidget', {'objectGroup': 'my-group'}), contains('createdByLocalProject')); + + await service.testExtension('setPubRootDirectories', {'arg0': '/invalid/$pubRootTest'}); + expect(await service.testExtension('getSelectedWidget', {'objectGroup': 'my-group'}), isNot(contains('createdByLocalProject'))); + + await service.testExtension('setPubRootDirectories', {'arg0': 'file://$pubRootTest'}); + expect(await service.testExtension('getSelectedWidget', {'objectGroup': 'my-group'}), contains('createdByLocalProject')); + + await service.testExtension('setPubRootDirectories', {'arg0': '$pubRootTest/different'}); + expect(await service.testExtension('getSelectedWidget', {'objectGroup': 'my-group'}), isNot(contains('createdByLocalProject'))); + + await service.testExtension('setPubRootDirectories', { + 'arg0': '/unrelated/$pubRootTest', + 'arg1': 'file://$pubRootTest', + }); + + expect(await service.testExtension('getSelectedWidget', {'objectGroup': '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([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 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('/'); + await service.testExtension('setPubRootDirectories', {'arg0': pubRootFramework}); + expect(await service.testExtension('getSelectedWidget', {'objectGroup': 'my-group'}), contains('createdByLocalProject')); + service.setSelection(elementA, 'my-group'); + expect(await service.testExtension('getSelectedWidget', {'objectGroup': 'my-group'}), isNot(contains('createdByLocalProject'))); + + await service.testExtension('setPubRootDirectories', {'arg0': pubRootFramework, 'arg1': pubRootTest}); + service.setSelection(elementA, 'my-group'); + expect(await service.testExtension('getSelectedWidget', {'objectGroup': 'my-group'}), contains('createdByLocalProject')); + service.setSelection(richText, 'my-group'); + expect(await service.testExtension('getSelectedWidget', {'objectGroup': 'my-group'}), contains('createdByLocalProject')); + }, 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', {'enabled': 'true'}), equals('true')); + expect(service.rebuildCount, equals(1)); + expect(await service.testBoolExtension('show', {}), equals('true')); + expect(WidgetsApp.debugShowWidgetInspectorOverride, isTrue); + expect(await service.testBoolExtension('show', {'enabled': 'true'}), equals('true')); + expect(service.rebuildCount, equals(1)); + expect(await service.testBoolExtension('show', {'enabled': 'false'}), equals('false')); + expect(await service.testBoolExtension('show', {}), equals('false')); + expect(service.rebuildCount, equals(2)); + expect(WidgetsApp.debugShowWidgetInspectorOverride, isFalse); + }); + } } diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 30f047acf83..0aa7d11db65 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -1183,7 +1183,7 @@ class Isolate extends ServiceObjectOwner { Future> flutterTogglePerformanceOverlayOverride() => _flutterToggle('showPerformanceOverlay'); - Future> flutterToggleWidgetInspector() => _flutterToggle('debugWidgetInspector'); + Future> flutterToggleWidgetInspector() => _flutterToggle('inspector.show'); Future flutterDebugAllowBanner(bool show) async { await invokeFlutterExtensionRpcRaw(