Skip to content

Add text previews to widget tree #3218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,41 @@ String addServiceExtensions() {
throw 'Enum value $name not found';
}

/// This is identical to Flutter's getRootWidgetSummaryTree service extension,
/// but with the added properties.
Future<Map<String, dynamic>> getRootWidgetSummaryTreeWithPreviews(
Map<String, String> parameters) {
final instance = WidgetInspectorService.instance;
final groupName = parameters['groupName'];

final result = instance._nodeToJson(
WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(),
InspectorSerializationDelegate(
groupName: groupName,
subtreeDepth: 1000000,
summaryTree: true,
service: instance,
addAdditionalPropertiesCallback: (DiagnosticsNode node, delegate) {
final additionalJson = <String, Object>{};

final value = node.value;
if (value is Element) {
final renderObject = value.renderObject;
if (renderObject is RenderParagraph) {
additionalJson['textPreview'] = renderObject.text.toPlainText();
}
}

return additionalJson;
},
),
);

return Future.value(<String, dynamic>{
'result': result,
});
}

Future<Map<String, dynamic>> getLayoutExplorerNode(
Map<String, String> parameters) {
final id = parameters['id'];
Expand Down Expand Up @@ -317,6 +352,11 @@ String addServiceExtensions() {
registerHelper('setFlexFactor', setFlexFactor);
registerHelper('setFlexProperties', setFlexProperties);
registerHelper('getPubRootDirectories', getPubRootDirectories);
registerHelper(
'getRootWidgetSummaryTreeWithPreviews',
getRootWidgetSummaryTreeWithPreviews,
);

return WidgetInspectorService.instance._safeJsonEncode(failures);
// INSPECTOR_POLYFILL_SCRIPT_END
}
10 changes: 10 additions & 0 deletions packages/devtools_app/lib/src/inspector/diagnostics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,19 @@ class DiagnosticsNodeDescription extends StatelessWidget {
return;
}
}

if (description?.isNotEmpty == true) {
yield TextSpan(text: description, style: textStyle);
}

final textPreview = diagnostic.json['textPreview'];
if (textPreview is String) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we be checking for is String or != null ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now need to check it's a string to strip newlines from it

final preview = textPreview.replaceAll('\n', ' ');
yield TextSpan(
text: ': "$preview"',
style: textStyle.merge(inspector_text_styles.unimportant(colorScheme)),
);
}
}

