Unverified Commit 8f62e342 authored by Loïc Sharma's avatar Loïc Sharma Committed by GitHub

[Focus] Add run key command to dump the focus tree (#123473)

[Focus] Add run key command to dump the focus tree
parent 89da0468
...@@ -81,6 +81,7 @@ Future<void> smokeDemo(WidgetTester tester, GalleryDemo demo) async { ...@@ -81,6 +81,7 @@ Future<void> smokeDemo(WidgetTester tester, GalleryDemo demo) async {
verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.rootElement!.toStringDeep()); verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.rootElement!.toStringDeep());
verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderView.toStringDeep()); verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderView.toStringDeep());
verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? ''); verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? '');
verifyToStringOutput('debugDumpFocusTree', routeName, WidgetsBinding.instance.focusManager.toStringDeep());
// Scroll the demo around a bit more. // Scroll the demo around a bit more.
await tester.flingFrom(const Offset(400.0, 300.0), const Offset(0.0, 400.0), 1000.0); await tester.flingFrom(const Offset(400.0, 300.0), const Offset(0.0, 400.0), 1000.0);
......
...@@ -68,7 +68,6 @@ enum RenderingServiceExtensions { ...@@ -68,7 +68,6 @@ enum RenderingServiceExtensions {
/// registered. /// registered.
debugDumpLayerTree, debugDumpLayerTree,
/// Name of service extension that, when called, will toggle whether all /// Name of service extension that, when called, will toggle whether all
/// clipping effects from the layer tree will be ignored. /// clipping effects from the layer tree will be ignored.
/// ///
......
...@@ -386,6 +386,16 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -386,6 +386,16 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
}, },
); );
registerServiceExtension(
name: WidgetsServiceExtensions.debugDumpFocusTree.name,
callback: (Map<String, String> parameters) async {
final String data = focusManager.toStringDeep();
return <String, Object>{
'data': data,
};
},
);
if (!kIsWeb) { if (!kIsWeb) {
registerBoolServiceExtension( registerBoolServiceExtension(
name: WidgetsServiceExtensions.showPerformanceOverlay.name, name: WidgetsServiceExtensions.showPerformanceOverlay.name,
......
...@@ -20,6 +20,15 @@ enum WidgetsServiceExtensions { ...@@ -20,6 +20,15 @@ enum WidgetsServiceExtensions {
/// registered. /// registered.
debugDumpApp, debugDumpApp,
/// Name of service extension that, when called, will output a string
/// representation of the focus tree to the console.
///
/// See also:
///
/// * [WidgetsBinding.initServiceExtensions], where the service extension is
/// registered.
debugDumpFocusTree,
/// Name of service extension that, when called, will overlay a performance /// Name of service extension that, when called, will overlay a performance
/// graph on top of this app. /// graph on top of this app.
/// ///
......
...@@ -177,7 +177,7 @@ void main() { ...@@ -177,7 +177,7 @@ void main() {
// framework, excluding any that are for the widget inspector // framework, excluding any that are for the widget inspector
// (see widget_inspector_test.dart for tests of the ext.flutter.inspector // (see widget_inspector_test.dart for tests of the ext.flutter.inspector
// service extensions). // service extensions).
const int serviceExtensionCount = 37; const int serviceExtensionCount = 38;
expect(binding.extensions.length, serviceExtensionCount + widgetInspectorExtensionCount - disabledExtensions); expect(binding.extensions.length, serviceExtensionCount + widgetInspectorExtensionCount - disabledExtensions);
...@@ -218,6 +218,19 @@ void main() { ...@@ -218,6 +218,19 @@ void main() {
}); });
}); });
test('Service extensions - debugDumpFocusTree', () async {
final Map<String, dynamic> result = await binding.testExtension(WidgetsServiceExtensions.debugDumpFocusTree.name, <String, String>{});
expect(result, <String, dynamic>{
'data': matches(
r'^'
r'FocusManager#[0-9a-f]{5}\n'
r' └─rootScope: FocusScopeNode#[0-9a-f]{5}\(Root Focus Scope\)\n'
r'$',
),
});
});
test('Service extensions - debugDumpRenderTree', () async { test('Service extensions - debugDumpRenderTree', () async {
await binding.doFrame(); await binding.doFrame();
final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpRenderTree.name, <String, String>{}); final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpRenderTree.name, <String, String>{});
......
...@@ -97,6 +97,12 @@ class CommandHelp { ...@@ -97,6 +97,12 @@ class CommandHelp {
'Detach (terminate "flutter run" but leave application running).', 'Detach (terminate "flutter run" but leave application running).',
); );
late final CommandHelpOption f = _makeOption(
'f',
'Dump focus tree to the console.',
'debugDumpFocusTree',
);
late final CommandHelpOption g = _makeOption( late final CommandHelpOption g = _makeOption(
'g', 'g',
'Run source code generators.' 'Run source code generators.'
......
...@@ -752,6 +752,22 @@ abstract class ResidentHandlers { ...@@ -752,6 +752,22 @@ abstract class ResidentHandlers {
return true; return true;
} }
Future<bool> debugDumpFocusTree() async {
if (!supportsServiceProtocol || !isRunningDebug) {
return false;
}
for (final FlutterDevice? device in flutterDevices) {
final List<FlutterView> views = await device!.vmService!.getFlutterViews();
for (final FlutterView view in views) {
final String data = await device.vmService!.flutterDebugDumpFocusTree(
isolateId: view.uiIsolate!.id!,
);
logger.printStatus(data);
}
}
return true;
}
/// Dump the application's current semantics tree to the terminal. /// Dump the application's current semantics tree to the terminal.
/// ///
/// If semantics are not enabled, nothing is returned. /// If semantics are not enabled, nothing is returned.
...@@ -1521,6 +1537,7 @@ abstract class ResidentRunner extends ResidentHandlers { ...@@ -1521,6 +1537,7 @@ abstract class ResidentRunner extends ResidentHandlers {
commandHelp.t.print(); commandHelp.t.print();
if (isRunningDebug) { if (isRunningDebug) {
commandHelp.L.print(); commandHelp.L.print();
commandHelp.f.print();
commandHelp.S.print(); commandHelp.S.print();
commandHelp.U.print(); commandHelp.U.print();
commandHelp.i.print(); commandHelp.i.print();
...@@ -1706,6 +1723,8 @@ class TerminalHandler { ...@@ -1706,6 +1723,8 @@ class TerminalHandler {
case 'D': case 'D':
await residentRunner.detach(); await residentRunner.detach();
return true; return true;
case 'f':
return residentRunner.debugDumpFocusTree();
case 'g': case 'g':
await residentRunner.runSourceGenerators(); await residentRunner.runSourceGenerators();
return true; return true;
......
...@@ -643,6 +643,16 @@ class FlutterVmService { ...@@ -643,6 +643,16 @@ class FlutterVmService {
return response?['data']?.toString() ?? ''; return response?['data']?.toString() ?? '';
} }
Future<String> flutterDebugDumpFocusTree({
required String isolateId,
}) async {
final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
'ext.flutter.debugDumpFocusTree',
isolateId: isolateId,
);
return response?['data']?.toString() ?? '';
}
Future<String> flutterDebugDumpSemanticsTreeInTraversalOrder({ Future<String> flutterDebugDumpSemanticsTreeInTraversalOrder({
required String isolateId, required String isolateId,
}) async { }) async {
......
...@@ -60,6 +60,7 @@ void _testMessageLength({ ...@@ -60,6 +60,7 @@ void _testMessageLength({
expect(commandHelp.b.toString().length, lessThanOrEqualTo(expectedWidth)); expect(commandHelp.b.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.c.toString().length, lessThanOrEqualTo(expectedWidth)); expect(commandHelp.c.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.d.toString().length, lessThanOrEqualTo(expectedWidth)); expect(commandHelp.d.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.f.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.g.toString().length, lessThanOrEqualTo(expectedWidth)); expect(commandHelp.g.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.hWithDetails.toString().length, lessThanOrEqualTo(expectedWidth)); expect(commandHelp.hWithDetails.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.hWithoutDetails.toString().length, lessThanOrEqualTo(expectedWidth)); expect(commandHelp.hWithoutDetails.toString().length, lessThanOrEqualTo(expectedWidth));
...@@ -137,6 +138,7 @@ void main() { ...@@ -137,6 +138,7 @@ void main() {
expect(commandHelp.U.toString(), endsWith('\x1B[90m(debugDumpSemantics)\x1B[39m\x1B[22m')); expect(commandHelp.U.toString(), endsWith('\x1B[90m(debugDumpSemantics)\x1B[39m\x1B[22m'));
expect(commandHelp.a.toString(), endsWith('\x1B[90m(debugProfileWidgetBuilds)\x1B[39m\x1B[22m')); expect(commandHelp.a.toString(), endsWith('\x1B[90m(debugProfileWidgetBuilds)\x1B[39m\x1B[22m'));
expect(commandHelp.b.toString(), endsWith('\x1B[90m(debugBrightnessOverride)\x1B[39m\x1B[22m')); expect(commandHelp.b.toString(), endsWith('\x1B[90m(debugBrightnessOverride)\x1B[39m\x1B[22m'));
expect(commandHelp.f.toString(), endsWith('\x1B[90m(debugDumpFocusTree)\x1B[39m\x1B[22m'));
expect(commandHelp.i.toString(), endsWith('\x1B[90m(WidgetsApp.showWidgetInspectorOverride)\x1B[39m\x1B[22m')); expect(commandHelp.i.toString(), endsWith('\x1B[90m(WidgetsApp.showWidgetInspectorOverride)\x1B[39m\x1B[22m'));
expect(commandHelp.o.toString(), endsWith('\x1B[90m(defaultTargetPlatform)\x1B[39m\x1B[22m')); expect(commandHelp.o.toString(), endsWith('\x1B[90m(defaultTargetPlatform)\x1B[39m\x1B[22m'));
expect(commandHelp.p.toString(), endsWith('\x1B[90m(debugPaintSizeEnabled)\x1B[39m\x1B[22m')); expect(commandHelp.p.toString(), endsWith('\x1B[90m(debugPaintSizeEnabled)\x1B[39m\x1B[22m'));
...@@ -193,6 +195,7 @@ void main() { ...@@ -193,6 +195,7 @@ void main() {
expect(commandHelp.b.toString(), equals('\x1B[1mb\x1B[22m Toggle platform brightness (dark and light mode). \x1B[90m(debugBrightnessOverride)\x1B[39m\x1B[22m')); expect(commandHelp.b.toString(), equals('\x1B[1mb\x1B[22m Toggle platform brightness (dark and light mode). \x1B[90m(debugBrightnessOverride)\x1B[39m\x1B[22m'));
expect(commandHelp.c.toString(), equals('\x1B[1mc\x1B[22m Clear the screen')); expect(commandHelp.c.toString(), equals('\x1B[1mc\x1B[22m Clear the screen'));
expect(commandHelp.d.toString(), equals('\x1B[1md\x1B[22m Detach (terminate "flutter run" but leave application running).')); expect(commandHelp.d.toString(), equals('\x1B[1md\x1B[22m Detach (terminate "flutter run" but leave application running).'));
expect(commandHelp.f.toString(), equals('\x1B[1mf\x1B[22m Dump focus tree to the console. \x1B[90m(debugDumpFocusTree)\x1B[39m\x1B[22m'));
expect(commandHelp.g.toString(), equals('\x1B[1mg\x1B[22m Run source code generators.')); expect(commandHelp.g.toString(), equals('\x1B[1mg\x1B[22m Run source code generators.'));
expect(commandHelp.hWithDetails.toString(), equals('\x1B[1mh\x1B[22m Repeat this help message.')); expect(commandHelp.hWithDetails.toString(), equals('\x1B[1mh\x1B[22m Repeat this help message.'));
expect(commandHelp.hWithoutDetails.toString(), equals('\x1B[1mh\x1B[22m List all available interactive commands.')); expect(commandHelp.hWithoutDetails.toString(), equals('\x1B[1mh\x1B[22m List all available interactive commands.'));
......
...@@ -1455,6 +1455,7 @@ flutter: ...@@ -1455,6 +1455,7 @@ flutter:
commandHelp.w, commandHelp.w,
commandHelp.t, commandHelp.t,
commandHelp.L, commandHelp.L,
commandHelp.f,
commandHelp.S, commandHelp.S,
commandHelp.U, commandHelp.U,
commandHelp.i, commandHelp.i,
......
...@@ -400,6 +400,52 @@ void main() { ...@@ -400,6 +400,52 @@ void main() {
await terminalHandler.processTerminalInput('L'); await terminalHandler.processTerminalInput('L');
}); });
testWithoutContext('f - debugDumpFocusTree', () async {
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[
listViews,
const FakeVmServiceRequest(
method: 'ext.flutter.debugDumpFocusTree',
args: <String, Object>{
'isolateId': '1',
},
jsonResponse: <String, Object>{
'data': 'FOCUS TREE',
}
),
]);
await terminalHandler.processTerminalInput('f');
expect(terminalHandler.logger.statusText, contains('FOCUS TREE'));
});
testWithoutContext('f - debugDumpLayerTree with web target', () async {
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[
listViews,
const FakeVmServiceRequest(
method: 'ext.flutter.debugDumpFocusTree',
args: <String, Object>{
'isolateId': '1',
},
jsonResponse: <String, Object>{
'data': 'FOCUS TREE',
}
),
], web: true);
await terminalHandler.processTerminalInput('f');
expect(terminalHandler.logger.statusText, contains('FOCUS TREE'));
});
testWithoutContext('f - debugDumpFocusTree with service protocol and profile mode is skipped', () async {
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], buildMode: BuildMode.profile);
await terminalHandler.processTerminalInput('f');
});
testWithoutContext('f - debugDumpFocusTree without service protocol is skipped', () async {
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false);
await terminalHandler.processTerminalInput('f');
});
testWithoutContext('o,O - debugTogglePlatform', () async { testWithoutContext('o,O - debugTogglePlatform', () async {
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[
// Request 1. // Request 1.
......
...@@ -447,6 +447,46 @@ void main() { ...@@ -447,6 +447,46 @@ void main() {
expect(fakeVmServiceHost.hasRemainingExpectations, false); expect(fakeVmServiceHost.hasRemainingExpectations, false);
}); });
testWithoutContext('flutterDebugDumpFocusTree handles missing method', () async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[
const FakeVmServiceRequest(
method: 'ext.flutter.debugDumpFocusTree',
args: <String, Object>{
'isolateId': '1',
},
errorCode: RPCErrorCodes.kMethodNotFound,
),
]
);
expect(await fakeVmServiceHost.vmService.flutterDebugDumpFocusTree(
isolateId: '1',
), '');
expect(fakeVmServiceHost.hasRemainingExpectations, false);
});
testWithoutContext('flutterDebugDumpFocusTree returns data', () async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[
const FakeVmServiceRequest(
method: 'ext.flutter.debugDumpFocusTree',
args: <String, Object>{
'isolateId': '1',
},
jsonResponse: <String, Object> {
'data': 'Hello world',
},
),
]
);
expect(await fakeVmServiceHost.vmService.flutterDebugDumpFocusTree(
isolateId: '1',
), 'Hello world');
expect(fakeVmServiceHost.hasRemainingExpectations, false);
});
testWithoutContext('Framework service extension invocations return null if service disappears ', () async { testWithoutContext('Framework service extension invocations return null if service disappears ', () async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[ requests: <VmServiceExpectation>[
......
...@@ -604,6 +604,7 @@ void main() { ...@@ -604,6 +604,7 @@ void main() {
'w Dump widget hierarchy to the console. (debugDumpApp)', 'w Dump widget hierarchy to the console. (debugDumpApp)',
't Dump rendering tree to the console. (debugDumpRenderTree)', 't Dump rendering tree to the console. (debugDumpRenderTree)',
'L Dump layer tree to the console. (debugDumpLayerTree)', 'L Dump layer tree to the console. (debugDumpLayerTree)',
'f Dump focus tree to the console. (debugDumpFocusTree)',
'S Dump accessibility tree in traversal order. (debugDumpSemantics)', 'S Dump accessibility tree in traversal order. (debugDumpSemantics)',
'U Dump accessibility tree in inverse hit test order. (debugDumpSemantics)', 'U Dump accessibility tree in inverse hit test order. (debugDumpSemantics)',
'i Toggle widget inspector. (WidgetsApp.showWidgetInspectorOverride)', 'i Toggle widget inspector. (WidgetsApp.showWidgetInspectorOverride)',
......
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