Skip to content

Commit 5d17c45

Browse files
authored
Add text previews to widget tree (#3218)
1 parent 89e2169 commit 5d17c45

File tree

5 files changed

+193
-9
lines changed

5 files changed

+193
-9
lines changed

packages/devtools_app/assets/scripts/inspector_polyfill_script.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,41 @@ String addServiceExtensions() {
111111
throw 'Enum value $name not found';
112112
}
113113

114+
/// This is identical to Flutter's getRootWidgetSummaryTree service extension,
115+
/// but with the added properties.
116+
Future<Map<String, dynamic>> getRootWidgetSummaryTreeWithPreviews(
117+
Map<String, String> parameters) {
118+
final instance = WidgetInspectorService.instance;
119+
final groupName = parameters['groupName'];
120+
121+
final result = instance._nodeToJson(
122+
WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(),
123+
InspectorSerializationDelegate(
124+
groupName: groupName,
125+
subtreeDepth: 1000000,
126+
summaryTree: true,
127+
service: instance,
128+
addAdditionalPropertiesCallback: (DiagnosticsNode node, delegate) {
129+
final additionalJson = <String, Object>{};
130+
131+
final value = node.value;
132+
if (value is Element) {
133+
final renderObject = value.renderObject;
134+
if (renderObject is RenderParagraph) {
135+
additionalJson['textPreview'] = renderObject.text.toPlainText();
136+
}
137+
}
138+
139+
return additionalJson;
140+
},
141+
),
142+
);
143+
144+
return Future.value(<String, dynamic>{
145+
'result': result,
146+
});
147+
}
148+
114149
Future<Map<String, dynamic>> getLayoutExplorerNode(
115150
Map<String, String> parameters) {
116151
final id = parameters['id'];
@@ -317,6 +352,11 @@ String addServiceExtensions() {
317352
registerHelper('setFlexFactor', setFlexFactor);
318353
registerHelper('setFlexProperties', setFlexProperties);
319354
registerHelper('getPubRootDirectories', getPubRootDirectories);
355+
registerHelper(
356+
'getRootWidgetSummaryTreeWithPreviews',
357+
getRootWidgetSummaryTreeWithPreviews,
358+
);
359+
320360
return WidgetInspectorService.instance._safeJsonEncode(failures);
321361
// INSPECTOR_POLYFILL_SCRIPT_END
322362
}

packages/devtools_app/lib/src/inspector/diagnostics.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,19 @@ class DiagnosticsNodeDescription extends StatelessWidget {
8282
return;
8383
}
8484
}
85+
8586
if (description?.isNotEmpty == true) {
8687
yield TextSpan(text: description, style: textStyle);
8788
}
89+
90+
final textPreview = diagnostic.json['textPreview'];
91+
if (textPreview is String) {
92+
final preview = textPreview.replaceAll('\n', ' ');
93+
yield TextSpan(
94+
text: ': "$preview"',
95+
style: textStyle.merge(inspector_text_styles.unimportant(colorScheme)),
96+
);
97+
}
8898
}
8999

