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

Support exposing the InspectorService over the Flutterservice extension...

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

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

We will probably remove most of the observatory protocol support once a
couple versions of the Flutter IntelliJ plugin have shipped that use the
Flutter service extension protocol. The only reason to continue supporting
the observatory protocol is it will allow using the inspector when paused
at a breakpoint.
parent 7dd166fa
......@@ -16,6 +16,7 @@ import 'package:flutter/services.dart';
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<Null> _forceRebuild() {
......
......@@ -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<String> _pubRootDirectories;
_RegisterServiceExtensionCallback _registerServiceExtensionCallback;
/// Registers a service extension method with the given name (full
/// name "ext.flutter.inspector.name").
///
/// The given callback is called when the extension method is called. The
/// callback must return a value that can be converted to JSON using
/// `json.encode()` (see [JsonEncoder]). The return value is stored as a
/// property named `result` in the JSON. In case of failure, the failure is
/// reported to the remote caller and is dumped to the logs.
@protected
void registerServiceExtension({
@required String name,
@required ServiceExtensionCallback callback,
}) {
_registerServiceExtensionCallback(
name: 'inspector.$name',
callback: callback,
);
}
/// Registers a service extension method with the given name (full
/// name "ext.flutter.inspector.name"), which takes no arguments.
void _registerSignalServiceExtension({
@required String name,
@required FutureOr<Object> callback(),
}) {
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
return <String, Object>{'result': await callback()};
},
);
}
/// Registers a service extension method with the given name (full
/// name "ext.flutter.inspector.name"), which takes a single required argument
/// "objectGroup" specifying what group is used to manage lifetimes of
/// object references in the returned JSON (see [disposeGroup]).
void _registerObjectGroupServiceExtension({
@required String name,
@required FutureOr<Object> callback(String objectGroup),
}) {
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
assert(parameters.containsKey('objectGroup'));
return <String, Object>{'result': await callback(parameters['objectGroup'])};
},
);
}
/// Registers a service extension method with the given name (full
/// name "ext.flutter.inspector.name"), which takes a single argument
/// "enabled" which can have the value "true" or the value "false"
/// or can be omitted to read the current value. (Any value other
/// than "true" is considered equivalent to "false". Other arguments
/// are ignored.)
///
/// Calls the `getter` callback to obtain the value when
/// responding to the service extension method being called.
///
/// Calls the `setter` callback with the new value when the
/// service extension method is called with a new value.
void _registerBoolServiceExtension({
@required String name,
@required AsyncValueGetter<bool> getter,
@required AsyncValueSetter<bool> setter
}) {
assert(name != null);
assert(getter != null);
assert(setter != null);
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
if (parameters.containsKey('enabled'))
await setter(parameters['enabled'] == 'true');
return <String, dynamic>{ 'enabled': await getter() ? 'true' : 'false' };
},
);
}
/// Registers a service extension method with the given name (full
/// name "ext.flutter.inspector.name") which takes an optional parameter named
/// "arg" and a required parameter named "objectGroup" used to control the
/// lifetimes of object references in the returned JSON (see [disposeGroup]).
void _registerServiceExtensionWithArg({
@required String name,
@required FutureOr<Object> callback(String objectId, String objectGroup),
}) {
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
assert(parameters.containsKey('objectGroup'));
return <String, Object>{
'result': await callback(parameters['arg'], parameters['objectGroup']),
};
},
);
}
/// Registers a service extension method with the given name (full
/// name "ext.flutter.inspector.name"), that takes arguments
/// "arg0", "arg1", "arg2", ..., "argn".
void _registerServiceExtensionVarArgs({
@required String name,
@required FutureOr<Object> callback(List<String> args),
}) {
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
const String argPrefix = 'arg';
final List<String> args = <String>[];
parameters.forEach((String name, String value) {
if (name.startsWith(argPrefix)) {
final int index = int.parse(name.substring(argPrefix.length));
if (index >= args.length) {
args.length = index + 1;
}
args[index] = value;
}
});
return <String, Object>{'result': await callback(args)};
},
);
}
@protected
Future<Null> forceRebuild() {
final WidgetsBinding binding = WidgetsBinding.instance;
if (binding.renderViewElement != null) {
binding.buildOwner.reassemble(binding.renderViewElement);
return binding.endOfFrame;
}
return new Future<Null>.value();
}
/// Called to register service extensions.
///
/// Service extensions are only exposed when the observatory is
/// included in the build, which should only happen in checked mode
/// and in profile mode.
///
/// See also:
///
/// * <https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#rpcs-requests-and-responses>
void initServiceExtensions(
_RegisterServiceExtensionCallback registerServiceExtensionCallback) {
_registerServiceExtensionCallback = registerServiceExtensionCallback;
assert(!_debugServiceExtensionsRegistered);
assert(() { _debugServiceExtensionsRegistered = true; return true; }());
_registerBoolServiceExtension(
name: 'show',
getter: () async => WidgetsApp.debugShowWidgetInspectorOverride,
setter: (bool value) {
if (WidgetsApp.debugShowWidgetInspectorOverride == value) {
return new Future<Null>.value();
}
WidgetsApp.debugShowWidgetInspectorOverride = value;
return forceRebuild();
},
);
_registerSignalServiceExtension(
name: 'disposeAllGroups',
callback: disposeAllGroups,
);
_registerObjectGroupServiceExtension(
name: 'disposeGroup',
callback: disposeGroup,
);
_registerSignalServiceExtension(
name: 'isWidgetTreeReady',
callback: isWidgetTreeReady,
);
_registerServiceExtensionWithArg(
name: 'disposeId',
callback: disposeId,
);
_registerServiceExtensionVarArgs(
name: 'setPubRootDirectories',
callback: setPubRootDirectories,
);
_registerServiceExtensionWithArg(
name: 'setSelectionById',
callback: setSelectionById,
);
_registerServiceExtensionWithArg(
name: 'getParentChain',
callback: _getParentChain,
);
_registerServiceExtensionWithArg(
name: 'getProperties',
callback: _getProperties,
);
_registerServiceExtensionWithArg(
name: 'getChildren',
callback: _getChildren,
);
_registerObjectGroupServiceExtension(
name: 'getRootWidget',
callback: _getRootWidget,
);
_registerObjectGroupServiceExtension(
name: 'getRootRenderObject',
callback: _getRootRenderObject,
);
_registerServiceExtensionWithArg(
name: 'getSelectedRenderObject',
callback: _getSelectedRenderObject,
);
_registerServiceExtensionWithArg(
name: 'getSelectedWidget',
callback: _getSelectedWidget,
);
_registerSignalServiceExtension(
name: 'isWidgetCreationTracked',
callback: isWidgetCreationTracked,
);
}
/// Clear all InspectorService object references.
///
/// 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<Object> pubRootDirectories) {
_pubRootDirectories = pubRootDirectories.map<String>(
(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<Object> _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<String, Object> _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<String, Object> _serializeToJson(DiagnosticsNode node, String groupName) {
return _nodeToJson(node, groupName);
}
List<Map<String, Object>> _nodesToJson(Iterable<DiagnosticsNode> 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<Object> _getProperties(String diagnosticsNodeId, String groupName) {
final DiagnosticsNode node = toObject(diagnosticsNodeId);
return json.encode(_nodesToJson(node == null ? const <DiagnosticsNode>[] : node.getProperties(), groupName));
return _nodesToJson(node == null ? const <DiagnosticsNode>[] : node.getProperties(), groupName);
}
/// Returns a JSON representation of the children of the [DiagnosticsNode]
/// object that `diagnosticsNodeId` references.
String getChildren(String diagnosticsNodeId, String groupName) {
return json.encode(_getChildren(diagnosticsNodeId, groupName));
}
List<Object> _getChildren(String diagnosticsNodeId, String groupName) {
final DiagnosticsNode node = toObject(diagnosticsNodeId);
return json.encode(_nodesToJson(node == null ? const <DiagnosticsNode>[] : node.getChildren(), groupName));
return _nodesToJson(node == null ? const <DiagnosticsNode>[] : node.getChildren(), groupName);
}
/// Returns a JSON representation of the [DiagnosticsNode] for the root
/// [Element].
String getRootWidget(String groupName) {
return _serialize(WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(), groupName);
return json.encode(_getRootWidget(groupName));
}
Map<String, Object> _getRootWidget(String groupName) {
return _serializeToJson(WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(), groupName);
}
/// Returns a JSON representation of the [DiagnosticsNode] for the root
/// [RenderObject].
@protected
String getRootRenderObject(String groupName) {
return _serialize(RendererBinding.instance?.renderView?.toDiagnosticsNode(), groupName);
return json.encode(_getRootRenderObject(groupName));
}
Map<String, Object> _getRootRenderObject(String groupName) {
return _serializeToJson(RendererBinding.instance?.renderView?.toDiagnosticsNode(), groupName);
}
/// Returns a [DiagnosticsNode] representing the currently selected
......@@ -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<String, Object> _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<String, Object> _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;
}
......
......@@ -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;
......
......@@ -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';
typedef FutureOr<Map<String, Object>> InspectorServiceExtensionCallback(Map<String, String> parameters);
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 <Widget>[
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 WidgetInspector(
selectButtonBuilder: null,
TestWidgetInspectorService.runTests();
}
class TestWidgetInspectorService extends Object with WidgetInspectorService {
final Map<String, InspectorServiceExtensionCallback> extensions = <String, InspectorServiceExtensionCallback>{};
@override
void registerServiceExtension({
@required String name,
@required FutureOr<Map<String, Object>> callback(Map<String, String> parameters),
}) {
assert(!extensions.containsKey(name));
extensions[name] = callback;
}
Future<Object> testExtension(String name, Map<String, String> arguments) async {
expect(extensions.containsKey(name), isTrue);
// Encode and decode to JSON to match behavior using a real service
// extension where only JSON is allowed.
return json.decode(json.encode(await extensions[name](arguments)))['result'];
}
Future<String> testBoolExtension(String name, Map<String, String> arguments) async {
expect(extensions.containsKey(name), isTrue);
// Encode and decode to JSON to match behavior using a real service
// extension where only JSON is allowed.
return json.decode(json.encode(await extensions[name](arguments)))['enabled'];
}
int rebuildCount = 0;
@override
Future<Null> forceRebuild() async {
rebuildCount++;
return null;
}
// These tests need access to protected members of WidgetInspectorService.
static void runTests() {
final TestWidgetInspectorService service = new TestWidgetInspectorService();
WidgetInspectorService.instance = service;
testWidgets('WidgetInspector smoke test', (WidgetTester tester) async {
// This is a smoke test to verify that adding the inspector doesn't crash.
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
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<String> log = <String>[];
final GlobalKey selectButtonKey = new GlobalKey();
final GlobalKey inspectorKey = new GlobalKey();
final GlobalKey topButtonKey = new GlobalKey();
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return new Material(child: new RaisedButton(onPressed: onPressed, key: selectButtonKey));
}
// State type is private, hence using dynamic.
dynamic getInspectorState() => inspectorKey.currentState;
String paragraphText(RenderParagraph paragraph) => paragraph.text.text;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
key: inspectorKey,
selectButtonBuilder: selectButtonBuilder,
child: new Material(
child: new ListView(
children: <Widget>[
new RaisedButton(
key: topButtonKey,
onPressed: () {
log.add('top');
},
child: const Text('TOP'),
),
new RaisedButton(
onPressed: () {
log.add('bottom');
},
child: const Text('BOTTOM'),
),
],
),
),
),
),
);
expect(getInspectorState().selection.current, isNull);
await tester.tap(find.text('TOP'));
await tester.pump();
// Tap intercepted by the inspector
expect(log, equals(<String>[]));
final InspectorSelection selection = getInspectorState().selection;
expect(paragraphText(selection.current), equals('TOP'));
final RenderObject topButton = find.byKey(topButtonKey).evaluate().first.renderObject;
expect(selection.candidates.contains(topButton), isTrue);
await tester.tap(find.text('TOP'));
expect(log, equals(<String>['top']));
log.clear();
await tester.tap(find.text('BOTTOM'));
expect(log, equals(<String>['bottom']));
log.clear();
// Ensure the inspector selection has not changed to bottom.
expect(paragraphText(getInspectorState().selection.current), equals('TOP'));
await tester.tap(find.byKey(selectButtonKey));
await tester.pump();
// We are now back in select mode so tapping the bottom button will have
// not trigger a click but will cause it to be selected.
await tester.tap(find.text('BOTTOM'));
expect(log, equals(<String>[]));
log.clear();
expect(paragraphText(getInspectorState().selection.current), equals('BOTTOM'));
});
testWidgets('WidgetInspector non-invertible transform regression test', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
selectButtonBuilder: null,
child: new Transform(
transform: new Matrix4.identity()..scale(0.0),
);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
selectButtonBuilder: null,
child: new Stack(
children: const <Widget>[
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: <Widget>[
new Container(
key: childKey,
height: 5000.0,
);
expect(true, isTrue); // Expect that we reach here without crashing.
});
testWidgets('WidgetInspector interaction test', (WidgetTester tester) async {
final List<String> log = <String>[];
final GlobalKey selectButtonKey = new GlobalKey();
final GlobalKey inspectorKey = new GlobalKey();
final GlobalKey topButtonKey = new GlobalKey();
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return new Material(child: new RaisedButton(onPressed: onPressed, key: selectButtonKey));
}
// State type is private, hence using dynamic.
dynamic getInspectorState() => inspectorKey.currentState;
String paragraphText(RenderParagraph paragraph) => paragraph.text.text;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
key: inspectorKey,
selectButtonBuilder: selectButtonBuilder,
child: new Material(
child: new ListView(
children: <Widget>[
new RaisedButton(
key: topButtonKey,
onPressed: () {
log.add('top');
},
child: const Text('TOP'),
),
new RaisedButton(
onPressed: () {
log.add('bottom');
},
child: const Text('BOTTOM'),
),
],
),
],
),
),
),
);
expect(getInspectorState().selection.current, isNull);
await tester.tap(find.text('TOP'));
await tester.pump();
// Tap intercepted by the inspector
expect(log, equals(<String>[]));
final InspectorSelection selection = getInspectorState().selection;
expect(paragraphText(selection.current), equals('TOP'));
final RenderObject topButton = find.byKey(topButtonKey).evaluate().first.renderObject;
expect(selection.candidates.contains(topButton), isTrue);
await tester.tap(find.text('TOP'));
expect(log, equals(<String>['top']));
log.clear();
await tester.tap(find.text('BOTTOM'));
expect(log, equals(<String>['bottom']));
log.clear();
// Ensure the inspector selection has not changed to bottom.
expect(paragraphText(getInspectorState().selection.current), equals('TOP'));
await tester.tap(find.byKey(selectButtonKey));
await tester.pump();
// We are now back in select mode so tapping the bottom button will have
// not trigger a click but will cause it to be selected.
await tester.tap(find.text('BOTTOM'));
expect(log, equals(<String>[]));
log.clear();
expect(paragraphText(getInspectorState().selection.current), equals('BOTTOM'));
});
testWidgets('WidgetInspector non-invertible transform regression test', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
selectButtonBuilder: null,
child: new Transform(
transform: new Matrix4.identity()..scale(0.0),
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
),
),
),
);
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
);
await tester.tap(find.byType(Transform));
await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0);
await tester.pump();
expect(true, isTrue); // Expect that we reach here without crashing.
});
// Fling does nothing as are in inspect mode.
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
testWidgets('WidgetInspector scroll test', (WidgetTester tester) async {
final Key childKey = new UniqueKey();
final GlobalKey selectButtonKey = new GlobalKey();
final GlobalKey inspectorKey = new GlobalKey();
await tester.fling(find.byType(ListView), const Offset(200.0, 0.0), 200.0);
await tester.pump();
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: <Widget>[
new Container(
key: childKey,
height: 5000.0,
),
],
),
),
),
);
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
// Fling still does nothing as are in inspect mode.
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();
await tester.tap(find.byType(ListView));
await tester.pump();
expect(getInspectorState().selection.current, isNotNull);
// Fling does nothing as are in inspect mode.
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
// Now out of inspect mode due to the click.
await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0);
await tester.pump();
await tester.fling(find.byType(ListView), const Offset(200.0, 0.0), 200.0);
await tester.pump();
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(-200.0));
// Fling still does nothing as are in inspect mode.
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
await tester.fling(find.byType(ListView), const Offset(0.0, 200.0), 200.0);
await tester.pump();
await tester.tap(find.byType(ListView));
await tester.pump();
expect(getInspectorState().selection.current, isNotNull);
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
});
// Now out of inspect mode due to the click.
await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0);
await tester.pump();
testWidgets('WidgetInspector long press', (WidgetTester tester) async {
bool didLongPress = false;
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(-200.0));
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.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: <Widget>[
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.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: <Widget>[
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: <OverlayEntry>[
new OverlayEntry(
opaque: false,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 94.0),
),
new OverlayEntry(
opaque: true,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 95.0),
),
new OverlayEntry(
opaque: false,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 96.0, key: clickTarget),
),
],
),
),
],
),
);
}
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
key: inspectorKey,
selectButtonBuilder: null,
child: new Overlay(
initialEntries: <OverlayEntry>[
new OverlayEntry(
opaque: false,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 94.0),
),
new OverlayEntry(
opaque: true,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 95.0),
),
new OverlayEntry(
opaque: false,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 96.0, key: clickTarget),
),
await tester.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 <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
),
);
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', () {
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
expect(service.toObject(null), isNull);
expect(service.toId(null, 'test-group'), isNull);
});
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);
});
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);
});
test('WidgetInspectorService toObjectForSourceLocation', () {
const String group = 'test-group';
const Text widget = const Text('a', textDirection: TextDirection.ltr);
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
final String id = service.toId(widget, group);
expect(service.toObjectForSourceLocation(id), equals(widget));
final Element element = widget.createElement();
final String elementId = service.toId(element, group);
expect(service.toObjectForSourceLocation(elementId), equals(widget));
expect(element, isNot(equals(widget)));
service.disposeGroup(group);
expect(() => service.toObjectForSourceLocation(elementId), throwsFlutterError);
});
test('WidgetInspectorService object id test', () {
const Text a = const Text('a', textDirection: TextDirection.ltr);
const Text b = const Text('b', textDirection: TextDirection.ltr);
const Text c = const Text('c', textDirection: TextDirection.ltr);
const Text d = const Text('d', textDirection: TextDirection.ltr);
const String group1 = 'group-1';
const String group2 = 'group-2';
const String group3 = 'group-3';
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
final String aId = service.toId(a, group1);
final String bId = service.toId(b, group2);
final String cId = service.toId(c, group3);
final String dId = service.toId(d, group1);
// Make sure we get a consistent id if we add the object to a group multiple
// times.
expect(aId, equals(service.toId(a, group1)));
expect(service.toObject(aId), equals(a));
expect(service.toObject(aId), isNot(equals(b)));
expect(service.toObject(bId), equals(b));
expect(service.toObject(cId), equals(c));
expect(service.toObject(dId), equals(d));
// Make sure we get a consistent id even if we add the object to a different
// group.
expect(aId, equals(service.toId(a, group3)));
expect(aId, isNot(equals(bId)));
expect(aId, isNot(equals(cId)));
service.disposeGroup(group3);
});
testWidgets('WidgetInspectorService maybeSetSelection', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
);
final Element elementA = find.text('a').evaluate().first;
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));
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 <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
),
);
final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
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.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));
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 <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
);
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<Object> chainElements = jsonList;
final List<Element> 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<String, Object> chainNode = chainElements[i];
final Element element = expectedChain[i];
expect(chainNode['node'], isMap);
final Map<String, Object> jsonNode = chainNode['node'];
expect(service.toObject(jsonNode['valueId']), equals(element));
expect(service.toObject(jsonNode['objectId']), const isInstanceOf<DiagnosticsNode>());
expect(chainNode['children'], isList);
final List<Object> jsonChildren = chainNode['children'];
final List<Element> childrenElements = <Element>[];
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<String, Object> childJson = jsonChildren[j];
expect(service.toObject(childJson['valueId']), equals(childrenElements[j]));
expect(service.toObject(childJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
}
});
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<Object> propertiesJson = json.decode(service.getProperties(id, group));
final List<DiagnosticsNode> properties = diagnostic.getProperties();
expect(properties, isNotEmpty);
expect(propertiesJson.length, equals(properties.length));
for (int i = 0; i < propertiesJson.length; ++i) {
final Map<String, Object> propertyJson = propertiesJson[i];
expect(service.toObject(propertyJson['valueId']), equals(properties[i].value));
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
});
testWidgets('WidgetInspectorService getChildren', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final DiagnosticsNode diagnostic = find.byType(Stack).evaluate().first.toDiagnosticsNode();
service.disposeAllGroups();
final String id = service.toId(diagnostic, group);
final List<Object> propertiesJson = json.decode(service.getChildren(id, group));
final List<DiagnosticsNode> children = diagnostic.getChildren();
expect(children.length, equals(3));
expect(propertiesJson.length, equals(children.length));
for (int i = 0; i < propertiesJson.length; ++i) {
final Map<String, Object> propertyJson = propertiesJson[i];
expect(service.toObject(propertyJson['valueId']), equals(children[i].value));
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
});
testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a'),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
service.disposeAllGroups();
service.setPubRootDirectories(<Object>[]);
service.setSelection(elementA, 'my-group');
final Map<String, Object> jsonA = json.decode(service.getSelectedWidget(null, 'my-group'));
final Map<String, Object> creationLocationA = jsonA['creationLocation'];
expect(creationLocationA, isNotNull);
final String fileA = creationLocationA['file'];
final int lineA = creationLocationA['line'];
final int columnA = creationLocationA['column'];
final List<Object> parameterLocationsA = creationLocationA['parameterLocations'];
service.setSelection(elementB, 'my-group');
final Map<String, Object> jsonB = json.decode(service.getSelectedWidget(null, 'my-group'));
final Map<String, Object> creationLocationB = jsonB['creationLocation'];
expect(creationLocationB, isNotNull);
final String fileB = creationLocationB['file'];
final int lineB = creationLocationB['line'];
final int columnB = creationLocationB['column'];
final List<Object> parameterLocationsB = creationLocationB['parameterLocations'];
expect(fileA, endsWith('widget_inspector_test.dart'));
expect(fileA, equals(fileB));
// We don't hardcode the actual lines the widgets are created on as that
// would make this test fragile.
expect(lineA + 1, equals(lineB));
// Column numbers are more stable than line numbers.
expect(columnA, equals(21));
expect(columnA, equals(columnB));
expect(parameterLocationsA.length, equals(1));
final Map<String, Object> paramA = parameterLocationsA[0];
expect(paramA['name'], equals('data'));
expect(paramA['line'], equals(lineA));
expect(paramA['column'], equals(26));
expect(parameterLocationsB.length, equals(2));
final Map<String, Object> paramB1 = parameterLocationsB[0];
expect(paramB1['name'], equals('data'));
expect(paramB1['line'], equals(lineB));
expect(paramB1['column'], equals(26));
final Map<String, Object> paramB2 = parameterLocationsB[1];
expect(paramB2['name'], equals('textDirection'));
expect(paramB2['line'], equals(lineB));
expect(paramB2['column'], equals(31));
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
testWidgets('WidgetInspectorService setPubRootDirectories', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a'),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
service.disposeAllGroups();
service.setPubRootDirectories(<Object>[]);
service.setSelection(elementA, 'my-group');
Map<String, Object> jsonObject = json.decode(service.getSelectedWidget(null, 'my-group'));
Map<String, Object> creationLocation = jsonObject['creationLocation'];
expect(creationLocation, isNotNull);
final String fileA = creationLocation['file'];
expect(fileA, endsWith('widget_inspector_test.dart'));
expect(jsonObject, isNot(contains('createdByLocalProject')));
final List<String> segments = Uri.parse(fileA).pathSegments;
// Strip a couple subdirectories away to generate a plausible pub root
// directory.
final String pubRootTest = '/' + segments.take(segments.length - 2).join('/');
service.setPubRootDirectories(<Object>[pubRootTest]);
service.setSelection(elementA, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setPubRootDirectories(<Object>['/invalid/$pubRootTest']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>['file://$pubRootTest']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setPubRootDirectories(<Object>['$pubRootTest/different']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>[
'/invalid/$pubRootTest',
pubRootTest,
]);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
// The RichText child of the Text widget is created by the core framework
// not the current package.
final Element richText = find.descendant(
of: find.text('a'),
matching: find.byType(RichText),
).evaluate().first;
service.setSelection(richText, 'my-group');
service.setPubRootDirectories(<Object>[pubRootTest]);
jsonObject = json.decode(service.getSelectedWidget(null, 'my-group'));
expect(jsonObject, isNot(contains('createdByLocalProject')));
creationLocation = jsonObject['creationLocation'];
expect(creationLocation, isNotNull);
// This RichText widget is created by the build method of the Text widget
// thus the creation location is in text.dart not basic.dart
final List<String> pathSegmentsFramework = Uri.parse(creationLocation['file']).pathSegments;
expect(pathSegmentsFramework.join('/'), endsWith('/packages/flutter/lib/src/widgets/text.dart'));
// Strip off /src/widgets/text.dart.
final String pubRootFramework = '/' + pathSegmentsFramework.take(pathSegmentsFramework.length - 3).join('/');
service.setPubRootDirectories(<Object>[pubRootFramework]);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setSelection(elementA, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>[pubRootFramework, pubRootTest]);
service.setSelection(elementA, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setSelection(richText, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
test('ext.flutter.inspector.disposeGroup', () async {
final Object a = new Object();
const String group1 = 'group-1';
const String group2 = 'group-2';
const String group3 = 'group-3';
final String aId = service.toId(a, group1);
expect(service.toId(a, group2), equals(aId));
expect(service.toId(a, group3), equals(aId));
await service.testExtension('disposeGroup', <String, String>{'objectGroup': group1});
await service.testExtension('disposeGroup', <String, String>{'objectGroup': group2});
expect(service.toObject(aId), equals(a));
await service.testExtension('disposeGroup', <String, String>{'objectGroup': group3});
expect(() => service.toObject(aId), throwsFlutterError);
});
test('ext.flutter.inspector.disposeId', () async {
final Object a = new Object();
final Object b = new Object();
const String group1 = 'group-1';
const String group2 = 'group-2';
final String aId = service.toId(a, group1);
final String bId = service.toId(b, group1);
expect(service.toId(a, group2), equals(aId));
await service.testExtension('disposeId', <String, String>{'arg': bId, 'objectGroup': group1});
expect(() => service.toObject(bId), throwsFlutterError);
await service.testExtension('disposeId', <String, String>{'arg': aId, 'objectGroup': group1});
expect(service.toObject(aId), equals(a));
await service.testExtension('disposeId', <String, String>{'arg': aId, 'objectGroup': group2});
expect(() => service.toObject(aId), throwsFlutterError);
});
testWidgets('ext.flutter.inspector.setSelection', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
service.disposeAllGroups();
service.selection.clear();
int selectionChangedCount = 0;
service.selectionChangedCallback = () => selectionChangedCount++;
service.setSelection('invalid selection');
expect(selectionChangedCount, equals(0));
expect(service.selection.currentElement, isNull);
service.setSelection(elementA);
expect(selectionChangedCount, equals(1));
expect(service.selection.currentElement, equals(elementA));
expect(service.selection.current, equals(elementA.renderObject));
service.setSelection(elementB.renderObject);
expect(selectionChangedCount, equals(2));
expect(service.selection.current, equals(elementB.renderObject));
expect(service.selection.currentElement, equals(elementB.renderObject.debugCreator.element));
service.setSelection('invalid selection');
expect(selectionChangedCount, equals(2));
expect(service.selection.current, equals(elementB.renderObject));
await service.testExtension('setSelectionById', <String, String>{'arg' : service.toId(elementA, 'my-group'), 'objectGroup': 'my-group'});
expect(selectionChangedCount, equals(3));
expect(service.selection.currentElement, equals(elementA));
expect(service.selection.current, equals(elementA.renderObject));
service.setSelectionById(service.toId(elementA, 'my-group'));
expect(selectionChangedCount, equals(3));
expect(service.selection.currentElement, equals(elementA));
});
testWidgets('ext.flutter.inspector.getParentChain', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
),
);
final 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<Object> chainElements = jsonList;
final List<Element> 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<String, Object> chainNode = chainElements[i];
final Element element = expectedChain[i];
expect(chainNode['node'], isMap);
final Map<String, Object> jsonNode = chainNode['node'];
expect(service.toObject(jsonNode['valueId']), equals(element));
expect(service.toObject(jsonNode['objectId']), const isInstanceOf<DiagnosticsNode>());
expect(chainNode['children'], isList);
final List<Object> jsonChildren = chainNode['children'];
final List<Element> childrenElements = <Element>[];
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])));
);
final Element elementB = find.text('b').evaluate().first;
final String bId = service.toId(elementB, group);
final Object jsonList = await service.testExtension('getParentChain', <String, String>{'arg': bId, 'objectGroup': group});
expect(jsonList, isList);
final List<Object> chainElements = jsonList;
final List<Element> 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<String, Object> chainNode = chainElements[i];
final Element element = expectedChain[i];
expect(chainNode['node'], isMap);
final Map<String, Object> jsonNode = chainNode['node'];
expect(service.toObject(jsonNode['valueId']), equals(element));
expect(service.toObject(jsonNode['objectId']), const isInstanceOf<DiagnosticsNode>());
expect(chainNode['children'], isList);
final List<Object> jsonChildren = chainNode['children'];
final List<Element> childrenElements = <Element>[];
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<String, Object> childJson = jsonChildren[j];
expect(service.toObject(childJson['valueId']), equals(childrenElements[j]));
expect(service.toObject(childJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
}
for (int j = 0; j < childrenElements.length; j += 1) {
expect(jsonChildren[j], isMap);
final Map<String, Object> childJson = jsonChildren[j];
expect(service.toObject(childJson['valueId']), equals(childrenElements[j]));
expect(service.toObject(childJson['objectId']), const isInstanceOf<DiagnosticsNode>());
});
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<Object> propertiesJson = await service.testExtension('getProperties', <String, String>{'arg': id, 'objectGroup': group});
final List<DiagnosticsNode> properties = diagnostic.getProperties();
expect(properties, isNotEmpty);
expect(propertiesJson.length, equals(properties.length));
for (int i = 0; i < propertiesJson.length; ++i) {
final Map<String, Object> propertyJson = propertiesJson[i];
expect(service.toObject(propertyJson['valueId']), equals(properties[i].value));
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
}
});
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<Object> propertiesJson = json.decode(service.getProperties(id, group));
final List<DiagnosticsNode> properties = diagnostic.getProperties();
expect(properties, isNotEmpty);
expect(propertiesJson.length, equals(properties.length));
for (int i = 0; i < propertiesJson.length; ++i) {
final Map<String, Object> propertyJson = propertiesJson[i];
expect(service.toObject(propertyJson['valueId']), equals(properties[i].value));
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
});
testWidgets('WidgetInspectorService getChildren', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
});
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 <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
),
);
final DiagnosticsNode diagnostic = find.byType(Stack).evaluate().first.toDiagnosticsNode();
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
final String id = service.toId(diagnostic, group);
final List<Object> propertiesJson = json.decode(service.getChildren(id, group));
final List<DiagnosticsNode> children = diagnostic.getChildren();
expect(children.length, equals(3));
expect(propertiesJson.length, equals(children.length));
for (int i = 0; i < propertiesJson.length; ++i) {
final Map<String, Object> propertyJson = propertiesJson[i];
expect(service.toObject(propertyJson['valueId']), equals(children[i].value));
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
});
testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async {
final WidgetInspectorService service = WidgetInspectorService.instance;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a'),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
);
final DiagnosticsNode diagnostic = find.byType(Stack).evaluate().first.toDiagnosticsNode();
final String id = service.toId(diagnostic, group);
final List<Object> propertiesJson = await service.testExtension('getChildren', <String, String>{'arg': id, 'objectGroup': group});
final List<DiagnosticsNode> children = diagnostic.getChildren();
expect(children.length, equals(3));
expect(propertiesJson.length, equals(children.length));
for (int i = 0; i < propertiesJson.length; ++i) {
final Map<String, Object> propertyJson = propertiesJson[i];
expect(service.toObject(propertyJson['valueId']), equals(children[i].value));
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
});
testWidgets('ext.flutter.inspector creationLocation', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a'),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
),
);
final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
service.disposeAllGroups();
service.setPubRootDirectories(<Object>[]);
service.setSelection(elementA, 'my-group');
final Map<String, Object> jsonA = json.decode(service.getSelectedWidget(null, 'my-group'));
final Map<String, Object> creationLocationA = jsonA['creationLocation'];
expect(creationLocationA, isNotNull);
final String fileA = creationLocationA['file'];
final int lineA = creationLocationA['line'];
final int columnA = creationLocationA['column'];
final List<Object> parameterLocationsA = creationLocationA['parameterLocations'];
service.setSelection(elementB, 'my-group');
final Map<String, Object> jsonB = json.decode(service.getSelectedWidget(null, 'my-group'));
final Map<String, Object> creationLocationB = jsonB['creationLocation'];
expect(creationLocationB, isNotNull);
final String fileB = creationLocationB['file'];
final int lineB = creationLocationB['line'];
final int columnB = creationLocationB['column'];
final List<Object> parameterLocationsB = creationLocationB['parameterLocations'];
expect(fileA, endsWith('widget_inspector_test.dart'));
expect(fileA, equals(fileB));
// We don't hardcode the actual lines the widgets are created on as that
// would make this test fragile.
expect(lineA + 1, equals(lineB));
// Column numbers are more stable than line numbers.
expect(columnA, equals(19));
expect(columnA, equals(columnB));
expect(parameterLocationsA.length, equals(1));
final Map<String, Object> paramA = parameterLocationsA[0];
expect(paramA['name'], equals('data'));
expect(paramA['line'], equals(lineA));
expect(paramA['column'], equals(24));
expect(parameterLocationsB.length, equals(2));
final Map<String, Object> paramB1 = parameterLocationsB[0];
expect(paramB1['name'], equals('data'));
expect(paramB1['line'], equals(lineB));
expect(paramB1['column'], equals(24));
final Map<String, Object> 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.
testWidgets('WidgetInspectorService setPubRootDirectories', (WidgetTester tester) async {
final WidgetInspectorService service = WidgetInspectorService.instance;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a'),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
);
final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
service.disposeAllGroups();
await service.testExtension('setPubRootDirectories', <String, String>{});
service.setSelection(elementA, 'my-group');
final Map<String, Object> jsonA = await service.testExtension('getSelectedWidget', <String, String>{'arg': null, 'objectGroup': 'my-group'});
final Map<String, Object> creationLocationA = jsonA['creationLocation'];
expect(creationLocationA, isNotNull);
final String fileA = creationLocationA['file'];
final int lineA = creationLocationA['line'];
final int columnA = creationLocationA['column'];
final List<Object> parameterLocationsA = creationLocationA['parameterLocations'];
service.setSelection(elementB, 'my-group');
final Map<String, Object> jsonB = await service.testExtension('getSelectedWidget', <String, String>{'arg': null, 'objectGroup': 'my-group'});
final Map<String, Object> creationLocationB = jsonB['creationLocation'];
expect(creationLocationB, isNotNull);
final String fileB = creationLocationB['file'];
final int lineB = creationLocationB['line'];
final int columnB = creationLocationB['column'];
final List<Object> parameterLocationsB = creationLocationB['parameterLocations'];
expect(fileA, endsWith('widget_inspector_test.dart'));
expect(fileA, equals(fileB));
// We don't hardcode the actual lines the widgets are created on as that
// would make this test fragile.
expect(lineA + 1, equals(lineB));
// Column numbers are more stable than line numbers.
expect(columnA, equals(21));
expect(columnA, equals(columnB));
expect(parameterLocationsA.length, equals(1));
final Map<String, Object> paramA = parameterLocationsA[0];
expect(paramA['name'], equals('data'));
expect(paramA['line'], equals(lineA));
expect(paramA['column'], equals(26));
expect(parameterLocationsB.length, equals(2));
final Map<String, Object> paramB1 = parameterLocationsB[0];
expect(paramB1['name'], equals('data'));
expect(paramB1['line'], equals(lineB));
expect(paramB1['column'], equals(26));
final Map<String, Object> paramB2 = parameterLocationsB[1];
expect(paramB2['name'], equals('textDirection'));
expect(paramB2['line'], equals(lineB));
expect(paramB2['column'], equals(31));
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
testWidgets('ext.flutter.inspector.setPubRootDirectories', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a'),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
),
);
final Element elementA = find.text('a').evaluate().first;
service.disposeAllGroups();
service.setPubRootDirectories(<Object>[]);
service.setSelection(elementA, 'my-group');
Map<String, Object> jsonObject = json.decode(service.getSelectedWidget(null, 'my-group'));
Map<String, Object> creationLocation = jsonObject['creationLocation'];
expect(creationLocation, isNotNull);
final String fileA = creationLocation['file'];
expect(fileA, endsWith('widget_inspector_test.dart'));
expect(jsonObject, isNot(contains('createdByLocalProject')));
final List<String> segments = Uri.parse(fileA).pathSegments;
// Strip a couple subdirectories away to generate a plausible pub root
// directory.
final String pubRootTest = '/' + segments.take(segments.length - 2).join('/');
service.setPubRootDirectories(<Object>[pubRootTest]);
service.setSelection(elementA, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setPubRootDirectories(<Object>['/invalid/$pubRootTest']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>['file://$pubRootTest']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setPubRootDirectories(<Object>['$pubRootTest/different']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>[
'/invalid/$pubRootTest',
pubRootTest,
]);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
// The RichText child of the Text widget is created by the core framework
// not the current package.
final Element richText = find.descendant(
of: find.text('a'),
matching: find.byType(RichText),
).evaluate().first;
service.setSelection(richText, 'my-group');
service.setPubRootDirectories(<Object>[pubRootTest]);
jsonObject = json.decode(service.getSelectedWidget(null, 'my-group'));
expect(jsonObject, isNot(contains('createdByLocalProject')));
creationLocation = jsonObject['creationLocation'];
expect(creationLocation, isNotNull);
// This RichText widget is created by the build method of the Text widget
// thus the creation location is in text.dart not basic.dart
final List<String> pathSegmentsFramework = Uri.parse(creationLocation['file']).pathSegments;
expect(pathSegmentsFramework.join('/'), endsWith('/packages/flutter/lib/src/widgets/text.dart'));
// Strip off /src/widgets/text.dart.
final String pubRootFramework = '/' + pathSegmentsFramework.take(pathSegmentsFramework.length - 3).join('/');
service.setPubRootDirectories(<Object>[pubRootFramework]);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setSelection(elementA, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>[pubRootFramework, pubRootTest]);
service.setSelection(elementA, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setSelection(richText, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
);
final Element elementA = find.text('a').evaluate().first;
await service.testExtension('setPubRootDirectories', <String, String>{});
service.setSelection(elementA, 'my-group');
Map<String, Object> jsonObject = await service.testExtension('getSelectedWidget', <String, String>{'arg': null, 'objectGroup': 'my-group'});
Map<String, Object> creationLocation = jsonObject['creationLocation'];
expect(creationLocation, isNotNull);
final String fileA = creationLocation['file'];
expect(fileA, endsWith('widget_inspector_test.dart'));
expect(jsonObject, isNot(contains('createdByLocalProject')));
final List<String> segments = Uri.parse(fileA).pathSegments;
// Strip a couple subdirectories away to generate a plausible pub root
// directory.
final String pubRootTest = '/' + segments.take(segments.length - 2).join('/');
await service.testExtension('setPubRootDirectories', <String, String>{'arg0': pubRootTest});
service.setSelection(elementA, 'my-group');
expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
await service.testExtension('setPubRootDirectories', <String, String>{'arg0': '/invalid/$pubRootTest'});
expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), isNot(contains('createdByLocalProject')));
await service.testExtension('setPubRootDirectories', <String, String>{'arg0': 'file://$pubRootTest'});
expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
await service.testExtension('setPubRootDirectories', <String, String>{'arg0': '$pubRootTest/different'});
expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), isNot(contains('createdByLocalProject')));
await service.testExtension('setPubRootDirectories', <String, String>{
'arg0': '/unrelated/$pubRootTest',
'arg1': 'file://$pubRootTest',
});
expect(await service.testExtension('getSelectedWidget', <String, String>{'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(<Object>[pubRootTest]);
jsonObject = json.decode(service.getSelectedWidget(null, 'my-group'));
expect(jsonObject, isNot(contains('createdByLocalProject')));
creationLocation = jsonObject['creationLocation'];
expect(creationLocation, isNotNull);
// This RichText widget is created by the build method of the Text widget
// thus the creation location is in text.dart not basic.dart
final List<String> pathSegmentsFramework = Uri.parse(creationLocation['file']).pathSegments;
expect(pathSegmentsFramework.join('/'), endsWith('/packages/flutter/lib/src/widgets/text.dart'));
// Strip off /src/widgets/text.dart.
final String pubRootFramework = '/' + pathSegmentsFramework.take(pathSegmentsFramework.length - 3).join('/');
await service.testExtension('setPubRootDirectories', <String, String>{'arg0': pubRootFramework});
expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
service.setSelection(elementA, 'my-group');
expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), isNot(contains('createdByLocalProject')));
await service.testExtension('setPubRootDirectories', <String, String>{'arg0': pubRootFramework, 'arg1': pubRootTest});
service.setSelection(elementA, 'my-group');
expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
service.setSelection(richText, 'my-group');
expect(await service.testExtension('getSelectedWidget', <String, String>{'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', <String, String>{'enabled': 'true'}), equals('true'));
expect(service.rebuildCount, equals(1));
expect(await service.testBoolExtension('show', <String, String>{}), equals('true'));
expect(WidgetsApp.debugShowWidgetInspectorOverride, isTrue);
expect(await service.testBoolExtension('show', <String, String>{'enabled': 'true'}), equals('true'));
expect(service.rebuildCount, equals(1));
expect(await service.testBoolExtension('show', <String, String>{'enabled': 'false'}), equals('false'));
expect(await service.testBoolExtension('show', <String, String>{}), equals('false'));
expect(service.rebuildCount, equals(2));
expect(WidgetsApp.debugShowWidgetInspectorOverride, isFalse);
});
}
}
......@@ -1183,7 +1183,7 @@ class Isolate extends ServiceObjectOwner {
Future<Map<String, dynamic>> flutterTogglePerformanceOverlayOverride() => _flutterToggle('showPerformanceOverlay');
Future<Map<String, dynamic>> flutterToggleWidgetInspector() => _flutterToggle('debugWidgetInspector');
Future<Map<String, dynamic>> flutterToggleWidgetInspector() => _flutterToggle('inspector.show');
Future<Null> flutterDebugAllowBanner(bool show) async {
await invokeFlutterExtensionRpcRaw(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment