Unverified Commit 277001d1 authored by Jacob Richman's avatar Jacob Richman Committed by GitHub

Inspector JSON protocol to support Flutter IntelliJ Plugin tree view. (#12932)

parent 792e7ce8
......@@ -743,6 +743,33 @@ abstract class DiagnosticsNode {
String get _separator => showSeparator ? ':' : '';
/// Serialize the node excluding its descendents to a JSON map.
///
/// Subclasses should override if they have additional properties that are
/// useful for the GUI tools that consume this JSON.
///
/// See also:
///
/// * [WidgetInspectorService], which forms the bridge between JSON returned
/// by this method and interactive tree views in the Flutter IntelliJ
/// plugin.
@mustCallSuper
Map<String, Object> toJsonMap() {
final Map<String, Object> data = <String, Object>{
'name': name,
'showSeparator': showSeparator,
'description': toDescription(),
'level': describeEnum(level),
'showName': showName,
'emptyBodyDescription': emptyBodyDescription,
'style': describeEnum(style),
'valueToString': value.toString(),
'type': runtimeType.toString(),
'hasChildren': getChildren().isNotEmpty,
};
return data;
}
/// Returns a string representation of this diagnostic that is compatible with
/// the style of the parent if the node is not the root.
///
......@@ -1054,6 +1081,13 @@ class StringProperty extends DiagnosticsProperty<String> {
/// Whether the value is enclosed in double quotes.
final bool quoted;
@override
Map<String, Object> toJsonMap() {
final Map<String, Object> json = super.toJsonMap();
json['quoted'] = quoted;
return json;
}
@override
String valueToString({ TextTreeConfiguration parentConfiguration }) {
String text = _description ?? value;
......@@ -1114,6 +1148,15 @@ abstract class _NumProperty<T extends num> extends DiagnosticsProperty<T> {
level: level,
);
@override
Map<String, Object> toJsonMap() {
final Map<String, Object> json = super.toJsonMap();
if (unit != null)
json['unit'] = unit;
json['numberToString'] = numberToString();
return json;
}
/// Optional unit the [value] is measured in.
///
......@@ -1326,6 +1369,17 @@ class FlagProperty extends DiagnosticsProperty<bool> {
level: level,
);
@override
Map<String, Object> toJsonMap() {
final Map<String, Object> json = super.toJsonMap();
if (ifTrue != null)
json['ifTrue'] = ifTrue;
if (ifFalse != null)
json['ifFalse'] = ifFalse;
return json;
}
/// Description to use if the property [value] is true.
///
/// If not specified and [value] equals true the property's priority [level]
......@@ -1443,6 +1497,15 @@ class IterableProperty<T> extends DiagnosticsProperty<Iterable<T>> {
return DiagnosticLevel.fine;
return super.level;
}
@override
Map<String, Object> toJsonMap() {
final Map<String, Object> json = super.toJsonMap();
if (value != null) {
json['values'] = value.map<String>((T value) => value.toString()).toList();
}
return json;
}
}
/// An property than displays enum values tersely.
......@@ -1581,6 +1644,14 @@ class ObjectFlagProperty<T> extends DiagnosticsProperty<T> {
return super.level;
}
@override
Map<String, Object> toJsonMap() {
final Map<String, Object> json = super.toJsonMap();
if (ifPresent != null)
json['ifPresent'] = ifPresent;
return json;
}
}
/// Signature for computing the value of a property.
......@@ -1683,6 +1754,28 @@ class DiagnosticsProperty<T> extends DiagnosticsNode {
final String _description;
@override
Map<String, Object> toJsonMap() {
final Map<String, Object> json = super.toJsonMap();
if (defaultValue != kNoDefaultValue)
json['defaultValue'] = defaultValue.toString();
if (ifEmpty != null)
json['ifEmpty'] = ifEmpty;
if (ifNull != null)
json['ifNull'] = ifNull;
if (tooltip != null)
json['tooltip'] = tooltip;
json['missingIfNull'] = missingIfNull;
if (exception != null)
json['exception'] = exception.toString();
json['propertyType'] = propertyType.toString();
json['valueToString'] = valueToString();
json['defaultLevel'] = describeEnum(_defaultLevel);
if (T is Diagnosticable)
json['isDiagnosticableValue'] = true;
return json;
}
/// Returns a string representation of the property value.
///
/// Subclasses should override this method instead of [toDescription] to
......
......@@ -3362,6 +3362,20 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
return chain.join(' \u2190 ');
}
/// Returns the parent chain from this element back to the root of the tree.
///
/// Useful for debug display of a tree of Elements with only nodes in the path
/// from the root to this Element expanded.
List<Element> debugGetDiagnosticChain() {
final List<Element> chain = <Element>[this];
Element node = _parent;
while (node != null) {
chain.add(node);
node = node._parent;
}
return chain;
}
/// A short, textual description of this element.
@override String toStringShort() {
return widget != null ? '${widget.toStringShort()}' : '[$runtimeType]';
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:collection';
import 'dart:convert';
import 'dart:developer' as developer;
import 'dart:math' as math;
import 'dart:ui' as ui show window, Picture, SceneBuilder, PictureRecorder;
......@@ -21,6 +22,383 @@ import 'gesture_detector.dart';
/// [WidgetInspector.selectButtonBuilder].
typedef Widget InspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed);
/// A class describing a step along a path through a tree of [DiagnosticsNode]
/// objects.
///
/// This class is used to bundle all data required to display the tree with just
/// the nodes along a path expanded into a single JSON payload.
class _DiagnosticsPathNode {
/// Creates a full description of a step in a path through a tree of
/// [DiagnosticsNode] objects.
///
/// The [node] and [child] arguments must not be null.
_DiagnosticsPathNode({ @required this.node, @required this.children, this.childIndex }) : assert(node != null), assert(children != null);
/// Node at the point in the path this [_DiagnosticsPathNode] is describing.
final DiagnosticsNode node;
/// Children of the [node] being described.
///
/// This value is cached instead of relying on `node.getChildren()` as that
/// method call might create new [DiagnosticsNode] objects for each child
/// and we would prefer to use the identical [DiagnosticsNode] for each time
/// a node exists in the path.
final List<DiagnosticsNode> children;
/// Index of the child that the path continues on.
///
/// Equal to `null` if the path does not continue.
final int childIndex;
}
List<_DiagnosticsPathNode> _followDiagnosticableChain(List<Diagnosticable> chain, {
String name,
DiagnosticsTreeStyle style,
}) {
final List<_DiagnosticsPathNode> path = <_DiagnosticsPathNode>[];
if (chain.isEmpty)
return path;
DiagnosticsNode diagnostic = chain.first.toDiagnosticsNode(name: name, style: style);
for (int i = 1; i < chain.length; i += 1) {
final Diagnosticable target = chain[i];
bool foundMatch = false;
final List<DiagnosticsNode> children = diagnostic.getChildren();
for (int j = 0; j < children.length; j += 1) {
final DiagnosticsNode child = children[j];
if (child.value == target) {
foundMatch = true;
path.add(new _DiagnosticsPathNode(
node: diagnostic,
children: children,
childIndex: j,
));
diagnostic = child;
break;
}
}
assert(foundMatch);
}
path.add(new _DiagnosticsPathNode(node: diagnostic, children: diagnostic.getChildren()));
return path;
}
/// Signature for the selection change callback used by
/// [WidgetInspectorService.selectionChangedCallback].
typedef void InspectorSelectionChangedCallback();
/// Structure to help reference count Dart objects referenced by a GUI tool
/// using [WidgetInspectorService].
class _InspectorReferenceData {
_InspectorReferenceData(this.object);
final Object object;
int count = 1;
}
/// Service used by GUI tools to interact with the [WidgetInspector].
///
/// Calls to this object are typically made from GUI tools such as the [Flutter
/// IntelliJ Plugin](https://github.com/flutter/flutter-intellij/blob/master/README.md)
/// using the [Dart VM Service protocol](https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md).
/// This class uses its own object id and manages object lifecycles itself
/// instead of depending on the [object ids](https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#getobject)
/// specified by the VM Service Protocol because the VM Service Protocol ids
/// expire unpredictably. Object references are tracked in groups so that tools
/// that clients can use dereference all objects in a group with a single
/// operation making it easier to avoid memory leaks.
///
/// All methods in this class are appropriate to invoke from debugging tools
/// using the Observatory service protocol to evaluate Dart expressions of the
/// form `WidgetInspectorService.instance.methodName(arg1, arg2, ...)`. If you
/// make changes to any instance method of this class you need to verify that
/// the [Flutter IntelliJ Plugin](https://github.com/flutter/flutter-intellij/blob/master/README.md)
/// widget inspector support still works with the changes.
///
/// All methods returning String values return JSON.
class WidgetInspectorService {
WidgetInspectorService._();
/// The current [WidgetInspectorService].
static WidgetInspectorService get instance => _instance;
static final WidgetInspectorService _instance = new WidgetInspectorService._();
/// Ground truth tracking what object(s) are currently selected used by both
/// GUI tools such as the Flutter IntelliJ Plugin and the [WidgetInspector]
/// displayed on the device.
final InspectorSelection selection = new InspectorSelection();
/// Callback typically registered by the [WidgetInspector] to receive
/// notifications when [selection] changes.
///
/// The Flutter IntelliJ Plugin does not need to listen for this event as it
/// instead listens for `dart:developer` `inspect` events which also trigger
/// when the inspection target changes on device.
InspectorSelectionChangedCallback selectionChangedCallback;
/// The Observatory protocol does not keep alive object references so this
/// class needs to manually manage groups of objects that should be kept
/// alive.
final Map<String, Set<_InspectorReferenceData>> _groups = <String, Set<_InspectorReferenceData>>{};
final Map<String, _InspectorReferenceData> _idToReferenceData = <String, _InspectorReferenceData>{};
final Map<Object, String> _objectToId = new Map<Object, String>.identity();
int _nextId = 0;
/// 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.
void disposeAllGroups() {
_groups.clear();
_idToReferenceData.clear();
_objectToId.clear();
_nextId = 0;
}
/// Free all references to objects in a group.
///
/// Objects and their associated ids in the group may be kept alive by
/// references from a different group.
void disposeGroup(String name) {
final Set<_InspectorReferenceData> references = _groups.remove(name);
if (references == null)
return;
references.forEach(_decrementReferenceCount);
}
void _decrementReferenceCount(_InspectorReferenceData reference) {
reference.count -= 1;
assert(reference.count >= 0);
if (reference.count == 0) {
final String id = _objectToId.remove(reference.object);
assert(id != null);
_idToReferenceData.remove(id);
}
}
/// 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.
String toId(Object object, String groupName) {
if (object == null)
return null;
final Set<_InspectorReferenceData> group = _groups.putIfAbsent(groupName, () => new Set<_InspectorReferenceData>.identity());
String id = _objectToId[object];
_InspectorReferenceData referenceData;
if (id == null) {
id = 'inspector-$_nextId';
_nextId += 1;
_objectToId[object] = id;
referenceData = new _InspectorReferenceData(object);
_idToReferenceData[id] = referenceData;
group.add(referenceData);
} else {
referenceData = _idToReferenceData[id];
if (group.add(referenceData))
referenceData.count += 1;
}
return id;
}
/// Returns the Dart object associated with a reference id.
///
/// 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.
Object toObject(String id, [String groupName]) {
if (id == null)
return null;
final _InspectorReferenceData data = _idToReferenceData[id];
if (data == null)
throw new FlutterError('Id does not exist');
return data.object;
}
/// Returns the object to introspect to determine the source location of an
/// object's class.
///
/// The Dart object for the id is returned for all cases but [Element] objects
/// where the [Widget] configuring the [Element] is returned instead as the
/// class of the [Widget] is more relevant than the class of the [Element].
///
/// The `groupName` parameter is not required by is added to regularize the
/// API surface of methods called from the Flutter IntelliJ Plugin.
Object toObjectForSourceLocation(String id, [String groupName]) {
final Object object = toObject(id);
if (object is Element) {
return object.widget;
}
return object;
}
/// Remove the object with the specified `id` from the specified object
/// group.
///
/// If the object exists in other groups it will remain alive and the object
/// id will remain valid.
void disposeId(String id, String groupName) {
if (id == null)
return;
final _InspectorReferenceData referenceData = _idToReferenceData[id];
if (referenceData == null)
throw new FlutterError('Id does not exist');
if (_groups[groupName]?.remove(referenceData) != true)
throw new FlutterError('Id is not in group');
_decrementReferenceCount(referenceData);
}
/// Set the [WidgetInspector] selection to the object matching the specified
/// id if the object is valid object to set as the inspector selection.
///
/// Returns `true` if the selection was changed.
///
/// The `groupName` parameter is not required by is added to regularize the
/// API surface of methods called from the Flutter IntelliJ Plugin.
bool setSelectionById(String id, [String groupName]) {
return setSelection(toObject(id), groupName);
}
/// Set the [WidgetInspector] selection to the specified `object` if it is
/// a valid object to set as the inspector selection.
///
/// Returns `true` if the selection was changed.
///
/// The `groupName` parameter is not needed but is specified to regularize the
/// API surface of methods called from the Flutter IntelliJ Plugin.
bool setSelection(Object object, [String groupName]) {
if (object is Element || object is RenderObject) {
if (object is Element) {
if (object == selection.currentElement) {
return false;
}
selection.currentElement = object;
} else {
if (object == selection.current) {
return false;
}
selection.current = object;
}
if (selectionChangedCallback != null) {
selectionChangedCallback();
}
return true;
}
return false;
}
/// Returns JSON representing the chain of [DiagnosticsNode] instances from
/// root of thee tree to the [Element] or [RenderObject] matching `id`.
///
/// The JSON contains all information required to display a tree view with
/// all nodes other than nodes along the path collapsed.
String getParentChain(String id, String groupName) {
final Object value = toObject(id);
List<_DiagnosticsPathNode> path;
if (value is RenderObject)
path = _getRenderObjectParentChain(value, groupName);
else if (value is Element)
path = _getElementParentChain(value, groupName);
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());
}
Map<String, Object> _pathNodeToJson(_DiagnosticsPathNode pathNode, String groupName) {
if (pathNode == null)
return null;
return <String, Object>{
'node': _nodeToJson(pathNode.node, groupName),
'children': _nodesToJson(pathNode.children, groupName),
'childIndex': pathNode.childIndex,
};
}
List<_DiagnosticsPathNode> _getElementParentChain(Element element, String groupName) {
return _followDiagnosticableChain(element?.debugGetDiagnosticChain()?.reversed?.toList()) ?? const <_DiagnosticsPathNode>[];
}
List<_DiagnosticsPathNode> _getRenderObjectParentChain(RenderObject renderObject, String groupName) {
final List<RenderObject> chain = <RenderObject>[];
while(renderObject != null) {
chain.add(renderObject);
renderObject = renderObject.parent;
}
return _followDiagnosticableChain(chain.reversed.toList());
}
Map<String, Object> _nodeToJson(DiagnosticsNode node, String groupName) {
if (node == null)
return null;
final Map<String, Object> json = node.toJsonMap();
json['objectId'] = toId(node, groupName);
json['valueId'] = toId(node.value, groupName);
return json;
}
String _serialize(DiagnosticsNode node, String groupName) {
return JSON.encode(_nodeToJson(node, groupName));
}
List<Map<String, Object>> _nodesToJson(Iterable<DiagnosticsNode> nodes, String groupName) {
if (nodes == null)
return <Map<String, Object>>[];
return nodes.map<Map<String, Object>>((DiagnosticsNode node) => _nodeToJson(node, groupName)).toList();
}
/// Returns a JSON representation of the properties of the [DiagnosticsNode]
/// object that `diagnosticsNodeId` references.
String getProperties(String diagnosticsNodeId, String groupName) {
final DiagnosticsNode node = toObject(diagnosticsNodeId);
return JSON.encode(_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) {
final DiagnosticsNode node = toObject(diagnosticsNodeId);
return JSON.encode(_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);
}
/// Returns a JSON representation of the [DiagnosticsNode] for the root
/// [RenderObject].
String getRootRenderObject(String groupName) {
return _serialize(RendererBinding.instance?.renderView?.toDiagnosticsNode(), groupName);
}
/// Returns a [DiagnosticsNode] representing the currently selected
/// [RenderObject].
///
/// If the currently selected [RenderObject] is identical to the
/// [RenderObject] referenced by `previousSelectionId` then the previous
/// [DiagnosticNode] is reused.
String getSelectedRenderObject(String previousSelectionId, String groupName) {
final DiagnosticsNode previousSelection = toObject(previousSelectionId);
final RenderObject current = selection?.current;
return _serialize(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), groupName);
}
/// Returns a [DiagnosticsNode] representing the currently selected [Element].
///
/// If the currently selected [Element] is identical to the [Element]
/// referenced by `previousSelectionId` then the previous [DiagnosticNode] is
/// reused.
String getSelectedWidget(String previousSelectionId, String groupName) {
final DiagnosticsNode previousSelection = toObject(previousSelectionId);
final Element current = selection?.currentElement;
return _serialize(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), groupName);
}
}
/// A widget that enables inspecting the child widget's structure.
///
/// Select a location on your device or emulator and view what widgets and
......@@ -70,9 +448,11 @@ class WidgetInspector extends StatefulWidget {
class _WidgetInspectorState extends State<WidgetInspector>
with WidgetsBindingObserver {
_WidgetInspectorState() : selection = WidgetInspectorService.instance.selection;
Offset _lastPointerLocation;
final InspectorSelection selection = new InspectorSelection();
final InspectorSelection selection;
/// Whether the inspector is in select mode.
///
......@@ -87,6 +467,17 @@ class _WidgetInspectorState extends State<WidgetInspector>
/// as selecting the edge of the bounding box.
static const double _kEdgeHitMargin = 2.0;
@override
void initState() {
super.initState();
WidgetInspectorService.instance.selectionChangedCallback = () {
setState(() {
// The [selection] property which the build method depends on has
// changed.
});
};
}
bool _hitTestHelper(
List<RenderObject> hits,
List<RenderObject> edgeHits,
......@@ -252,30 +643,66 @@ class InspectorSelection {
List<RenderObject> _candidates = <RenderObject>[];
set candidates(List<RenderObject> value) {
_candidates = value;
index = 0;
_index = 0;
_computeCurrent();
}
/// Index within the list of candidates that is currently selected.
int index = 0;
int get index => _index;
int _index = 0;
set index(int value) {
_index = value;
_computeCurrent();
}
/// Set the selection to empty.
void clear() {
_candidates = <RenderObject>[];
index = 0;
_index = 0;
_computeCurrent();
}
/// Selected render object typically from the [candidates] list.
///
/// Setting [candidates] or calling [clear] resets the selection.
///
/// Returns null if the selection is invalid.
RenderObject get current => _current;
RenderObject _current;
set current(RenderObject value) {
if (_current != value) {
_current = value;
_currentElement = value.debugCreator.element;
}
}
/// Selected render object from the [candidates] list.
/// Selected [Element] consistent with the [current] selected [RenderObject].
///
/// Setting [candidates] or calling [clear] resets the selection.
///
/// Returns null if the selection is invalid.
RenderObject get current {
return candidates != null && index < candidates.length ? candidates[index] : null;
Element get currentElement => _currentElement;
Element _currentElement;
set currentElement(Element element) {
if (currentElement != element) {
_currentElement = element;
_current = element.findRenderObject();
}
}
void _computeCurrent() {
if (_index < candidates.length) {
_current = candidates[index];
_currentElement = _current.debugCreator.element;
} else {
_current = null;
_currentElement = null;
}
}
/// Whether the selected render object is attached to the tree or has gone
/// out of scope.
bool get active => current != null && current.attached;
bool get active => _current != null && _current.attached;
}
class _InspectorOverlay extends LeafRenderObjectWidget {
......@@ -448,7 +875,7 @@ class _InspectorOverlayLayer extends Layer {
final _InspectorOverlayRenderState state = new _InspectorOverlayRenderState(
overlayRect: overlayRect,
selected: new _TransformedRect(selected),
tooltip: selected.toString(),
tooltip: selection.currentElement.toStringShort(),
textDirection: TextDirection.ltr,
candidates: candidates,
);
......
......@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -46,6 +48,136 @@ enum ExampleEnum {
deferToChild,
}
/// Encode and decode to JSON to make sure all objects in the JSON for the
/// [DiagnosticsNode] are valid JSON.
Map<String, Object> simulateJsonSerialization(DiagnosticsNode node) {
return JSON.decode(JSON.encode(node.toJsonMap()));
}
void validateNodeJsonSerialization(DiagnosticsNode node) {
validateNodeJsonSerializationHelper(simulateJsonSerialization(node), node);
}
void validateNodeJsonSerializationHelper(Map<String, Object> json, DiagnosticsNode node) {
expect(json['name'], equals(node.name));
expect(json['showSeparator'], equals(node.showSeparator));
expect(json['description'], equals(node.toDescription()));
expect(json['level'], equals(describeEnum(node.level)));
expect(json['showName'], equals(node.showName));
expect(json['emptyBodyDescription'], equals(node.emptyBodyDescription));
expect(json['style'], equals(describeEnum(node.style)));
final String valueToString = node is DiagnosticsProperty ? node.valueToString() : node.value.toString();
expect(json['valueToString'], equals(valueToString));
expect(json['type'], equals(node.runtimeType.toString()));
expect(json['hasChildren'], equals(node.getChildren().isNotEmpty));
}
void validatePropertyJsonSerialization(DiagnosticsProperty<Object> property) {
validatePropertyJsonSerializationHelper(simulateJsonSerialization(property), property);
}
void validateStringPropertyJsonSerialization(StringProperty property) {
final Map<String, Object> json = simulateJsonSerialization(property);
expect(json['quoted'], equals(property.quoted));
validatePropertyJsonSerializationHelper(json, property);
}
void validateFlagPropertyJsonSerialization(FlagProperty property) {
final Map<String, Object> json = simulateJsonSerialization(property);
expect(json['ifTrue'], equals(property.ifTrue));
if (property.ifTrue != null) {
expect(json['ifTrue'], equals(property.ifTrue));
} else {
expect(json.containsKey('ifTrue'), isFalse);
}
if (property.ifFalse != null) {
expect(json['ifFalse'], property.ifFalse);
} else {
expect(json.containsKey('isFalse'), isFalse);
}
validatePropertyJsonSerializationHelper(json, property);
}
void validateDoublePropertyJsonSerialization(DoubleProperty property) {
final Map<String, Object> json = simulateJsonSerialization(property);
if (property.unit != null) {
expect(json['unit'], equals(property.unit));
} else {
expect(json.containsKey('unit'), isFalse);
}
expect(json['numberToString'], equals(property.numberToString()));
validatePropertyJsonSerializationHelper(json, property);
}
void validateObjectFlagPropertyJsonSerialization(ObjectFlagProperty<Object> property) {
final Map<String, Object> json = simulateJsonSerialization(property);
if (property.ifPresent != null) {
expect(json['ifPresent'], equals(property.ifPresent));
} else {
expect(json.containsKey('ifPresent'), isFalse);
}
validatePropertyJsonSerializationHelper(json, property);
}
void validateIterablePropertyJsonSerialization(IterableProperty<Object> property) {
final Map<String, Object> json = simulateJsonSerialization(property);
if (property.value != null) {
final List<Object> valuesJson = json['values'];
final List<String> expectedValues = property.value.map<String>((Object value) => value.toString()).toList();
expect(listEquals(valuesJson, expectedValues), isTrue);
} else {
expect(json.containsKey('values'), isFalse);
}
validatePropertyJsonSerializationHelper(json, property);
}
void validatePropertyJsonSerializationHelper(final Map<String, Object> json, DiagnosticsProperty<Object> property) {
if (property.defaultValue != kNoDefaultValue) {
expect(json['defaultValue'], equals(property.defaultValue.toString()));
} else {
expect(json.containsKey('defaultValue'), isFalse);
}
if (property.ifEmpty != null) {
expect(json['ifEmpty'], equals(property.ifEmpty));
} else {
expect(json.containsKey('ifEmpty'), isFalse);
}
if (property.ifNull != null) {
expect(json['ifNull'], equals(property.ifNull));
} else {
expect(json.containsKey('ifNull'), isFalse);
}
if (property.tooltip != null) {
expect(json['tooltip'], equals(property.tooltip));
} else {
expect(json.containsKey('tooltip'), isFalse);
}
expect(json['missingIfNull'], equals(property.missingIfNull));
if (property.exception != null) {
expect(json['exception'], equals(property.exception.toString()));
} else {
expect(json.containsKey('exception'), isFalse);
}
expect(json['propertyType'], equals(property.propertyType.toString()));
expect(json['valueToString'], equals(property.valueToString()));
expect(json.containsKey('defaultLevel'), isTrue);
if (property.value is Diagnosticable) {
expect(json['isDiagnosticableValue'], isTrue);
} else {
expect(json.containsKey('isDiagnosticableValue'), isFalse);
}
validateNodeJsonSerializationHelper(json, property);
}
void main() {
test('TreeDiagnosticsMixin control test', () async {
void goldenStyleTest(String description,
......@@ -72,6 +204,7 @@ void main() {
equalsIgnoringHashCodes(golden),
reason: description,
);
validateNodeJsonSerialization(tree.toDiagnosticsNode());
}
goldenStyleTest(
......@@ -242,6 +375,7 @@ void main() {
equalsIgnoringHashCodes(golden),
reason: description,
);
validateNodeJsonSerialization(tree.toDiagnosticsNode());
}
goldenStyleTest(
......@@ -552,16 +686,15 @@ void main() {
equals('name: value'),
);
expect(
new StringProperty(
'name',
'value',
description: 'VALUE',
ifEmpty: '<hidden>',
quoted: false,
).toString(),
equals('name: VALUE'),
final StringProperty stringProperty = new StringProperty(
'name',
'value',
description: 'VALUE',
ifEmpty: '<hidden>',
quoted: false,
);
expect(stringProperty.toString(), equals('name: VALUE'));
validateStringPropertyJsonSerialization(stringProperty);
expect(
new StringProperty(
......@@ -592,14 +725,13 @@ void main() {
expect(new StringProperty('name', null).isFiltered(DiagnosticLevel.info), isFalse);
expect(new StringProperty('name', 'value', level: DiagnosticLevel.hidden).isFiltered(DiagnosticLevel.info), isTrue);
expect(new StringProperty('name', null, defaultValue: null).isFiltered(DiagnosticLevel.info), isTrue);
expect(
new StringProperty(
'name',
'value',
quoted: true,
).toString(),
equals('name: "value"'),
final StringProperty quoted = new StringProperty(
'name',
'value',
quoted: true,
);
expect(quoted.toString(), equals('name: "value"'));
validateStringPropertyJsonSerialization(quoted);
expect(
new StringProperty('name', 'value', showName: false).toString(),
......@@ -626,14 +758,18 @@ void main() {
expect(falseProperty.toString(), equals('name: false'));
expect(falseProperty.value, isFalse);
expect(falseProperty.isFiltered(DiagnosticLevel.info), isFalse);
validatePropertyJsonSerialization(trueProperty);
validatePropertyJsonSerialization(falseProperty);
final DiagnosticsProperty<bool> truthyProperty = new DiagnosticsProperty<bool>(
'name',
true,
description: 'truthy',
);
expect(
new DiagnosticsProperty<bool>(
'name',
true,
description: 'truthy',
).toString(),
truthyProperty.toString(),
equals('name: truthy'),
);
validatePropertyJsonSerialization(truthyProperty);
expect(
new DiagnosticsProperty<bool>('name', true, showName: false).toString(),
equals('true'),
......@@ -642,10 +778,12 @@ void main() {
expect(new DiagnosticsProperty<bool>('name', null).isFiltered(DiagnosticLevel.info), isFalse);
expect(new DiagnosticsProperty<bool>('name', true, level: DiagnosticLevel.hidden).isFiltered(DiagnosticLevel.info), isTrue);
expect(new DiagnosticsProperty<bool>('name', null, defaultValue: null).isFiltered(DiagnosticLevel.info), isTrue);
final DiagnosticsProperty<bool> missingBool = new DiagnosticsProperty<bool>('name', null, ifNull: 'missing');
expect(
new DiagnosticsProperty<bool>('name', null, ifNull: 'missing').toString(),
missingBool.toString(),
equals('name: missing'),
);
validatePropertyJsonSerialization(missingBool);
});
test('flag property test', () {
......@@ -660,6 +798,8 @@ void main() {
ifTrue: 'myFlag',
);
expect(trueFlag.toString(), equals('myFlag'));
validateFlagPropertyJsonSerialization(trueFlag);
validateFlagPropertyJsonSerialization(falseFlag);
expect(trueFlag.value, isTrue);
expect(falseFlag.value, isFalse);
......@@ -680,6 +820,7 @@ void main() {
);
expect(withTooltip.value, equals('value'));
expect(withTooltip.isFiltered(DiagnosticLevel.fine), isFalse);
validatePropertyJsonSerialization(withTooltip);
});
test('double property test', () {
......@@ -690,6 +831,7 @@ void main() {
expect(doubleProperty.toString(), equals('name: 42.0'));
expect(doubleProperty.isFiltered(DiagnosticLevel.info), isFalse);
expect(doubleProperty.value, equals(42.0));
validateDoublePropertyJsonSerialization(doubleProperty);
expect(new DoubleProperty('name', 1.3333).toString(), equals('name: 1.3'));
......@@ -701,11 +843,9 @@ void main() {
equals('name: missing'),
);
expect(
new DoubleProperty('name', 42.0, unit: 'px',
).toString(),
equals('name: 42.0px'),
);
final DoubleProperty doubleWithUnit = new DoubleProperty('name', 42.0, unit: 'px');
expect(doubleWithUnit.toString(), equals('name: 42.0px'));
validateDoublePropertyJsonSerialization(doubleWithUnit);
});
......@@ -717,7 +857,7 @@ void main() {
expect(safe.toString(), equals('name: 42.0'));
expect(safe.isFiltered(DiagnosticLevel.info), isFalse);
expect(safe.value, equals(42.0));
validateDoublePropertyJsonSerialization(safe);
expect(
new DoubleProperty.lazy('name', () => 1.3333).toString(),
equals('name: 1.3'),
......@@ -745,6 +885,7 @@ void main() {
equals('name: EXCEPTION (FlutterError)'),
);
expect(throwingProperty.level, equals(DiagnosticLevel.error));
validateDoublePropertyJsonSerialization(throwingProperty);
});
test('percent property', () {
......@@ -753,10 +894,12 @@ void main() {
equals('name: 40.0%'),
);
final PercentProperty complexPercentProperty = new PercentProperty('name', 0.99, unit: 'invisible', tooltip: 'almost transparent');
expect(
new PercentProperty('name', 0.99, unit: 'invisible', tooltip: 'almost transparent').toString(),
complexPercentProperty.toString(),
equals('name: 99.0% invisible (almost transparent)'),
);
validateDoublePropertyJsonSerialization(complexPercentProperty);
expect(
new PercentProperty('name', null, unit: 'invisible', tooltip: '!').toString(),
......@@ -830,8 +973,10 @@ void main() {
expect(present.toString(), equals('clickable'));
expect(present.isFiltered(DiagnosticLevel.info), isFalse);
expect(present.value, equals(onClick));
validateObjectFlagPropertyJsonSerialization(present);
expect(missing.toString(), equals('onClick: null'));
expect(missing.isFiltered(DiagnosticLevel.fine), isTrue);
validateObjectFlagPropertyJsonSerialization(missing);
});
test('missing callback property test', () {
......@@ -852,6 +997,8 @@ void main() {
expect(present.value, equals(onClick));
expect(missing.toString(), equals('disabled'));
expect(missing.isFiltered(DiagnosticLevel.info), isFalse);
validateObjectFlagPropertyJsonSerialization(present);
validateObjectFlagPropertyJsonSerialization(missing);
});
test('describe bool property', () {
......@@ -872,9 +1019,11 @@ void main() {
expect(yes.toString(), equals('name: YES'));
expect(yes.level, equals(DiagnosticLevel.info));
expect(yes.value, isTrue);
validateFlagPropertyJsonSerialization(yes);
expect(no.toString(), equals('name: NO'));
expect(no.level, equals(DiagnosticLevel.info));
expect(no.value, isFalse);
validateFlagPropertyJsonSerialization(no);
expect(
new FlagProperty(
......@@ -929,18 +1078,22 @@ void main() {
expect(hello.level, equals(DiagnosticLevel.info));
expect(hello.value, equals(ExampleEnum.hello));
expect(hello.toString(), equals('name: hello'));
validatePropertyJsonSerialization(hello);
expect(world.level, equals(DiagnosticLevel.info));
expect(world.value, equals(ExampleEnum.world));
expect(world.toString(), equals('name: world'));
validatePropertyJsonSerialization(world);
expect(deferToChild.level, equals(DiagnosticLevel.info));
expect(deferToChild.value, equals(ExampleEnum.deferToChild));
expect(deferToChild.toString(), equals('name: defer-to-child'));
validatePropertyJsonSerialization(deferToChild);
expect(nullEnum.level, equals(DiagnosticLevel.info));
expect(nullEnum.value, isNull);
expect(nullEnum.toString(), equals('name: null'));
validatePropertyJsonSerialization(nullEnum);
final EnumProperty<ExampleEnum> matchesDefault = new EnumProperty<ExampleEnum>(
'name',
......@@ -950,7 +1103,7 @@ void main() {
expect(matchesDefault.toString(), equals('name: hello'));
expect(matchesDefault.value, equals(ExampleEnum.hello));
expect(matchesDefault.isFiltered(DiagnosticLevel.info), isTrue);
validatePropertyJsonSerialization(matchesDefault);
expect(
new EnumProperty<ExampleEnum>(
......@@ -1052,6 +1205,7 @@ void main() {
expect(simple.value, equals(rect));
expect(simple.level, equals(DiagnosticLevel.info));
expect(simple.toString(), equals('name: Rect.fromLTRB(0.0, 0.0, 20.0, 20.0)'));
validatePropertyJsonSerialization(simple);
final DiagnosticsNode withDescription = new DiagnosticsProperty<Rect>(
'name',
......@@ -1061,6 +1215,7 @@ void main() {
expect(withDescription.value, equals(rect));
expect(withDescription.level, equals(DiagnosticLevel.info));
expect(withDescription.toString(), equals('name: small rect'));
validatePropertyJsonSerialization(withDescription);
final DiagnosticsProperty<Object> nullProperty = new DiagnosticsProperty<Object>(
'name',
......@@ -1069,6 +1224,7 @@ void main() {
expect(nullProperty.value, isNull);
expect(nullProperty.level, equals(DiagnosticLevel.info));
expect(nullProperty.toString(), equals('name: null'));
validatePropertyJsonSerialization(nullProperty);
final DiagnosticsProperty<Object> hideNullProperty = new DiagnosticsProperty<Object>(
'name',
......@@ -1078,6 +1234,7 @@ void main() {
expect(hideNullProperty.value, isNull);
expect(hideNullProperty.isFiltered(DiagnosticLevel.info), isTrue);
expect(hideNullProperty.toString(), equals('name: null'));
validatePropertyJsonSerialization(hideNullProperty);
final DiagnosticsNode nullDescription = new DiagnosticsProperty<Object>(
'name',
......@@ -1087,6 +1244,7 @@ void main() {
expect(nullDescription.value, isNull);
expect(nullDescription.level, equals(DiagnosticLevel.info));
expect(nullDescription.toString(), equals('name: missing'));
validatePropertyJsonSerialization(nullDescription);
final DiagnosticsProperty<Rect> hideName = new DiagnosticsProperty<Rect>(
'name',
......@@ -1097,6 +1255,7 @@ void main() {
expect(hideName.value, equals(rect));
expect(hideName.level, equals(DiagnosticLevel.warning));
expect(hideName.toString(), equals('Rect.fromLTRB(0.0, 0.0, 20.0, 20.0)'));
validatePropertyJsonSerialization(hideName);
final DiagnosticsProperty<Rect> hideSeparator = new DiagnosticsProperty<Rect>(
'Creator',
......@@ -1109,6 +1268,7 @@ void main() {
hideSeparator.toString(),
equals('Creator Rect.fromLTRB(0.0, 0.0, 20.0, 20.0)'),
);
validatePropertyJsonSerialization(hideSeparator);
});
test('lazy object property test', () {
......@@ -1121,8 +1281,9 @@ void main() {
expect(simple.value, equals(rect));
expect(simple.level, equals(DiagnosticLevel.info));
expect(simple.toString(), equals('name: small rect'));
validatePropertyJsonSerialization(simple);
final DiagnosticsNode nullProperty = new DiagnosticsProperty<Object>.lazy(
final DiagnosticsProperty<Object> nullProperty = new DiagnosticsProperty<Object>.lazy(
'name',
() => null,
description: 'missing',
......@@ -1130,6 +1291,7 @@ void main() {
expect(nullProperty.value, isNull);
expect(nullProperty.isFiltered(DiagnosticLevel.info), isFalse);
expect(nullProperty.toString(), equals('name: missing'));
validatePropertyJsonSerialization(nullProperty);
final DiagnosticsNode hideNullProperty = new DiagnosticsProperty<Object>.lazy(
'name',
......@@ -1140,6 +1302,7 @@ void main() {
expect(hideNullProperty.value, isNull);
expect(hideNullProperty.isFiltered(DiagnosticLevel.info), isTrue);
expect(hideNullProperty.toString(), equals('name: missing'));
validatePropertyJsonSerialization(hideNullProperty);
final DiagnosticsNode hideName = new DiagnosticsProperty<Rect>.lazy(
'name',
......@@ -1150,6 +1313,7 @@ void main() {
expect(hideName.value, equals(rect));
expect(hideName.isFiltered(DiagnosticLevel.info), isFalse);
expect(hideName.toString(), equals('small rect'));
validatePropertyJsonSerialization(hideName);
final DiagnosticsProperty<Object> throwingWithDescription = new DiagnosticsProperty<Object>.lazy(
'name',
......@@ -1161,6 +1325,7 @@ void main() {
expect(throwingWithDescription.exception, isFlutterError);
expect(throwingWithDescription.isFiltered(DiagnosticLevel.info), false);
expect(throwingWithDescription.toString(), equals('name: missing'));
validatePropertyJsonSerialization(throwingWithDescription);
final DiagnosticsProperty<Object> throwingProperty = new DiagnosticsProperty<Object>.lazy(
'name',
......@@ -1171,7 +1336,7 @@ void main() {
expect(throwingProperty.exception, isFlutterError);
expect(throwingProperty.isFiltered(DiagnosticLevel.info), false);
expect(throwingProperty.toString(), equals('name: EXCEPTION (FlutterError)'));
validatePropertyJsonSerialization(throwingProperty);
});
test('color property test', () {
......@@ -1182,9 +1347,12 @@ void main() {
'name',
color,
);
validatePropertyJsonSerialization(simple);
expect(simple.isFiltered(DiagnosticLevel.info), isFalse);
expect(simple.value, equals(color));
expect(simple.propertyType, equals(Color));
expect(simple.toString(), equals('name: Color(0xffffffff)'));
validatePropertyJsonSerialization(simple);
});
test('flag property test', () {
......@@ -1197,6 +1365,7 @@ void main() {
expect(show.value, isTrue);
expect(show.isFiltered(DiagnosticLevel.info), isFalse);
expect(show.toString(), equals('layout computed'));
validateFlagPropertyJsonSerialization(show);
final FlagProperty hide = new FlagProperty(
'wasLayout',
......@@ -1207,6 +1376,7 @@ void main() {
expect(hide.value, isFalse);
expect(hide.level, equals(DiagnosticLevel.hidden));
expect(hide.toString(), equals('wasLayout: false'));
validateFlagPropertyJsonSerialization(hide);
final FlagProperty hideTrue = new FlagProperty(
'wasLayout',
......@@ -1217,6 +1387,7 @@ void main() {
expect(hideTrue.value, isTrue);
expect(hideTrue.level, equals(DiagnosticLevel.hidden));
expect(hideTrue.toString(), equals('wasLayout: true'));
validateFlagPropertyJsonSerialization(hideTrue);
});
test('has property test', () {
......@@ -1229,6 +1400,7 @@ void main() {
expect(has.value, equals(onClick));
expect(has.isFiltered(DiagnosticLevel.info), isFalse);
expect(has.toString(), equals('has onClick'));
validateObjectFlagPropertyJsonSerialization(has);
final ObjectFlagProperty<Function> missing = new ObjectFlagProperty<Function>.has(
'onClick',
......@@ -1238,6 +1410,7 @@ void main() {
expect(missing.value, isNull);
expect(missing.isFiltered(DiagnosticLevel.info), isTrue);
expect(missing.toString(), equals('onClick: null'));
validateObjectFlagPropertyJsonSerialization(missing);
});
test('iterable property test', () {
......@@ -1257,6 +1430,7 @@ void main() {
expect(emptyProperty.value, isEmpty);
expect(emptyProperty.isFiltered(DiagnosticLevel.info), isFalse);
expect(emptyProperty.toString(), equals('name: []'));
validateIterablePropertyJsonSerialization(emptyProperty);
final IterableProperty<Object> nullProperty = new IterableProperty<Object>(
'list',
......@@ -1265,6 +1439,7 @@ void main() {
expect(nullProperty.value, isNull);
expect(nullProperty.isFiltered(DiagnosticLevel.info), isFalse);
expect(nullProperty.toString(), equals('list: null'));
validateIterablePropertyJsonSerialization(nullProperty);
final IterableProperty<Object> hideNullProperty = new IterableProperty<Object>(
'list',
......@@ -1275,6 +1450,7 @@ void main() {
expect(hideNullProperty.isFiltered(DiagnosticLevel.info), isTrue);
expect(hideNullProperty.level, equals(DiagnosticLevel.fine));
expect(hideNullProperty.toString(), equals('list: null'));
validateIterablePropertyJsonSerialization(hideNullProperty);
final List<Object> objects = <Object>[
new Rect.fromLTRB(0.0, 0.0, 20.0, 20.0),
......@@ -1290,6 +1466,7 @@ void main() {
objectsProperty.toString(),
equals('objects: Rect.fromLTRB(0.0, 0.0, 20.0, 20.0), Color(0xffffffff)'),
);
validateIterablePropertyJsonSerialization(objectsProperty);
final IterableProperty<Object> multiLineProperty = new IterableProperty<Object>(
'objects',
......@@ -1314,6 +1491,7 @@ void main() {
' Color(0xffffffff)\n',
),
);
validateIterablePropertyJsonSerialization(multiLineProperty);
expect(
new TestTree(
......@@ -1356,6 +1534,7 @@ void main() {
objectProperty.toStringDeep(),
equals('object: Color(0xffffffff)\n'),
);
validateIterablePropertyJsonSerialization(objectProperty);
expect(
new TestTree(
name: 'root',
......@@ -1374,12 +1553,13 @@ void main() {
expect(message.name, isEmpty);
expect(message.value, isNull);
expect(message.showName, isFalse);
validateNodeJsonSerialization(message);
final DiagnosticsNode messageProperty = new MessageProperty('diagnostics', 'hello world');
expect(messageProperty.toString(), equals('diagnostics: hello world'));
expect(messageProperty.name, equals('diagnostics'));
expect(messageProperty.value, isNull);
expect(messageProperty.showName, isTrue);
validatePropertyJsonSerialization(messageProperty);
});
}
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
......@@ -143,7 +145,6 @@ void main() {
),
),
);
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0);
......@@ -256,4 +257,245 @@ void main() {
// 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();
final String group1 = 'group-1';
final String group2 = 'group-2';
final 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();
final String group1 = 'group-1';
final 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', () {
final String group = 'test-group';
final 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', () {
final Text a = const Text('a', textDirection: TextDirection.ltr);
final Text b = const Text('b', textDirection: TextDirection.ltr);
final Text c = const Text('c', textDirection: TextDirection.ltr);
final Text d = const Text('d', textDirection: TextDirection.ltr);
final String group1 = 'group-1';
final String group2 = 'group-2';
final 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: <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 {
final String group = 'test-group';
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: <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 json = JSON.decode(service.getParentChain(bId, group));
expect(json, isList);
final List<Object> chainElements = json;
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();
final 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 {
final String group = 'test-group';
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: <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>());
}
});
}
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