Cover focus tests with leak tracking (#134457)

This commit is contained in:
Kostia Sokolovskyi 2023-09-12 00:35:09 +02:00 committed by GitHub
parent b90c1a8766
commit adaf78a60d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 501 additions and 151 deletions

View File

@ -21,13 +21,16 @@ void main() {
} }
group(FocusNode, () { group(FocusNode, () {
testWidgets('Can add children.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can add children.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusNode parent = FocusNode(); final FocusNode parent = FocusNode();
addTearDown(parent.dispose);
final FocusAttachment parentAttachment = parent.attach(context); final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode(); final FocusNode child1 = FocusNode();
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(); final FocusNode child2 = FocusNode();
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
parentAttachment.reparent(parent: tester.binding.focusManager.rootScope); parentAttachment.reparent(parent: tester.binding.focusManager.rootScope);
child1Attachment.reparent(parent: parent); child1Attachment.reparent(parent: parent);
@ -41,13 +44,16 @@ void main() {
expect(parent.children.last, equals(child2)); expect(parent.children.last, equals(child2));
}); });
testWidgets('Can remove children.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can remove children.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusNode parent = FocusNode(); final FocusNode parent = FocusNode();
addTearDown(parent.dispose);
final FocusAttachment parentAttachment = parent.attach(context); final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode(); final FocusNode child1 = FocusNode();
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(); final FocusNode child2 = FocusNode();
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
parentAttachment.reparent(parent: tester.binding.focusManager.rootScope); parentAttachment.reparent(parent: tester.binding.focusManager.rootScope);
child1Attachment.reparent(parent: parent); child1Attachment.reparent(parent: parent);
@ -67,9 +73,12 @@ void main() {
expect(parent.children, isEmpty); expect(parent.children, isEmpty);
}); });
testWidgets('Geometry is transformed properly.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Geometry is transformed properly.', (WidgetTester tester) async {
final FocusNode focusNode1 = FocusNode(debugLabel: 'Test Node 1'); final FocusNode focusNode1 = FocusNode(debugLabel: 'Test Node 1');
addTearDown(focusNode1.dispose);
final FocusNode focusNode2 = FocusNode(debugLabel: 'Test Node 2'); final FocusNode focusNode2 = FocusNode(debugLabel: 'Test Node 2');
addTearDown(focusNode2.dispose);
await tester.pumpWidget( await tester.pumpWidget(
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@ -104,17 +113,22 @@ void main() {
expect(focusNode2.offset, equals(const Offset(443.0, 194.5))); expect(focusNode2.offset, equals(const Offset(443.0, 194.5)));
}); });
testWidgets('descendantsAreFocusable disables focus for descendants.', (WidgetTester tester) async { testWidgetsWithLeakTracking('descendantsAreFocusable disables focus for descendants.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
addTearDown(scope.dispose);
final FocusAttachment scopeAttachment = scope.attach(context); final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope); parent1Attachment.reparent(parent: scope);
@ -152,17 +166,22 @@ void main() {
expect(scope.traversalDescendants.contains(child2), isFalse); expect(scope.traversalDescendants.contains(child2), isFalse);
}); });
testWidgets('descendantsAreTraversable disables traversal for descendants.', (WidgetTester tester) async { testWidgetsWithLeakTracking('descendantsAreTraversable disables traversal for descendants.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
addTearDown(scope.dispose);
final FocusAttachment scopeAttachment = scope.attach(context); final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
@ -185,17 +204,22 @@ void main() {
expect(scope.traversalDescendants, equals(<FocusNode>[])); expect(scope.traversalDescendants, equals(<FocusNode>[]));
}); });
testWidgets("canRequestFocus doesn't affect traversalChildren", (WidgetTester tester) async { testWidgetsWithLeakTracking("canRequestFocus doesn't affect traversalChildren", (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
addTearDown(scope.dispose);
final FocusAttachment scopeAttachment = scope.attach(context); final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope); parent1Attachment.reparent(parent: scope);
@ -216,11 +240,11 @@ void main() {
expect(scope.traversalChildren.contains(parent2), isFalse); expect(scope.traversalChildren.contains(parent2), isFalse);
}); });
testWidgets('implements debugFillProperties', (WidgetTester tester) async { testWidgetsWithLeakTracking('implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
FocusNode( final FocusNode focusNode = FocusNode(debugLabel: 'Label');
debugLabel: 'Label', addTearDown(focusNode.dispose);
).debugFillProperties(builder); focusNode.debugFillProperties(builder);
final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList(); final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[ expect(description, <String>[
'context: null', 'context: null',
@ -232,8 +256,13 @@ void main() {
]); ]);
}); });
testWidgets('onKeyEvent and onKey correctly cooperate', (WidgetTester tester) async { testWidgetsWithLeakTracking('onKeyEvent and onKey correctly cooperate', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node 3'); final FocusNode focusNode1 = FocusNode(debugLabel: 'Test Node 1');
addTearDown(focusNode1.dispose);
final FocusNode focusNode2 = FocusNode(debugLabel: 'Test Node 2');
addTearDown(focusNode2.dispose);
final FocusNode focusNode3 = FocusNode(debugLabel: 'Test Node 3');
addTearDown(focusNode3.dispose);
List<List<KeyEventResult>> results = <List<KeyEventResult>>[ List<List<KeyEventResult>> results = <List<KeyEventResult>>[
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored], <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored], <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
@ -243,7 +272,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Focus( Focus(
focusNode: FocusNode(debugLabel: 'Test Node 1'), focusNode: focusNode1,
onKeyEvent: (_, KeyEvent event) { onKeyEvent: (_, KeyEvent event) {
logs.add(0); logs.add(0);
return results[0][0]; return results[0][0];
@ -253,7 +282,7 @@ void main() {
return results[0][1]; return results[0][1];
}, },
child: Focus( child: Focus(
focusNode: FocusNode(debugLabel: 'Test Node 2'), focusNode: focusNode2,
onKeyEvent: (_, KeyEvent event) { onKeyEvent: (_, KeyEvent event) {
logs.add(10); logs.add(10);
return results[1][0]; return results[1][0];
@ -263,7 +292,7 @@ void main() {
return results[1][1]; return results[1][1];
}, },
child: Focus( child: Focus(
focusNode: focusNode, focusNode: focusNode3,
onKeyEvent: (_, KeyEvent event) { onKeyEvent: (_, KeyEvent event) {
logs.add(20); logs.add(20);
return results[2][0]; return results[2][0];
@ -277,7 +306,7 @@ void main() {
), ),
), ),
); );
focusNode.requestFocus(); focusNode3.requestFocus();
await tester.pump(); await tester.pump();
// All ignored. // All ignored.
@ -328,15 +357,19 @@ void main() {
group(FocusScopeNode, () { group(FocusScopeNode, () {
testWidgets('Can setFirstFocus on a scope with no manager.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can setFirstFocus on a scope with no manager.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
addTearDown(scope.dispose);
scope.attach(context); scope.attach(context);
final FocusScopeNode parent = FocusScopeNode(debugLabel: 'Parent'); final FocusScopeNode parent = FocusScopeNode(debugLabel: 'Parent');
addTearDown(parent.dispose);
parent.attach(context); parent.attach(context);
final FocusScopeNode child1 = FocusScopeNode(debugLabel: 'Child 1'); final FocusScopeNode child1 = FocusScopeNode(debugLabel: 'Child 1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusScopeNode child2 = FocusScopeNode(debugLabel: 'Child 2'); final FocusScopeNode child2 = FocusScopeNode(debugLabel: 'Child 2');
addTearDown(child2.dispose);
child2.attach(context); child2.attach(context);
scope.setFirstFocus(parent); scope.setFirstFocus(parent);
parent.setFirstFocus(child1); parent.setFirstFocus(child1);
@ -353,15 +386,19 @@ void main() {
expect(scope.focusedChild, equals(parent)); expect(scope.focusedChild, equals(parent));
}); });
testWidgets('Removing a node removes it from scope.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Removing a node removes it from scope.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(); final FocusScopeNode scope = FocusScopeNode();
addTearDown(scope.dispose);
final FocusAttachment scopeAttachment = scope.attach(context); final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent = FocusNode(); final FocusNode parent = FocusNode();
addTearDown(parent.dispose);
final FocusAttachment parentAttachment = parent.attach(context); final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode(); final FocusNode child1 = FocusNode();
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(); final FocusNode child2 = FocusNode();
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parentAttachment.reparent(parent: scope); parentAttachment.reparent(parent: scope);
@ -378,15 +415,19 @@ void main() {
expect(scope.focusedChild, isNull); expect(scope.focusedChild, isNull);
}); });
testWidgets('Can add children to scope and focus', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can add children to scope and focus', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(); final FocusScopeNode scope = FocusScopeNode();
addTearDown(scope.dispose);
final FocusAttachment scopeAttachment = scope.attach(context); final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent = FocusNode(); final FocusNode parent = FocusNode();
addTearDown(parent.dispose);
final FocusAttachment parentAttachment = parent.attach(context); final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode(); final FocusNode child1 = FocusNode();
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(); final FocusNode child2 = FocusNode();
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parentAttachment.reparent(parent: scope); parentAttachment.reparent(parent: scope);
@ -418,11 +459,13 @@ void main() {
expect(child2.hasPrimaryFocus, isTrue); expect(child2.hasPrimaryFocus, isTrue);
}); });
testWidgets('Requesting focus before adding to tree results in a request after adding', (WidgetTester tester) async { testWidgetsWithLeakTracking('Requesting focus before adding to tree results in a request after adding', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(); final FocusScopeNode scope = FocusScopeNode();
addTearDown(scope.dispose);
final FocusAttachment scopeAttachment = scope.attach(context); final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode child = FocusNode(); final FocusNode child = FocusNode();
addTearDown(child.dispose);
child.requestFocus(); child.requestFocus();
expect(child.hasPrimaryFocus, isFalse); // not attached yet. expect(child.hasPrimaryFocus, isFalse); // not attached yet.
@ -438,15 +481,19 @@ void main() {
expect(child.hasPrimaryFocus, isTrue); // now attached and parented, so focus finally happened. expect(child.hasPrimaryFocus, isTrue); // now attached and parented, so focus finally happened.
}); });
testWidgets('Autofocus works.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Autofocus works.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
addTearDown(scope.dispose);
final FocusAttachment scopeAttachment = scope.attach(context); final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent = FocusNode(debugLabel: 'Parent'); final FocusNode parent = FocusNode(debugLabel: 'Parent');
addTearDown(parent.dispose);
final FocusAttachment parentAttachment = parent.attach(context); final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parentAttachment.reparent(parent: scope); parentAttachment.reparent(parent: scope);
@ -475,15 +522,19 @@ void main() {
expect(child2.hasPrimaryFocus, isFalse); expect(child2.hasPrimaryFocus, isFalse);
}); });
testWidgets('Adding a focusedChild to a scope sets scope as focusedChild in parent scope', (WidgetTester tester) async { testWidgetsWithLeakTracking('Adding a focusedChild to a scope sets scope as focusedChild in parent scope', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(); final FocusScopeNode scope1 = FocusScopeNode();
addTearDown(scope1.dispose);
final FocusAttachment scope1Attachment = scope1.attach(context); final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(); final FocusScopeNode scope2 = FocusScopeNode();
addTearDown(scope2.dispose);
final FocusAttachment scope2Attachment = scope2.attach(context); final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode child1 = FocusNode(); final FocusNode child1 = FocusNode();
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(); final FocusNode child2 = FocusNode();
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: scope1); scope2Attachment.reparent(parent: scope1);
@ -507,17 +558,22 @@ void main() {
expect(child2.hasPrimaryFocus, isFalse); expect(child2.hasPrimaryFocus, isFalse);
}); });
testWidgets('Can move node with focus without losing focus', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can move node with focus without losing focus', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
addTearDown(scope.dispose);
final FocusAttachment scopeAttachment = scope.attach(context); final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope); parent1Attachment.reparent(parent: scope);
@ -544,17 +600,22 @@ void main() {
expect(parent2.children.first, equals(child1)); expect(parent2.children.first, equals(child1));
}); });
testWidgets('canRequestFocus affects children.', (WidgetTester tester) async { testWidgetsWithLeakTracking('canRequestFocus affects children.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
addTearDown(scope.dispose);
final FocusAttachment scopeAttachment = scope.attach(context); final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope); parent1Attachment.reparent(parent: scope);
@ -584,17 +645,22 @@ void main() {
expect(parent1.traversalChildren.contains(child2), isFalse); expect(parent1.traversalChildren.contains(child2), isFalse);
}); });
testWidgets("skipTraversal doesn't affect children.", (WidgetTester tester) async { testWidgetsWithLeakTracking("skipTraversal doesn't affect children.", (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
addTearDown(scope.dispose);
final FocusAttachment scopeAttachment = scope.attach(context); final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope); parent1Attachment.reparent(parent: scope);
@ -619,23 +685,31 @@ void main() {
expect(scope.traversalDescendants.contains(child2), isTrue); expect(scope.traversalDescendants.contains(child2), isTrue);
}); });
testWidgets('Can move node between scopes and lose scope focus', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can move node between scopes and lose scope focus', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
addTearDown(scope1.dispose);
final FocusAttachment scope1Attachment = scope1.attach(context); final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
addTearDown(scope2.dispose);
final FocusAttachment scope2Attachment = scope2.attach(context); final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'parent1'); final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'parent2'); final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1'); final FocusNode child1 = FocusNode(debugLabel: 'child1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2'); final FocusNode child2 = FocusNode(debugLabel: 'child2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'child3'); final FocusNode child3 = FocusNode(debugLabel: 'child3');
addTearDown(child3.dispose);
final FocusAttachment child3Attachment = child3.attach(context); final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'child4'); final FocusNode child4 = FocusNode(debugLabel: 'child4');
addTearDown(child4.dispose);
final FocusAttachment child4Attachment = child4.attach(context); final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
@ -657,23 +731,31 @@ void main() {
expect(parent2.children.contains(child1), isTrue); expect(parent2.children.contains(child1), isTrue);
}); });
testWidgets('ancestors and descendants are computed and recomputed properly', (WidgetTester tester) async { testWidgetsWithLeakTracking('ancestors and descendants are computed and recomputed properly', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1'); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1');
addTearDown(scope1.dispose);
final FocusAttachment scope1Attachment = scope1.attach(context); final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
addTearDown(scope2.dispose);
final FocusAttachment scope2Attachment = scope2.attach(context); final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'parent1'); final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'parent2'); final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1'); final FocusNode child1 = FocusNode(debugLabel: 'child1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2'); final FocusNode child2 = FocusNode(debugLabel: 'child2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'child3'); final FocusNode child3 = FocusNode(debugLabel: 'child3');
addTearDown(child3.dispose);
final FocusAttachment child3Attachment = child3.attach(context); final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'child4'); final FocusNode child4 = FocusNode(debugLabel: 'child4');
addTearDown(child4.dispose);
final FocusAttachment child4Attachment = child4.attach(context); final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
@ -693,23 +775,31 @@ void main() {
expect(tester.binding.focusManager.rootScope.descendants, equals(<FocusNode>[child1, child3, child4, parent2, scope2, child2, parent1, scope1])); expect(tester.binding.focusManager.rootScope.descendants, equals(<FocusNode>[child1, child3, child4, parent2, scope2, child2, parent1, scope1]));
}); });
testWidgets('Can move focus between scopes and keep focus', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can move focus between scopes and keep focus', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(); final FocusScopeNode scope1 = FocusScopeNode();
addTearDown(scope1.dispose);
final FocusAttachment scope1Attachment = scope1.attach(context); final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(); final FocusScopeNode scope2 = FocusScopeNode();
addTearDown(scope2.dispose);
final FocusAttachment scope2Attachment = scope2.attach(context); final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(); final FocusNode parent1 = FocusNode();
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(); final FocusNode parent2 = FocusNode();
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(); final FocusNode child1 = FocusNode();
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(); final FocusNode child2 = FocusNode();
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(); final FocusNode child3 = FocusNode();
addTearDown(child3.dispose);
final FocusAttachment child3Attachment = child3.attach(context); final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(); final FocusNode child4 = FocusNode();
addTearDown(child4.dispose);
final FocusAttachment child4Attachment = child4.attach(context); final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
@ -751,23 +841,31 @@ void main() {
expect(scope2.focusedChild, equals(child4)); expect(scope2.focusedChild, equals(child4));
}); });
testWidgets('Unfocus with disposition previouslyFocusedChild works properly', (WidgetTester tester) async { testWidgetsWithLeakTracking('Unfocus with disposition previouslyFocusedChild works properly', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
addTearDown(scope1.dispose);
final FocusAttachment scope1Attachment = scope1.attach(context); final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
addTearDown(scope2.dispose);
final FocusAttachment scope2Attachment = scope2.attach(context); final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'parent1'); final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'parent2'); final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1'); final FocusNode child1 = FocusNode(debugLabel: 'child1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2'); final FocusNode child2 = FocusNode(debugLabel: 'child2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'child3'); final FocusNode child3 = FocusNode(debugLabel: 'child3');
addTearDown(child3.dispose);
final FocusAttachment child3Attachment = child3.attach(context); final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'child4'); final FocusNode child4 = FocusNode(debugLabel: 'child4');
addTearDown(child4.dispose);
final FocusAttachment child4Attachment = child4.attach(context); final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
@ -832,23 +930,31 @@ void main() {
expect(child3.hasPrimaryFocus, isTrue); expect(child3.hasPrimaryFocus, isTrue);
}); });
testWidgets('Unfocus with disposition scope works properly', (WidgetTester tester) async { testWidgetsWithLeakTracking('Unfocus with disposition scope works properly', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
addTearDown(scope1.dispose);
final FocusAttachment scope1Attachment = scope1.attach(context); final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
addTearDown(scope2.dispose);
final FocusAttachment scope2Attachment = scope2.attach(context); final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'parent1'); final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'parent2'); final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1'); final FocusNode child1 = FocusNode(debugLabel: 'child1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2'); final FocusNode child2 = FocusNode(debugLabel: 'child2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'child3'); final FocusNode child3 = FocusNode(debugLabel: 'child3');
addTearDown(child3.dispose);
final FocusAttachment child3Attachment = child3.attach(context); final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'child4'); final FocusNode child4 = FocusNode(debugLabel: 'child4');
addTearDown(child4.dispose);
final FocusAttachment child4Attachment = child4.attach(context); final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
@ -917,23 +1023,31 @@ void main() {
expect(FocusManager.instance.rootScope.hasPrimaryFocus, isTrue); expect(FocusManager.instance.rootScope.hasPrimaryFocus, isTrue);
}); });
testWidgets('Unfocus works properly when some nodes are unfocusable', (WidgetTester tester) async { testWidgetsWithLeakTracking('Unfocus works properly when some nodes are unfocusable', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
addTearDown(scope1.dispose);
final FocusAttachment scope1Attachment = scope1.attach(context); final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
addTearDown(scope2.dispose);
final FocusAttachment scope2Attachment = scope2.attach(context); final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'parent1'); final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'parent2'); final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1'); final FocusNode child1 = FocusNode(debugLabel: 'child1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2'); final FocusNode child2 = FocusNode(debugLabel: 'child2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'child3'); final FocusNode child3 = FocusNode(debugLabel: 'child3');
addTearDown(child3.dispose);
final FocusAttachment child3Attachment = child3.attach(context); final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'child4'); final FocusNode child4 = FocusNode(debugLabel: 'child4');
addTearDown(child4.dispose);
final FocusAttachment child4Attachment = child4.attach(context); final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
@ -983,23 +1097,31 @@ void main() {
expect(child2.hasPrimaryFocus, isFalse); expect(child2.hasPrimaryFocus, isFalse);
}); });
testWidgets('Requesting focus on a scope works properly when some focusedChild nodes are unfocusable', (WidgetTester tester) async { testWidgetsWithLeakTracking('Requesting focus on a scope works properly when some focusedChild nodes are unfocusable', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
addTearDown(scope1.dispose);
final FocusAttachment scope1Attachment = scope1.attach(context); final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
addTearDown(scope2.dispose);
final FocusAttachment scope2Attachment = scope2.attach(context); final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'parent1'); final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'parent2'); final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1'); final FocusNode child1 = FocusNode(debugLabel: 'child1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2'); final FocusNode child2 = FocusNode(debugLabel: 'child2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'child3'); final FocusNode child3 = FocusNode(debugLabel: 'child3');
addTearDown(child3.dispose);
final FocusAttachment child3Attachment = child3.attach(context); final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'child4'); final FocusNode child4 = FocusNode(debugLabel: 'child4');
addTearDown(child4.dispose);
final FocusAttachment child4Attachment = child4.attach(context); final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
@ -1037,7 +1159,7 @@ void main() {
expect(child4.hasPrimaryFocus, isTrue); expect(child4.hasPrimaryFocus, isTrue);
}); });
testWidgets('Key handling bubbles up and terminates when handled.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Key handling bubbles up and terminates when handled.', (WidgetTester tester) async {
final Set<FocusNode> receivedAnEvent = <FocusNode>{}; final Set<FocusNode> receivedAnEvent = <FocusNode>{};
final Set<FocusNode> shouldHandle = <FocusNode>{}; final Set<FocusNode> shouldHandle = <FocusNode>{};
KeyEventResult handleEvent(FocusNode node, RawKeyEvent event) { KeyEventResult handleEvent(FocusNode node, RawKeyEvent event) {
@ -1055,20 +1177,28 @@ void main() {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'Scope 1'); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'Scope 1');
addTearDown(scope1.dispose);
final FocusAttachment scope1Attachment = scope1.attach(context, onKey: handleEvent); final FocusAttachment scope1Attachment = scope1.attach(context, onKey: handleEvent);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'Scope 2'); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'Scope 2');
addTearDown(scope2.dispose);
final FocusAttachment scope2Attachment = scope2.attach(context, onKey: handleEvent); final FocusAttachment scope2Attachment = scope2.attach(context, onKey: handleEvent);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1', onKey: handleEvent); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1', onKey: handleEvent);
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2', onKey: handleEvent); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2', onKey: handleEvent);
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context, onKey: handleEvent); final FocusAttachment child1Attachment = child1.attach(context, onKey: handleEvent);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context, onKey: handleEvent); final FocusAttachment child2Attachment = child2.attach(context, onKey: handleEvent);
final FocusNode child3 = FocusNode(debugLabel: 'Child 3'); final FocusNode child3 = FocusNode(debugLabel: 'Child 3');
addTearDown(child3.dispose);
final FocusAttachment child3Attachment = child3.attach(context, onKey: handleEvent); final FocusAttachment child3Attachment = child3.attach(context, onKey: handleEvent);
final FocusNode child4 = FocusNode(debugLabel: 'Child 4'); final FocusNode child4 = FocusNode(debugLabel: 'Child 4');
addTearDown(child4.dispose);
final FocusAttachment child4Attachment = child4.attach(context, onKey: handleEvent); final FocusAttachment child4Attachment = child4.attach(context, onKey: handleEvent);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
@ -1101,7 +1231,7 @@ void main() {
expect(receivedAnEvent, isEmpty); expect(receivedAnEvent, isEmpty);
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Initial highlight mode guesses correctly.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Initial highlight mode guesses correctly.', (WidgetTester tester) async {
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.automatic; FocusManager.instance.highlightStrategy = FocusHighlightStrategy.automatic;
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
@ -1115,7 +1245,7 @@ void main() {
} }
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
testWidgets('Mouse events change initial focus highlight mode on mobile.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Mouse events change initial focus highlight mode on mobile.', (WidgetTester tester) async {
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch)); expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
RendererBinding.instance.initMouseTracker(); // Clear out the mouse state. RendererBinding.instance.initMouseTracker(); // Clear out the mouse state.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 0); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 0);
@ -1123,7 +1253,7 @@ void main() {
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional)); expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
}, variant: TargetPlatformVariant.mobile()); }, variant: TargetPlatformVariant.mobile());
testWidgets('Mouse events change initial focus highlight mode on desktop.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Mouse events change initial focus highlight mode on desktop.', (WidgetTester tester) async {
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional)); expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
RendererBinding.instance.initMouseTracker(); // Clear out the mouse state. RendererBinding.instance.initMouseTracker(); // Clear out the mouse state.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 0); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 0);
@ -1131,12 +1261,12 @@ void main() {
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional)); expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
}, variant: TargetPlatformVariant.desktop()); }, variant: TargetPlatformVariant.desktop());
testWidgets('Keyboard events change initial focus highlight mode.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Keyboard events change initial focus highlight mode.', (WidgetTester tester) async {
await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional)); expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
testWidgets('Events change focus highlight mode.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Events change focus highlight mode.', (WidgetTester tester) async {
await setupWidget(tester); await setupWidget(tester);
int callCount = 0; int callCount = 0;
FocusHighlightMode? lastMode; FocusHighlightMode? lastMode;
@ -1177,11 +1307,11 @@ void main() {
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch)); expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
}); });
testWidgets('implements debugFillProperties', (WidgetTester tester) async { testWidgetsWithLeakTracking('implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
FocusScopeNode( final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope Label');
debugLabel: 'Scope Label', addTearDown(scope.dispose);
).debugFillProperties(builder); scope.debugFillProperties(builder);
final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList(); final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[ expect(description, <String>[
'context: null', 'context: null',
@ -1193,23 +1323,31 @@ void main() {
]); ]);
}); });
testWidgets('debugDescribeFocusTree produces correct output', (WidgetTester tester) async { testWidgetsWithLeakTracking('debugDescribeFocusTree produces correct output', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'Scope 1'); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'Scope 1');
addTearDown(scope1.dispose);
final FocusAttachment scope1Attachment = scope1.attach(context); final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(); // No label, Just to test that it works. final FocusScopeNode scope2 = FocusScopeNode(); // No label, Just to test that it works.
addTearDown(scope2.dispose);
final FocusAttachment scope2Attachment = scope2.attach(context); final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(); // No label, Just to test that it works. final FocusNode child2 = FocusNode(); // No label, Just to test that it works.
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'Child 3'); final FocusNode child3 = FocusNode(debugLabel: 'Child 3');
addTearDown(child3.dispose);
final FocusAttachment child3Attachment = child3.attach(context); final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'Child 4'); final FocusNode child4 = FocusNode(debugLabel: 'Child 4');
addTearDown(child4.dispose);
final FocusAttachment child4Attachment = child4.attach(context); final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
@ -1269,11 +1407,13 @@ void main() {
}); });
group('Autofocus', () { group('Autofocus', () {
testWidgets( testWidgetsWithLeakTracking(
'works when the previous focused node is detached', 'works when the previous focused node is detached',
(WidgetTester tester) async { (WidgetTester tester) async {
final FocusNode node1 = FocusNode(); final FocusNode node1 = FocusNode();
addTearDown(node1.dispose);
final FocusNode node2 = FocusNode(); final FocusNode node2 = FocusNode();
addTearDown(node2.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( FocusScope(
@ -1294,11 +1434,13 @@ void main() {
expect(node2.hasPrimaryFocus, isTrue); expect(node2.hasPrimaryFocus, isTrue);
}); });
testWidgets( testWidgetsWithLeakTracking(
'node detached before autofocus is applied', 'node detached before autofocus is applied',
(WidgetTester tester) async { (WidgetTester tester) async {
final FocusScopeNode scopeNode = FocusScopeNode(); final FocusScopeNode scopeNode = FocusScopeNode();
addTearDown(scopeNode.dispose);
final FocusNode node1 = FocusNode(); final FocusNode node1 = FocusNode();
addTearDown(node1.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( FocusScope(
@ -1322,10 +1464,13 @@ void main() {
expect(scopeNode.hasPrimaryFocus, isTrue); expect(scopeNode.hasPrimaryFocus, isTrue);
}); });
testWidgets('autofocus the first candidate', (WidgetTester tester) async { testWidgetsWithLeakTracking('autofocus the first candidate', (WidgetTester tester) async {
final FocusNode node1 = FocusNode(); final FocusNode node1 = FocusNode();
addTearDown(node1.dispose);
final FocusNode node2 = FocusNode(); final FocusNode node2 = FocusNode();
addTearDown(node2.dispose);
final FocusNode node3 = FocusNode(); final FocusNode node3 = FocusNode();
addTearDown(node3.dispose);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
@ -1355,10 +1500,13 @@ void main() {
expect(node1.hasPrimaryFocus, isTrue); expect(node1.hasPrimaryFocus, isTrue);
}); });
testWidgets('Autofocus works with global key reparenting', (WidgetTester tester) async { testWidgetsWithLeakTracking('Autofocus works with global key reparenting', (WidgetTester tester) async {
final FocusNode node = FocusNode(); final FocusNode node = FocusNode();
addTearDown(node.dispose);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1'); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1');
addTearDown(scope1.dispose);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
addTearDown(scope2.dispose);
final GlobalKey key = GlobalKey(); final GlobalKey key = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
@ -1409,15 +1557,19 @@ void main() {
}); });
}); });
testWidgets("Doesn't lose focused child when reparenting if the nearestScope doesn't change.", (WidgetTester tester) async { testWidgetsWithLeakTracking("Doesn't lose focused child when reparenting if the nearestScope doesn't change.", (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1'); final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
addTearDown(parent1.dispose);
final FocusScopeNode parent2 = FocusScopeNode(debugLabel: 'parent2'); final FocusScopeNode parent2 = FocusScopeNode(debugLabel: 'parent2');
addTearDown(parent2.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1'); final FocusNode child1 = FocusNode(debugLabel: 'child1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2'); final FocusNode child2 = FocusNode(debugLabel: 'child2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope); parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
child1Attachment.reparent(parent: parent1); child1Attachment.reparent(parent: parent1);
@ -1435,7 +1587,7 @@ void main() {
expect(parent1.focusedChild, equals(child2)); expect(parent1.focusedChild, equals(child2));
}); });
testWidgets('Ancestors get notified exactly as often as needed if focused child changes focus.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Ancestors get notified exactly as often as needed if focused child changes focus.', (WidgetTester tester) async {
bool topFocus = false; bool topFocus = false;
bool parent1Focus = false; bool parent1Focus = false;
bool parent2Focus = false; bool parent2Focus = false;
@ -1460,14 +1612,19 @@ void main() {
} }
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode top = FocusScopeNode(debugLabel: 'top'); final FocusScopeNode top = FocusScopeNode(debugLabel: 'top');
addTearDown(top.dispose);
final FocusAttachment topAttachment = top.attach(context); final FocusAttachment topAttachment = top.attach(context);
final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1'); final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusScopeNode parent2 = FocusScopeNode(debugLabel: 'parent2'); final FocusScopeNode parent2 = FocusScopeNode(debugLabel: 'parent2');
addTearDown(parent2.dispose);
final FocusAttachment parent2Attachment = parent2.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1'); final FocusNode child1 = FocusNode(debugLabel: 'child1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2'); final FocusNode child2 = FocusNode(debugLabel: 'child2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
topAttachment.reparent(parent: tester.binding.focusManager.rootScope); topAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: top); parent1Attachment.reparent(parent: top);
@ -1566,13 +1723,16 @@ void main() {
expect(child2Notify, equals(0)); expect(child2Notify, equals(0));
}); });
testWidgets('Focus changes notify listeners.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus changes notify listeners.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1'); final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1'); final FocusNode child1 = FocusNode(debugLabel: 'child1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2'); final FocusNode child2 = FocusNode(debugLabel: 'child2');
addTearDown(child2.dispose);
final FocusAttachment child2Attachment = child2.attach(context); final FocusAttachment child2Attachment = child2.attach(context);
parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope); parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
child1Attachment.reparent(parent: parent1); child1Attachment.reparent(parent: parent1);
@ -1618,9 +1778,12 @@ void main() {
expect(()=> FocusNode().dispose(), dispatchesMemoryEvents(FocusNode)); expect(()=> FocusNode().dispose(), dispatchesMemoryEvents(FocusNode));
}); });
testWidgets('FocusManager notifies listeners when a widget loses focus because it was removed.', (WidgetTester tester) async { testWidgetsWithLeakTracking('FocusManager notifies listeners when a widget loses focus because it was removed.', (WidgetTester tester) async {
final FocusNode nodeA = FocusNode(debugLabel: 'a'); final FocusNode nodeA = FocusNode(debugLabel: 'a');
addTearDown(nodeA.dispose);
final FocusNode nodeB = FocusNode(debugLabel: 'b'); final FocusNode nodeB = FocusNode(debugLabel: 'b');
addTearDown(nodeB.dispose);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
@ -1664,7 +1827,7 @@ void main() {
tester.binding.focusManager.removeListener(handleFocusChange); tester.binding.focusManager.removeListener(handleFocusChange);
}); });
testWidgets('debugFocusChanges causes logging of focus changes', (WidgetTester tester) async { testWidgetsWithLeakTracking('debugFocusChanges causes logging of focus changes', (WidgetTester tester) async {
final bool oldDebugFocusChanges = debugFocusChanges; final bool oldDebugFocusChanges = debugFocusChanges;
final DebugPrintCallback oldDebugPrint = debugPrint; final DebugPrintCallback oldDebugPrint = debugPrint;
final StringBuffer messages = StringBuffer(); final StringBuffer messages = StringBuffer();
@ -1675,8 +1838,10 @@ void main() {
try { try {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1'); final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
addTearDown(parent1.dispose);
final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1'); final FocusNode child1 = FocusNode(debugLabel: 'child1');
addTearDown(child1.dispose);
final FocusAttachment child1Attachment = child1.attach(context); final FocusAttachment child1Attachment = child1.attach(context);
parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope); parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
child1Attachment.reparent(parent: parent1); child1Attachment.reparent(parent: parent1);
@ -1709,7 +1874,7 @@ void main() {
expect(messagesStr, contains(RegExp(r'FOCUS: Scheduling update, current focus is null, next focus will be FocusScopeNode#.*parent1'))); expect(messagesStr, contains(RegExp(r'FOCUS: Scheduling update, current focus is null, next focus will be FocusScopeNode#.*parent1')));
}); });
testWidgets("doesn't call toString on a focus node when debugFocusChanges is false", (WidgetTester tester) async { testWidgetsWithLeakTracking("doesn't call toString on a focus node when debugFocusChanges is false", (WidgetTester tester) async {
final bool oldDebugFocusChanges = debugFocusChanges; final bool oldDebugFocusChanges = debugFocusChanges;
final DebugPrintCallback oldDebugPrint = debugPrint; final DebugPrintCallback oldDebugPrint = debugPrint;
final StringBuffer messages = StringBuffer(); final StringBuffer messages = StringBuffer();

View File

@ -6,12 +6,13 @@ import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
void main() { void main() {
group('FocusScope', () { group('FocusScope', () {
testWidgets('Can focus', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can focus', (WidgetTester tester) async {
final GlobalKey<TestFocusState> key = GlobalKey(); final GlobalKey<TestFocusState> key = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
@ -27,7 +28,7 @@ void main() {
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
}); });
testWidgets('Can unfocus', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can unfocus', (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
@ -62,7 +63,7 @@ void main() {
expect(find.text('B FOCUSED'), findsOneWidget); expect(find.text('B FOCUSED'), findsOneWidget);
}); });
testWidgets('Autofocus works', (WidgetTester tester) async { testWidgetsWithLeakTracking('Autofocus works', (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
@ -82,7 +83,7 @@ void main() {
expect(find.text('B FOCUSED'), findsOneWidget); expect(find.text('B FOCUSED'), findsOneWidget);
}); });
testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can have multiple focused children and they update accordingly', (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
@ -129,9 +130,11 @@ void main() {
// This moves a focus node first into a focus scope that is added to its // This moves a focus node first into a focus scope that is added to its
// parent, and then out of that focus scope again. // parent, and then out of that focus scope again.
testWidgets('Can move focus in and out of FocusScope', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can move focus in and out of FocusScope', (WidgetTester tester) async {
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node'); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node');
addTearDown(parentFocusScope.dispose);
final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node'); final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node');
addTearDown(childFocusScope.dispose);
final GlobalKey<TestFocusState> key = GlobalKey(); final GlobalKey<TestFocusState> key = GlobalKey();
// Initially create the focus inside of the parent FocusScope. // Initially create the focus inside of the parent FocusScope.
@ -274,10 +277,13 @@ void main() {
childAttachment.detach(); childAttachment.detach();
}); });
testWidgets('Setting first focus requests focus for the scope properly.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Setting first focus requests focus for the scope properly.', (WidgetTester tester) async {
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node'); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node');
addTearDown(parentFocusScope.dispose);
final FocusScopeNode childFocusScope1 = FocusScopeNode(debugLabel: 'Child Scope Node 1'); final FocusScopeNode childFocusScope1 = FocusScopeNode(debugLabel: 'Child Scope Node 1');
addTearDown(childFocusScope1.dispose);
final FocusScopeNode childFocusScope2 = FocusScopeNode(debugLabel: 'Child Scope Node 2'); final FocusScopeNode childFocusScope2 = FocusScopeNode(debugLabel: 'Child Scope Node 2');
addTearDown(childFocusScope2.dispose);
final GlobalKey<TestFocusState> keyA = GlobalKey(debugLabel: 'Key A'); final GlobalKey<TestFocusState> keyA = GlobalKey(debugLabel: 'Key A');
final GlobalKey<TestFocusState> keyB = GlobalKey(debugLabel: 'Key B'); final GlobalKey<TestFocusState> keyB = GlobalKey(debugLabel: 'Key B');
final GlobalKey<TestFocusState> keyC = GlobalKey(debugLabel: 'Key C'); final GlobalKey<TestFocusState> keyC = GlobalKey(debugLabel: 'Key C');
@ -376,7 +382,7 @@ void main() {
expect(childFocusScope2.isFirstFocus, isFalse); expect(childFocusScope2.isFirstFocus, isFalse);
}); });
testWidgets('Removing focused widget moves focus to next widget', (WidgetTester tester) async { testWidgetsWithLeakTracking('Removing focused widget moves focus to next widget', (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
@ -420,10 +426,12 @@ void main() {
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
}); });
testWidgets('Adding a new FocusScope attaches the child to its parent.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Adding a new FocusScope attaches the child to its parent.', (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node'); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node');
addTearDown(parentFocusScope.dispose);
final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node'); final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node');
addTearDown(childFocusScope.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( FocusScope(
@ -466,11 +474,15 @@ void main() {
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
}); });
testWidgets('Setting parentNode determines focus tree hierarchy.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Setting parentNode determines focus tree hierarchy.', (WidgetTester tester) async {
final FocusNode topNode = FocusNode(debugLabel: 'Top'); final FocusNode topNode = FocusNode(debugLabel: 'Top');
addTearDown(topNode.dispose);
final FocusNode parentNode = FocusNode(debugLabel: 'Parent'); final FocusNode parentNode = FocusNode(debugLabel: 'Parent');
addTearDown(parentNode.dispose);
final FocusNode childNode = FocusNode(debugLabel: 'Child'); final FocusNode childNode = FocusNode(debugLabel: 'Child');
addTearDown(childNode.dispose);
final FocusNode insertedNode = FocusNode(debugLabel: 'Inserted'); final FocusNode insertedNode = FocusNode(debugLabel: 'Inserted');
addTearDown(insertedNode.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( FocusScope(
@ -532,11 +544,15 @@ void main() {
expect(insertedNode.hasFocus, isFalse); expect(insertedNode.hasFocus, isFalse);
}); });
testWidgets('Setting parentNode determines focus scope tree hierarchy.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Setting parentNode determines focus scope tree hierarchy.', (WidgetTester tester) async {
final FocusScopeNode topNode = FocusScopeNode(debugLabel: 'Top'); final FocusScopeNode topNode = FocusScopeNode(debugLabel: 'Top');
addTearDown(topNode.dispose);
final FocusScopeNode parentNode = FocusScopeNode(debugLabel: 'Parent'); final FocusScopeNode parentNode = FocusScopeNode(debugLabel: 'Parent');
addTearDown(parentNode.dispose);
final FocusScopeNode childNode = FocusScopeNode(debugLabel: 'Child'); final FocusScopeNode childNode = FocusScopeNode(debugLabel: 'Child');
addTearDown(childNode.dispose);
final FocusScopeNode insertedNode = FocusScopeNode(debugLabel: 'Inserted'); final FocusScopeNode insertedNode = FocusScopeNode(debugLabel: 'Inserted');
addTearDown(insertedNode.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusScope.withExternalFocusNode( FocusScope.withExternalFocusNode(
@ -599,10 +615,11 @@ void main() {
}); });
// Arguably, this isn't correct behavior, but it is what happens now. // Arguably, this isn't correct behavior, but it is what happens now.
testWidgets("Removing focused widget doesn't move focus to next widget within FocusScope", (WidgetTester tester) async { testWidgetsWithLeakTracking("Removing focused widget doesn't move focus to next widget within FocusScope", (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope'); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope');
addTearDown(parentFocusScope.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( FocusScope(
@ -656,12 +673,13 @@ void main() {
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
}); });
testWidgets('Removing a FocusScope removes its node from the tree', (WidgetTester tester) async { testWidgetsWithLeakTracking('Removing a FocusScope removes its node from the tree', (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
final GlobalKey<TestFocusState> scopeKeyA = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyA = GlobalKey();
final GlobalKey<TestFocusState> scopeKeyB = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyB = GlobalKey();
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope'); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope');
addTearDown(parentFocusScope.dispose);
// This checks both FocusScopes that have their own nodes, as well as those // This checks both FocusScopes that have their own nodes, as well as those
// that use external nodes. // that use external nodes.
@ -719,13 +737,15 @@ void main() {
}); });
// By "pinned", it means kept in the tree by a GlobalKey. // By "pinned", it means kept in the tree by a GlobalKey.
testWidgets("Removing pinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async { testWidgetsWithLeakTracking("Removing pinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
final GlobalKey<TestFocusState> scopeKeyA = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyA = GlobalKey();
final GlobalKey<TestFocusState> scopeKeyB = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyB = GlobalKey();
final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1'); final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1');
addTearDown(parentFocusScope1.dispose);
final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2'); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2');
addTearDown(parentFocusScope2.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( FocusTraversalGroup(
@ -805,11 +825,13 @@ void main() {
expect(find.text('B FOCUSED'), findsOneWidget); expect(find.text('B FOCUSED'), findsOneWidget);
}); });
testWidgets("Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async { testWidgetsWithLeakTracking("Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1'); final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1');
addTearDown(parentFocusScope1.dispose);
final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2'); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2');
addTearDown(parentFocusScope2.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( FocusTraversalGroup(
@ -885,9 +907,11 @@ void main() {
expect(find.text('B FOCUSED'), findsOneWidget); expect(find.text('B FOCUSED'), findsOneWidget);
}); });
testWidgets('Moving widget from one scope to another retains focus', (WidgetTester tester) async { testWidgetsWithLeakTracking('Moving widget from one scope to another retains focus', (WidgetTester tester) async {
final FocusScopeNode parentFocusScope1 = FocusScopeNode(); final FocusScopeNode parentFocusScope1 = FocusScopeNode();
addTearDown(parentFocusScope1.dispose);
final FocusScopeNode parentFocusScope2 = FocusScopeNode(); final FocusScopeNode parentFocusScope2 = FocusScopeNode();
addTearDown(parentFocusScope2.dispose);
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
@ -966,9 +990,11 @@ void main() {
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
}); });
testWidgets('Moving FocusScopeNodes retains focus', (WidgetTester tester) async { testWidgetsWithLeakTracking('Moving FocusScopeNodes retains focus', (WidgetTester tester) async {
final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Scope 1'); final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Scope 1');
addTearDown(parentFocusScope1.dispose);
final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Scope 2'); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Scope 2');
addTearDown(parentFocusScope2.dispose);
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
@ -1052,7 +1078,7 @@ void main() {
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
}); });
testWidgets('Can focus root node.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can focus root node.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
await tester.pumpWidget( await tester.pumpWidget(
Focus( Focus(
@ -1071,8 +1097,9 @@ void main() {
expect(rootNode, equals(firstElement.owner!.focusManager.rootScope)); expect(rootNode, equals(firstElement.owner!.focusManager.rootScope));
}); });
testWidgets('Can autofocus a node.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can autofocus a node.', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
addTearDown(focusNode.dispose);
await tester.pumpWidget( await tester.pumpWidget(
Focus( Focus(
focusNode: focusNode, focusNode: focusNode,
@ -1095,9 +1122,11 @@ void main() {
expect(focusNode.hasPrimaryFocus, isTrue); expect(focusNode.hasPrimaryFocus, isTrue);
}); });
testWidgets("Won't autofocus a node if one is already focused.", (WidgetTester tester) async { testWidgetsWithLeakTracking("Won't autofocus a node if one is already focused.", (WidgetTester tester) async {
final FocusNode focusNodeA = FocusNode(debugLabel: 'Test Node A'); final FocusNode focusNodeA = FocusNode(debugLabel: 'Test Node A');
addTearDown(focusNodeA.dispose);
final FocusNode focusNodeB = FocusNode(debugLabel: 'Test Node B'); final FocusNode focusNodeB = FocusNode(debugLabel: 'Test Node B');
addTearDown(focusNodeB.dispose);
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
children: <Widget>[ children: <Widget>[
@ -1134,9 +1163,10 @@ void main() {
expect(focusNodeA.hasPrimaryFocus, isTrue); expect(focusNodeA.hasPrimaryFocus, isTrue);
}); });
testWidgets("FocusScope doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async { testWidgetsWithLeakTracking("FocusScope doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusScopeNode focusScopeNode = FocusScopeNode(); final FocusScopeNode focusScopeNode = FocusScopeNode();
addTearDown(focusScopeNode.dispose);
bool? keyEventHandled; bool? keyEventHandled;
KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) { KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) {
keyEventHandled = true; keyEventHandled = true;
@ -1205,7 +1235,7 @@ void main() {
}); });
group('Focus', () { group('Focus', () {
testWidgets('Focus.of stops at the nearest Focus widget.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus.of stops at the nearest Focus widget.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key3 = GlobalKey(debugLabel: '3');
@ -1213,6 +1243,7 @@ void main() {
final GlobalKey key5 = GlobalKey(debugLabel: '5'); final GlobalKey key5 = GlobalKey(debugLabel: '5');
final GlobalKey key6 = GlobalKey(debugLabel: '6'); final GlobalKey key6 = GlobalKey(debugLabel: '6');
final FocusScopeNode scopeNode = FocusScopeNode(); final FocusScopeNode scopeNode = FocusScopeNode();
addTearDown(scopeNode.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( FocusScope(
key: key1, key: key1,
@ -1252,7 +1283,7 @@ void main() {
expect(Focus.of(element5).parent!.parent, equals(root)); expect(Focus.of(element5).parent!.parent, equals(root));
expect(Focus.of(element6).parent!.parent!.parent, equals(root)); expect(Focus.of(element6).parent!.parent!.parent, equals(root));
}); });
testWidgets('Can traverse Focus children.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can traverse Focus children.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key3 = GlobalKey(debugLabel: '3');
@ -1326,7 +1357,7 @@ void main() {
expect(keys, equals(<Key>[key7, key8])); expect(keys, equals(<Key>[key7, key8]));
}); });
testWidgets('Can set focus.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can set focus.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
late bool gotFocus; late bool gotFocus;
await tester.pumpWidget( await tester.pumpWidget(
@ -1346,7 +1377,7 @@ void main() {
expect(node.hasFocus, isTrue); expect(node.hasFocus, isTrue);
}); });
testWidgets('Focus is ignored when set to not focusable.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus is ignored when set to not focusable.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
bool? gotFocus; bool? gotFocus;
await tester.pumpWidget( await tester.pumpWidget(
@ -1367,7 +1398,7 @@ void main() {
expect(node.hasFocus, isFalse); expect(node.hasFocus, isFalse);
}); });
testWidgets('Focus is lost when set to not focusable.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus is lost when set to not focusable.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
bool? gotFocus; bool? gotFocus;
await tester.pumpWidget( await tester.pumpWidget(
@ -1407,10 +1438,11 @@ void main() {
expect(node.hasFocus, isFalse); expect(node.hasFocus, isFalse);
}); });
testWidgets('Child of unfocusable Focus can get focus.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Child of unfocusable Focus can get focus.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
bool? gotFocus; bool? gotFocus;
await tester.pumpWidget( await tester.pumpWidget(
Focus( Focus(
@ -1439,7 +1471,7 @@ void main() {
expect(unfocusableNode.hasFocus, isTrue); expect(unfocusableNode.hasFocus, isTrue);
}); });
testWidgets('Nodes are removed when all Focuses are removed.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Nodes are removed when all Focuses are removed.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
late bool gotFocus; late bool gotFocus;
await tester.pumpWidget( await tester.pumpWidget(
@ -1465,7 +1497,7 @@ void main() {
expect(FocusManager.instance.rootScope.descendants, isEmpty); expect(FocusManager.instance.rootScope.descendants, isEmpty);
}); });
testWidgets('Focus widgets set Semantics information about focus', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus widgets set Semantics information about focus', (WidgetTester tester) async {
final GlobalKey<TestFocusState> key = GlobalKey(); final GlobalKey<TestFocusState> key = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
@ -1494,7 +1526,7 @@ void main() {
expect(semantics.hasFlag(SemanticsFlag.isFocusable), isFalse); expect(semantics.hasFlag(SemanticsFlag.isFocusable), isFalse);
}); });
testWidgets('Setting canRequestFocus on focus node causes update.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Setting canRequestFocus on focus node causes update.', (WidgetTester tester) async {
final GlobalKey<TestFocusState> key = GlobalKey(); final GlobalKey<TestFocusState> key = GlobalKey();
final TestFocus testFocus = TestFocus(key: key); final TestFocus testFocus = TestFocus(key: key);
@ -1511,7 +1543,7 @@ void main() {
expect(key.currentState!.focusNode.canRequestFocus, isFalse); expect(key.currentState!.focusNode.canRequestFocus, isFalse);
}); });
testWidgets('canRequestFocus causes descendants of scope to be skipped.', (WidgetTester tester) async { testWidgetsWithLeakTracking('canRequestFocus causes descendants of scope to be skipped.', (WidgetTester tester) async {
final GlobalKey scope1 = GlobalKey(debugLabel: 'scope1'); final GlobalKey scope1 = GlobalKey(debugLabel: 'scope1');
final GlobalKey scope2 = GlobalKey(debugLabel: 'scope2'); final GlobalKey scope2 = GlobalKey(debugLabel: 'scope2');
final GlobalKey focus1 = GlobalKey(debugLabel: 'focus1'); final GlobalKey focus1 = GlobalKey(debugLabel: 'focus1');
@ -1620,11 +1652,15 @@ void main() {
expect(Focus.of(container1.currentContext!).hasFocus, isTrue); expect(Focus.of(container1.currentContext!).hasFocus, isTrue);
}); });
testWidgets('skipTraversal works as expected.', (WidgetTester tester) async { testWidgetsWithLeakTracking('skipTraversal works as expected.', (WidgetTester tester) async {
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1'); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1');
addTearDown(scope1.dispose);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
addTearDown(scope2.dispose);
final FocusNode focus1 = FocusNode(debugLabel: 'focus1'); final FocusNode focus1 = FocusNode(debugLabel: 'focus1');
addTearDown(focus1.dispose);
final FocusNode focus2 = FocusNode(debugLabel: 'focus2'); final FocusNode focus2 = FocusNode(debugLabel: 'focus2');
addTearDown(focus2.dispose);
Future<void> pumpTest({ Future<void> pumpTest({
bool traverseScope1 = false, bool traverseScope1 = false,
@ -1674,10 +1710,11 @@ void main() {
expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1, scope2])); expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1, scope2]));
}); });
testWidgets('descendantsAreFocusable works as expected.', (WidgetTester tester) async { testWidgetsWithLeakTracking('descendantsAreFocusable works as expected.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
bool? gotFocus; bool? gotFocus;
await tester.pumpWidget( await tester.pumpWidget(
Focus( Focus(
@ -1713,11 +1750,15 @@ void main() {
expect(unfocusableNode.hasFocus, isFalse); expect(unfocusableNode.hasFocus, isFalse);
}); });
testWidgets('descendantsAreTraversable works as expected.', (WidgetTester tester) async { testWidgetsWithLeakTracking('descendantsAreTraversable works as expected.', (WidgetTester tester) async {
final FocusScopeNode scopeNode = FocusScopeNode(debugLabel: 'scope'); final FocusScopeNode scopeNode = FocusScopeNode(debugLabel: 'scope');
addTearDown(scopeNode.dispose);
final FocusNode node1 = FocusNode(debugLabel: 'node 1'); final FocusNode node1 = FocusNode(debugLabel: 'node 1');
addTearDown(node1.dispose);
final FocusNode node2 = FocusNode(debugLabel: 'node 2'); final FocusNode node2 = FocusNode(debugLabel: 'node 2');
addTearDown(node2.dispose);
final FocusNode node3 = FocusNode(debugLabel: 'node 3'); final FocusNode node3 = FocusNode(debugLabel: 'node 3');
addTearDown(node3.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( FocusScope(
@ -1746,7 +1787,7 @@ void main() {
expect(node2.traversalDescendants, equals(<FocusNode>[])); expect(node2.traversalDescendants, equals(<FocusNode>[]));
}); });
testWidgets("Focus doesn't introduce a Semantics node when includeSemantics is false", (WidgetTester tester) async { testWidgetsWithLeakTracking("Focus doesn't introduce a Semantics node when includeSemantics is false", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(Focus(includeSemantics: false, child: Container())); await tester.pumpWidget(Focus(includeSemantics: false, child: Container()));
final TestSemantics expectedSemantics = TestSemantics.root(); final TestSemantics expectedSemantics = TestSemantics.root();
@ -1754,9 +1795,10 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('Focus updates the onKey handler when the widget updates', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus updates the onKey handler when the widget updates', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
bool? keyEventHandled; bool? keyEventHandled;
KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) { KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) {
keyEventHandled = true; keyEventHandled = true;
@ -1803,9 +1845,10 @@ void main() {
expect(keyEventHandled, isTrue); expect(keyEventHandled, isTrue);
}); });
testWidgets('Focus updates the onKeyEvent handler when the widget updates', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus updates the onKeyEvent handler when the widget updates', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
bool? keyEventHandled; bool? keyEventHandled;
KeyEventResult handleEventCallback(FocusNode node, KeyEvent event) { KeyEventResult handleEventCallback(FocusNode node, KeyEvent event) {
keyEventHandled = true; keyEventHandled = true;
@ -1852,9 +1895,10 @@ void main() {
expect(keyEventHandled, isTrue); expect(keyEventHandled, isTrue);
}); });
testWidgets("Focus doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async { testWidgetsWithLeakTracking("Focus doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
bool? keyEventHandled; bool? keyEventHandled;
KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) { KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) {
keyEventHandled = true; keyEventHandled = true;
@ -1921,7 +1965,7 @@ void main() {
expect(keyEventHandled, isTrue); expect(keyEventHandled, isTrue);
}); });
testWidgets('Focus passes changes in attribute values to its focus node', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus passes changes in attribute values to its focus node', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Focus( Focus(
child: Container(), child: Container(),
@ -1931,10 +1975,11 @@ void main() {
}); });
group('ExcludeFocus', () { group('ExcludeFocus', () {
testWidgets("Descendants of ExcludeFocus aren't focusable.", (WidgetTester tester) async { testWidgetsWithLeakTracking("Descendants of ExcludeFocus aren't focusable.", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
bool? gotFocus; bool? gotFocus;
await tester.pumpWidget( await tester.pumpWidget(
ExcludeFocus( ExcludeFocus(
@ -1970,10 +2015,13 @@ void main() {
}); });
// Regression test for https://github.com/flutter/flutter/issues/61700 // Regression test for https://github.com/flutter/flutter/issues/61700
testWidgets("ExcludeFocus doesn't transfer focus to another descendant.", (WidgetTester tester) async { testWidgetsWithLeakTracking("ExcludeFocus doesn't transfer focus to another descendant.", (WidgetTester tester) async {
final FocusNode parentFocusNode = FocusNode(debugLabel: 'group'); final FocusNode parentFocusNode = FocusNode(debugLabel: 'group');
addTearDown(parentFocusNode.dispose);
final FocusNode focusNode1 = FocusNode(debugLabel: 'node 1'); final FocusNode focusNode1 = FocusNode(debugLabel: 'node 1');
addTearDown(focusNode1.dispose);
final FocusNode focusNode2 = FocusNode(debugLabel: 'node 2'); final FocusNode focusNode2 = FocusNode(debugLabel: 'node 2');
addTearDown(focusNode2.dispose);
await tester.pumpWidget( await tester.pumpWidget(
ExcludeFocus( ExcludeFocus(
excluding: false, excluding: false,
@ -2039,7 +2087,7 @@ void main() {
expect(parentFocusNode.enclosingScope!.hasPrimaryFocus, isTrue); expect(parentFocusNode.enclosingScope!.hasPrimaryFocus, isTrue);
}); });
testWidgets("ExcludeFocus doesn't introduce a Semantics node", (WidgetTester tester) async { testWidgetsWithLeakTracking("ExcludeFocus doesn't introduce a Semantics node", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(ExcludeFocus(child: Container())); await tester.pumpWidget(ExcludeFocus(child: Container()));
final TestSemantics expectedSemantics = TestSemantics.root(); final TestSemantics expectedSemantics = TestSemantics.root();
@ -2048,8 +2096,9 @@ void main() {
}); });
// Regression test for https://github.com/flutter/flutter/issues/92693 // Regression test for https://github.com/flutter/flutter/issues/92693
testWidgets('Setting parent FocusScope.canRequestFocus to false, does not set descendant Focus._internalNode._canRequestFocus to false', (WidgetTester tester) async { testWidgetsWithLeakTracking('Setting parent FocusScope.canRequestFocus to false, does not set descendant Focus._internalNode._canRequestFocus to false', (WidgetTester tester) async {
final FocusNode childFocusNode = FocusNode(debugLabel: 'node 1'); final FocusNode childFocusNode = FocusNode(debugLabel: 'node 1');
addTearDown(childFocusNode.dispose);
Widget buildFocusTree({required bool parentCanRequestFocus}) { Widget buildFocusTree({required bool parentCanRequestFocus}) {
return FocusScope( return FocusScope(

View File

@ -8,12 +8,13 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
void main() { void main() {
group(WidgetOrderTraversalPolicy, () { group(WidgetOrderTraversalPolicy, () {
testWidgets('Find the initial focus if there is none yet.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Find the initial focus if there is none yet.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key3 = GlobalKey(debugLabel: '3');
@ -52,7 +53,7 @@ void main() {
expect(scope.hasFocus, isTrue); expect(scope.hasFocus, isTrue);
}); });
testWidgets('Find the initial focus if there is none yet and traversing backwards.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Find the initial focus if there is none yet and traversing backwards.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key3 = GlobalKey(debugLabel: '3');
@ -95,7 +96,7 @@ void main() {
expect(scope.hasFocus, isTrue); expect(scope.hasFocus, isTrue);
}); });
testWidgets('Move focus to next node.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Move focus to next node.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key3 = GlobalKey(debugLabel: '3');
@ -212,7 +213,7 @@ void main() {
expect(scope.hasFocus, isTrue); expect(scope.hasFocus, isTrue);
}); });
testWidgets('Move focus to previous node.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Move focus to previous node.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key3 = GlobalKey(debugLabel: '3');
@ -286,9 +287,15 @@ void main() {
expect(scope.hasFocus, isTrue); expect(scope.hasFocus, isTrue);
}); });
testWidgets('Move focus to next/previous node while skipping nodes in policy', (WidgetTester tester) async { testWidgetsWithLeakTracking('Move focus to next/previous node while skipping nodes in policy', (WidgetTester tester) async {
final List<FocusNode> nodes = final List<FocusNode> nodes =
List<FocusNode>.generate(7, (int index) => FocusNode(debugLabel: 'Node $index')); List<FocusNode>.generate(7, (int index) => FocusNode(debugLabel: 'Node $index'));
addTearDown(() {
for (final FocusNode node in nodes) {
node.dispose();
}
});
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( FocusTraversalGroup(
policy: SkipAllButFirstAndLastPolicy(), policy: SkipAllButFirstAndLastPolicy(),
@ -320,11 +327,14 @@ void main() {
expect(nodes[0].hasPrimaryFocus, isTrue); expect(nodes[0].hasPrimaryFocus, isTrue);
}); });
testWidgets('Find the initial focus when a route is pushed or popped.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Find the initial focus when a route is pushed or popped.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final FocusNode testNode1 = FocusNode(debugLabel: 'First Focus Node'); final FocusNode testNode1 = FocusNode(debugLabel: 'First Focus Node');
addTearDown(testNode1.dispose);
final FocusNode testNode2 = FocusNode(debugLabel: 'Second Focus Node'); final FocusNode testNode2 = FocusNode(debugLabel: 'Second Focus Node');
addTearDown(testNode2.dispose);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: FocusTraversalGroup( home: FocusTraversalGroup(
@ -386,9 +396,10 @@ void main() {
expect(scope.hasFocus, isTrue); expect(scope.hasFocus, isTrue);
}); });
testWidgets('Custom requestFocusCallback gets called on the next/previous focus.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Custom requestFocusCallback gets called on the next/previous focus.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode testNode1 = FocusNode(debugLabel: 'Focus Node'); final FocusNode testNode1 = FocusNode(debugLabel: 'Focus Node');
addTearDown(testNode1.dispose);
bool calledCallback = false; bool calledCallback = false;
await tester.pumpWidget( await tester.pumpWidget(
@ -430,10 +441,14 @@ void main() {
}); });
testWidgets('Nested navigator does not trap focus', (WidgetTester tester) async { testWidgetsWithLeakTracking('Nested navigator does not trap focus', (WidgetTester tester) async {
final FocusNode node1 = FocusNode(); final FocusNode node1 = FocusNode();
addTearDown(node1.dispose);
final FocusNode node2 = FocusNode(); final FocusNode node2 = FocusNode();
addTearDown(node2.dispose);
final FocusNode node3 = FocusNode(); final FocusNode node3 = FocusNode();
addTearDown(node3.dispose);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
@ -517,7 +532,7 @@ void main() {
}); });
group(ReadingOrderTraversalPolicy, () { group(ReadingOrderTraversalPolicy, () {
testWidgets('Find the initial focus if there is none yet.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Find the initial focus if there is none yet.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key3 = GlobalKey(debugLabel: '3');
@ -556,7 +571,7 @@ void main() {
expect(scope.hasFocus, isTrue); expect(scope.hasFocus, isTrue);
}); });
testWidgets('Move reading focus to next node.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Move reading focus to next node.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key3 = GlobalKey(debugLabel: '3');
@ -671,7 +686,7 @@ void main() {
expect(scope.hasFocus, isTrue); expect(scope.hasFocus, isTrue);
}); });
testWidgets('Move reading focus to previous node.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Move reading focus to previous node.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key3 = GlobalKey(debugLabel: '3');
@ -745,10 +760,17 @@ void main() {
expect(scope.hasFocus, isTrue); expect(scope.hasFocus, isTrue);
}); });
testWidgets('Focus order is correct in the presence of different directionalities.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus order is correct in the presence of different directionalities.', (WidgetTester tester) async {
const int nodeCount = 10; const int nodeCount = 10;
final FocusScopeNode scopeNode = FocusScopeNode(); final FocusScopeNode scopeNode = FocusScopeNode();
addTearDown(scopeNode.dispose);
final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index')); final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
addTearDown(() {
for (final FocusNode node in nodes) {
node.dispose();
}
});
Widget buildTest(TextDirection topDirection) { Widget buildTest(TextDirection topDirection) {
return Directionality( return Directionality(
textDirection: topDirection, textDirection: topDirection,
@ -860,9 +882,15 @@ void main() {
expect(order, orderedEquals(<int>[0, 1, 2, 4, 3, 5, 6, 8, 7, 9])); expect(order, orderedEquals(<int>[0, 1, 2, 4, 3, 5, 6, 8, 7, 9]));
}); });
testWidgets('Focus order is reading order regardless of widget order, even when overlapping.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus order is reading order regardless of widget order, even when overlapping.', (WidgetTester tester) async {
const int nodeCount = 10; const int nodeCount = 10;
final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index')); final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
addTearDown(() {
for (final FocusNode node in nodes) {
node.dispose();
}
});
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
@ -954,9 +982,10 @@ void main() {
expect(order, orderedEquals(<int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])); expect(order, orderedEquals(<int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]));
}); });
testWidgets('Custom requestFocusCallback gets called on the next/previous focus.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Custom requestFocusCallback gets called on the next/previous focus.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode testNode1 = FocusNode(debugLabel: 'Focus Node'); final FocusNode testNode1 = FocusNode(debugLabel: 'Focus Node');
addTearDown(testNode1.dispose);
bool calledCallback = false; bool calledCallback = false;
await tester.pumpWidget( await tester.pumpWidget(
@ -1001,7 +1030,7 @@ void main() {
}); });
group(OrderedTraversalPolicy, () { group(OrderedTraversalPolicy, () {
testWidgets('Find the initial focus if there is none yet.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Find the initial focus if there is none yet.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
await tester.pumpWidget(FocusTraversalGroup( await tester.pumpWidget(FocusTraversalGroup(
@ -1040,9 +1069,15 @@ void main() {
expect(scope.hasFocus, isTrue); expect(scope.hasFocus, isTrue);
}); });
testWidgets('Fall back to the secondary sort if no FocusTraversalOrder exists.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Fall back to the secondary sort if no FocusTraversalOrder exists.', (WidgetTester tester) async {
const int nodeCount = 10; const int nodeCount = 10;
final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index')); final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
addTearDown(() {
for (final FocusNode node in nodes) {
node.dispose();
}
});
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
@ -1079,9 +1114,15 @@ void main() {
} }
}); });
testWidgets('Move focus to next/previous node using numerical order.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Move focus to next/previous node using numerical order.', (WidgetTester tester) async {
const int nodeCount = 10; const int nodeCount = 10;
final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index')); final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
addTearDown(() {
for (final FocusNode node in nodes) {
node.dispose();
}
});
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
@ -1120,12 +1161,18 @@ void main() {
} }
}); });
testWidgets('Move focus to next/previous node using lexical order.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Move focus to next/previous node using lexical order.', (WidgetTester tester) async {
const int nodeCount = 10; const int nodeCount = 10;
/// Generate ['J' ... 'A']; /// Generate ['J' ... 'A'];
final List<String> keys = List<String>.generate(nodeCount, (int index) => String.fromCharCode('A'.codeUnits[0] + nodeCount - index - 1)); final List<String> keys = List<String>.generate(nodeCount, (int index) => String.fromCharCode('A'.codeUnits[0] + nodeCount - index - 1));
final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node ${keys[index]}')); final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node ${keys[index]}'));
addTearDown(() {
for (final FocusNode node in nodes) {
node.dispose();
}
});
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
@ -1164,10 +1211,17 @@ void main() {
} }
}); });
testWidgets('Focus order is correct in the presence of FocusTraversalPolicyGroups.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus order is correct in the presence of FocusTraversalPolicyGroups.', (WidgetTester tester) async {
const int nodeCount = 10; const int nodeCount = 10;
final FocusScopeNode scopeNode = FocusScopeNode(); final FocusScopeNode scopeNode = FocusScopeNode();
addTearDown(scopeNode.dispose);
final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index')); final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
addTearDown(() {
for (final FocusNode node in nodes) {
node.dispose();
}
});
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
@ -1291,11 +1345,14 @@ void main() {
expect(order, orderedEquals(expectedOrder)); expect(order, orderedEquals(expectedOrder));
}); });
testWidgets('Find the initial focus when a route is pushed or popped.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Find the initial focus when a route is pushed or popped.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final FocusNode testNode1 = FocusNode(debugLabel: 'First Focus Node'); final FocusNode testNode1 = FocusNode(debugLabel: 'First Focus Node');
addTearDown(testNode1.dispose);
final FocusNode testNode2 = FocusNode(debugLabel: 'Second Focus Node'); final FocusNode testNode2 = FocusNode(debugLabel: 'Second Focus Node');
addTearDown(testNode2.dispose);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: FocusTraversalGroup( home: FocusTraversalGroup(
@ -1363,9 +1420,10 @@ void main() {
expect(scope.hasFocus, isTrue); expect(scope.hasFocus, isTrue);
}); });
testWidgets('Custom requestFocusCallback gets called on the next/previous focus.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Custom requestFocusCallback gets called on the next/previous focus.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode testNode1 = FocusNode(debugLabel: 'Focus Node'); final FocusNode testNode1 = FocusNode(debugLabel: 'Focus Node');
addTearDown(testNode1.dispose);
bool calledCallback = false; bool calledCallback = false;
await tester.pumpWidget( await tester.pumpWidget(
@ -1410,7 +1468,7 @@ void main() {
}); });
group(DirectionalFocusTraversalPolicyMixin, () { group(DirectionalFocusTraversalPolicyMixin, () {
testWidgets('Move focus in all directions.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Move focus in all directions.', (WidgetTester tester) async {
final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey');
final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey');
final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey');
@ -1550,9 +1608,15 @@ void main() {
expect(scope.hasFocus, isTrue); expect(scope.hasFocus, isTrue);
}); });
testWidgets('Directional focus avoids hysteresis.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Directional focus avoids hysteresis.', (WidgetTester tester) async {
List<bool?> focus = List<bool?>.generate(6, (int _) => null); List<bool?> focus = List<bool?>.generate(6, (int _) => null);
final List<FocusNode> nodes = List<FocusNode>.generate(6, (int index) => FocusNode(debugLabel: 'Node $index')); final List<FocusNode> nodes = List<FocusNode>.generate(6, (int index) => FocusNode(debugLabel: 'Node $index'));
addTearDown(() {
for (final FocusNode node in nodes) {
node.dispose();
}
});
Focus makeFocus(int index) { Focus makeFocus(int index) {
return Focus( return Focus(
debugLabel: '[$index]', debugLabel: '[$index]',
@ -1668,11 +1732,16 @@ void main() {
clear(); clear();
}); });
testWidgets('Directional prefers the closest node even on irregular grids', (WidgetTester tester) async { testWidgetsWithLeakTracking('Directional prefers the closest node even on irregular grids', (WidgetTester tester) async {
const int cols = 3; const int cols = 3;
const int rows = 3; const int rows = 3;
List<bool?> focus = List<bool?>.generate(rows * cols, (int _) => null); List<bool?> focus = List<bool?>.generate(rows * cols, (int _) => null);
final List<FocusNode> nodes = List<FocusNode>.generate(rows * cols, (int index) => FocusNode(debugLabel: 'Node $index')); final List<FocusNode> nodes = List<FocusNode>.generate(rows * cols, (int index) => FocusNode(debugLabel: 'Node $index'));
addTearDown(() {
for (final FocusNode node in nodes) {
node.dispose();
}
});
Widget makeFocus(int row, int col) { Widget makeFocus(int row, int col) {
final int index = row * rows + col; final int index = row * rows + col;
@ -1804,10 +1873,15 @@ void main() {
clear(); clear();
}); });
testWidgets('Closest vertical is picked when only out of band items are considered', (WidgetTester tester) async { testWidgetsWithLeakTracking('Closest vertical is picked when only out of band items are considered', (WidgetTester tester) async {
const int rows = 4; const int rows = 4;
List<bool?> focus = List<bool?>.generate(rows, (int _) => null); List<bool?> focus = List<bool?>.generate(rows, (int _) => null);
final List<FocusNode> nodes = List<FocusNode>.generate(rows, (int index) => FocusNode(debugLabel: 'Node $index')); final List<FocusNode> nodes = List<FocusNode>.generate(rows, (int index) => FocusNode(debugLabel: 'Node $index'));
addTearDown(() {
for (final FocusNode node in nodes) {
node.dispose();
}
});
Widget makeFocus(int row) { Widget makeFocus(int row) {
return Padding( return Padding(
@ -1890,10 +1964,15 @@ void main() {
clear(); clear();
}); });
testWidgets('Closest horizontal is picked when only out of band items are considered', (WidgetTester tester) async { testWidgetsWithLeakTracking('Closest horizontal is picked when only out of band items are considered', (WidgetTester tester) async {
const int cols = 4; const int cols = 4;
List<bool?> focus = List<bool?>.generate(cols, (int _) => null); List<bool?> focus = List<bool?>.generate(cols, (int _) => null);
final List<FocusNode> nodes = List<FocusNode>.generate(cols, (int index) => FocusNode(debugLabel: 'Node $index')); final List<FocusNode> nodes = List<FocusNode>.generate(cols, (int index) => FocusNode(debugLabel: 'Node $index'));
addTearDown(() {
for (final FocusNode node in nodes) {
node.dispose();
}
});
Widget makeFocus(int col) { Widget makeFocus(int col) {
return Padding( return Padding(
@ -1976,7 +2055,7 @@ void main() {
clear(); clear();
}); });
testWidgets('Can find first focus in all directions.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can find first focus in all directions.', (WidgetTester tester) async {
final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey');
final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey');
final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey');
@ -2036,10 +2115,13 @@ void main() {
expect(policy.findFirstFocusInDirection(scope, TraversalDirection.right), equals(upperLeftNode)); expect(policy.findFirstFocusInDirection(scope, TraversalDirection.right), equals(upperLeftNode));
}); });
testWidgets('Can find focus when policy data dirty', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can find focus when policy data dirty', (WidgetTester tester) async {
final FocusNode focusTop = FocusNode(debugLabel: 'top'); final FocusNode focusTop = FocusNode(debugLabel: 'top');
addTearDown(focusTop.dispose);
final FocusNode focusCenter = FocusNode(debugLabel: 'center'); final FocusNode focusCenter = FocusNode(debugLabel: 'center');
addTearDown(focusCenter.dispose);
final FocusNode focusBottom = FocusNode(debugLabel: 'bottom'); final FocusNode focusBottom = FocusNode(debugLabel: 'bottom');
addTearDown(focusBottom.dispose);
final FocusTraversalPolicy policy = ReadingOrderTraversalPolicy(); final FocusTraversalPolicy policy = ReadingOrderTraversalPolicy();
await tester.pumpWidget(FocusTraversalGroup( await tester.pumpWidget(FocusTraversalGroup(
@ -2087,7 +2169,7 @@ void main() {
expect(focusTop.hasFocus, isTrue); expect(focusTop.hasFocus, isTrue);
}); });
testWidgets('Focus traversal actions are invoked when shortcuts are used.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus traversal actions are invoked when shortcuts are used.', (WidgetTester tester) async {
final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey');
final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey');
final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey');
@ -2176,7 +2258,7 @@ void main() {
expect(Focus.of(upperLeftKey.currentContext!).hasPrimaryFocus, isTrue); expect(Focus.of(upperLeftKey.currentContext!).hasPrimaryFocus, isTrue);
}, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347 }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347
testWidgets('Focus traversal actions works when current focus skip traversal', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus traversal actions works when current focus skip traversal', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: 'key1'); final GlobalKey key1 = GlobalKey(debugLabel: 'key1');
final GlobalKey key2 = GlobalKey(debugLabel: 'key2'); final GlobalKey key2 = GlobalKey(debugLabel: 'key2');
final GlobalKey key3 = GlobalKey(debugLabel: 'key3'); final GlobalKey key3 = GlobalKey(debugLabel: 'key3');
@ -2231,12 +2313,21 @@ void main() {
expect(Focus.of(key3.currentContext!).hasPrimaryFocus, isTrue); expect(Focus.of(key3.currentContext!).hasPrimaryFocus, isTrue);
}, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347 }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347
testWidgets('Focus traversal inside a vertical scrollable scrolls to stay visible.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus traversal inside a vertical scrollable scrolls to stay visible.', (WidgetTester tester) async {
final List<int> items = List<int>.generate(11, (int index) => index).toList(); final List<int> items = List<int>.generate(11, (int index) => index).toList();
final List<FocusNode> nodes = List<FocusNode>.generate(11, (int index) => FocusNode(debugLabel: 'Item ${index + 1}')).toList(); final List<FocusNode> nodes = List<FocusNode>.generate(11, (int index) => FocusNode(debugLabel: 'Item ${index + 1}')).toList();
addTearDown(() {
for (final FocusNode node in nodes) {
node.dispose();
}
});
final FocusNode topNode = FocusNode(debugLabel: 'Header'); final FocusNode topNode = FocusNode(debugLabel: 'Header');
addTearDown(topNode.dispose);
final FocusNode bottomNode = FocusNode(debugLabel: 'Footer'); final FocusNode bottomNode = FocusNode(debugLabel: 'Footer');
addTearDown(bottomNode.dispose);
final ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
addTearDown(controller.dispose);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Column( home: Column(
@ -2328,12 +2419,21 @@ void main() {
expect(controller.offset, equals(0.0)); expect(controller.offset, equals(0.0));
}, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347 }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347
testWidgets('Focus traversal inside a horizontal scrollable scrolls to stay visible.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus traversal inside a horizontal scrollable scrolls to stay visible.', (WidgetTester tester) async {
final List<int> items = List<int>.generate(11, (int index) => index).toList(); final List<int> items = List<int>.generate(11, (int index) => index).toList();
final List<FocusNode> nodes = List<FocusNode>.generate(11, (int index) => FocusNode(debugLabel: 'Item ${index + 1}')).toList(); final List<FocusNode> nodes = List<FocusNode>.generate(11, (int index) => FocusNode(debugLabel: 'Item ${index + 1}')).toList();
addTearDown(() {
for (final FocusNode node in nodes) {
node.dispose();
}
});
final FocusNode leftNode = FocusNode(debugLabel: 'Left Side'); final FocusNode leftNode = FocusNode(debugLabel: 'Left Side');
addTearDown(leftNode.dispose);
final FocusNode rightNode = FocusNode(debugLabel: 'Right Side'); final FocusNode rightNode = FocusNode(debugLabel: 'Right Side');
addTearDown(rightNode.dispose);
final ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
addTearDown(controller.dispose);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Row( home: Row(
@ -2426,23 +2526,31 @@ void main() {
expect(controller.offset, equals(0.0)); expect(controller.offset, equals(0.0));
}, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347 }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347
testWidgets('Arrow focus traversal actions can be re-enabled for text fields.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Arrow focus traversal actions can be re-enabled for text fields.', (WidgetTester tester) async {
final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey');
final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey');
final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey');
final GlobalKey lowerRightKey = GlobalKey(debugLabel: 'lowerRightKey'); final GlobalKey lowerRightKey = GlobalKey(debugLabel: 'lowerRightKey');
final TextEditingController controller1 = TextEditingController(); final TextEditingController controller1 = TextEditingController();
addTearDown(controller1.dispose);
final TextEditingController controller2 = TextEditingController(); final TextEditingController controller2 = TextEditingController();
addTearDown(controller2.dispose);
final TextEditingController controller3 = TextEditingController(); final TextEditingController controller3 = TextEditingController();
addTearDown(controller3.dispose);
final TextEditingController controller4 = TextEditingController(); final TextEditingController controller4 = TextEditingController();
addTearDown(controller4.dispose);
final FocusNode focusNodeUpperLeft = FocusNode(debugLabel: 'upperLeft'); final FocusNode focusNodeUpperLeft = FocusNode(debugLabel: 'upperLeft');
addTearDown(focusNodeUpperLeft.dispose);
final FocusNode focusNodeUpperRight = FocusNode(debugLabel: 'upperRight'); final FocusNode focusNodeUpperRight = FocusNode(debugLabel: 'upperRight');
addTearDown(focusNodeUpperRight.dispose);
final FocusNode focusNodeLowerLeft = FocusNode(debugLabel: 'lowerLeft'); final FocusNode focusNodeLowerLeft = FocusNode(debugLabel: 'lowerLeft');
addTearDown(focusNodeLowerLeft.dispose);
final FocusNode focusNodeLowerRight = FocusNode(debugLabel: 'lowerRight'); final FocusNode focusNodeLowerRight = FocusNode(debugLabel: 'lowerRight');
addTearDown(focusNodeLowerRight.dispose);
Widget generateTestWidgets(bool ignoreTextFields) { Widget generatetestWidgetsWithLeakTracking(bool ignoreTextFields) {
final Map<ShortcutActivator, Intent> shortcuts = <ShortcutActivator, Intent>{ final Map<ShortcutActivator, Intent> shortcuts = <ShortcutActivator, Intent>{
const SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(TraversalDirection.left, ignoreTextFields: ignoreTextFields), const SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(TraversalDirection.left, ignoreTextFields: ignoreTextFields),
const SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent(TraversalDirection.right, ignoreTextFields: ignoreTextFields), const SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent(TraversalDirection.right, ignoreTextFields: ignoreTextFields),
@ -2521,7 +2629,7 @@ void main() {
); );
} }
await tester.pumpWidget(generateTestWidgets(false)); await tester.pumpWidget(generatetestWidgetsWithLeakTracking(false));
expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
@ -2533,7 +2641,7 @@ void main() {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue);
await tester.pumpWidget(generateTestWidgets(true)); await tester.pumpWidget(generatetestWidgetsWithLeakTracking(true));
expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
@ -2549,7 +2657,7 @@ void main() {
expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue);
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Focus traversal does not break when no focusable is available on a MaterialApp', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus traversal does not break when no focusable is available on a MaterialApp', (WidgetTester tester) async {
final List<Object> events = <Object>[]; final List<Object> events = <Object>[];
await tester.pumpWidget(MaterialApp(home: Container())); await tester.pumpWidget(MaterialApp(home: Container()));
@ -2565,7 +2673,7 @@ void main() {
expect(events.length, 2); expect(events.length, 2);
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Focus traversal does not throw when no focusable is available in a group', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus traversal does not throw when no focusable is available in a group', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: ListTile(title: Text('title'))))); await tester.pumpWidget(const MaterialApp(home: Scaffold(body: ListTile(title: Text('title')))));
final FocusNode? initialFocus = primaryFocus; final FocusNode? initialFocus = primaryFocus;
await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.sendKeyEvent(LogicalKeyboardKey.tab);
@ -2573,7 +2681,7 @@ void main() {
expect(primaryFocus, equals(initialFocus)); expect(primaryFocus, equals(initialFocus));
}); });
testWidgets('Focus traversal does not break when no focusable is available on a WidgetsApp', (WidgetTester tester) async { testWidgetsWithLeakTracking('Focus traversal does not break when no focusable is available on a WidgetsApp', (WidgetTester tester) async {
final List<RawKeyEvent> events = <RawKeyEvent>[]; final List<RawKeyEvent> events = <RawKeyEvent>[];
await tester.pumpWidget( await tester.pumpWidget(
@ -2599,9 +2707,10 @@ void main() {
expect(events.length, 2); expect(events.length, 2);
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Custom requestFocusCallback gets called on focusInDirection up/down/left/right.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Custom requestFocusCallback gets called on focusInDirection up/down/left/right.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode testNode1 = FocusNode(debugLabel: 'Focus Node'); final FocusNode testNode1 = FocusNode(debugLabel: 'Focus Node');
addTearDown(testNode1.dispose);
bool calledCallback = false; bool calledCallback = false;
await tester.pumpWidget( await tester.pumpWidget(
@ -2655,7 +2764,7 @@ void main() {
}); });
group(FocusTraversalGroup, () { group(FocusTraversalGroup, () {
testWidgets("Focus traversal group doesn't introduce a Semantics node", (WidgetTester tester) async { testWidgetsWithLeakTracking("Focus traversal group doesn't introduce a Semantics node", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(FocusTraversalGroup(child: Container())); await tester.pumpWidget(FocusTraversalGroup(child: Container()));
final TestSemantics expectedSemantics = TestSemantics.root(); final TestSemantics expectedSemantics = TestSemantics.root();
@ -2663,11 +2772,13 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets("Descendants of FocusTraversalGroup aren't focusable if descendantsAreFocusable is false.", (WidgetTester tester) async { testWidgetsWithLeakTracking("Descendants of FocusTraversalGroup aren't focusable if descendantsAreFocusable is false.", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
bool? gotFocus; bool? gotFocus;
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( FocusTraversalGroup(
descendantsAreFocusable: false, descendantsAreFocusable: false,
@ -2702,13 +2813,16 @@ void main() {
expect(unfocusableNode.hasFocus, isFalse); expect(unfocusableNode.hasFocus, isFalse);
}); });
testWidgets('Group applies correct policy if focus tree is different from widget tree.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Group applies correct policy if focus tree is different from widget tree.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key3 = GlobalKey(debugLabel: '3');
final GlobalKey key4 = GlobalKey(debugLabel: '4'); final GlobalKey key4 = GlobalKey(debugLabel: '4');
final FocusNode focusNode = FocusNode(debugLabel: 'child'); final FocusNode focusNode = FocusNode(debugLabel: 'child');
addTearDown(focusNode.dispose);
final FocusNode parentFocusNode = FocusNode(debugLabel: 'parent'); final FocusNode parentFocusNode = FocusNode(debugLabel: 'parent');
addTearDown(parentFocusNode.dispose);
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
children: <Widget>[ children: <Widget>[
@ -2744,9 +2858,11 @@ void main() {
expect(FocusTraversalGroup.of(key2.currentContext!), const TypeMatcher<SkipAllButFirstAndLastPolicy>()); expect(FocusTraversalGroup.of(key2.currentContext!), const TypeMatcher<SkipAllButFirstAndLastPolicy>());
}); });
testWidgets("Descendants of FocusTraversalGroup aren't traversable if descendantsAreTraversable is false.", (WidgetTester tester) async { testWidgetsWithLeakTracking("Descendants of FocusTraversalGroup aren't traversable if descendantsAreTraversable is false.", (WidgetTester tester) async {
final FocusNode node1 = FocusNode(); final FocusNode node1 = FocusNode();
addTearDown(node1.dispose);
final FocusNode node2 = FocusNode(); final FocusNode node2 = FocusNode();
addTearDown(node2.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( FocusTraversalGroup(
@ -2779,9 +2895,11 @@ void main() {
expect(node2.hasPrimaryFocus, isFalse); expect(node2.hasPrimaryFocus, isFalse);
}); });
testWidgets("FocusTraversalGroup with skipTraversal for all descendants set to true doesn't cause an exception.", (WidgetTester tester) async { testWidgetsWithLeakTracking("FocusTraversalGroup with skipTraversal for all descendants set to true doesn't cause an exception.", (WidgetTester tester) async {
final FocusNode node1 = FocusNode(); final FocusNode node1 = FocusNode();
addTearDown(node1.dispose);
final FocusNode node2 = FocusNode(); final FocusNode node2 = FocusNode();
addTearDown(node2.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( FocusTraversalGroup(
@ -2815,11 +2933,13 @@ void main() {
expect(node2.hasPrimaryFocus, isFalse); expect(node2.hasPrimaryFocus, isFalse);
}); });
testWidgets("Nested FocusTraversalGroup with unfocusable children doesn't assert.", (WidgetTester tester) async { testWidgetsWithLeakTracking("Nested FocusTraversalGroup with unfocusable children doesn't assert.", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
bool? gotFocus; bool? gotFocus;
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( FocusTraversalGroup(
child: Column( child: Column(
@ -2864,9 +2984,11 @@ void main() {
expect(unfocusableNode.hasFocus, isFalse); expect(unfocusableNode.hasFocus, isFalse);
}); });
testWidgets("Empty FocusTraversalGroup doesn't cause an exception.", (WidgetTester tester) async { testWidgetsWithLeakTracking("Empty FocusTraversalGroup doesn't cause an exception.", (WidgetTester tester) async {
final GlobalKey key = GlobalKey(debugLabel: 'Test Key'); final GlobalKey key = GlobalKey(debugLabel: 'Test Key');
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
addTearDown(focusNode.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( FocusTraversalGroup(
child: Directionality( child: Directionality(
@ -2895,9 +3017,11 @@ void main() {
}); });
group(RawKeyboardListener, () { group(RawKeyboardListener, () {
testWidgets('Raw keyboard listener introduces a Semantics node by default', (WidgetTester tester) async { testWidgetsWithLeakTracking('Raw keyboard listener introduces a Semantics node by default', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget( await tester.pumpWidget(
RawKeyboardListener( RawKeyboardListener(
focusNode: focusNode, focusNode: focusNode,
@ -2922,9 +3046,11 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets("Raw keyboard listener doesn't introduce a Semantics node when specified", (WidgetTester tester) async { testWidgetsWithLeakTracking("Raw keyboard listener doesn't introduce a Semantics node when specified", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget( await tester.pumpWidget(
RawKeyboardListener( RawKeyboardListener(
focusNode: focusNode, focusNode: focusNode,
@ -2939,11 +3065,15 @@ void main() {
}); });
group(ExcludeFocusTraversal, () { group(ExcludeFocusTraversal, () {
testWidgets("Descendants aren't traversable", (WidgetTester tester) async { testWidgetsWithLeakTracking("Descendants aren't traversable", (WidgetTester tester) async {
final FocusNode node1 = FocusNode(debugLabel: 'node 1'); final FocusNode node1 = FocusNode(debugLabel: 'node 1');
addTearDown(node1.dispose);
final FocusNode node2 = FocusNode(debugLabel: 'node 2'); final FocusNode node2 = FocusNode(debugLabel: 'node 2');
addTearDown(node2.dispose);
final FocusNode node3 = FocusNode(debugLabel: 'node 3'); final FocusNode node3 = FocusNode(debugLabel: 'node 3');
addTearDown(node3.dispose);
final FocusNode node4 = FocusNode(debugLabel: 'node 4'); final FocusNode node4 = FocusNode(debugLabel: 'node 4');
addTearDown(node4.dispose);
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( FocusTraversalGroup(
@ -2987,7 +3117,7 @@ void main() {
expect(node4.hasPrimaryFocus, isTrue); expect(node4.hasPrimaryFocus, isTrue);
}); });
testWidgets("Doesn't introduce a Semantics node", (WidgetTester tester) async { testWidgetsWithLeakTracking("Doesn't introduce a Semantics node", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(ExcludeFocusTraversal(child: Container())); await tester.pumpWidget(ExcludeFocusTraversal(child: Container()));
final TestSemantics expectedSemantics = TestSemantics.root(); final TestSemantics expectedSemantics = TestSemantics.root();
@ -3003,9 +3133,11 @@ void main() {
// other focusable HTML elements surrounding Flutter. // other focusable HTML elements surrounding Flutter.
// //
// See also: https://github.com/flutter/flutter/issues/114463 // See also: https://github.com/flutter/flutter/issues/114463
testWidgets('Default route edge traversal behavior', (WidgetTester tester) async { testWidgetsWithLeakTracking('Default route edge traversal behavior', (WidgetTester tester) async {
final FocusNode nodeA = FocusNode(); final FocusNode nodeA = FocusNode();
addTearDown(nodeA.dispose);
final FocusNode nodeB = FocusNode(); final FocusNode nodeB = FocusNode();
addTearDown(nodeB.dispose);
Future<bool> nextFocus() async { Future<bool> nextFocus() async {
final bool result = Actions.invoke( final bool result = Actions.invoke(
@ -3083,12 +3215,15 @@ void main() {
// This test creates a FocusScopeNode configured to traverse focus in a closed // This test creates a FocusScopeNode configured to traverse focus in a closed
// loop. After traversing one loop, it changes the behavior to leave the // loop. After traversing one loop, it changes the behavior to leave the
// FlutterView, then verifies that the new behavior did indeed take effect. // FlutterView, then verifies that the new behavior did indeed take effect.
testWidgets('FocusScopeNode.traversalEdgeBehavior takes effect after update', (WidgetTester tester) async { testWidgetsWithLeakTracking('FocusScopeNode.traversalEdgeBehavior takes effect after update', (WidgetTester tester) async {
final FocusScopeNode scope = FocusScopeNode(); final FocusScopeNode scope = FocusScopeNode();
addTearDown(scope.dispose);
expect(scope.traversalEdgeBehavior, TraversalEdgeBehavior.closedLoop); expect(scope.traversalEdgeBehavior, TraversalEdgeBehavior.closedLoop);
final FocusNode nodeA = FocusNode(); final FocusNode nodeA = FocusNode();
addTearDown(nodeA.dispose);
final FocusNode nodeB = FocusNode(); final FocusNode nodeB = FocusNode();
addTearDown(nodeB.dispose);
Future<bool> nextFocus() async { Future<bool> nextFocus() async {
final bool result = Actions.invoke( final bool result = Actions.invoke(
@ -3172,7 +3307,7 @@ void main() {
expect(nodeB.hasFocus, true); expect(nodeB.hasFocus, true);
}); });
testWidgets('NextFocusAction converts invoke result to KeyEventResult', (WidgetTester tester) async { testWidgetsWithLeakTracking('NextFocusAction converts invoke result to KeyEventResult', (WidgetTester tester) async {
expect( expect(
NextFocusAction().toKeyEventResult(const NextFocusIntent(), true), NextFocusAction().toKeyEventResult(const NextFocusIntent(), true),
KeyEventResult.handled, KeyEventResult.handled,
@ -3183,7 +3318,7 @@ void main() {
); );
}); });
testWidgets('PreviousFocusAction converts invoke result to KeyEventResult', (WidgetTester tester) async { testWidgetsWithLeakTracking('PreviousFocusAction converts invoke result to KeyEventResult', (WidgetTester tester) async {
expect( expect(
PreviousFocusAction().toKeyEventResult(const PreviousFocusIntent(), true), PreviousFocusAction().toKeyEventResult(const PreviousFocusIntent(), true),
KeyEventResult.handled, KeyEventResult.handled,
@ -3194,9 +3329,10 @@ void main() {
); );
}); });
testWidgets('RequestFocusAction calls the RequestFocusIntent.requestFocusCallback', (WidgetTester tester) async { testWidgetsWithLeakTracking('RequestFocusAction calls the RequestFocusIntent.requestFocusCallback', (WidgetTester tester) async {
bool calledCallback = false; bool calledCallback = false;
final FocusNode nodeA = FocusNode(); final FocusNode nodeA = FocusNode();
addTearDown(nodeA.dispose);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(