Unverified Commit cdbb1e60 authored by Kenzie Davisson's avatar Kenzie Davisson Committed by GitHub

Move Widget Inspector service extensions from DevTools to Flutter (#113861)

parent bd5ab248
......@@ -111,8 +111,6 @@ enum WidgetInspectorServiceExtensions {
///
/// * [WidgetInspectorService.initServiceExtensions], where the service
/// extension is registered.
/// * [WidgetInspectorService._reportStructuredError], which is the error
/// reporter that will be used when this service extension is enabled.
structuredErrors,
/// Name of service extension that, when called, will change the value of
......@@ -126,38 +124,24 @@ enum WidgetInspectorServiceExtensions {
/// extension is registered.
show,
/// Name of service extension that, when called, changes the value of
/// [WidgetInspectorService._trackRebuildDirtyWidgets], which determines
/// Name of service extension that, when called, determines
/// whether a callback is invoked for every dirty [Widget] built each frame.
///
/// This service extension is only supported if
/// [WidgetInspectorService._widgetCreationTracked] is true.
///
/// See also:
///
/// * [debugOnRebuildDirtyWidget], which is the nullable callback that is
/// called for every dirty widget built per frame
/// * [WidgetInspectorService._onRebuildWidget], which is the callback we
/// assign to [debugOnRebuildDirtyWidget] when this service extension is set
/// to true.
/// * [WidgetInspectorService.initServiceExtensions], where the service
/// extension is registered.
trackRebuildDirtyWidgets,
/// Name of service extension that, when called, changes the value of
/// [WidgetInspectorService._trackRepaintWidgets], which determines whether
/// Name of service extension that, when called, determines whether
/// a callback is invoked for every [RenderObject] painted each frame.
///
/// This service extension is only supported if
/// [WidgetInspectorService._widgetCreationTracked] is true.
///
/// See also:
///
/// * [debugOnProfilePaint], which is the nullable callback that is called for
/// every dirty widget built per frame
/// * [WidgetInspectorService._onPaint], which is the callback we
/// assign to [debugOnRebuildDirtyWidget] when this service extension is set
/// to true.
/// * [WidgetInspectorService.initServiceExtensions], where the service
/// extension is registered.
trackRepaintWidgets,
......@@ -237,6 +221,8 @@ enum WidgetInspectorServiceExtensions {
/// service extension calls.
/// * [WidgetInspectorService.removePubRootDirectories], which should be used
/// to remove directories.
/// * [WidgetInspectorService.pubRootDirectories], which should be used
/// to return the active list of directories.
/// * [WidgetInspectorService.initServiceExtensions], where the service
/// extension is registered.
addPubRootDirectories,
......@@ -251,10 +237,28 @@ enum WidgetInspectorServiceExtensions {
/// service extension calls.
/// * [WidgetInspectorService.addPubRootDirectories], which should be used
/// to add directories.
/// * [WidgetInspectorService.pubRootDirectories], which should be used
/// to return the active list of directories.
/// * [WidgetInspectorService.initServiceExtensions], where the service
/// extension is registered.
removePubRootDirectories,
/// Name of service extension that, when called, will return the list of
/// directories that are considered part of the local project
/// for the Widget inspector summary tree.
///
/// See also:
///
/// * [WidgetInspectorService.pubRootDirectories], the method that this
/// service extension calls.
/// * [WidgetInspectorService.addPubRootDirectories], which should be used
/// to add directories.
/// * [WidgetInspectorService.removePubRootDirectories], which should be used
/// to remove directories.
/// * [WidgetInspectorService.initServiceExtensions], where the service
/// extension is registered.
getPubRootDirectories,
/// Name of service extension that, when called, will set the
/// [WidgetInspector] selection to the object matching the specified id and
/// will return whether the selection was changed.
......@@ -273,8 +277,6 @@ enum WidgetInspectorServiceExtensions {
///
/// See also:
///
/// * [WidgetInspectorService._getParentChain], the method that this service
/// extension calls.
/// * [WidgetInspectorService.getParentChain], which returns a json encoded
/// String representation of this data.
/// * [WidgetInspectorService.initServiceExtensions], where the service
......@@ -287,8 +289,6 @@ enum WidgetInspectorServiceExtensions {
///
/// See also:
///
/// * [WidgetInspectorService._getProperties], the method that this service
/// extension calls.
/// * [WidgetInspectorService.getProperties], which returns a json encoded
/// String representation of this data.
/// * [WidgetInspectorService.initServiceExtensions], where the service
......@@ -301,8 +301,6 @@ enum WidgetInspectorServiceExtensions {
///
/// See also:
///
/// * [WidgetInspectorService._getChildren], the method that this service
/// extension calls.
/// * [WidgetInspectorService.getChildren], which returns a json encoded
/// String representation of this data.
/// * [WidgetInspectorService.initServiceExtensions], where the service
......@@ -315,8 +313,6 @@ enum WidgetInspectorServiceExtensions {
///
/// See also:
///
/// * [WidgetInspectorService._getChildrenSummaryTree], the method that this
/// service extension calls.
/// * [WidgetInspectorService.getChildrenSummaryTree], which returns a json
/// encoded String representation of this data.
/// * [WidgetInspectorService.initServiceExtensions], where the service
......@@ -329,8 +325,6 @@ enum WidgetInspectorServiceExtensions {
///
/// See also:
///
/// * [WidgetInspectorService._getChildrenDetailsSubtree], the method that
/// this service extension calls.
/// * [WidgetInspectorService.getChildrenDetailsSubtree], which returns a json
/// encoded String representation of this data.
/// * [WidgetInspectorService.initServiceExtensions], where the service
......@@ -342,8 +336,6 @@ enum WidgetInspectorServiceExtensions {
///
/// See also:
///
/// * [WidgetInspectorService._getRootWidget], the method that this service
/// extension calls.
/// * [WidgetInspectorService.getRootWidget], which returns a json encoded
/// String representation of this data.
/// * [WidgetInspectorService.initServiceExtensions], where the service
......@@ -355,8 +347,6 @@ enum WidgetInspectorServiceExtensions {
///
/// See also:
///
/// * [WidgetInspectorService._getRootRenderObject], the method that this
/// service extension calls.
/// * [WidgetInspectorService.getRootRenderObject], which returns a json
/// encoded String representation of this data.
/// * [WidgetInspectorService.initServiceExtensions], where the service
......@@ -369,14 +359,26 @@ enum WidgetInspectorServiceExtensions {
///
/// See also:
///
/// * [WidgetInspectorService._getRootWidgetSummaryTree], the method that this
/// service extension calls.
/// * [WidgetInspectorService.getRootWidgetSummaryTree], which returns a json
/// encoded String representation of this data.
/// * [WidgetInspectorService.initServiceExtensions], where the service
/// extension is registered.
getRootWidgetSummaryTree,
/// Name of service extension that, when called, will return the
/// [DiagnosticsNode] data for the root [Element] of the summary tree with
/// text previews included.
///
/// The summary tree only includes [Element]s that were created by user code.
/// Text previews will only be available for [Element]s with a corresponding
/// [RenderObject] of type [RenderParagraph].
///
/// See also:
///
/// * [WidgetInspectorService.initServiceExtensions], where the service
/// extension is registered.
getRootWidgetSummaryTreeWithPreviews,
/// Name of service extension that, when called, will return the details
/// subtree, which includes properties, rooted at the [DiagnosticsNode] object
/// matching the specified id and the having a size matching the specified
......@@ -397,8 +399,6 @@ enum WidgetInspectorServiceExtensions {
///
/// See also:
///
/// * [WidgetInspectorService._getSelectedRenderObject], the method that this
/// service extension calls.
/// * [WidgetInspectorService.getSelectedRenderObject], which returns a json
/// encoded String representation of this data.
/// * [WidgetInspectorService.initServiceExtensions], where the service
......@@ -410,8 +410,6 @@ enum WidgetInspectorServiceExtensions {
///
/// See also:
///
/// * [WidgetInspectorService._getSelectedWidget], the method that this
/// service extension calls.
/// * [WidgetInspectorService.getSelectedWidget], which returns a json
/// encoded String representation of this data.
/// * [WidgetInspectorService.initServiceExtensions], where the service
......@@ -428,8 +426,6 @@ enum WidgetInspectorServiceExtensions {
///
/// See also:
///
/// * [WidgetInspectorService._getSelectedSummaryWidget], the method that this
/// service extension calls.
/// * [WidgetInspectorService.getSelectedSummaryWidget], which returns a json
/// encoded String representation of this data.
/// * [WidgetInspectorService.initServiceExtensions], where the service
......@@ -459,4 +455,47 @@ enum WidgetInspectorServiceExtensions {
/// * [WidgetInspectorService.initServiceExtensions], where the service
/// extension is registered.
screenshot,
/// Name of service extension that, when called, will return the
/// [DiagnosticsNode] data for the currently selected [Element] and will
/// include information about the [Element]'s layout properties.
///
/// See also:
///
/// * [WidgetInspectorService.initServiceExtensions], where the service
/// extension is registered.
getLayoutExplorerNode,
/// Name of service extension that, when called, will set the [FlexFit] value
/// for the [FlexParentData] of the [RenderObject] matching the specified
/// `id`, passed as an argument.
///
/// See also:
///
/// * [WidgetInspectorService.initServiceExtensions], where the service
/// extension is registered.
setFlexFit,
/// Name of service extension that, when called, will set the flex value
/// for the [FlexParentData] of the [RenderObject] matching the specified
/// `id`, passed as an argument.
///
/// See also:
///
/// * [WidgetInspectorService.initServiceExtensions], where the service
/// extension is registered.
setFlexFactor,
/// Name of service extension that, when called, will set the
/// [MainAxisAlignment] and [CrossAxisAlignment] values for the [RenderFlex]
/// matching the specified `id`, passed as an argument.
///
/// The [MainAxisAlignment] and [CrossAxisAlignment] values will be passed as
/// arguments `mainAxisAlignment` and `crossAxisAlignment`, respectively.
///
/// See also:
///
/// * [WidgetInspectorService.initServiceExtensions], where the service
/// extension is registered.
setFlexProperties,
}
......@@ -749,13 +749,9 @@ mixin WidgetInspectorService {
final Map<Object, String> _objectToId = Map<Object, String>.identity();
int _nextId = 0;
/// the pubRootDirectories that are currently configured for the widget inspector.
///
/// This is for testing use only.
@visibleForTesting
@protected
List<String>? get pubRootDirectories => _pubRootDirectories == null ? const <String>[] : List<String>.from(_pubRootDirectories!);
/// The pubRootDirectories that are currently configured for the widget inspector.
List<String>? _pubRootDirectories;
/// Memoization for [_isLocalCreationLocation].
final HashMap<String, bool> _isLocalCreationCache = HashMap<String, bool>();
......@@ -1127,6 +1123,10 @@ mixin WidgetInspectorService {
return null;
},
);
registerServiceExtension(
name: WidgetInspectorServiceExtensions.getPubRootDirectories.name,
callback: pubRootDirectories,
);
_registerServiceExtensionWithArg(
name: WidgetInspectorServiceExtensions.setSelectionById.name,
callback: setSelectionById,
......@@ -1166,6 +1166,10 @@ mixin WidgetInspectorService {
name: WidgetInspectorServiceExtensions.getRootWidgetSummaryTree.name,
callback: _getRootWidgetSummaryTree,
);
registerServiceExtension(
name: WidgetInspectorServiceExtensions.getRootWidgetSummaryTreeWithPreviews.name,
callback: _getRootWidgetSummaryTreeWithPreviews,
);
registerServiceExtension(
name: WidgetInspectorServiceExtensions.getDetailsSubtree.name,
callback: (Map<String, String> parameters) async {
......@@ -1224,6 +1228,22 @@ mixin WidgetInspectorService {
};
},
);
registerServiceExtension(
name: WidgetInspectorServiceExtensions.getLayoutExplorerNode.name,
callback: _getLayoutExplorerNode,
);
registerServiceExtension(
name: WidgetInspectorServiceExtensions.setFlexFit.name,
callback: _setFlexFit,
);
registerServiceExtension(
name: WidgetInspectorServiceExtensions.setFlexFactor.name,
callback: _setFlexFactor,
);
registerServiceExtension(
name: WidgetInspectorServiceExtensions.setFlexProperties.name,
callback: _setFlexProperties,
);
}
void _clearStats() {
......@@ -1439,6 +1459,18 @@ mixin WidgetInspectorService {
_isLocalCreationCache.clear();
}
/// Returns the list of directories that should be considered part of the
/// local project.
@protected
@visibleForTesting
Future<Map<String, dynamic>> pubRootDirectories(
Map<String, String> parameters,
) {
return Future<Map<String, Object>>.value(<String, Object>{
'result': _pubRootDirectories ?? <String>[],
});
}
/// Set the [WidgetInspector] selection to the object matching the specified
/// id if the object is valid object to set as the inspector selection.
///
......@@ -1796,13 +1828,46 @@ mixin WidgetInspectorService {
return _safeJsonEncode(_getRootWidgetSummaryTree(groupName));
}
Map<String, Object?>? _getRootWidgetSummaryTree(String groupName) {
Map<String, Object?>? _getRootWidgetSummaryTree(
String groupName, {
Map<String, Object>? Function(DiagnosticsNode, InspectorSerializationDelegate)? addAdditionalPropertiesCallback,
}) {
return _nodeToJson(
WidgetsBinding.instance.renderViewElement?.toDiagnosticsNode(),
InspectorSerializationDelegate(groupName: groupName, subtreeDepth: 1000000, summaryTree: true, service: this),
InspectorSerializationDelegate(
groupName: groupName,
subtreeDepth: 1000000,
summaryTree: true,
service: this,
addAdditionalPropertiesCallback: addAdditionalPropertiesCallback,
),
);
}
Future<Map<String, Object?>> _getRootWidgetSummaryTreeWithPreviews(
Map<String, String> parameters,
) {
final String groupName = parameters['groupName']!;
final Map<String, Object?>? result = _getRootWidgetSummaryTree(
groupName,
addAdditionalPropertiesCallback: (DiagnosticsNode node, InspectorSerializationDelegate? delegate) {
final Map<String, Object> additionalJson = <String, Object>{};
final Object? value = node.value;
if (value is Element) {
final RenderObject? renderObject = value.renderObject;
if (renderObject is RenderParagraph) {
additionalJson['textPreview'] = renderObject.text.toPlainText();
}
}
return additionalJson;
},
);
return Future<Map<String, dynamic>>.value(<String, dynamic>{
'result': result,
});
}
/// Returns a JSON representation of the [DiagnosticsNode] for the root
/// [RenderObject].
@protected
......@@ -1952,6 +2017,200 @@ mixin WidgetInspectorService {
);
}
Future<Map<String, Object?>> _getLayoutExplorerNode(
Map<String, String> parameters,
) {
final String? id = parameters['id'];
final int subtreeDepth = int.parse(parameters['subtreeDepth']!);
final String? groupName = parameters['groupName'];
Map<String, dynamic>? result = <String, dynamic>{};
final Object? root = toObject(id);
if (root == null) {
return Future<Map<String, dynamic>>.value(<String, dynamic>{
'result': result,
});
}
result = _nodeToJson(
root as DiagnosticsNode,
InspectorSerializationDelegate(
groupName: groupName,
summaryTree: true,
subtreeDepth: subtreeDepth,
service: this,
addAdditionalPropertiesCallback: (DiagnosticsNode node, InspectorSerializationDelegate delegate) {
final Map<String, Object> additionalJson = <String, Object>{};
final Object? value = node.value;
if (value is Element) {
final RenderObject? renderObject = value.renderObject;
if (renderObject != null) {
additionalJson['renderObject'] =
renderObject.toDiagnosticsNode().toJsonMap(
delegate.copyWith(
subtreeDepth: 0,
includeProperties: true,
),
);
final AbstractNode? renderParent = renderObject.parent;
if (renderParent is RenderObject && subtreeDepth > 0) {
final Object? parentCreator = renderParent.debugCreator;
if (parentCreator is DebugCreator) {
additionalJson['parentRenderElement'] =
parentCreator.element.toDiagnosticsNode().toJsonMap(
delegate.copyWith(
subtreeDepth: 0,
includeProperties: true,
),
);
// TODO(jacobr): also describe the path back up the tree to
// the RenderParentElement from the current element. It
// could be a surprising distance up the tree if a lot of
// elements don't have their own RenderObjects.
}
}
try {
if (!renderObject.debugNeedsLayout) {
// ignore: invalid_use_of_protected_member
final Constraints constraints = renderObject.constraints;
final Map<String, Object>constraintsProperty = <String, Object>{
'type': constraints.runtimeType.toString(),
'description': constraints.toString(),
};
if (constraints is BoxConstraints) {
constraintsProperty.addAll(<String, Object>{
'minWidth': constraints.minWidth.toString(),
'minHeight': constraints.minHeight.toString(),
'maxWidth': constraints.maxWidth.toString(),
'maxHeight': constraints.maxHeight.toString(),
});
}
additionalJson['constraints'] = constraintsProperty;
}
} catch (e) {
// Constraints are sometimes unavailable even though
// debugNeedsLayout is false.
}
try {
if (renderObject is RenderBox) {
additionalJson['isBox'] = true;
additionalJson['size'] = <String, Object>{
'width': renderObject.size.width.toString(),
'height': renderObject.size.height.toString(),
};
final ParentData? parentData = renderObject.parentData;
if (parentData is FlexParentData) {
additionalJson['flexFactor'] = parentData.flex!;
additionalJson['flexFit'] =
describeEnum(parentData.fit ?? FlexFit.tight);
} else if (parentData is BoxParentData) {
final Offset offset = parentData.offset;
additionalJson['parentData'] = <String, Object>{
'offsetX': offset.dx.toString(),
'offsetY': offset.dy.toString(),
};
}
} else if (renderObject is RenderView) {
additionalJson['size'] = <String, Object>{
'width': renderObject.size.width.toString(),
'height': renderObject.size.height.toString(),
};
}
} catch (e) {
// Not laid out yet.
}
}
}
return additionalJson;
},
),
);
return Future<Map<String, dynamic>>.value(<String, dynamic>{
'result': result,
});
}
Future<Map<String, dynamic>> _setFlexFit(Map<String, String> parameters) {
final String? id = parameters['id'];
final String parameter = parameters['flexFit']!;
final FlexFit flexFit = _toEnumEntry<FlexFit>(FlexFit.values, parameter);
final Object? object = toObject(id);
bool succeed = false;
if (object != null && object is Element) {
final RenderObject? render = object.renderObject;
final ParentData? parentData = render?.parentData;
if (parentData is FlexParentData) {
parentData.fit = flexFit;
render!.markNeedsLayout();
succeed = true;
}
}
return Future<Map<String, Object>>.value(<String, Object>{
'result': succeed,
});
}
Future<Map<String, dynamic>> _setFlexFactor(Map<String, String> parameters) {
final String? id = parameters['id'];
final String flexFactor = parameters['flexFactor']!;
final int? factor = flexFactor == 'null' ? null : int.parse(flexFactor);
final dynamic object = toObject(id);
bool succeed = false;
if (object != null && object is Element) {
final RenderObject? render = object.renderObject;
final ParentData? parentData = render?.parentData;
if (parentData is FlexParentData) {
parentData.flex = factor;
render!.markNeedsLayout();
succeed = true;
}
}
return Future<Map<String, Object>>.value(<String, Object>{
'result': succeed
});
}
Future<Map<String, dynamic>> _setFlexProperties(
Map<String, String> parameters,
) {
final String? id = parameters['id'];
final MainAxisAlignment mainAxisAlignment = _toEnumEntry<MainAxisAlignment>(
MainAxisAlignment.values,
parameters['mainAxisAlignment']!,
);
final CrossAxisAlignment crossAxisAlignment =
_toEnumEntry<CrossAxisAlignment>(
CrossAxisAlignment.values,
parameters['crossAxisAlignment']!,
);
final Object? object = toObject(id);
bool succeed = false;
if (object != null && object is Element) {
final RenderObject? render = object.renderObject;
if (render is RenderFlex) {
render.mainAxisAlignment = mainAxisAlignment;
render.crossAxisAlignment = crossAxisAlignment;
render.markNeedsLayout();
render.markNeedsPaint();
succeed = true;
}
}
return Future<Map<String, Object>>.value(<String, Object>{
'result': succeed
});
}
T _toEnumEntry<T>(List<T> enumEntries, String name) {
for (final T entry in enumEntries) {
if (entry.toString() == name) {
return entry;
}
}
throw Exception('Enum value $name not found');
}
Map<String, Object?>? _getSelectedWidget(String? previousSelectionId, String groupName) {
final DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?;
final Element? current = selection.currentElement;
......
......@@ -160,7 +160,7 @@ void main() {
tearDownAll(() async {
// See widget_inspector_test.dart for tests of the ext.flutter.inspector
// service extensions included in this count.
int widgetInspectorExtensionCount = 16;
int widgetInspectorExtensionCount = 22;
if (WidgetInspectorService.instance.isWidgetCreationTracked()) {
// Some inspector extensions are only exposed if widget creation locations
// are tracked.
......
......@@ -1210,25 +1210,27 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
});
group('addPubRootDirectories', () {
test('can add multiple directories', () {
test('can add multiple directories', () async {
const List<String> directories = <String>[directoryA, directoryB];
service.addPubRootDirectories(directories);
expect(service.pubRootDirectories, unorderedEquals(directories));
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, unorderedEquals(directories));
});
test('can add multiple directories seperately', () {
test('can add multiple directories seperately', () async {
service.addPubRootDirectories(<String>[directoryA]);
service.addPubRootDirectories(<String>[directoryB]);
service.addPubRootDirectories(<String>[]);
expect(service.pubRootDirectories, unorderedEquals(<String>[
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, unorderedEquals(<String>[
directoryA,
directoryB,
]));
});
test('handles duplicates', () {
test('handles duplicates', () async {
const List<String> directories = <String>[
directoryA,
'file://$directoryA',
......@@ -1237,7 +1239,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
];
service.addPubRootDirectories(directories);
expect(service.pubRootDirectories, unorderedEquals(<String>[
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, unorderedEquals(<String>[
directoryA,
directoryB,
]));
......@@ -1250,21 +1253,23 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
service.addPubRootDirectories(<String>[directoryA, directoryB, directoryC]);
});
test('removes multiple directories', () {
test('removes multiple directories', () async {
service.removePubRootDirectories(<String>[directoryA, directoryB,]);
expect(service.pubRootDirectories, equals(<String>[directoryC]));
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, equals(<String>[directoryC]));
});
test('removes multiple directories seperately', () {
test('removes multiple directories seperately', () async {
service.removePubRootDirectories(<String>[directoryA]);
service.removePubRootDirectories(<String>[directoryB]);
service.removePubRootDirectories(<String>[]);
expect(service.pubRootDirectories, equals(<String>[directoryC]));
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, equals(<String>[directoryC]));
});
test('handles duplicates', () {
test('handles duplicates', () async {
service.removePubRootDirectories(<String>[
'file://$directoryA',
directoryA,
......@@ -1272,13 +1277,15 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
directoryB,
]);
expect(service.pubRootDirectories, equals(<String>[directoryC]));
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, equals(<String>[directoryC]));
});
test("does nothing if the directories doesn't exist ", () {
test("does nothing if the directories doesn't exist ", () async {
service.removePubRootDirectories(<String>['/x/y/z']);
expect(service.pubRootDirectories, unorderedEquals(<String>[
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, unorderedEquals(<String>[
directoryA,
directoryB,
directoryC,
......@@ -2216,6 +2223,97 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
expect(childJson['chidlren'], isNull);
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag.
testWidgets('ext.flutter.inspector.getRootWidgetSummaryTreeWithPreviews', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: const <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
service
..disposeAllGroups()
..resetPubRootDirectories()
..setSelection(elementA, 'my-group');
final Map<String, dynamic> jsonA = (await service.testExtension(
WidgetInspectorServiceExtensions.getSelectedWidget.name,
<String, String>{'objectGroup': 'my-group'},
))! as Map<String, dynamic>;
final Map<String, Object?> creationLocation = jsonA['creationLocation']! as Map<String, Object?>;
expect(creationLocation, isNotNull);
final String testFile = creationLocation['file']! as String;
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('/')}';
service
..resetPubRootDirectories()
..addPubRootDirectories(<String>[pubRootTest]);
final Map<String, Object?> rootJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getRootWidgetSummaryTreeWithPreviews.name,
<String, String>{'groupName': group},
))! as Map<String, Object?>;
List<Object?> childrenJson = rootJson['children']! as List<Object?>;
// The tree of nodes returned contains all widgets created directly by the
// test.
childrenJson = rootJson['children']! as List<Object?>;
expect(childrenJson.length, equals(1));
List<Object?> alternateChildrenJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getChildrenSummaryTree.name,
<String, String>{'arg': rootJson['objectId']! as String, 'objectGroup': group},
))! as List<Object?>;
expect(alternateChildrenJson.length, equals(1));
Map<String, Object?> childJson = childrenJson[0]! as Map<String, Object?>;
Map<String, Object?> alternateChildJson = alternateChildrenJson[0]! as Map<String, Object?>;
expect(childJson['description'], startsWith('Directionality'));
expect(alternateChildJson['description'], startsWith('Directionality'));
expect(alternateChildJson['valueId'], equals(childJson['valueId']));
childrenJson = childJson['children']! as List<Object?>;
alternateChildrenJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getChildrenSummaryTree.name,
<String, String>{'arg': childJson['objectId']! as String, 'objectGroup': group},
))! as List<Object?>;
expect(alternateChildrenJson.length, equals(1));
expect(childrenJson.length, equals(1));
alternateChildJson = alternateChildrenJson[0]! as Map<String, Object?>;
childJson = childrenJson[0]! as Map<String, Object?>;
expect(childJson['description'], startsWith('Stack'));
expect(alternateChildJson['description'], startsWith('Stack'));
expect(alternateChildJson['valueId'], equals(childJson['valueId']));
childrenJson = childJson['children']! as List<Object?>;
childrenJson = childJson['children']! as List<Object?>;
alternateChildrenJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getChildrenSummaryTree.name,
<String, String>{'arg': childJson['objectId']! as String, 'objectGroup': group},
))! as List<Object?>;
expect(alternateChildrenJson.length, equals(3));
expect(childrenJson.length, equals(3));
alternateChildJson = alternateChildrenJson[2]! as Map<String, Object?>;
childJson = childrenJson[2]! as Map<String, Object?>;
expect(childJson['description'], startsWith('Text'));
// [childJson] contains the 'textPreview' key since the tree was requested
// with previews [getRootWidgetSummaryTreeWithPreviews].
expect(childJson['textPreview'], equals('c'));
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag.
testWidgets('ext.flutter.inspector.getSelectedSummaryWidget', (WidgetTester tester) async {
const String group = 'test-group';
......@@ -4055,6 +4153,292 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
);
});
group('layout explorer', () {
const String group = 'test-group';
tearDown(() {
service.disposeAllGroups();
});
Future<void> pumpWidgetForLayoutExplorer(WidgetTester tester) async {
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Row(
children: <Widget>[
Flexible(
child: Container(
color: Colors.green,
child: const Text('a'),
),
),
const Text('b'),
],
),
),
);
await tester.pumpWidget(widget);
}
testWidgets('ext.flutter.inspector.getLayoutExplorerNode for RenderBox with BoxParentData',(WidgetTester tester) async {
await pumpWidgetForLayoutExplorer(tester);
final Element rowElement = tester.element(find.byType(Row));
service.setSelection(rowElement, group);
final DiagnosticsNode diagnostic = rowElement.toDiagnosticsNode();
final String id = service.toId(diagnostic, group)!;
final Map<String, Object?> result = (await service.testExtension(
WidgetInspectorServiceExtensions.getLayoutExplorerNode.name,
<String, String>{'id': id, 'groupName': group, 'subtreeDepth': '1'},
))! as Map<String, Object?>;
expect(result['description'], equals('Row'));
final Map<String, Object?>? renderObject = result['renderObject'] as Map<String, Object?>?;
expect(renderObject, isNotNull);
expect(renderObject!['description'], startsWith('RenderFlex'));
final Map<String, Object?>? parentRenderElement = result['parentRenderElement'] as Map<String, Object?>?;
expect(parentRenderElement, isNotNull);
expect(parentRenderElement!['description'], equals('Center'));
final Map<String, Object?>? constraints = result['constraints'] as Map<String, Object?>?;
expect(constraints, isNotNull);
expect(constraints!['type'], equals('BoxConstraints'));
expect(constraints['minWidth'], equals('0.0'));
expect(constraints['minHeight'], equals('0.0'));
expect(constraints['maxWidth'], equals('800.0'));
expect(constraints['maxHeight'], equals('600.0'));
expect(result['isBox'], equals(true));
final Map<String, Object?>? size = result['size'] as Map<String, Object?>?;
expect(size, isNotNull);
expect(size!['width'], equals('800.0'));
expect(size['height'], equals('14.0'));
expect(result['flexFactor'], isNull);
expect(result['flexFit'], isNull);
final Map<String, Object?>? parentData = result['parentData'] as Map<String, Object?>?;
expect(parentData, isNotNull);
expect(parentData!['offsetX'], equals('0.0'));
expect(parentData['offsetY'], equals('293.0'));
});
testWidgets('ext.flutter.inspector.getLayoutExplorerNode for RenderBox with FlexParentData',(WidgetTester tester) async {
await pumpWidgetForLayoutExplorer(tester);
final Element flexibleElement = tester.element(find.byType(Flexible).first);
service.setSelection(flexibleElement, group);
final DiagnosticsNode diagnostic = flexibleElement.toDiagnosticsNode();
final String id = service.toId(diagnostic, group)!;
final Map<String, Object?> result = (await service.testExtension(
WidgetInspectorServiceExtensions.getLayoutExplorerNode.name,
<String, String>{'id': id, 'groupName': group, 'subtreeDepth': '1'},
))! as Map<String, Object?>;
expect(result['description'], equals('Flexible'));
final Map<String, Object?>? renderObject = result['renderObject'] as Map<String, Object?>?;
expect(renderObject, isNotNull);
expect(renderObject!['description'], startsWith('_RenderColoredBox'));
final Map<String, Object?>? parentRenderElement = result['parentRenderElement'] as Map<String, Object?>?;
expect(parentRenderElement, isNotNull);
expect(parentRenderElement!['description'], equals('Row'));
final Map<String, Object?>? constraints = result['constraints'] as Map<String, Object?>?;
expect(constraints, isNotNull);
expect(constraints!['type'], equals('BoxConstraints'));
expect(constraints['minWidth'], equals('0.0'));
expect(constraints['minHeight'], equals('0.0'));
expect(constraints['maxWidth'], equals('786.0'));
expect(constraints['maxHeight'], equals('600.0'));
expect(result['isBox'], equals(true));
final Map<String, Object?>? size = result['size'] as Map<String, Object?>?;
expect(size, isNotNull);
expect(size!['width'], equals('14.0'));
expect(size['height'], equals('14.0'));
expect(result['flexFactor'], equals(1));
expect(result['flexFit'], equals('loose'));
expect(result['parentData'], isNull);
});
testWidgets('ext.flutter.inspector.getLayoutExplorerNode for RenderView',(WidgetTester tester) async {
await pumpWidgetForLayoutExplorer(tester);
final Element element = tester.element(find.byType(Directionality).first);
Element? root;
element.visitAncestorElements((Element ancestor) {
if (root == null) {
root = ancestor;
// Stop traversing ancestors.
return false;
}
return true;
});
expect(root, isNotNull);
service.setSelection(root, group);
final DiagnosticsNode diagnostic = root!.toDiagnosticsNode();
final String id = service.toId(diagnostic, group)!;
final Map<String, Object?> result = (await service.testExtension(
WidgetInspectorServiceExtensions.getLayoutExplorerNode.name,
<String, String>{'id': id, 'groupName': group, 'subtreeDepth': '1'},
))! as Map<String, Object?>;
expect(result['description'], equals('[root]'));
final Map<String, Object?>? renderObject = result['renderObject'] as Map<String, Object?>?;
expect(renderObject, isNotNull);
expect(renderObject!['description'], startsWith('RenderView'));
expect(result['parentRenderElement'], isNull);
expect(result['constraints'], isNull);
expect(result['isBox'], isNull);
final Map<String, Object?>? size = result['size'] as Map<String, Object?>?;
expect(size, isNotNull);
expect(size!['width'], equals('800.0'));
expect(size['height'], equals('600.0'));
expect(result['flexFactor'], isNull);
expect(result['flexFit'], isNull);
expect(result['parentData'], isNull);
});
testWidgets('ext.flutter.inspector.setFlexFit', (WidgetTester tester) async {
await pumpWidgetForLayoutExplorer(tester);
final Element childElement = tester.element(find.byType(Flexible).first);
service.setSelection(childElement, group);
final DiagnosticsNode diagnostic = childElement.toDiagnosticsNode();
final String id = service.toId(diagnostic, group)!;
Map<String, Object?> result = (await service.testExtension(
WidgetInspectorServiceExtensions.getLayoutExplorerNode.name,
<String, String>{'id': id, 'groupName': group, 'subtreeDepth': '1'},
))! as Map<String, Object?>;
expect(result['description'], equals('Flexible'));
expect(result['flexFit'], equals('loose'));
final String valueId = result['valueId']! as String;
final bool flexFitSuccess = (await service.testExtension(
WidgetInspectorServiceExtensions.setFlexFit.name,
<String, String>{'id': valueId, 'flexFit': 'FlexFit.tight'},
))! as bool;
expect(flexFitSuccess, isTrue);
result = (await service.testExtension(
WidgetInspectorServiceExtensions.getLayoutExplorerNode.name,
<String, String>{'id': id, 'groupName': group, 'subtreeDepth': '1'},
))! as Map<String, Object?>;
expect(result['description'], equals('Flexible'));
expect(result['flexFit'], equals('tight'));
});
testWidgets('ext.flutter.inspector.setFlexFactor', (WidgetTester tester) async {
await pumpWidgetForLayoutExplorer(tester);
final Element childElement = tester.element(find.byType(Flexible).first);
service.setSelection(childElement, group);
final DiagnosticsNode diagnostic = childElement.toDiagnosticsNode();
final String id = service.toId(diagnostic, group)!;
Map<String, Object?> result = (await service.testExtension(
WidgetInspectorServiceExtensions.getLayoutExplorerNode.name,
<String, String>{'id': id, 'groupName': group, 'subtreeDepth': '1'},
))! as Map<String, Object?>;
expect(result['description'], equals('Flexible'));
expect(result['flexFactor'], equals(1));
final String valueId = result['valueId']! as String;
final bool flexFactorSuccess = (await service.testExtension(
WidgetInspectorServiceExtensions.setFlexFactor.name,
<String, String>{'id': valueId, 'flexFactor': '3'},
))! as bool;
expect(flexFactorSuccess, isTrue);
result = (await service.testExtension(
WidgetInspectorServiceExtensions.getLayoutExplorerNode.name,
<String, String>{'id': id, 'groupName': group, 'subtreeDepth': '1'},
))! as Map<String, Object?>;
expect(result['description'], equals('Flexible'));
expect(result['flexFactor'], equals(3));
});
testWidgets('ext.flutter.inspector.setFlexProperties', (WidgetTester tester) async {
await pumpWidgetForLayoutExplorer(tester);
final Element rowElement = tester.element(find.byType(Row).first);
service.setSelection(rowElement, group);
final DiagnosticsNode diagnostic = rowElement.toDiagnosticsNode();
final String id = service.toId(diagnostic, group)!;
Map<String, Object?> result = (await service.testExtension(
WidgetInspectorServiceExtensions.getLayoutExplorerNode.name,
<String, String>{'id': id, 'groupName': group, 'subtreeDepth': '1'},
))! as Map<String, Object?>;
expect(result['description'], equals('Row'));
Map<String, Object?> renderObject = result['renderObject']! as Map<String, Object?>;
List<Map<String, Object?>> properties =
(renderObject['properties']! as List<dynamic>).cast<Map<String, Object?>>();
Map<String, Object?> mainAxisAlignmentProperties =
properties.firstWhere(
(Map<String, Object?> p) => p['type'] == 'EnumProperty<MainAxisAlignment>',
);
Map<String, Object?> crossAxisAlignmentProperties =
properties.firstWhere(
(Map<String, Object?> p) => p['type'] == 'EnumProperty<CrossAxisAlignment>',
);
String mainAxisAlignment = mainAxisAlignmentProperties['description']! as String;
String crossAxisAlignment = crossAxisAlignmentProperties['description']! as String;
expect(mainAxisAlignment, equals('start'));
expect(crossAxisAlignment, equals('center'));
final String valueId = result['valueId']! as String;
final bool flexFactorSuccess = (await service.testExtension(
WidgetInspectorServiceExtensions.setFlexProperties.name,
<String, String>{
'id': valueId,
'mainAxisAlignment': 'MainAxisAlignment.center',
'crossAxisAlignment': 'CrossAxisAlignment.start',
},
))! as bool;
expect(flexFactorSuccess, isTrue);
result = (await service.testExtension(
WidgetInspectorServiceExtensions.getLayoutExplorerNode.name,
<String, String>{'id': id, 'groupName': group, 'subtreeDepth': '1'},
))! as Map<String, Object?>;
expect(result['description'], equals('Row'));
renderObject = result['renderObject']! as Map<String, Object?>;
properties =
(renderObject['properties']! as List<dynamic>).cast<Map<String, Object?>>();
mainAxisAlignmentProperties =
properties.firstWhere(
(Map<String, Object?> p) => p['type'] == 'EnumProperty<MainAxisAlignment>',
);
crossAxisAlignmentProperties =
properties.firstWhere(
(Map<String, Object?> p) => p['type'] == 'EnumProperty<CrossAxisAlignment>',
);
mainAxisAlignment = mainAxisAlignmentProperties['description']! as String;
crossAxisAlignment = crossAxisAlignmentProperties['description']! as String;
expect(mainAxisAlignment, equals('center'));
expect(crossAxisAlignment, equals('start'));
});
});
test('ext.flutter.inspector.structuredErrors', () async {
List<Map<Object, Object?>> flutterErrorEvents = service.getEventsDispatched('Flutter.Error');
expect(flutterErrorEvents, isEmpty);
......@@ -4618,3 +5002,11 @@ void _addToKnownLocationsMap({
}
});
}
extension WidgetInspectorServiceExtension on WidgetInspectorService {
Future<List<String>> get currentPubRootDirectories async {
return ((await pubRootDirectories(
<String, String>{},
))['result'] as List<Object?>).cast<String>();
}
}
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