From ab9ba3f90983021175e62f2d58d1e90cda0cf60c Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Mon, 16 Apr 2018 10:04:40 -0700 Subject: [PATCH] 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. --- packages/flutter/lib/src/widgets/binding.dart | 5 + .../lib/src/widgets/widget_inspector.dart | 305 +++- .../foundation/service_extensions_test.dart | 5 +- .../test/widgets/widget_inspector_test.dart | 1563 ++++++++++------- packages/flutter_tools/lib/src/vmservice.dart | 2 +- 5 files changed, 1267 insertions(+), 613 deletions(-) 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(