// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/semantics.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { group('FocusScope', () { testWidgetsWithLeakTracking('Can focus', (WidgetTester tester) async { final GlobalKey<TestFocusState> key = GlobalKey(); await tester.pumpWidget( TestFocus(key: key), ); expect(key.currentState!.focusNode.hasFocus, isFalse); FocusScope.of(key.currentContext!).requestFocus(key.currentState!.focusNode); await tester.pumpAndSettle(); expect(key.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); }); testWidgetsWithLeakTracking('Can unfocus', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); await tester.pumpWidget( Column( children: <Widget>[ TestFocus(key: keyA), TestFocus(key: keyB, name: 'b'), ], ), ); expect(keyA.currentState!.focusNode.hasFocus, isFalse); expect(find.text('a'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode); await tester.pumpAndSettle(); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); // Set focus to the "B" node to unfocus the "A" node. FocusScope.of(keyB.currentContext!).requestFocus(keyB.currentState!.focusNode); await tester.pumpAndSettle(); expect(keyA.currentState!.focusNode.hasFocus, isFalse); expect(find.text('a'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isTrue); expect(find.text('B FOCUSED'), findsOneWidget); }); testWidgetsWithLeakTracking('Autofocus works', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); await tester.pumpWidget( Column( children: <Widget>[ TestFocus(key: keyA), TestFocus(key: keyB, name: 'b', autofocus: true), ], ), ); await tester.pump(); expect(keyA.currentState!.focusNode.hasFocus, isFalse); expect(find.text('a'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isTrue); expect(find.text('B FOCUSED'), findsOneWidget); }); testWidgetsWithLeakTracking('Can have multiple focused children and they update accordingly', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); await tester.pumpWidget( Column( children: <Widget>[ TestFocus( key: keyA, autofocus: true, ), TestFocus( key: keyB, name: 'b', ), ], ), ); // Autofocus is delayed one frame. await tester.pump(); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); await tester.tap(find.text('A FOCUSED')); await tester.pump(); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); await tester.tap(find.text('b')); await tester.pump(); expect(keyA.currentState!.focusNode.hasFocus, isFalse); expect(find.text('a'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isTrue); expect(find.text('B FOCUSED'), findsOneWidget); await tester.tap(find.text('a')); await tester.pump(); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); }); // This moves a focus node first into a focus scope that is added to its // parent, and then out of that focus scope again. testWidgetsWithLeakTracking('Can move focus in and out of FocusScope', (WidgetTester tester) async { final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node'); addTearDown(parentFocusScope.dispose); final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node'); addTearDown(childFocusScope.dispose); final GlobalKey<TestFocusState> key = GlobalKey(); // Initially create the focus inside of the parent FocusScope. await tester.pumpWidget( FocusScope( debugLabel: 'Parent Scope', node: parentFocusScope, autofocus: true, child: Column( children: <Widget>[ TestFocus( key: key, debugLabel: 'Child', ), ], ), ), ); expect(key.currentState!.focusNode.hasFocus, isFalse); expect(find.text('a'), findsOneWidget); FocusScope.of(key.currentContext!).requestFocus(key.currentState!.focusNode); await tester.pumpAndSettle(); expect(key.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(parentFocusScope, hasAGoodToStringDeep); expect( parentFocusScope.toStringDeep(), equalsIgnoringHashCodes( 'FocusScopeNode#00000(Parent Scope Node [IN FOCUS PATH])\n' ' │ context: FocusScope\n' ' │ IN FOCUS PATH\n' ' │ focusedChildren: FocusNode#00000(Child [PRIMARY FOCUS])\n' ' │\n' ' └─Child 1: FocusNode#00000(Child [PRIMARY FOCUS])\n' ' context: Focus\n' ' PRIMARY FOCUS\n', ), ); expect(FocusManager.instance.rootScope, hasAGoodToStringDeep); expect( FocusManager.instance.rootScope.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( 'FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n' ' │ IN FOCUS PATH\n' ' │ focusedChildren: FocusScopeNode#00000(Parent Scope Node [IN FOCUS\n' ' │ PATH])\n' ' │\n' ' └─Child 1: FocusScopeNode#00000(Parent Scope Node [IN FOCUS PATH])\n' ' │ context: FocusScope\n' ' │ IN FOCUS PATH\n' ' │ focusedChildren: FocusNode#00000(Child [PRIMARY FOCUS])\n' ' │\n' ' └─Child 1: FocusNode#00000(Child [PRIMARY FOCUS])\n' ' context: Focus\n' ' PRIMARY FOCUS\n', ), ); // Add the child focus scope to the focus tree. final FocusAttachment childAttachment = childFocusScope.attach(key.currentContext); parentFocusScope.setFirstFocus(childFocusScope); await tester.pumpAndSettle(); expect(childFocusScope.isFirstFocus, isTrue); // Now add the child focus scope with no child focusable in it to the tree. await tester.pumpWidget( FocusScope( debugLabel: 'Parent Scope', node: parentFocusScope, child: Column( children: <Widget>[ TestFocus( key: key, debugLabel: 'Child', ), FocusScope( debugLabel: 'Child Scope', node: childFocusScope, child: Container(), ), ], ), ), ); expect(key.currentState!.focusNode.hasFocus, isFalse); expect(find.text('a'), findsOneWidget); // Now move the existing focus node into the child focus scope. await tester.pumpWidget( FocusScope( debugLabel: 'Parent Scope', node: parentFocusScope, child: Column( children: <Widget>[ FocusScope( debugLabel: 'Child Scope', node: childFocusScope, child: TestFocus( key: key, debugLabel: 'Child', ), ), ], ), ), ); await tester.pumpAndSettle(); expect(key.currentState!.focusNode.hasFocus, isFalse); expect(find.text('a'), findsOneWidget); // Now remove the child focus scope. await tester.pumpWidget( FocusScope( debugLabel: 'Parent Scope', node: parentFocusScope, child: Column( children: <Widget>[ TestFocus( key: key, debugLabel: 'Child', ), ], ), ), ); await tester.pumpAndSettle(); expect(key.currentState!.focusNode.hasFocus, isFalse); expect(find.text('a'), findsOneWidget); // Must detach the child because we had to attach it in order to call // setFirstFocus before adding to the widget. childAttachment.detach(); }); testWidgetsWithLeakTracking('Setting first focus requests focus for the scope properly.', (WidgetTester tester) async { final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node'); addTearDown(parentFocusScope.dispose); final FocusScopeNode childFocusScope1 = FocusScopeNode(debugLabel: 'Child Scope Node 1'); addTearDown(childFocusScope1.dispose); final FocusScopeNode childFocusScope2 = FocusScopeNode(debugLabel: 'Child Scope Node 2'); addTearDown(childFocusScope2.dispose); final GlobalKey<TestFocusState> keyA = GlobalKey(debugLabel: 'Key A'); final GlobalKey<TestFocusState> keyB = GlobalKey(debugLabel: 'Key B'); final GlobalKey<TestFocusState> keyC = GlobalKey(debugLabel: 'Key C'); await tester.pumpWidget( FocusScope( debugLabel: 'Parent Scope', node: parentFocusScope, child: Column( children: <Widget>[ FocusScope( debugLabel: 'Child Scope 1', node: childFocusScope1, child: Column( children: <Widget>[ TestFocus( key: keyA, autofocus: true, debugLabel: 'Child A', ), TestFocus( key: keyB, name: 'b', debugLabel: 'Child B', ), ], ), ), FocusScope( debugLabel: 'Child Scope 2', node: childFocusScope2, child: TestFocus( key: keyC, name: 'c', debugLabel: 'Child C', ), ), ], ), ), ); await tester.pumpAndSettle(); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); parentFocusScope.setFirstFocus(childFocusScope2); await tester.pumpAndSettle(); expect(keyA.currentState!.focusNode.hasFocus, isFalse); expect(find.text('a'), findsOneWidget); parentFocusScope.setFirstFocus(childFocusScope1); await tester.pumpAndSettle(); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); keyB.currentState!.focusNode.requestFocus(); await tester.pumpAndSettle(); expect(keyB.currentState!.focusNode.hasFocus, isTrue); expect(find.text('B FOCUSED'), findsOneWidget); expect(parentFocusScope.isFirstFocus, isTrue); expect(childFocusScope1.isFirstFocus, isTrue); parentFocusScope.setFirstFocus(childFocusScope2); await tester.pumpAndSettle(); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); expect(parentFocusScope.isFirstFocus, isTrue); expect(childFocusScope1.isFirstFocus, isFalse); expect(childFocusScope2.isFirstFocus, isTrue); keyC.currentState!.focusNode.requestFocus(); await tester.pumpAndSettle(); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); expect(keyC.currentState!.focusNode.hasFocus, isTrue); expect(find.text('C FOCUSED'), findsOneWidget); expect(parentFocusScope.isFirstFocus, isTrue); expect(childFocusScope1.isFirstFocus, isFalse); expect(childFocusScope2.isFirstFocus, isTrue); childFocusScope1.requestFocus(); await tester.pumpAndSettle(); expect(keyB.currentState!.focusNode.hasFocus, isTrue); expect(find.text('B FOCUSED'), findsOneWidget); expect(keyC.currentState!.focusNode.hasFocus, isFalse); expect(find.text('c'), findsOneWidget); expect(parentFocusScope.isFirstFocus, isTrue); expect(childFocusScope1.isFirstFocus, isTrue); expect(childFocusScope2.isFirstFocus, isFalse); }); testWidgetsWithLeakTracking('Removing focused widget moves focus to next widget', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); await tester.pumpWidget( Column( children: <Widget>[ TestFocus( key: keyA, ), TestFocus( key: keyB, name: 'b', ), ], ), ); FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode); await tester.pumpAndSettle(); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); await tester.pumpWidget( Column( children: <Widget>[ TestFocus( key: keyB, name: 'b', ), ], ), ); await tester.pump(); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); }); testWidgetsWithLeakTracking('Adding a new FocusScope attaches the child to its parent.', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node'); addTearDown(parentFocusScope.dispose); final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node'); addTearDown(childFocusScope.dispose); await tester.pumpWidget( FocusScope( node: childFocusScope, child: TestFocus( debugLabel: 'Child', key: keyA, ), ), ); FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode); expect(FocusScope.of(keyA.currentContext!), equals(childFocusScope)); expect(Focus.of(keyA.currentContext!, scopeOk: true), equals(childFocusScope)); FocusManager.instance.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext!)); await tester.pumpAndSettle(); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(childFocusScope.isFirstFocus, isTrue); await tester.pumpWidget( FocusScope( node: parentFocusScope, child: FocusScope( node: childFocusScope, child: TestFocus( debugLabel: 'Child', key: keyA, ), ), ), ); await tester.pump(); expect(childFocusScope.isFirstFocus, isTrue); // Node keeps it's focus when moved to the new scope. expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); }); testWidgetsWithLeakTracking('Setting parentNode determines focus tree hierarchy.', (WidgetTester tester) async { final FocusNode topNode = FocusNode(debugLabel: 'Top'); addTearDown(topNode.dispose); final FocusNode parentNode = FocusNode(debugLabel: 'Parent'); addTearDown(parentNode.dispose); final FocusNode childNode = FocusNode(debugLabel: 'Child'); addTearDown(childNode.dispose); final FocusNode insertedNode = FocusNode(debugLabel: 'Inserted'); addTearDown(insertedNode.dispose); await tester.pumpWidget( FocusScope( child: Focus.withExternalFocusNode( focusNode: topNode, child: Column( children: <Widget>[ Focus.withExternalFocusNode( focusNode: parentNode, child: const SizedBox(), ), Focus.withExternalFocusNode( focusNode: childNode, parentNode: parentNode, autofocus: true, child: const SizedBox(), ) ], ), ), ), ); await tester.pump(); expect(childNode.hasPrimaryFocus, isTrue); expect(parentNode.hasFocus, isTrue); expect(topNode.hasFocus, isTrue); // Check that inserting a Focus in between doesn't reparent the child. await tester.pumpWidget( FocusScope( child: Focus.withExternalFocusNode( focusNode: topNode, child: Column( children: <Widget>[ Focus.withExternalFocusNode( focusNode: parentNode, child: const SizedBox(), ), Focus.withExternalFocusNode( focusNode: insertedNode, child: Focus.withExternalFocusNode( focusNode: childNode, parentNode: parentNode, autofocus: true, child: const SizedBox(), ), ) ], ), ), ), ); await tester.pump(); expect(childNode.hasPrimaryFocus, isTrue); expect(parentNode.hasFocus, isTrue); expect(topNode.hasFocus, isTrue); expect(insertedNode.hasFocus, isFalse); }); testWidgetsWithLeakTracking('Setting parentNode determines focus scope tree hierarchy.', (WidgetTester tester) async { final FocusScopeNode topNode = FocusScopeNode(debugLabel: 'Top'); addTearDown(topNode.dispose); final FocusScopeNode parentNode = FocusScopeNode(debugLabel: 'Parent'); addTearDown(parentNode.dispose); final FocusScopeNode childNode = FocusScopeNode(debugLabel: 'Child'); addTearDown(childNode.dispose); final FocusScopeNode insertedNode = FocusScopeNode(debugLabel: 'Inserted'); addTearDown(insertedNode.dispose); await tester.pumpWidget( FocusScope.withExternalFocusNode( focusScopeNode: topNode, child: Column( children: <Widget>[ FocusScope.withExternalFocusNode( focusScopeNode: parentNode, child: const SizedBox(), ), FocusScope.withExternalFocusNode( focusScopeNode: childNode, parentNode: parentNode, child: const Focus( autofocus: true, child: SizedBox(), ), ) ], ), ), ); await tester.pump(); expect(childNode.hasFocus, isTrue); expect(parentNode.hasFocus, isTrue); expect(topNode.hasFocus, isTrue); // Check that inserting a Focus in between doesn't reparent the child. await tester.pumpWidget( FocusScope.withExternalFocusNode( focusScopeNode: topNode, child: Column( children: <Widget>[ FocusScope.withExternalFocusNode( focusScopeNode: parentNode, child: const SizedBox(), ), FocusScope.withExternalFocusNode( focusScopeNode: insertedNode, child: FocusScope.withExternalFocusNode( focusScopeNode: childNode, parentNode: parentNode, child: const Focus( autofocus: true, child: SizedBox(), ), ), ) ], ), ), ); await tester.pump(); expect(childNode.hasFocus, isTrue); expect(parentNode.hasFocus, isTrue); expect(topNode.hasFocus, isTrue); expect(insertedNode.hasFocus, isFalse); }); // Arguably, this isn't correct behavior, but it is what happens now. testWidgetsWithLeakTracking("Removing focused widget doesn't move focus to next widget within FocusScope", (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope'); addTearDown(parentFocusScope.dispose); await tester.pumpWidget( FocusScope( debugLabel: 'Parent Scope', node: parentFocusScope, autofocus: true, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Widget A', key: keyA, ), TestFocus( debugLabel: 'Widget B', key: keyB, name: 'b', ), ], ), ), ); FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode); final FocusScopeNode scope = FocusScope.of(keyA.currentContext!); FocusManager.instance.rootScope.setFirstFocus(scope); await tester.pumpAndSettle(); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); await tester.pumpWidget( FocusScope( node: parentFocusScope, child: Column( children: <Widget>[ TestFocus( key: keyB, name: 'b', ), ], ), ), ); await tester.pump(); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); }); testWidgetsWithLeakTracking('Removing a FocusScope removes its node from the tree', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyA = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyB = GlobalKey(); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope'); addTearDown(parentFocusScope.dispose); // This checks both FocusScopes that have their own nodes, as well as those // that use external nodes. await tester.pumpWidget( FocusTraversalGroup( child: Column( children: <Widget>[ FocusScope( key: scopeKeyA, node: parentFocusScope, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child A', key: keyA, ), ], ), ), FocusScope( key: scopeKeyB, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child B', key: keyB, name: 'b', ), ], ), ), ], ), ), ); FocusScope.of(keyB.currentContext!).requestFocus(keyB.currentState!.focusNode); FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode); final FocusScopeNode aScope = FocusScope.of(keyA.currentContext!); final FocusScopeNode bScope = FocusScope.of(keyB.currentContext!); FocusManager.instance.rootScope.setFirstFocus(bScope); FocusManager.instance.rootScope.setFirstFocus(aScope); await tester.pumpAndSettle(); expect(FocusScope.of(keyA.currentContext!).isFirstFocus, isTrue); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); await tester.pumpWidget(Container()); expect(FocusManager.instance.rootScope.children, isEmpty); }); // By "pinned", it means kept in the tree by a GlobalKey. 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> keyB = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyA = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyB = GlobalKey(); final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1'); addTearDown(parentFocusScope1.dispose); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2'); addTearDown(parentFocusScope2.dispose); await tester.pumpWidget( FocusTraversalGroup( child: Column( children: <Widget>[ FocusScope( key: scopeKeyA, node: parentFocusScope1, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child A', key: keyA, ), ], ), ), FocusScope( key: scopeKeyB, node: parentFocusScope2, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child B', key: keyB, name: 'b', ), ], ), ), ], ), ), ); FocusScope.of(keyB.currentContext!).requestFocus(keyB.currentState!.focusNode); FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode); final FocusScopeNode bScope = FocusScope.of(keyB.currentContext!); final FocusScopeNode aScope = FocusScope.of(keyA.currentContext!); FocusManager.instance.rootScope.setFirstFocus(bScope); FocusManager.instance.rootScope.setFirstFocus(aScope); await tester.pumpAndSettle(); expect(FocusScope.of(keyA.currentContext!).isFirstFocus, isTrue); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); await tester.pumpWidget( FocusTraversalGroup( child: Column( children: <Widget>[ FocusScope( key: scopeKeyB, node: parentFocusScope2, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child B', key: keyB, name: 'b', autofocus: true, ), ], ), ), ], ), ), ); await tester.pump(); expect(keyB.currentState!.focusNode.hasFocus, isTrue); expect(find.text('B FOCUSED'), findsOneWidget); }); 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> keyB = GlobalKey(); final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1'); addTearDown(parentFocusScope1.dispose); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2'); addTearDown(parentFocusScope2.dispose); await tester.pumpWidget( FocusTraversalGroup( child: Column( children: <Widget>[ FocusScope( node: parentFocusScope1, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child A', key: keyA, ), ], ), ), FocusScope( node: parentFocusScope2, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child B', key: keyB, name: 'b', ), ], ), ), ], ), ), ); FocusScope.of(keyB.currentContext!).requestFocus(keyB.currentState!.focusNode); FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode); final FocusScopeNode bScope = FocusScope.of(keyB.currentContext!); final FocusScopeNode aScope = FocusScope.of(keyA.currentContext!); FocusManager.instance.rootScope.setFirstFocus(bScope); FocusManager.instance.rootScope.setFirstFocus(aScope); await tester.pumpAndSettle(); expect(FocusScope.of(keyA.currentContext!).isFirstFocus, isTrue); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); await tester.pumpWidget( FocusTraversalGroup( child: Column( children: <Widget>[ FocusScope( node: parentFocusScope2, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child B', key: keyB, name: 'b', autofocus: true, ), ], ), ), ], ), ), ); await tester.pump(); expect(keyB.currentState!.focusNode.hasFocus, isTrue); expect(find.text('B FOCUSED'), findsOneWidget); }); testWidgetsWithLeakTracking('Moving widget from one scope to another retains focus', (WidgetTester tester) async { final FocusScopeNode parentFocusScope1 = FocusScopeNode(); addTearDown(parentFocusScope1.dispose); final FocusScopeNode parentFocusScope2 = FocusScopeNode(); addTearDown(parentFocusScope2.dispose); final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); await tester.pumpWidget( Column( children: <Widget>[ FocusScope( node: parentFocusScope1, child: Column( children: <Widget>[ TestFocus( key: keyA, ), ], ), ), FocusScope( node: parentFocusScope2, child: Column( children: <Widget>[ TestFocus( key: keyB, name: 'b', ), ], ), ), ], ), ); FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode); final FocusScopeNode aScope = FocusScope.of(keyA.currentContext!); FocusManager.instance.rootScope.setFirstFocus(aScope); await tester.pumpAndSettle(); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); await tester.pumpWidget( Column( children: <Widget>[ FocusScope( node: parentFocusScope1, child: Column( children: <Widget>[ TestFocus( key: keyB, name: 'b', ), ], ), ), FocusScope( node: parentFocusScope2, child: Column( children: <Widget>[ TestFocus( key: keyA, ), ], ), ), ], ), ); await tester.pump(); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); }); testWidgetsWithLeakTracking('Moving FocusScopeNodes retains focus', (WidgetTester tester) async { final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Scope 1'); addTearDown(parentFocusScope1.dispose); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Scope 2'); addTearDown(parentFocusScope2.dispose); final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); await tester.pumpWidget( Column( children: <Widget>[ FocusScope( node: parentFocusScope1, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child A', key: keyA, ), ], ), ), FocusScope( node: parentFocusScope2, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child B', key: keyB, name: 'b', ), ], ), ), ], ), ); FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode); final FocusScopeNode aScope = FocusScope.of(keyA.currentContext!); FocusManager.instance.rootScope.setFirstFocus(aScope); await tester.pumpAndSettle(); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); // This just swaps the FocusScopeNodes that the FocusScopes have in them. await tester.pumpWidget( Column( children: <Widget>[ FocusScope( node: parentFocusScope2, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child A', key: keyA, ), ], ), ), FocusScope( node: parentFocusScope1, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child B', key: keyB, name: 'b', ), ], ), ), ], ), ); await tester.pump(); expect(keyA.currentState!.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget); expect(keyB.currentState!.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget); }); testWidgetsWithLeakTracking('Can focus root node.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); await tester.pumpWidget( Focus( key: key1, child: Container(), ), ); final Element firstElement = tester.element(find.byKey(key1)); final FocusScopeNode rootNode = FocusScope.of(firstElement); rootNode.requestFocus(); await tester.pump(); expect(rootNode.hasFocus, isTrue); expect(rootNode, equals(firstElement.owner!.focusManager.rootScope)); }); testWidgetsWithLeakTracking('Can autofocus a node.', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); addTearDown(focusNode.dispose); await tester.pumpWidget( Focus( focusNode: focusNode, child: Container(), ), ); await tester.pump(); expect(focusNode.hasPrimaryFocus, isFalse); await tester.pumpWidget( Focus( autofocus: true, focusNode: focusNode, child: Container(), ), ); await tester.pump(); expect(focusNode.hasPrimaryFocus, isTrue); }); testWidgetsWithLeakTracking("Won't autofocus a node if one is already focused.", (WidgetTester tester) async { final FocusNode focusNodeA = FocusNode(debugLabel: 'Test Node A'); addTearDown(focusNodeA.dispose); final FocusNode focusNodeB = FocusNode(debugLabel: 'Test Node B'); addTearDown(focusNodeB.dispose); await tester.pumpWidget( Column( children: <Widget>[ Focus( focusNode: focusNodeA, autofocus: true, child: Container(), ), ], ), ); await tester.pump(); expect(focusNodeA.hasPrimaryFocus, isTrue); await tester.pumpWidget( Column( children: <Widget>[ Focus( focusNode: focusNodeA, child: Container(), ), Focus( focusNode: focusNodeB, autofocus: true, child: Container(), ), ], ), ); await tester.pump(); expect(focusNodeB.hasPrimaryFocus, isFalse); expect(focusNodeA.hasPrimaryFocus, isTrue); }); 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 FocusScopeNode focusScopeNode = FocusScopeNode(); addTearDown(focusScopeNode.dispose); bool? keyEventHandled; KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) { keyEventHandled = true; return KeyEventResult.handled; } KeyEventResult handleEventCallback(FocusNode node, KeyEvent event) { keyEventHandled = true; return KeyEventResult.handled; } KeyEventResult ignoreCallback(FocusNode node, RawKeyEvent event) => KeyEventResult.ignored; KeyEventResult ignoreEventCallback(FocusNode node, KeyEvent event) => KeyEventResult.ignored; focusScopeNode.onKey = ignoreCallback; focusScopeNode.onKeyEvent = ignoreEventCallback; focusScopeNode.descendantsAreFocusable = false; focusScopeNode.descendantsAreTraversable = false; focusScopeNode.skipTraversal = false; focusScopeNode.canRequestFocus = true; FocusScope focusScopeWidget = FocusScope.withExternalFocusNode( focusScopeNode: focusScopeNode, child: Container(key: key1), ); await tester.pumpWidget(focusScopeWidget); expect(focusScopeNode.onKey, equals(ignoreCallback)); expect(focusScopeNode.onKeyEvent, equals(ignoreEventCallback)); expect(focusScopeNode.descendantsAreFocusable, isFalse); expect(focusScopeNode.descendantsAreTraversable, isFalse); expect(focusScopeNode.skipTraversal, isFalse); expect(focusScopeNode.canRequestFocus, isTrue); expect(focusScopeWidget.onKey, equals(focusScopeNode.onKey)); expect(focusScopeWidget.onKeyEvent, equals(focusScopeNode.onKeyEvent)); expect(focusScopeWidget.descendantsAreFocusable, equals(focusScopeNode.descendantsAreFocusable)); expect(focusScopeWidget.descendantsAreTraversable, equals(focusScopeNode.descendantsAreTraversable)); expect(focusScopeWidget.skipTraversal, equals(focusScopeNode.skipTraversal)); expect(focusScopeWidget.canRequestFocus, equals(focusScopeNode.canRequestFocus)); FocusScope.of(key1.currentContext!).requestFocus(); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.enter); expect(keyEventHandled, isNull); focusScopeNode.onKey = handleCallback; focusScopeNode.onKeyEvent = handleEventCallback; focusScopeNode.descendantsAreFocusable = true; focusScopeNode.descendantsAreTraversable = true; focusScopeWidget = FocusScope.withExternalFocusNode( focusScopeNode: focusScopeNode, child: Container(key: key1), ); await tester.pumpWidget(focusScopeWidget); expect(focusScopeNode.onKey, equals(handleCallback)); expect(focusScopeNode.onKeyEvent, equals(handleEventCallback)); expect(focusScopeNode.descendantsAreFocusable, isTrue); expect(focusScopeNode.descendantsAreTraversable, isTrue); expect(focusScopeNode.skipTraversal, isFalse); expect(focusScopeNode.canRequestFocus, isTrue); expect(focusScopeWidget.onKey, equals(focusScopeNode.onKey)); expect(focusScopeWidget.onKeyEvent, equals(focusScopeNode.onKeyEvent)); expect(focusScopeWidget.descendantsAreFocusable, equals(focusScopeNode.descendantsAreFocusable)); expect(focusScopeWidget.descendantsAreTraversable, equals(focusScopeNode.descendantsAreTraversable)); expect(focusScopeWidget.skipTraversal, equals(focusScopeNode.skipTraversal)); expect(focusScopeWidget.canRequestFocus, equals(focusScopeNode.canRequestFocus)); await tester.sendKeyEvent(LogicalKeyboardKey.enter); expect(keyEventHandled, isTrue); }); }); group('Focus', () { testWidgetsWithLeakTracking('Focus.of stops at the nearest Focus widget.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key4 = GlobalKey(debugLabel: '4'); final GlobalKey key5 = GlobalKey(debugLabel: '5'); final GlobalKey key6 = GlobalKey(debugLabel: '6'); final FocusScopeNode scopeNode = FocusScopeNode(); addTearDown(scopeNode.dispose); await tester.pumpWidget( FocusScope( key: key1, node: scopeNode, debugLabel: 'Key 1', child: Container( key: key2, child: Focus( debugLabel: 'Key 3', key: key3, child: Container( key: key4, child: Focus( debugLabel: 'Key 5', key: key5, child: Container( key: key6, ), ), ), ), ), ), ); final Element element1 = tester.element(find.byKey(key1)); final Element element2 = tester.element(find.byKey(key2)); final Element element3 = tester.element(find.byKey(key3)); final Element element4 = tester.element(find.byKey(key4)); final Element element5 = tester.element(find.byKey(key5)); final Element element6 = tester.element(find.byKey(key6)); final FocusNode root = element1.owner!.focusManager.rootScope; expect(Focus.maybeOf(element1), isNull); expect(Focus.maybeOf(element2), isNull); expect(Focus.maybeOf(element3), isNull); expect(Focus.of(element4).parent!.parent, equals(root)); expect(Focus.of(element5).parent!.parent, equals(root)); expect(Focus.of(element6).parent!.parent!.parent, equals(root)); }); testWidgetsWithLeakTracking('Can traverse Focus children.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key4 = GlobalKey(debugLabel: '4'); final GlobalKey key5 = GlobalKey(debugLabel: '5'); final GlobalKey key6 = GlobalKey(debugLabel: '6'); final GlobalKey key7 = GlobalKey(debugLabel: '7'); final GlobalKey key8 = GlobalKey(debugLabel: '8'); await tester.pumpWidget( Focus( child: Column( key: key1, children: <Widget>[ Focus( key: key2, child: Focus( key: key3, child: Container(), ), ), Focus( key: key4, child: Focus( key: key5, child: Container(), ), ), Focus( key: key6, child: Column( children: <Widget>[ Focus( key: key7, child: Container(), ), Focus( key: key8, child: Container(), ), ], ), ), ], ), ), ); final Element firstScope = tester.element(find.byKey(key1)); final List<FocusNode> nodes = <FocusNode>[]; final List<Key> keys = <Key>[]; bool visitor(FocusNode node) { nodes.add(node); keys.add(node.context!.widget.key!); return true; } await tester.pump(); Focus.of(firstScope).descendants.forEach(visitor); expect(nodes.length, equals(7)); expect(keys.length, equals(7)); // Depth first. expect(keys, equals(<Key>[key3, key2, key5, key4, key7, key8, key6])); // Just traverses a sub-tree. final Element secondScope = tester.element(find.byKey(key7)); nodes.clear(); keys.clear(); Focus.of(secondScope).descendants.forEach(visitor); expect(nodes.length, equals(2)); expect(keys, equals(<Key>[key7, key8])); }); testWidgetsWithLeakTracking('Can set focus.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); late bool gotFocus; await tester.pumpWidget( Focus( onFocusChange: (bool focused) => gotFocus = focused, child: Container(key: key1), ), ); final Element firstNode = tester.element(find.byKey(key1)); final FocusNode node = Focus.of(firstNode); node.requestFocus(); await tester.pump(); expect(gotFocus, isTrue); expect(node.hasFocus, isTrue); }); testWidgetsWithLeakTracking('Focus is ignored when set to not focusable.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); bool? gotFocus; await tester.pumpWidget( Focus( canRequestFocus: false, onFocusChange: (bool focused) => gotFocus = focused, child: Container(key: key1), ), ); final Element firstNode = tester.element(find.byKey(key1)); final FocusNode node = Focus.of(firstNode); node.requestFocus(); await tester.pump(); expect(gotFocus, isNull); expect(node.hasFocus, isFalse); }); testWidgetsWithLeakTracking('Focus is lost when set to not focusable.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); bool? gotFocus; await tester.pumpWidget( Focus( autofocus: true, canRequestFocus: true, onFocusChange: (bool focused) => gotFocus = focused, child: Container(key: key1), ), ); Element firstNode = tester.element(find.byKey(key1)); FocusNode node = Focus.of(firstNode); node.requestFocus(); await tester.pump(); expect(gotFocus, isTrue); expect(node.hasFocus, isTrue); gotFocus = null; await tester.pumpWidget( Focus( canRequestFocus: false, onFocusChange: (bool focused) => gotFocus = focused, child: Container(key: key1), ), ); firstNode = tester.element(find.byKey(key1)); node = Focus.of(firstNode); node.requestFocus(); await tester.pump(); expect(gotFocus, false); expect(node.hasFocus, isFalse); }); testWidgetsWithLeakTracking('Child of unfocusable Focus can get focus.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final FocusNode focusNode = FocusNode(); addTearDown(focusNode.dispose); bool? gotFocus; await tester.pumpWidget( Focus( canRequestFocus: false, onFocusChange: (bool focused) => gotFocus = focused, child: Focus(key: key1, focusNode: focusNode, child: Container(key: key2)), ), ); final Element childWidget = tester.element(find.byKey(key1)); final FocusNode unfocusableNode = Focus.of(childWidget); unfocusableNode.requestFocus(); await tester.pump(); expect(gotFocus, isNull); expect(unfocusableNode.hasFocus, isFalse); final Element containerWidget = tester.element(find.byKey(key2)); final FocusNode focusableNode = Focus.of(containerWidget); focusableNode.requestFocus(); await tester.pump(); expect(gotFocus, isTrue); expect(unfocusableNode.hasFocus, isTrue); }); testWidgetsWithLeakTracking('Nodes are removed when all Focuses are removed.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); late bool gotFocus; await tester.pumpWidget( FocusScope( child: Focus( onFocusChange: (bool focused) => gotFocus = focused, child: Container(key: key1), ), ), ); final Element firstNode = tester.element(find.byKey(key1)); final FocusNode node = Focus.of(firstNode); node.requestFocus(); await tester.pump(); expect(gotFocus, isTrue); expect(node.hasFocus, isTrue); await tester.pumpWidget(Container()); expect(FocusManager.instance.rootScope.descendants, isEmpty); }); testWidgetsWithLeakTracking('Focus widgets set Semantics information about focus', (WidgetTester tester) async { final GlobalKey<TestFocusState> key = GlobalKey(); await tester.pumpWidget( TestFocus(key: key), ); final SemanticsNode semantics = tester.getSemantics(find.byKey(key)); expect(key.currentState!.focusNode.hasFocus, isFalse); expect(semantics.hasFlag(SemanticsFlag.isFocused), isFalse); expect(semantics.hasFlag(SemanticsFlag.isFocusable), isTrue); FocusScope.of(key.currentContext!).requestFocus(key.currentState!.focusNode); await tester.pumpAndSettle(); expect(key.currentState!.focusNode.hasFocus, isTrue); expect(semantics.hasFlag(SemanticsFlag.isFocused), isTrue); expect(semantics.hasFlag(SemanticsFlag.isFocusable), isTrue); key.currentState!.focusNode.canRequestFocus = false; await tester.pumpAndSettle(); expect(key.currentState!.focusNode.hasFocus, isFalse); expect(key.currentState!.focusNode.canRequestFocus, isFalse); expect(semantics.hasFlag(SemanticsFlag.isFocused), isFalse); expect(semantics.hasFlag(SemanticsFlag.isFocusable), isFalse); }); testWidgetsWithLeakTracking('Setting canRequestFocus on focus node causes update.', (WidgetTester tester) async { final GlobalKey<TestFocusState> key = GlobalKey(); final TestFocus testFocus = TestFocus(key: key); await tester.pumpWidget( testFocus, ); await tester.pumpAndSettle(); key.currentState!.built = false; key.currentState!.focusNode.canRequestFocus = false; await tester.pumpAndSettle(); key.currentState!.built = true; expect(key.currentState!.focusNode.canRequestFocus, isFalse); }); testWidgetsWithLeakTracking('canRequestFocus causes descendants of scope to be skipped.', (WidgetTester tester) async { final GlobalKey scope1 = GlobalKey(debugLabel: 'scope1'); final GlobalKey scope2 = GlobalKey(debugLabel: 'scope2'); final GlobalKey focus1 = GlobalKey(debugLabel: 'focus1'); final GlobalKey focus2 = GlobalKey(debugLabel: 'focus2'); final GlobalKey container1 = GlobalKey(debugLabel: 'container'); Future<void> pumpTest({ bool allowScope1 = true, bool allowScope2 = true, bool allowFocus1 = true, bool allowFocus2 = true, }) async { await tester.pumpWidget( FocusScope( key: scope1, canRequestFocus: allowScope1, child: FocusScope( key: scope2, canRequestFocus: allowScope2, child: Focus( key: focus1, canRequestFocus: allowFocus1, child: Focus( key: focus2, canRequestFocus: allowFocus2, child: Container( key: container1, ), ), ), ), ), ); await tester.pump(); } // Check childless node (focus2). await pumpTest(); Focus.of(container1.currentContext!).requestFocus(); await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isTrue); await pumpTest(allowFocus2: false); expect(Focus.of(container1.currentContext!).hasFocus, isFalse); Focus.of(container1.currentContext!).requestFocus(); await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isFalse); await pumpTest(); Focus.of(container1.currentContext!).requestFocus(); await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isTrue); // Check FocusNode with child (focus1). Shouldn't affect children. await pumpTest(allowFocus1: false); expect(Focus.of(container1.currentContext!).hasFocus, isTrue); // focus2 has focus. Focus.of(focus2.currentContext!).requestFocus(); // Try to focus focus1 await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isTrue); // focus2 still has focus. Focus.of(container1.currentContext!).requestFocus(); // Now try to focus focus2 await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isTrue); await pumpTest(); // Try again, now that we've set focus1's canRequestFocus to true again. Focus.of(container1.currentContext!).unfocus(); await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isFalse); Focus.of(container1.currentContext!).requestFocus(); await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isTrue); // Check FocusScopeNode with only FocusNode children (scope2). Should affect children. await pumpTest(allowScope2: false); expect(Focus.of(container1.currentContext!).hasFocus, isFalse); FocusScope.of(focus1.currentContext!).requestFocus(); // Try to focus scope2 await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isFalse); Focus.of(focus2.currentContext!).requestFocus(); // Try to focus focus1 await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isFalse); Focus.of(container1.currentContext!).requestFocus(); // Try to focus focus2 await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isFalse); await pumpTest(); // Try again, now that we've set scope2's canRequestFocus to true again. Focus.of(container1.currentContext!).requestFocus(); await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isTrue); // Check FocusScopeNode with both FocusNode children and FocusScope children (scope1). Should affect children. await pumpTest(allowScope1: false); expect(Focus.of(container1.currentContext!).hasFocus, isFalse); FocusScope.of(scope2.currentContext!).requestFocus(); // Try to focus scope1 await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isFalse); FocusScope.of(focus1.currentContext!).requestFocus(); // Try to focus scope2 await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isFalse); Focus.of(focus2.currentContext!).requestFocus(); // Try to focus focus1 await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isFalse); Focus.of(container1.currentContext!).requestFocus(); // Try to focus focus2 await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isFalse); await pumpTest(); // Try again, now that we've set scope1's canRequestFocus to true again. Focus.of(container1.currentContext!).requestFocus(); await tester.pump(); expect(Focus.of(container1.currentContext!).hasFocus, isTrue); }); testWidgetsWithLeakTracking('skipTraversal works as expected.', (WidgetTester tester) async { final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1'); addTearDown(scope1.dispose); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); addTearDown(scope2.dispose); final FocusNode focus1 = FocusNode(debugLabel: 'focus1'); addTearDown(focus1.dispose); final FocusNode focus2 = FocusNode(debugLabel: 'focus2'); addTearDown(focus2.dispose); Future<void> pumpTest({ bool traverseScope1 = false, bool traverseScope2 = false, bool traverseFocus1 = false, bool traverseFocus2 = false, }) async { await tester.pumpWidget( FocusScope( node: scope1, skipTraversal: traverseScope1, child: FocusScope( node: scope2, skipTraversal: traverseScope2, child: Focus( focusNode: focus1, skipTraversal: traverseFocus1, child: Focus( focusNode: focus2, skipTraversal: traverseFocus2, child: Container(), ), ), ), ), ); await tester.pump(); } await pumpTest(); expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1, scope2])); // Check childless node (focus2). await pumpTest(traverseFocus2: true); expect(scope1.traversalDescendants, equals(<FocusNode>[focus1, scope2])); // Check FocusNode with child (focus1). Shouldn't affect children. await pumpTest(traverseFocus1: true); expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, scope2])); // Check FocusScopeNode with only FocusNode children (scope2). Should affect children. await pumpTest(traverseScope2: true); expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1])); // Check FocusScopeNode with both FocusNode children and FocusScope children (scope1). Should affect children. await pumpTest(traverseScope1: true); expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1, scope2])); }); testWidgetsWithLeakTracking('descendantsAreFocusable works as expected.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final FocusNode focusNode = FocusNode(); addTearDown(focusNode.dispose); bool? gotFocus; await tester.pumpWidget( Focus( descendantsAreFocusable: false, child: Focus( onFocusChange: (bool focused) => gotFocus = focused, child: Focus( key: key1, focusNode: focusNode, child: Container(key: key2), ), ), ), ); final Element childWidget = tester.element(find.byKey(key1)); final FocusNode unfocusableNode = Focus.of(childWidget); final Element containerWidget = tester.element(find.byKey(key2)); final FocusNode containerNode = Focus.of(containerWidget); unfocusableNode.requestFocus(); await tester.pump(); expect(gotFocus, isNull); expect(containerNode.hasFocus, isFalse); expect(unfocusableNode.hasFocus, isFalse); containerNode.requestFocus(); await tester.pump(); expect(gotFocus, isNull); expect(containerNode.hasFocus, isFalse); expect(unfocusableNode.hasFocus, isFalse); }); testWidgetsWithLeakTracking('descendantsAreTraversable works as expected.', (WidgetTester tester) async { final FocusScopeNode scopeNode = FocusScopeNode(debugLabel: 'scope'); addTearDown(scopeNode.dispose); final FocusNode node1 = FocusNode(debugLabel: 'node 1'); addTearDown(node1.dispose); final FocusNode node2 = FocusNode(debugLabel: 'node 2'); addTearDown(node2.dispose); final FocusNode node3 = FocusNode(debugLabel: 'node 3'); addTearDown(node3.dispose); await tester.pumpWidget( FocusScope( node: scopeNode, child: Column( children: <Widget>[ Focus( focusNode: node1, child: Container(), ), Focus( focusNode: node2, descendantsAreTraversable: false, child: Focus( focusNode: node3, child: Container(), ) ), ], ), ), ); await tester.pump(); expect(scopeNode.traversalDescendants, equals(<FocusNode>[node1, node2])); expect(node2.traversalDescendants, equals(<FocusNode>[])); }); testWidgetsWithLeakTracking("Focus doesn't introduce a Semantics node when includeSemantics is false", (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Focus(includeSemantics: false, child: Container())); final TestSemantics expectedSemantics = TestSemantics.root(); expect(semantics, hasSemantics(expectedSemantics)); semantics.dispose(); }); testWidgetsWithLeakTracking('Focus updates the onKey handler when the widget updates', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final FocusNode focusNode = FocusNode(); addTearDown(focusNode.dispose); bool? keyEventHandled; KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) { keyEventHandled = true; return KeyEventResult.handled; } KeyEventResult ignoreCallback(FocusNode node, RawKeyEvent event) => KeyEventResult.ignored; Focus focusWidget = Focus( onKey: ignoreCallback, // This one does nothing. focusNode: focusNode, skipTraversal: true, canRequestFocus: true, child: Container(key: key1), ); focusNode.onKeyEvent = null; await tester.pumpWidget(focusWidget); expect(focusNode.onKey, equals(ignoreCallback)); expect(focusWidget.onKey, equals(focusNode.onKey)); expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent)); expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable)); expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal)); expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus)); Focus.of(key1.currentContext!).requestFocus(); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.enter); expect(keyEventHandled, isNull); focusWidget = Focus( onKey: handleCallback, focusNode: focusNode, skipTraversal: true, canRequestFocus: true, child: Container(key: key1), ); await tester.pumpWidget(focusWidget); expect(focusNode.onKey, equals(handleCallback)); expect(focusWidget.onKey, equals(focusNode.onKey)); expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent)); expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable)); expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal)); expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus)); await tester.sendKeyEvent(LogicalKeyboardKey.enter); expect(keyEventHandled, isTrue); }); testWidgetsWithLeakTracking('Focus updates the onKeyEvent handler when the widget updates', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final FocusNode focusNode = FocusNode(); addTearDown(focusNode.dispose); bool? keyEventHandled; KeyEventResult handleEventCallback(FocusNode node, KeyEvent event) { keyEventHandled = true; return KeyEventResult.handled; } KeyEventResult ignoreEventCallback(FocusNode node, KeyEvent event) => KeyEventResult.ignored; Focus focusWidget = Focus( onKeyEvent: ignoreEventCallback, // This one does nothing. focusNode: focusNode, skipTraversal: true, canRequestFocus: true, child: Container(key: key1), ); focusNode.onKeyEvent = null; await tester.pumpWidget(focusWidget); expect(focusNode.onKeyEvent, equals(ignoreEventCallback)); expect(focusWidget.onKey, equals(focusNode.onKey)); expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent)); expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable)); expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal)); expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus)); Focus.of(key1.currentContext!).requestFocus(); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.enter); expect(keyEventHandled, isNull); focusWidget = Focus( onKeyEvent: handleEventCallback, focusNode: focusNode, skipTraversal: true, canRequestFocus: true, child: Container(key: key1), ); await tester.pumpWidget(focusWidget); expect(focusNode.onKeyEvent, equals(handleEventCallback)); expect(focusWidget.onKey, equals(focusNode.onKey)); expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent)); expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable)); expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal)); expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus)); await tester.sendKeyEvent(LogicalKeyboardKey.enter); expect(keyEventHandled, isTrue); }); 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 FocusNode focusNode = FocusNode(); addTearDown(focusNode.dispose); bool? keyEventHandled; KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) { keyEventHandled = true; return KeyEventResult.handled; } KeyEventResult handleEventCallback(FocusNode node, KeyEvent event) { keyEventHandled = true; return KeyEventResult.handled; } KeyEventResult ignoreCallback(FocusNode node, RawKeyEvent event) => KeyEventResult.ignored; KeyEventResult ignoreEventCallback(FocusNode node, KeyEvent event) => KeyEventResult.ignored; focusNode.onKey = ignoreCallback; focusNode.onKeyEvent = ignoreEventCallback; focusNode.descendantsAreFocusable = false; focusNode.descendantsAreTraversable = false; focusNode.skipTraversal = false; focusNode.canRequestFocus = true; Focus focusWidget = Focus.withExternalFocusNode( focusNode: focusNode, child: Container(key: key1), ); await tester.pumpWidget(focusWidget); expect(focusNode.onKey, equals(ignoreCallback)); expect(focusNode.onKeyEvent, equals(ignoreEventCallback)); expect(focusNode.descendantsAreFocusable, isFalse); expect(focusNode.descendantsAreTraversable, isFalse); expect(focusNode.skipTraversal, isFalse); expect(focusNode.canRequestFocus, isTrue); expect(focusWidget.onKey, equals(focusNode.onKey)); expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent)); expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable)); expect(focusWidget.descendantsAreTraversable, equals(focusNode.descendantsAreTraversable)); expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal)); expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus)); Focus.of(key1.currentContext!).requestFocus(); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.enter); expect(keyEventHandled, isNull); focusNode.onKey = handleCallback; focusNode.onKeyEvent = handleEventCallback; focusNode.descendantsAreFocusable = true; focusNode.descendantsAreTraversable = true; focusWidget = Focus.withExternalFocusNode( focusNode: focusNode, child: Container(key: key1), ); await tester.pumpWidget(focusWidget); expect(focusNode.onKey, equals(handleCallback)); expect(focusNode.onKeyEvent, equals(handleEventCallback)); expect(focusNode.descendantsAreFocusable, isTrue); expect(focusNode.descendantsAreTraversable, isTrue); expect(focusNode.skipTraversal, isFalse); expect(focusNode.canRequestFocus, isTrue); expect(focusWidget.onKey, equals(focusNode.onKey)); expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent)); expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable)); expect(focusWidget.descendantsAreTraversable, equals(focusNode.descendantsAreTraversable)); expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal)); expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus)); await tester.sendKeyEvent(LogicalKeyboardKey.enter); expect(keyEventHandled, isTrue); }); testWidgetsWithLeakTracking('Focus passes changes in attribute values to its focus node', (WidgetTester tester) async { await tester.pumpWidget( Focus( child: Container(), ), ); }); }); group('ExcludeFocus', () { testWidgetsWithLeakTracking("Descendants of ExcludeFocus aren't focusable.", (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final FocusNode focusNode = FocusNode(); addTearDown(focusNode.dispose); bool? gotFocus; await tester.pumpWidget( ExcludeFocus( child: Focus( onFocusChange: (bool focused) => gotFocus = focused, child: Focus( key: key1, focusNode: focusNode, child: Container(key: key2), ), ), ), ); final Element childWidget = tester.element(find.byKey(key1)); final FocusNode unfocusableNode = Focus.of(childWidget); final Element containerWidget = tester.element(find.byKey(key2)); final FocusNode containerNode = Focus.of(containerWidget); unfocusableNode.requestFocus(); await tester.pump(); expect(gotFocus, isNull); expect(containerNode.hasFocus, isFalse); expect(unfocusableNode.hasFocus, isFalse); containerNode.requestFocus(); await tester.pump(); expect(gotFocus, isNull); expect(containerNode.hasFocus, isFalse); expect(unfocusableNode.hasFocus, isFalse); }); // Regression test for https://github.com/flutter/flutter/issues/61700 testWidgetsWithLeakTracking("ExcludeFocus doesn't transfer focus to another descendant.", (WidgetTester tester) async { final FocusNode parentFocusNode = FocusNode(debugLabel: 'group'); addTearDown(parentFocusNode.dispose); final FocusNode focusNode1 = FocusNode(debugLabel: 'node 1'); addTearDown(focusNode1.dispose); final FocusNode focusNode2 = FocusNode(debugLabel: 'node 2'); addTearDown(focusNode2.dispose); await tester.pumpWidget( ExcludeFocus( excluding: false, child: Focus( focusNode: parentFocusNode, child: Column( children: <Widget>[ Focus( autofocus: true, focusNode: focusNode1, child: Container(), ), Focus( focusNode: focusNode2, child: Container(), ), ], ), ), ), ); await tester.pump(); expect(parentFocusNode.hasFocus, isTrue); expect(focusNode1.hasPrimaryFocus, isTrue); expect(focusNode2.hasFocus, isFalse); // Move focus to the second node to create some focus history for the scope. focusNode2.requestFocus(); await tester.pump(); expect(parentFocusNode.hasFocus, isTrue); expect(focusNode1.hasFocus, isFalse); expect(focusNode2.hasPrimaryFocus, isTrue); // Now turn off the focus for the subtree. await tester.pumpWidget( ExcludeFocus( child: Focus( focusNode: parentFocusNode, child: Column( children: <Widget>[ Focus( autofocus: true, focusNode: focusNode1, child: Container(), ), Focus( focusNode: focusNode2, child: Container(), ), ], ), ), ), ); await tester.pump(); expect(focusNode1.hasFocus, isFalse); expect(focusNode2.hasFocus, isFalse); expect(parentFocusNode.hasFocus, isFalse); expect(parentFocusNode.enclosingScope!.hasPrimaryFocus, isTrue); }); testWidgetsWithLeakTracking("ExcludeFocus doesn't introduce a Semantics node", (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(ExcludeFocus(child: Container())); final TestSemantics expectedSemantics = TestSemantics.root(); expect(semantics, hasSemantics(expectedSemantics)); semantics.dispose(); }); // Regression test for https://github.com/flutter/flutter/issues/92693 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'); addTearDown(childFocusNode.dispose); Widget buildFocusTree({required bool parentCanRequestFocus}) { return FocusScope( canRequestFocus: parentCanRequestFocus, child: Column( children: <Widget>[ Focus( focusNode: childFocusNode, child: Container(), ), ], ), ); } // childFocusNode.canRequestFocus is true when parent canRequestFocus is true await tester.pumpWidget(buildFocusTree(parentCanRequestFocus: true)); expect(childFocusNode.canRequestFocus, isTrue); // childFocusNode.canRequestFocus is false when parent canRequestFocus is false await tester.pumpWidget(buildFocusTree(parentCanRequestFocus: false)); expect(childFocusNode.canRequestFocus, isFalse); // childFocusNode.canRequestFocus is true again when parent canRequestFocus is changed back to true await tester.pumpWidget(buildFocusTree(parentCanRequestFocus: true)); expect(childFocusNode.canRequestFocus, isTrue); }); }); } class TestFocus extends StatefulWidget { const TestFocus({ super.key, this.debugLabel, this.name = 'a', this.autofocus = false, this.parentNode, }); final String? debugLabel; final String name; final bool autofocus; final FocusNode? parentNode; @override TestFocusState createState() => TestFocusState(); } class TestFocusState extends State<TestFocus> { late FocusNode focusNode; late String _label; bool built = false; @override void dispose() { focusNode.removeListener(_updateLabel); focusNode.dispose(); super.dispose(); } String get label => focusNode.hasFocus ? '${widget.name.toUpperCase()} FOCUSED' : widget.name.toLowerCase(); @override void initState() { super.initState(); focusNode = FocusNode(debugLabel: widget.debugLabel); _label = label; focusNode.addListener(_updateLabel); } void _updateLabel() { setState(() { _label = label; }); } @override Widget build(BuildContext context) { built = true; return GestureDetector( onTap: () { FocusScope.of(context).requestFocus(focusNode); }, child: Focus( autofocus: widget.autofocus, focusNode: focusNode, parentNode: widget.parentNode, debugLabel: widget.debugLabel, child: Text( _label, textDirection: TextDirection.ltr, ), ), ); } }