Widget buildDescription(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class RegistrableServiceExtension {
static const setFlexFactor = RegistrableServiceExtension('setFlexFactor');
static const setFlexProperties =
RegistrableServiceExtension('setFlexProperties');
static const getRootWidgetSummaryTreeWithPreviews =
RegistrableServiceExtension('getRootWidgetSummaryTreeWithPreviews');

static const getPubRootDirectories =
RegistrableServiceExtension('getPubRootDirectories');
Expand Down Expand Up @@ -974,7 +976,10 @@ class ObjectGroup implements Disposable {
}

Future<RemoteDiagnosticsNode> getRootWidget() {
return invokeServiceMethodReturningNode('getRootWidgetSummaryTree');
return parseDiagnosticsNodeDaemon(invokeServiceExtensionMethod(
RegistrableServiceExtension.getRootWidgetSummaryTreeWithPreviews,
{'groupName': groupName},
));
}

Future<RemoteDiagnosticsNode> getRootWidgetFullTree() {
Expand Down
78 changes: 70 additions & 8 deletions packages/devtools_app/test/inspector_tree_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,28 @@ import 'package:devtools_app/src/inspector/inspector_tree.dart';
import 'package:devtools_app/src/inspector/inspector_tree_flutter.dart';
import 'package:devtools_app/src/service_manager.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart' hide Fake;
import 'package:mockito/mockito.dart';

import 'support/inspector_tree.dart';
import 'support/mocks.dart';
import 'support/utils.dart';
import 'support/wrappers.dart';

void main() {
FakeServiceManager fakeServiceManager;
group('InspectorTreeController', () {
setUp(() {
fakeServiceManager = FakeServiceManager();
when(fakeServiceManager.connectedApp.isFlutterAppNow).thenReturn(true);
when(fakeServiceManager.connectedApp.isProfileBuildNow).thenReturn(false);

setGlobal(ServiceConnectionManager, fakeServiceManager);
mockIsFlutterApp(serviceManager.connectedApp);
});
setUp(() {
fakeServiceManager = FakeServiceManager();
when(fakeServiceManager.connectedApp.isFlutterAppNow).thenReturn(true);
when(fakeServiceManager.connectedApp.isProfileBuildNow).thenReturn(false);

setGlobal(ServiceConnectionManager, fakeServiceManager);
mockIsFlutterApp(serviceManager.connectedApp);
});

group('InspectorTreeController', () {
testWidgets('Row with negative index regression test',
(WidgetTester tester) async {
final controller = InspectorTreeControllerFlutter()
Expand Down Expand Up @@ -64,4 +68,62 @@ void main() {
controller.scrollToRect(const Rect.fromLTWH(0, -20, 100, 100));
});
});

group('Inspector tree content preview', () {
testWidgets('Shows simple text preview', (WidgetTester tester) async {
final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode(
widget: const Text('Content'),
tester: tester,
);

final treeController = inspectorTreeControllerFromNode(diagnosticNode);
await tester.pumpWidget(wrap(InspectorTree(
controller: treeController,
debuggerController: DebuggerController(),
)));

expect(find.richText('Text: "Content"'), findsOneWidget);
});

testWidgets('Shows preview from Text.rich', (WidgetTester tester) async {
final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode(
widget: const Text.rich(
TextSpan(
children: [
TextSpan(text: 'Rich '),
TextSpan(text: 'text'),
],
),
),
tester: tester,
);

final treeController = inspectorTreeControllerFromNode(diagnosticNode);
await tester.pumpWidget(wrap(InspectorTree(
controller: treeController,
debuggerController: DebuggerController(),
)));

expect(find.richText('Text: "Rich text"'), findsOneWidget);
});

testWidgets('Strips new lines from text preview',
(WidgetTester tester) async {
final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode(
widget: const Text('Multiline\ntext\n\ncontent'),
tester: tester,
);

final treeController = inspectorTreeControllerFromNode(diagnosticNode);

await tester.pumpWidget(
wrap(InspectorTree(
controller: treeController,
debuggerController: DebuggerController(),
)),
);

expect(find.richText('Text: "Multiline text content"'), findsOneWidget);
});
});
}
67 changes: 67 additions & 0 deletions packages/devtools_app/test/support/inspector_tree.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:devtools_app/src/inspector/diagnostics_node.dart';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing copyright header

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

import 'package:devtools_app/src/inspector/inspector_service.dart';
import 'package:devtools_app/src/inspector/inspector_tree.dart';
import 'package:devtools_app/src/inspector/inspector_tree_flutter.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';

import 'wrappers.dart';

/// Create an `InspectorTreeControllerFlutter` from a single `RemoteDiagnosticsNode`
InspectorTreeControllerFlutter inspectorTreeControllerFromNode(
RemoteDiagnosticsNode node) {
final controller = InspectorTreeControllerFlutter()
..config = InspectorTreeConfig(
summaryTree: false,
treeType: FlutterTreeType.widget,
onNodeAdded: (_, __) {},
onClientActiveChange: (_) {},
);

controller.root = InspectorTreeNode()
..appendChild(
InspectorTreeNode()..diagnostic = node,
);

return controller;
}

/// Replicates the functionality of `getRootWidgetSummaryTreeWithPreviews` from
/// inspector_polyfill_script.dart
Future<RemoteDiagnosticsNode> widgetToInspectorTreeDiagnosticsNode({
@required Widget widget,
@required WidgetTester tester,
}) async {
await tester.pumpWidget(wrap(widget));
final element = find.byWidget(widget).evaluate().first;
final nodeJson =
element.toDiagnosticsNode(style: DiagnosticsTreeStyle.dense).toJsonMap(
InspectorSerializationDelegate(
service: WidgetInspectorService.instance,
subtreeDepth: 1000000,
summaryTree: true,
addAdditionalPropertiesCallback: (node, delegate) {
final additionalJson = <String, Object>{};

final value = node.value;
if (value is Element) {
final renderObject = value.renderObject;
if (renderObject is RenderParagraph) {
additionalJson['textPreview'] =
renderObject.text.toPlainText();
}
}

return additionalJson;
},
),
);

return RemoteDiagnosticsNode(nodeJson, null, false, null);
}