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 {
verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.rootElement!.toStringDeep());
verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderView.toStringDeep());
verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? '');
verifyToStringOutput('debugDumpFocusTree', routeName, WidgetsBinding.instance.focusManager.toStringDeep());
// Scroll the demo around a bit more.
await tester.flingFrom(const Offset(400.0, 300.0), const Offset(0.0, 400.0), 1000.0);
......
......@@ -68,7 +68,6 @@ enum RenderingServiceExtensions {
/// registered.
debugDumpLayerTree,
/// Name of service extension that, when called, will toggle whether all
/// clipping effects from the layer tree will be ignored.
///
......
......@@ -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) {
registerBoolServiceExtension(
name: WidgetsServiceExtensions.showPerformanceOverlay.name,
......
......@@ -20,6 +20,15 @@ enum WidgetsServiceExtensions {
/// registered.
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
/// graph on top of this app.
///
......
......@@ -177,7 +177,7 @@ void main() {
// framework, excluding any that are for the widget inspector
// (see widget_inspector_test.dart for tests of the ext.flutter.inspector
// service extensions).
const int serviceExtensionCount = 37;
const int serviceExtensionCount = 38;
expect(binding.extensions.length, serviceExtensionCount + widgetInspectorExtensionCount - disabledExtensions);
......@@ -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 {
await binding.doFrame();
final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpRenderTree.name, <String, String>{});
......
......@@ -97,6 +97,12 @@ class CommandHelp {
'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(
'g',
'Run source code generators.'
......
......@@ -752,6 +752,22 @@ abstract class ResidentHandlers {
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.
///
/// If semantics are not enabled, nothing is returned.
......@@ -1521,6 +1537,7 @@ abstract class ResidentRunner extends ResidentHandlers {
commandHelp.t.print();
if (isRunningDebug) {
commandHelp.L.print();
commandHelp.f.print();
commandHelp.S.print();
commandHelp.U.print();
commandHelp.i.print();
......@@ -1706,6 +1723,8 @@ class TerminalHandler {
case 'D':
await residentRunner.detach();
return true;
case 'f':
return residentRunner.debugDumpFocusTree();
case 'g':
await residentRunner.runSourceGenerators();
return true;
......
......@@ -643,6 +643,16 @@ class FlutterVmService {
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({
required String isolateId,
}) async {
......
......@@ -60,6 +60,7 @@ void _testMessageLength({
expect(commandHelp.b.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.c.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.hWithDetails.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.hWithoutDetails.toString().length, lessThanOrEqualTo(expectedWidth));
......@@ -137,6 +138,7 @@ void main() {
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.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.o.toString(), endsWith('\x1B[90m(defaultTargetPlatform)\x1B[39m\x1B[22m'));
expect(commandHelp.p.toString(), endsWith('\x1B[90m(debugPaintSizeEnabled)\x1B[39m\x1B[22m'));
......@@ -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.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.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.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.'));
......
......@@ -1455,6 +1455,7 @@ flutter:
commandHelp.w,
commandHelp.t,
commandHelp.L,
commandHelp.f,
commandHelp.S,
commandHelp.U,
commandHelp.i,
......
......@@ -400,6 +400,52 @@ void main() {
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 {
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[
// Request 1.
......
......@@ -447,6 +447,46 @@ void main() {
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 {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[
......
......@@ -604,6 +604,7 @@ void main() {
'w Dump widget hierarchy to the console. (debugDumpApp)',
't Dump rendering tree to the console. (debugDumpRenderTree)',
'L Dump layer tree to the console. (debugDumpLayerTree)',
'f Dump focus tree to the console. (debugDumpFocusTree)',
'S Dump accessibility tree in traversal order. (debugDumpSemantics)',
'U Dump accessibility tree in inverse hit test order. (debugDumpSemantics)',
'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