Unverified Commit 928c41e9 authored by Jacob Richman's avatar Jacob Richman Committed by GitHub

Support summary-details tree view (#16638)

Support summary-details tree view.
parent 1080c298
......@@ -20,6 +20,7 @@ import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'icon_data.dart';
/// Signature for the builder callback used by
/// [WidgetInspector.selectButtonBuilder].
......@@ -103,6 +104,52 @@ class _InspectorReferenceData {
int count = 1;
}
/// Configuration controlling how [DiagnosticsNode] objects are serialized to
/// JSON mainly focused on if and how children are included in the JSON.
class _SerializeConfig {
_SerializeConfig({
@required this.groupName,
this.summaryTree: false,
this.subtreeDepth : 1,
this.pathToInclude,
this.includeProperties: false,
this.expandPropertyValues: true,
});
_SerializeConfig.merge(
_SerializeConfig base, {
int subtreeDepth,
bool omitChildren,
Iterable<Diagnosticable> pathToInclude,
}) :
groupName = base.groupName,
summaryTree = base.summaryTree,
subtreeDepth = subtreeDepth ?? base.subtreeDepth,
pathToInclude = pathToInclude ?? base.pathToInclude,
includeProperties = base.includeProperties,
expandPropertyValues = base.expandPropertyValues;
final String groupName;
/// Whether to only include children that would exist in the summary tree.
final bool summaryTree;
/// How many levels of children to include in the JSON payload.
final int subtreeDepth;
/// Path of nodes through the children of this node to include even if
/// subtreeDepth is exceeded.
final Iterable<Diagnosticable> pathToInclude;
/// Include information about properties in the JSON instead of requiring
/// a separate request to determine properties.
final bool includeProperties;
/// Expand children of properties that have values that are themselves
/// Diagnosticable objects.
final bool expandPropertyValues;
}
class _WidgetInspectorService extends Object with WidgetInspectorService {
}
......@@ -131,6 +178,11 @@ class WidgetInspectorService {
// [instance] for production purposes.
factory WidgetInspectorService._() => new _WidgetInspectorService();
/// Ring of cached JSON values to prevent json from being garbage
/// collected before it can be requested over the Observatory protocol.
final List<String> _serializeRing = new List<String>(20);
int _serializeRingIndex = 0;
/// The current [WidgetInspectorService].
static WidgetInspectorService get instance => _instance;
static WidgetInspectorService _instance = new WidgetInspectorService._();
......@@ -368,6 +420,17 @@ class WidgetInspectorService {
name: 'getChildren',
callback: _getChildren,
);
_registerServiceExtensionWithArg(
name: 'getChildrenSummaryTree',
callback: _getChildrenSummaryTree,
);
_registerServiceExtensionWithArg(
name: 'getChildrenDetailsSubtree',
callback: _getChildrenDetailsSubtree,
);
_registerObjectGroupServiceExtension(
name: 'getRootWidget',
callback: _getRootWidget,
......@@ -376,6 +439,15 @@ class WidgetInspectorService {
name: 'getRootRenderObject',
callback: _getRootRenderObject,
);
_registerObjectGroupServiceExtension(
name: 'getRootWidgetSummaryTree',
callback: _getRootWidgetSummaryTree,
);
_registerServiceExtensionWithArg(
name: 'getDetailsSubtree',
callback: _getDetailsSubtree,
);
_registerServiceExtensionWithArg(
name: 'getSelectedRenderObject',
callback: _getSelectedRenderObject,
......@@ -384,6 +456,11 @@ class WidgetInspectorService {
name: 'getSelectedWidget',
callback: _getSelectedWidget,
);
_registerServiceExtensionWithArg(
name: 'getSelectedSummaryWidget',
callback: _getSelectedSummaryWidget,
);
_registerSignalServiceExtension(
name: 'isWidgetCreationTracked',
callback: isWidgetCreationTracked,
......@@ -580,7 +657,7 @@ class WidgetInspectorService {
/// all nodes other than nodes along the path collapsed.
@protected
String getParentChain(String id, String groupName) {
return json.encode(_getParentChain(id, groupName));
return _safeJsonEncode(_getParentChain(id, groupName));
}
List<Object> _getParentChain(String id, String groupName) {
......@@ -593,24 +670,45 @@ class WidgetInspectorService {
else
throw new FlutterError('Cannot get parent chain for node of type ${value.runtimeType}');
return path.map((_DiagnosticsPathNode node) => _pathNodeToJson(node, groupName)).toList();
return path.map((_DiagnosticsPathNode node) => _pathNodeToJson(
node,
new _SerializeConfig(groupName: groupName),
)).toList();
}
Map<String, Object> _pathNodeToJson(_DiagnosticsPathNode pathNode, String groupName) {
Map<String, Object> _pathNodeToJson(_DiagnosticsPathNode pathNode, _SerializeConfig config) {
if (pathNode == null)
return null;
return <String, Object>{
'node': _nodeToJson(pathNode.node, groupName),
'children': _nodesToJson(pathNode.children, groupName),
'node': _nodeToJson(pathNode.node, config),
'children': _nodesToJson(pathNode.children, config),
'childIndex': pathNode.childIndex,
};
}
List<_DiagnosticsPathNode> _getElementParentChain(Element element, String groupName) {
return _followDiagnosticableChain(element?.debugGetDiagnosticChain()?.reversed?.toList()) ?? const <_DiagnosticsPathNode>[];
List<Element> _getRawElementParentChain(Element element, {int numLocalParents}) {
List<Element> elements = element?.debugGetDiagnosticChain();
if (numLocalParents != null) {
for (int i = 0; i < elements.length; i += 1) {
if (_isValueCreatedByLocalProject(elements[i])) {
numLocalParents--;
if (numLocalParents <= 0) {
elements = elements.take(i + 1).toList();
break;
}
}
}
}
return elements?.reversed?.toList();
}
List<_DiagnosticsPathNode> _getElementParentChain(Element element, String groupName, {int numLocalParents}) {
return _followDiagnosticableChain(
_getRawElementParentChain(element, numLocalParents: numLocalParents),
) ?? const <_DiagnosticsPathNode>[];
}
List<_DiagnosticsPathNode> _getRenderObjectParentChain(RenderObject renderObject, String groupName) {
List<_DiagnosticsPathNode> _getRenderObjectParentChain(RenderObject renderObject, String groupName, {int maxparents}) {
final List<RenderObject> chain = <RenderObject>[];
while (renderObject != null) {
chain.add(renderObject);
......@@ -619,25 +717,84 @@ class WidgetInspectorService {
return _followDiagnosticableChain(chain.reversed.toList());
}
Map<String, Object> _nodeToJson(DiagnosticsNode node, String groupName) {
Map<String, Object> _nodeToJson(
DiagnosticsNode node,
_SerializeConfig config,
) {
if (node == null)
return null;
final Map<String, Object> json = node.toJsonMap();
json['objectId'] = toId(node, groupName);
json['objectId'] = toId(node, config.groupName);
final Object value = node.value;
json['valueId'] = toId(value, groupName);
json['valueId'] = toId(value, config.groupName);
if (config.summaryTree) {
json['summaryTree'] = true;
}
final _Location creationLocation = _getCreationLocation(value);
bool createdByLocalProject = false;
if (creationLocation != null) {
json['creationLocation'] = creationLocation.toJsonMap();
if (_isLocalCreationLocation(creationLocation)) {
createdByLocalProject = true;
json['createdByLocalProject'] = true;
}
}
if (config.subtreeDepth > 0 ||
(config.pathToInclude != null && config.pathToInclude.isNotEmpty)) {
json['children'] = _nodesToJson(_getChildrenHelper(node, config), config);
}
if (config.includeProperties) {
json['properties'] = _nodesToJson(
node.getProperties().where(
(DiagnosticsNode node) => !node.isFiltered(createdByLocalProject ? DiagnosticLevel.fine : DiagnosticLevel.info),
),
new _SerializeConfig(groupName: config.groupName, subtreeDepth: 1, expandPropertyValues: true),
);
}
if (node is DiagnosticsProperty) {
// Add additional information about properties needed for graphical
// display of properties.
if (value is Color) {
json['valueProperties'] = <String, Object>{
'red': value.red,
'green': value.green,
'blue': value.blue,
'alpha': value.alpha,
};
} else if (value is IconData) {
json['valueProperties'] = <String, Object>{
'codePoint': value.codePoint,
};
}
if (config.expandPropertyValues && value is Diagnosticable) {
json['properties'] = _nodesToJson(
value.toDiagnosticsNode().getProperties().where(
(DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info),
),
new _SerializeConfig(groupName: config.groupName,
subtreeDepth: 0,
expandPropertyValues: false,
),
);
}
}
return json;
}
bool _isValueCreatedByLocalProject(Object value) {
final _Location creationLocation = _getCreationLocation(value);
if (creationLocation == null) {
return false;
}
return _isLocalCreationLocation(creationLocation);
}
bool _isLocalCreationLocation(_Location location) {
if (_pubRootDirectories == null || location == null || location.file == null) {
return false;
......@@ -651,58 +808,205 @@ class WidgetInspectorService {
return false;
}
Map<String, Object> _serializeToJson(DiagnosticsNode node, String groupName) {
return _nodeToJson(node, groupName);
/// Wrapper around `json.encode` that uses a ring of cached values to prevent
/// the Dart garbage collector from collecting objects between when
/// the value is returned over the Observatory protocol and when the
/// separate observatory protocol command has to be used to retrieve its full
/// contents.
/// TODO(jacobr): Replace this with a better solution once
/// https://github.com/dart-lang/sdk/issues/32919 is fixed.
String _safeJsonEncode(Object object) {
final String jsonString = json.encode(object);
_serializeRing[_serializeRingIndex] = jsonString;
_serializeRingIndex = (_serializeRingIndex + 1) % _serializeRing.length;
return jsonString;
}
List<Map<String, Object>> _nodesToJson(Iterable<DiagnosticsNode> nodes, String groupName) {
List<Map<String, Object>> _nodesToJson(
Iterable<DiagnosticsNode> nodes,
_SerializeConfig config,
) {
if (nodes == null)
return <Map<String, Object>>[];
return nodes.map<Map<String, Object>>((DiagnosticsNode node) => _nodeToJson(node, groupName)).toList();
return nodes.map<Map<String, Object>>(
(DiagnosticsNode node) {
if (config.pathToInclude != null && config.pathToInclude.isNotEmpty) {
if (config.pathToInclude.first == node.value) {
return _nodeToJson(
node,
new _SerializeConfig.merge(config, pathToInclude: config.pathToInclude.skip(1)),
);
} else {
return _nodeToJson(node, new _SerializeConfig.merge(config, omitChildren: true));
}
}
// The tricky special case here is that when in the detailsTree,
// we keep subtreeDepth from going down to zero until we reach nodes
// that also exist in the summary tree. This ensures that every time
// you expand a node in the details tree, you expand the entire subtree
// up until you reach the next nodes shared with the summary tree.
return _nodeToJson(
node,
config.summaryTree || config.subtreeDepth > 1 || _shouldShowInSummaryTree(node) ?
new _SerializeConfig.merge(config, subtreeDepth: config.subtreeDepth - 1) : config,
);
}).toList();
}
/// 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));
return _safeJsonEncode(_getProperties(diagnosticsNodeId, groupName));
}
List<Object> _getProperties(String diagnosticsNodeId, String groupName) {
final DiagnosticsNode node = toObject(diagnosticsNodeId);
return _nodesToJson(node == null ? const <DiagnosticsNode>[] : node.getProperties(), groupName);
return _nodesToJson(node == null ? const <DiagnosticsNode>[] : node.getProperties(), new _SerializeConfig(groupName: 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));
return _safeJsonEncode(_getChildren(diagnosticsNodeId, groupName));
}
List<Object> _getChildren(String diagnosticsNodeId, String groupName) {
final DiagnosticsNode node = toObject(diagnosticsNodeId);
return _nodesToJson(node == null ? const <DiagnosticsNode>[] : node.getChildren(), groupName);
final _SerializeConfig config = new _SerializeConfig(groupName: groupName);
return _nodesToJson(node == null ? const <DiagnosticsNode>[] : _getChildrenHelper(node, config), config);
}
/// Returns a JSON representation of the children of the [DiagnosticsNode]
/// object that `diagnosticsNodeId` references only including children that
/// were created directly by user code.
///
/// Requires [Widget] creation locations which are only available for debug
/// mode builds when the `--track-widget-creation` flag is passed to
/// `flutter_tool`.
///
/// See also:
///
/// * [isWidgetCreationTracked] which indicates whether this method can be
/// used.
String getChildrenSummaryTree(String diagnosticsNodeId, String groupName) {
return _safeJsonEncode(_getChildrenSummaryTree(diagnosticsNodeId, groupName));
}
List<Object> _getChildrenSummaryTree(String diagnosticsNodeId, String groupName) {
final DiagnosticsNode node = toObject(diagnosticsNodeId);
final _SerializeConfig config = new _SerializeConfig(groupName: groupName, summaryTree: true);
return _nodesToJson(node == null ? const <DiagnosticsNode>[] : _getChildrenHelper(node, config), config);
}
/// Returns a JSON representation of the children of the [DiagnosticsNode]
/// object that `diagnosticsNodeId` references providing information needed
/// for the details subtree view.
///
/// The details subtree shows properties inline and includes all children
/// rather than a filtered set of important children.
String getChildrenDetailsSubtree(String diagnosticsNodeId, String groupName) {
return _safeJsonEncode(_getChildrenDetailsSubtree(diagnosticsNodeId, groupName));
}
List<Object> _getChildrenDetailsSubtree(String diagnosticsNodeId, String groupName) {
final DiagnosticsNode node = toObject(diagnosticsNodeId);
// With this value of minDepth we only expand one extra level of important nodes.
final _SerializeConfig config = new _SerializeConfig(groupName: groupName, subtreeDepth: 1, includeProperties: true);
return _nodesToJson(node == null ? const <DiagnosticsNode>[] : _getChildrenHelper(node, config), config);
}
List<DiagnosticsNode> _getChildrenHelper(DiagnosticsNode node, _SerializeConfig config) {
return _getChildrenFiltered(node, config).toList();
}
bool _shouldShowInSummaryTree(DiagnosticsNode node) {
final Object value = node.value;
if (value is! Diagnosticable) {
return true;
}
if (value is! Element || !isWidgetCreationTracked()) {
// Creation locations are not availabe so include all nodes in the
// summary tree.
return true;
}
return _isValueCreatedByLocalProject(value);
}
List<DiagnosticsNode> _getChildrenFiltered(
DiagnosticsNode node,
_SerializeConfig config,
) {
final List<DiagnosticsNode> children = <DiagnosticsNode>[];
for (DiagnosticsNode child in node.getChildren()) {
if (!config.summaryTree || _shouldShowInSummaryTree(child)) {
children.add(child);
} else {
children.addAll(_getChildrenFiltered(child, config));
}
}
return children;
}
/// Returns a JSON representation of the [DiagnosticsNode] for the root
/// [Element].
String getRootWidget(String groupName) {
return json.encode(_getRootWidget(groupName));
return _safeJsonEncode(_getRootWidget(groupName));
}
Map<String, Object> _getRootWidget(String groupName) {
return _serializeToJson(WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(), groupName);
return _nodeToJson(WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(), new _SerializeConfig(groupName: groupName));
}
/// Returns a JSON representation of the [DiagnosticsNode] for the root
/// [Element] showing only nodes that should be included in a summary tree.
String getRootWidgetSummaryTree(String groupName) {
return _safeJsonEncode(_getRootWidgetSummaryTree(groupName));
}
Map<String, Object> _getRootWidgetSummaryTree(String groupName) {
return _nodeToJson(
WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(),
new _SerializeConfig(groupName: groupName, subtreeDepth: 1000000, summaryTree: true),
);
}
/// Returns a JSON representation of the [DiagnosticsNode] for the root
/// [RenderObject].
@protected
String getRootRenderObject(String groupName) {
return json.encode(_getRootRenderObject(groupName));
return _safeJsonEncode(_getRootRenderObject(groupName));
}
Map<String, Object> _getRootRenderObject(String groupName) {
return _serializeToJson(RendererBinding.instance?.renderView?.toDiagnosticsNode(), groupName);
return _nodeToJson(RendererBinding.instance?.renderView?.toDiagnosticsNode(), new _SerializeConfig(groupName: groupName));
}
/// Returns a JSON representation of the subtree rooted at the
/// [DiagnosticsNode] object that `diagnosticsNodeId` references providing
/// information needed for the details subtree view.
///
/// See also:
/// * [getChildrenDetailsSubtree], a method to get children of a node
/// in the details subtree.
String getDetailsSubtree(String id, String groupName) {
return _safeJsonEncode(_getDetailsSubtree( id, groupName));
}
Map<String, Object> _getDetailsSubtree(String id, String groupName) {
final DiagnosticsNode root = toObject(id);
if (root == null) {
return null;
}
return _nodeToJson(
root,
new _SerializeConfig(
groupName: groupName,
summaryTree: false,
subtreeDepth: 2, // TODO(jacobr): make subtreeDepth configurable.
includeProperties: true,
),
);
}
/// Returns a [DiagnosticsNode] representing the currently selected
......@@ -713,13 +1017,13 @@ class WidgetInspectorService {
/// [DiagnosticNode] is reused.
@protected
String getSelectedRenderObject(String previousSelectionId, String groupName) {
return json.encode(_getSelectedRenderObject(previousSelectionId, groupName));
return _safeJsonEncode(_getSelectedRenderObject(previousSelectionId, groupName));
}
Map<String, Object> _getSelectedRenderObject(String previousSelectionId, String groupName) {
final DiagnosticsNode previousSelection = toObject(previousSelectionId);
final RenderObject current = selection?.current;
return _serializeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), groupName);
return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), new _SerializeConfig(groupName: groupName));
}
/// Returns a [DiagnosticsNode] representing the currently selected [Element].
......@@ -729,13 +1033,44 @@ class WidgetInspectorService {
/// reused.
@protected
String getSelectedWidget(String previousSelectionId, String groupName) {
return json.encode(_getSelectedWidget(previousSelectionId, groupName));
return _safeJsonEncode(_getSelectedWidget(previousSelectionId, groupName));
}
Map<String, Object> _getSelectedWidget(String previousSelectionId, String groupName) {
final DiagnosticsNode previousSelection = toObject(previousSelectionId);
final Element current = selection?.currentElement;
return _serializeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), groupName);
return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), new _SerializeConfig(groupName: groupName));
}
/// Returns a [DiagnosticsNode] representing the currently selected [Element]
/// if the selected [Element] should be shown in the summary tree otherwise
/// returns the first ancestor of the selected [Element] shown in the summary
/// tree.
///
/// If the currently selected [Element] is identical to the [Element]
/// referenced by `previousSelectionId` then the previous [DiagnosticNode] is
/// reused.
String getSelectedSummaryWidget(String previousSelectionId, String groupName) {
return _safeJsonEncode(_getSelectedSummaryWidget(previousSelectionId, groupName));
}
Map<String, Object> _getSelectedSummaryWidget(String previousSelectionId, String groupName) {
if (!isWidgetCreationTracked()) {
return _getSelectedWidget(previousSelectionId, groupName);
}
final DiagnosticsNode previousSelection = toObject(previousSelectionId);
Element current = selection?.currentElement;
if (current != null && !_isValueCreatedByLocalProject(current)) {
Element firstLocal;
for (Element candidate in current.debugGetDiagnosticChain()) {
if (_isValueCreatedByLocalProject(candidate)) {
firstLocal = candidate;
break;
}
}
current = firstLocal;
}
return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), new _SerializeConfig(groupName: groupName));
}
/// Returns whether [Widget] creation locations are available.
......@@ -745,7 +1080,12 @@ class WidgetInspectorService {
/// 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;
bool isWidgetCreationTracked() {
_widgetCreationTracked ??= new _WidgetForTypeTests() is _HasCreationLocation;
return _widgetCreationTracked;
}
bool _widgetCreationTracked;
}
class _WidgetForTypeTests extends Widget {
......
......@@ -511,7 +511,7 @@ void main() {
// If you add a service extension... TEST IT! :-)
// ...then increment this number.
expect(binding.extensions.length, 32);
expect(binding.extensions.length, 37);
expect(console, isEmpty);
debugPrint = debugPrintThrottled;
......
......@@ -876,6 +876,218 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
}
});
testWidgets('ext.flutter.inspector.getChildrenDetailsSubtree', (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 String id = service.toId(diagnostic, group);
final List<Object> childrenJson = await service.testExtension('getChildrenDetailsSubtree', <String, String>{'arg': id, 'objectGroup': group});
final List<DiagnosticsNode> children = diagnostic.getChildren();
expect(children.length, equals(3));
expect(childrenJson.length, equals(children.length));
for (int i = 0; i < childrenJson.length; ++i) {
final Map<String, Object> childJson = childrenJson[i];
expect(service.toObject(childJson['valueId']), equals(children[i].value));
expect(service.toObject(childJson['objectId']), const isInstanceOf<DiagnosticsNode>());
final List<Object> propertiesJson = childJson['properties'];
final DiagnosticsNode diagnosticsNode = service.toObject(childJson['objectId']);
final List<DiagnosticsNode> expectedProperties = diagnosticsNode.getProperties();
for (Map<String, Object> propertyJson in propertiesJson) {
final Object property = service.toObject(propertyJson['objectId']);
expect(property, const isInstanceOf<DiagnosticsNode>());
expect(expectedProperties.contains(property), isTrue);
}
}
});
testWidgets('WidgetInspectorService getDetailsSubtree', (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 String id = service.toId(diagnostic, group);
final Map<String, Object> subtreeJson = await service.testExtension('getDetailsSubtree', <String, String>{'arg': id, 'objectGroup': group});
expect(subtreeJson['objectId'], equals(id));
final List<Object> childrenJson = subtreeJson['children'];
final List<DiagnosticsNode> children = diagnostic.getChildren();
expect(children.length, equals(3));
expect(childrenJson.length, equals(children.length));
for (int i = 0; i < childrenJson.length; ++i) {
final Map<String, Object> childJson = childrenJson[i];
expect(service.toObject(childJson['valueId']), equals(children[i].value));
expect(service.toObject(childJson['objectId']), const isInstanceOf<DiagnosticsNode>());
final List<Object> propertiesJson = childJson['properties'];
final DiagnosticsNode diagnosticsNode = service.toObject(childJson['objectId']);
final List<DiagnosticsNode> expectedProperties = diagnosticsNode.getProperties();
for (Map<String, Object> propertyJson in propertiesJson) {
final Object property = service.toObject(propertyJson['objectId']);
expect(property, const isInstanceOf<DiagnosticsNode>());
expect(expectedProperties.contains(property), isTrue);
}
}
});
testWidgets('ext.flutter.inspector.getRootWidgetSummaryTree', (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;
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'});
await service.testExtension('setPubRootDirectories', <String, String>{});
Map<String, Object> rootJson = await service.testExtension('getRootWidgetSummaryTree', <String, String>{'objectGroup': group});
// We haven't yet properly specified which directories are summary tree
// directories so we get an empty tree other than the root that is always
// included.
final Object rootWidget = service.toObject(rootJson['valueId']);
expect(rootWidget, equals(WidgetsBinding.instance?.renderViewElement));
List<Object> childrenJson = rootJson['children'];
// There are no summary tree children.
expect(childrenJson.length, equals(0));
final Map<String, Object> creationLocation = jsonA['creationLocation'];
expect(creationLocation, isNotNull);
final String testFile = creationLocation['file'];
expect(testFile, endsWith('widget_inspector_test.dart'));
final List<String> segments = Uri.parse(testFile).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});
rootJson = await service.testExtension('getRootWidgetSummaryTree', <String, String>{'objectGroup': group});
childrenJson = rootJson['children'];
// The tree of nodes returned contains all widgets created directly by the
// test.
childrenJson = rootJson['children'];
expect(childrenJson.length, equals(1));
List<Object> alternateChildrenJson = await service.testExtension('getChildrenSummaryTree', <String, String>{'arg': rootJson['objectId'], 'objectGroup': group});
expect(alternateChildrenJson.length, equals(1));
Map<String, Object> childJson = childrenJson[0];
Map<String, Object> alternateChildJson = alternateChildrenJson[0];
expect(childJson['description'], startsWith('Directionality'));
expect(alternateChildJson['description'], startsWith('Directionality'));
expect(alternateChildJson['valueId'], equals(childJson['valueId']));
childrenJson = childJson['children'];
alternateChildrenJson = await service.testExtension('getChildrenSummaryTree', <String, String>{'arg': childJson['objectId'], 'objectGroup': group});
expect(alternateChildrenJson.length, equals(1));
expect(childrenJson.length, equals(1));
alternateChildJson = alternateChildrenJson[0];
childJson = childrenJson[0];
expect(childJson['description'], startsWith('Stack'));
expect(alternateChildJson['description'], startsWith('Stack'));
expect(alternateChildJson['valueId'], equals(childJson['valueId']));
childrenJson = childJson['children'];
childrenJson = childJson['children'];
alternateChildrenJson = await service.testExtension('getChildrenSummaryTree', <String, String>{'arg': childJson['objectId'], 'objectGroup': group});
expect(alternateChildrenJson.length, equals(3));
expect(childrenJson.length, equals(3));
alternateChildJson = alternateChildrenJson[2];
childJson = childrenJson[2];
expect(childJson['description'], startsWith('Text'));
expect(alternateChildJson['description'], startsWith('Text'));
expect(alternateChildJson['valueId'], equals(childJson['valueId']));
alternateChildrenJson = await service.testExtension('getChildrenSummaryTree', <String, String>{'arg': childJson['objectId'], 'objectGroup': group});
expect(alternateChildrenJson.length , equals(0));
expect(childJson['chidlren'], isNull);
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
testWidgets('ext.flutter.inspector.getSelectedSummaryWidget', (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 List<DiagnosticsNode> children = elementA.debugDescribeChildren();
expect(children.length, equals(1));
final DiagnosticsNode richTextDiagnostic = children.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'});
service.setSelection(richTextDiagnostic.value, 'my-group');
await service.testExtension('setPubRootDirectories', <String, String>{});
Map<String, Object> summarySelection = await service.testExtension('getSelectedSummaryWidget', <String, String>{'objectGroup': group});
// No summary selection because we haven't set the pub root directories
// yet to indicate what directories are in the summary tree.
expect(summarySelection, isNull);
final Map<String, Object> creationLocation = jsonA['creationLocation'];
expect(creationLocation, isNotNull);
final String testFile = creationLocation['file'];
expect(testFile, endsWith('widget_inspector_test.dart'));
final List<String> segments = Uri.parse(testFile).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});
summarySelection = await service.testExtension('getSelectedSummaryWidget', <String, String>{'objectGroup': group});
expect(summarySelection['valueId'], isNotNull);
// We got the Text element instead of the selected RichText element
// because only the RichText element is part of the summary tree.
expect(service.toObject(summarySelection['valueId']), elementA);
// Verify tha the regular getSelectedWidget method still returns
// the RichText object not the Text element.
final Map<String, Object> regularSelection = await service.testExtension('getSelectedWidget', <String, String>{'arg': null, 'objectGroup': 'my-group'});
expect(service.toObject(regularSelection['valueId']), richTextDiagnostic.value);
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
testWidgets('ext.flutter.inspector creationLocation', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
......
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