90100
Widget buildDescription(

packages/devtools_app/lib/src/inspector/inspector_service.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ class RegistrableServiceExtension {
3838
static const setFlexFactor = RegistrableServiceExtension('setFlexFactor');
3939
static const setFlexProperties =
4040
RegistrableServiceExtension('setFlexProperties');
41+
static const getRootWidgetSummaryTreeWithPreviews =
42+
RegistrableServiceExtension('getRootWidgetSummaryTreeWithPreviews');
4143

4244
static const getPubRootDirectories =
4345
RegistrableServiceExtension('getPubRootDirectories');
@@ -974,7 +976,10 @@ class ObjectGroup implements Disposable {
974976
}
975977

976978
Future<RemoteDiagnosticsNode> getRootWidget() {
977-
return invokeServiceMethodReturningNode('getRootWidgetSummaryTree');
979+
return parseDiagnosticsNodeDaemon(invokeServiceExtensionMethod(
980+
RegistrableServiceExtension.getRootWidgetSummaryTreeWithPreviews,
981+
{'groupName': groupName},
982+
));
978983
}
979984

980985
Future<RemoteDiagnosticsNode> getRootWidgetFullTree() {

packages/devtools_app/test/inspector_tree_test.dart

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,28 @@ import 'package:devtools_app/src/inspector/inspector_tree.dart';
99
import 'package:devtools_app/src/inspector/inspector_tree_flutter.dart';
1010
import 'package:devtools_app/src/service_manager.dart';
1111
import 'package:flutter/material.dart';
12+
import 'package:flutter/rendering.dart';
1213
import 'package:flutter_test/flutter_test.dart' hide Fake;
1314
import 'package:mockito/mockito.dart';
1415

16+
import 'support/inspector_tree.dart';
1517
import 'support/mocks.dart';
18+
import 'support/utils.dart';
1619
import 'support/wrappers.dart';
1720

1821
void main() {
1922
FakeServiceManager fakeServiceManager;
20-
group('InspectorTreeController', () {
21-
setUp(() {
22-
fakeServiceManager = FakeServiceManager();
23-
when(fakeServiceManager.connectedApp.isFlutterAppNow).thenReturn(true);
24-
when(fakeServiceManager.connectedApp.isProfileBuildNow).thenReturn(false);
2523

26-
setGlobal(ServiceConnectionManager, fakeServiceManager);
27-
mockIsFlutterApp(serviceManager.connectedApp);
28-
});
24+
setUp(() {
25+
fakeServiceManager = FakeServiceManager();
26+
when(fakeServiceManager.connectedApp.isFlutterAppNow).thenReturn(true);
27+
when(fakeServiceManager.connectedApp.isProfileBuildNow).thenReturn(false);
28+
29+
setGlobal(ServiceConnectionManager, fakeServiceManager);
30+
mockIsFlutterApp(serviceManager.connectedApp);
31+
});
2932

33+
group('InspectorTreeController', () {
3034
testWidgets('Row with negative index regression test',
3135
(WidgetTester tester) async {
3236
final controller = InspectorTreeControllerFlutter()
@@ -64,4 +68,62 @@ void main() {
6468
controller.scrollToRect(const Rect.fromLTWH(0, -20, 100, 100));
6569
});
6670
});
71+
72+
group('Inspector tree content preview', () {
73+
testWidgets('Shows simple text preview', (WidgetTester tester) async {
74+
final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode(
75+
widget: const Text('Content'),
76+
tester: tester,
77+
);
78+
79+
final treeController = inspectorTreeControllerFromNode(diagnosticNode);
80+
await tester.pumpWidget(wrap(InspectorTree(
81+
controller: treeController,
82+
debuggerController: DebuggerController(),
83+
)));
84+
85+
expect(find.richText('Text: "Content"'), findsOneWidget);
86+
});
87+
88+
testWidgets('Shows preview from Text.rich', (WidgetTester tester) async {
89+
final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode(
90+
widget: const Text.rich(
91+
TextSpan(
92+
children: [
93+
TextSpan(text: 'Rich '),
94+
TextSpan(text: 'text'),
95+
],
96+
),
97+
),
98+
tester: tester,
99+
);
100+
101+
final treeController = inspectorTreeControllerFromNode(diagnosticNode);
102+
await tester.pumpWidget(wrap(InspectorTree(
103+
controller: treeController,
104+
debuggerController: DebuggerController(),
105+
)));
106+
107+
expect(find.richText('Text: "Rich text"'), findsOneWidget);
108+
});
109+
110+
testWidgets('Strips new lines from text preview',
111+
(WidgetTester tester) async {
112+
final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode(
113+
widget: const Text('Multiline\ntext\n\ncontent'),
114+
tester: tester,
115+
);
116+
117+
final treeController = inspectorTreeControllerFromNode(diagnosticNode);
118+
119+
await tester.pumpWidget(
120+
wrap(InspectorTree(
121+
controller: treeController,
122+
debuggerController: DebuggerController(),
123+
)),
124+
);
125+
126+
expect(find.richText('Text: "Multiline text content"'), findsOneWidget);
127+
});
128+
});
67129
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2021 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:devtools_app/src/inspector/diagnostics_node.dart';
6+
import 'package:devtools_app/src/inspector/inspector_service.dart';
7+
import 'package:devtools_app/src/inspector/inspector_tree.dart';
8+
import 'package:devtools_app/src/inspector/inspector_tree_flutter.dart';
9+
import 'package:flutter/foundation.dart';
10+
import 'package:flutter/material.dart';
11+
import 'package:flutter/rendering.dart';
12+
import 'package:flutter_test/flutter_test.dart';
13+
14+
import 'wrappers.dart';
15+
16+
/// Create an `InspectorTreeControllerFlutter` from a single `RemoteDiagnosticsNode`
17+
InspectorTreeControllerFlutter inspectorTreeControllerFromNode(
18+
RemoteDiagnosticsNode node) {
19+
final controller = InspectorTreeControllerFlutter()
20+
..config = InspectorTreeConfig(
21+
summaryTree: false,
22+
treeType: FlutterTreeType.widget,
23+
onNodeAdded: (_, __) {},
24+
onClientActiveChange: (_) {},
25+
);
26+
27+
controller.root = InspectorTreeNode()
28+
..appendChild(
29+
InspectorTreeNode()..diagnostic = node,
30+
);
31+
32+
return controller;
33+
}
34+
35+
/// Replicates the functionality of `getRootWidgetSummaryTreeWithPreviews` from
36+
/// inspector_polyfill_script.dart
37+
Future<RemoteDiagnosticsNode> widgetToInspectorTreeDiagnosticsNode({
38+
@required Widget widget,
39+
@required WidgetTester tester,
40+
}) async {
41+
await tester.pumpWidget(wrap(widget));
42+
final element = find.byWidget(widget).evaluate().first;
43+
final nodeJson =
44+
element.toDiagnosticsNode(style: DiagnosticsTreeStyle.dense).toJsonMap(
45+
InspectorSerializationDelegate(
46+
service: WidgetInspectorService.instance,
47+
subtreeDepth: 1000000,
48+
summaryTree: true,
49+
addAdditionalPropertiesCallback: (node, delegate) {
50+
final additionalJson = <String, Object>{};
51+
52+
final value = node.value;
53+
if (value is Element) {
54+
final renderObject = value.renderObject;
55+
if (renderObject is RenderParagraph) {
56+
additionalJson['textPreview'] =
57+
renderObject.text.toPlainText();
58+
}
59+
}
60+
61+
return additionalJson;
62+
},
63+
),
64+
);
65+
66+
return RemoteDiagnosticsNode(nodeJson, null, false, null);
67+
}

0 commit comments

Comments
 (0)