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

[Focus] Add run key command to dump the focus tree
This commit is contained in:
Loïc Sharma 2023-03-27 17:33:39 -07:00 committed by GitHub
parent 89da04682c
commit 8f62e34267
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 160 additions and 2 deletions

View File

@ -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);

View File

@ -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.
/// ///

View File

@ -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,

View File

@ -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.
/// ///

View File

@ -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() {
}); });
}); });
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>{});

View File

@ -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.'

View File

@ -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 {
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 {
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;

View File

@ -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 {

View File

@ -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() {
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() {
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.'));

View File

@ -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,

View File

@ -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.

View File

@ -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>[

View File

@ -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)',