Skip to content

Commit 8f62e34

Browse files
authored
[Focus] Add run key command to dump the focus tree (#123473)
[Focus] Add run key command to dump the focus tree
1 parent 89da046 commit 8f62e34

File tree

13 files changed

+160
-2
lines changed

13 files changed

+160
-2
lines changed

dev/integration_tests/flutter_gallery/test/smoke_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Future<void> smokeDemo(WidgetTester tester, GalleryDemo demo) async {
8181
verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.rootElement!.toStringDeep());
8282
verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderView.toStringDeep());
8383
verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? '');
84+
verifyToStringOutput('debugDumpFocusTree', routeName, WidgetsBinding.instance.focusManager.toStringDeep());
8485

8586
// Scroll the demo around a bit more.
8687
await tester.flingFrom(const Offset(400.0, 300.0), const Offset(0.0, 400.0), 1000.0);

packages/flutter/lib/src/rendering/service_extensions.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ enum RenderingServiceExtensions {
6868
/// registered.
6969
debugDumpLayerTree,
7070

71-
7271
/// Name of service extension that, when called, will toggle whether all
7372
/// clipping effects from the layer tree will be ignored.
7473
///

packages/flutter/lib/src/widgets/binding.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,16 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
386386
},
387387
);
388388

389+
registerServiceExtension(
390+
name: WidgetsServiceExtensions.debugDumpFocusTree.name,
391+
callback: (Map<String, String> parameters) async {
392+
final String data = focusManager.toStringDeep();
393+
return <String, Object>{
394+
'data': data,
395+
};
396+
},
397+
);
398+
389399
if (!kIsWeb) {
390400
registerBoolServiceExtension(
391401
name: WidgetsServiceExtensions.showPerformanceOverlay.name,

packages/flutter/lib/src/widgets/service_extensions.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ enum WidgetsServiceExtensions {
2020
/// registered.
2121
debugDumpApp,
2222

23+
/// Name of service extension that, when called, will output a string
24+
/// representation of the focus tree to the console.
25+
///
26+
/// See also:
27+
///
28+
/// * [WidgetsBinding.initServiceExtensions], where the service extension is
29+
/// registered.
30+
debugDumpFocusTree,
31+
2332
/// Name of service extension that, when called, will overlay a performance
2433
/// graph on top of this app.
2534
///

packages/flutter/test/foundation/service_extensions_test.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ void main() {
177177
// framework, excluding any that are for the widget inspector
178178
// (see widget_inspector_test.dart for tests of the ext.flutter.inspector
179179
// service extensions).
180-
const int serviceExtensionCount = 37;
180+
const int serviceExtensionCount = 38;
181181

182182
expect(binding.extensions.length, serviceExtensionCount + widgetInspectorExtensionCount - disabledExtensions);
183183

@@ -218,6 +218,19 @@ void main() {
218218
});
219219
});
220220

221+
test('Service extensions - debugDumpFocusTree', () async {
222+
final Map<String, dynamic> result = await binding.testExtension(WidgetsServiceExtensions.debugDumpFocusTree.name, <String, String>{});
223+
224+
expect(result, <String, dynamic>{
225+
'data': matches(
226+
r'^'
227+
r'FocusManager#[0-9a-f]{5}\n'
228+
r' └─rootScope: FocusScopeNode#[0-9a-f]{5}\(Root Focus Scope\)\n'
229+
r'$',
230+
),
231+
});
232+
});
233+
221234
test('Service extensions - debugDumpRenderTree', () async {
222235
await binding.doFrame();
223236
final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpRenderTree.name, <String, String>{});

packages/flutter_tools/lib/src/base/command_help.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ class CommandHelp {
9797
'Detach (terminate "flutter run" but leave application running).',
9898
);
9999

100+
late final CommandHelpOption f = _makeOption(
101+
'f',
102+
'Dump focus tree to the console.',
103+
'debugDumpFocusTree',
104+
);
105+
100106
late final CommandHelpOption g = _makeOption(
101107
'g',
102108
'Run source code generators.'

packages/flutter_tools/lib/src/resident_runner.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,22 @@ abstract class ResidentHandlers {
752752
return true;
753753
}
754754

755+
Future<bool> debugDumpFocusTree() async {
756+
if (!supportsServiceProtocol || !isRunningDebug) {
757+
return false;
758+
}
759+
for (final FlutterDevice? device in flutterDevices) {
760+
final List<FlutterView> views = await device!.vmService!.getFlutterViews();
761+
for (final FlutterView view in views) {
762+
final String data = await device.vmService!.flutterDebugDumpFocusTree(
763+
isolateId: view.uiIsolate!.id!,
764+
);
765+
logger.printStatus(data);
766+
}
767+
}
768+
return true;
769+
}
770+
755771
/// Dump the application's current semantics tree to the terminal.
756772
///
757773
/// If semantics are not enabled, nothing is returned.
@@ -1521,6 +1537,7 @@ abstract class ResidentRunner extends ResidentHandlers {
15211537
commandHelp.t.print();
15221538
if (isRunningDebug) {
15231539
commandHelp.L.print();
1540+
commandHelp.f.print();
15241541
commandHelp.S.print();
15251542
commandHelp.U.print();
15261543
commandHelp.i.print();
@@ -1706,6 +1723,8 @@ class TerminalHandler {
17061723
case 'D':
17071724
await residentRunner.detach();
17081725
return true;
1726+
case 'f':
1727+
return residentRunner.debugDumpFocusTree();
17091728
case 'g':
17101729
await residentRunner.runSourceGenerators();
17111730
return true;

packages/flutter_tools/lib/src/vmservice.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,16 @@ class FlutterVmService {
643643
return response?['data']?.toString() ?? '';
644644
}
645645

646+
Future<String> flutterDebugDumpFocusTree({
647+
required String isolateId,
648+
}) async {
649+
final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
650+
'ext.flutter.debugDumpFocusTree',
651+
isolateId: isolateId,
652+
);
653+
return response?['data']?.toString() ?? '';
654+
}
655+
646656
Future<String> flutterDebugDumpSemanticsTreeInTraversalOrder({
647657
required String isolateId,
648658
}) async {

packages/flutter_tools/test/general.shard/base/command_help_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ void _testMessageLength({
6060
expect(commandHelp.b.toString().length, lessThanOrEqualTo(expectedWidth));
6161
expect(commandHelp.c.toString().length, lessThanOrEqualTo(expectedWidth));
6262
expect(commandHelp.d.toString().length, lessThanOrEqualTo(expectedWidth));
63+
expect(commandHelp.f.toString().length, lessThanOrEqualTo(expectedWidth));
6364
expect(commandHelp.g.toString().length, lessThanOrEqualTo(expectedWidth));
6465
expect(commandHelp.hWithDetails.toString().length, lessThanOrEqualTo(expectedWidth));
6566
expect(commandHelp.hWithoutDetails.toString().length, lessThanOrEqualTo(expectedWidth));
@@ -137,6 +138,7 @@ void main() {
137138
expect(commandHelp.U.toString(), endsWith('\x1B[90m(debugDumpSemantics)\x1B[39m\x1B[22m'));
138139
expect(commandHelp.a.toString(), endsWith('\x1B[90m(debugProfileWidgetBuilds)\x1B[39m\x1B[22m'));
139140
expect(commandHelp.b.toString(), endsWith('\x1B[90m(debugBrightnessOverride)\x1B[39m\x1B[22m'));
141+
expect(commandHelp.f.toString(), endsWith('\x1B[90m(debugDumpFocusTree)\x1B[39m\x1B[22m'));
140142
expect(commandHelp.i.toString(), endsWith('\x1B[90m(WidgetsApp.showWidgetInspectorOverride)\x1B[39m\x1B[22m'));
141143
expect(commandHelp.o.toString(), endsWith('\x1B[90m(defaultTargetPlatform)\x1B[39m\x1B[22m'));
142144
expect(commandHelp.p.toString(), endsWith('\x1B[90m(debugPaintSizeEnabled)\x1B[39m\x1B[22m'));
@@ -193,6 +195,7 @@ void main() {
193195
expect(commandHelp.b.toString(), equals('\x1B[1mb\x1B[22m Toggle platform brightness (dark and light mode). \x1B[90m(debugBrightnessOverride)\x1B[39m\x1B[22m'));
194196
expect(commandHelp.c.toString(), equals('\x1B[1mc\x1B[22m Clear the screen'));
195197
expect(commandHelp.d.toString(), equals('\x1B[1md\x1B[22m Detach (terminate "flutter run" but leave application running).'));
198+
expect(commandHelp.f.toString(), equals('\x1B[1mf\x1B[22m Dump focus tree to the console. \x1B[90m(debugDumpFocusTree)\x1B[39m\x1B[22m'));
196199
expect(commandHelp.g.toString(), equals('\x1B[1mg\x1B[22m Run source code generators.'));
197200
expect(commandHelp.hWithDetails.toString(), equals('\x1B[1mh\x1B[22m Repeat this help message.'));
198201
expect(commandHelp.hWithoutDetails.toString(), equals('\x1B[1mh\x1B[22m List all available interactive commands.'));

packages/flutter_tools/test/general.shard/resident_runner_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,6 +1455,7 @@ flutter:
14551455
commandHelp.w,
14561456
commandHelp.t,
14571457
commandHelp.L,
1458+
commandHelp.f,
14581459
commandHelp.S,
14591460
commandHelp.U,
14601461
commandHelp.i,

packages/flutter_tools/test/general.shard/terminal_handler_test.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,52 @@ void main() {
400400
await terminalHandler.processTerminalInput('L');
401401
});
402402

403+
testWithoutContext('f - debugDumpFocusTree', () async {
404+
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[
405+
listViews,
406+
const FakeVmServiceRequest(
407+
method: 'ext.flutter.debugDumpFocusTree',
408+
args: <String, Object>{
409+
'isolateId': '1',
410+
},
411+
jsonResponse: <String, Object>{
412+
'data': 'FOCUS TREE',
413+
}
414+
),
415+
]);
416+
await terminalHandler.processTerminalInput('f');
417+
418+
expect(terminalHandler.logger.statusText, contains('FOCUS TREE'));
419+
});
420+
421+
testWithoutContext('f - debugDumpLayerTree with web target', () async {
422+
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[
423+
listViews,
424+
const FakeVmServiceRequest(
425+
method: 'ext.flutter.debugDumpFocusTree',
426+
args: <String, Object>{
427+
'isolateId': '1',
428+
},
429+
jsonResponse: <String, Object>{
430+
'data': 'FOCUS TREE',
431+
}
432+
),
433+
], web: true);
434+
await terminalHandler.processTerminalInput('f');
435+
436+
expect(terminalHandler.logger.statusText, contains('FOCUS TREE'));
437+
});
438+
439+
testWithoutContext('f - debugDumpFocusTree with service protocol and profile mode is skipped', () async {
440+
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], buildMode: BuildMode.profile);
441+
await terminalHandler.processTerminalInput('f');
442+
});
443+
444+
testWithoutContext('f - debugDumpFocusTree without service protocol is skipped', () async {
445+
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false);
446+
await terminalHandler.processTerminalInput('f');
447+
});
448+
403449
testWithoutContext('o,O - debugTogglePlatform', () async {
404450
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[
405451
// Request 1.

packages/flutter_tools/test/general.shard/vmservice_test.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,46 @@ void main() {
447447
expect(fakeVmServiceHost.hasRemainingExpectations, false);
448448
});
449449

450+
testWithoutContext('flutterDebugDumpFocusTree handles missing method', () async {
451+
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
452+
requests: <VmServiceExpectation>[
453+
const FakeVmServiceRequest(
454+
method: 'ext.flutter.debugDumpFocusTree',
455+
args: <String, Object>{
456+
'isolateId': '1',
457+
},
458+
errorCode: RPCErrorCodes.kMethodNotFound,
459+
),
460+
]
461+
);
462+
463+
expect(await fakeVmServiceHost.vmService.flutterDebugDumpFocusTree(
464+
isolateId: '1',
465+
), '');
466+
expect(fakeVmServiceHost.hasRemainingExpectations, false);
467+
});
468+
469+
testWithoutContext('flutterDebugDumpFocusTree returns data', () async {
470+
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
471+
requests: <VmServiceExpectation>[
472+
const FakeVmServiceRequest(
473+
method: 'ext.flutter.debugDumpFocusTree',
474+
args: <String, Object>{
475+
'isolateId': '1',
476+
},
477+
jsonResponse: <String, Object> {
478+
'data': 'Hello world',
479+
},
480+
),
481+
]
482+
);
483+
484+
expect(await fakeVmServiceHost.vmService.flutterDebugDumpFocusTree(
485+
isolateId: '1',
486+
), 'Hello world');
487+
expect(fakeVmServiceHost.hasRemainingExpectations, false);
488+
});
489+
450490
testWithoutContext('Framework service extension invocations return null if service disappears ', () async {
451491
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
452492
requests: <VmServiceExpectation>[

packages/flutter_tools/test/integration.shard/overall_experience_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,7 @@ void main() {
604604
'w Dump widget hierarchy to the console. (debugDumpApp)',
605605
't Dump rendering tree to the console. (debugDumpRenderTree)',
606606
'L Dump layer tree to the console. (debugDumpLayerTree)',
607+
'f Dump focus tree to the console. (debugDumpFocusTree)',
607608
'S Dump accessibility tree in traversal order. (debugDumpSemantics)',
608609
'U Dump accessibility tree in inverse hit test order. (debugDumpSemantics)',
609610
'i Toggle widget inspector. (WidgetsApp.showWidgetInspectorOverride)',

0 commit comments

Comments
 (0)