// 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 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/semantics.dart'; class TestFocus extends StatefulWidget { const TestFocus({ Key key, this.debugLabel, this.name = 'a', this.autofocus = false, }) : super(key: key); final String debugLabel; final String name; final bool autofocus; @override TestFocusState createState() => TestFocusState(); } class TestFocusState extends State<TestFocus> { FocusNode focusNode; 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, debugLabel: widget.debugLabel, child: Text( _label, textDirection: TextDirection.ltr, ), ), ); } } void main() { group(FocusScope, () { testWidgets('Can focus', (WidgetTester tester) async { final GlobalKey<TestFocusState> key = GlobalKey(); await tester.pumpWidget( TestFocus(key: key, name: 'a'), ); 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); }); testWidgets('Can unfocus', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); await tester.pumpWidget( Column( children: <Widget>[ TestFocus(key: keyA, name: 'a'), 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); }); testWidgets('Autofocus works', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); await tester.pumpWidget( Column( children: <Widget>[ TestFocus(key: keyA, name: 'a'), 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); }); testWidgets('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, name: 'a', 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. testWidgets('Can move focus in and out of FocusScope', (WidgetTester tester) async { final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node'); final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node'); 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, name: 'a', 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(); }); testWidgets('Setting first focus requests focus for the scope properly.', (WidgetTester tester) async { final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node'); final FocusScopeNode childFocusScope1 = FocusScopeNode(debugLabel: 'Child Scope Node 1'); final FocusScopeNode childFocusScope2 = FocusScopeNode(debugLabel: 'Child Scope Node 2'); 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, name: 'a', 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); }); testWidgets('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, name: 'a', ), 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); }); testWidgets('Adding a new FocusScope attaches the child it to its parent.', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node'); final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node'); 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); }); // 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 { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope'); await tester.pumpWidget( FocusScope( debugLabel: 'Parent Scope', node: parentFocusScope, autofocus: true, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Widget A', key: keyA, name: 'a', ), 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); }); testWidgets('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'); // This checks both FocusScopes that have their own nodes, as well as those // that use external nodes. await tester.pumpWidget( DefaultFocusTraversal( child: Column( children: <Widget>[ FocusScope( key: scopeKeyA, node: parentFocusScope, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child A', key: keyA, name: 'a', ), ], ), ), 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. testWidgets("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'); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2'); await tester.pumpWidget( DefaultFocusTraversal( child: Column( children: <Widget>[ FocusScope( key: scopeKeyA, node: parentFocusScope1, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child A', key: keyA, name: 'a', ), ], ), ), 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( DefaultFocusTraversal( 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); }); testWidgets("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'); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2'); await tester.pumpWidget( DefaultFocusTraversal( child: Column( children: <Widget>[ FocusScope( node: parentFocusScope1, child: Column( children: <Widget>[ TestFocus( debugLabel: 'Child A', key: keyA, name: 'a', ), ], ), ), 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( DefaultFocusTraversal( 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); }); testWidgets('Moving widget from one scope to another retains focus', (WidgetTester tester) async { final FocusScopeNode parentFocusScope1 = FocusScopeNode(); final FocusScopeNode parentFocusScope2 = FocusScopeNode(); 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, name: 'a', ), ], ), ), 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, name: '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); }); testWidgets('Moving FocusScopeNodes retains focus', (WidgetTester tester) async { final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Scope 1'); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Scope 2'); 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, name: 'a', ), ], ), ), 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, name: 'a', ), ], ), ), 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); }); testWidgets('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)); }); testWidgets('Can autofocus a node.', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); 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); }); testWidgets("Won't autofocus a node if one is already focused.", (WidgetTester tester) async { final FocusNode focusNodeA = FocusNode(debugLabel: 'Test Node A'); final FocusNode focusNodeB = FocusNode(debugLabel: 'Test Node B'); 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); }); }); group(Focus, () { testWidgets('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(); 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.of(element1, nullOk: true), isNull); expect(Focus.of(element2, nullOk: true), isNull); expect(Focus.of(element3, nullOk: true), 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)); }); testWidgets('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: Container( child: Focus( key: key3, child: Container(), ), ), ), Focus( key: key4, child: Container( 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])); }); testWidgets('Can set focus.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); 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); }); testWidgets('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); }); testWidgets('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); }); testWidgets('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(); 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); }); }); testWidgets('Nodes are removed when all Focuses are removed.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); 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); }); testWidgets('Focus widgets set Semantics information about focus', (WidgetTester tester) async { final GlobalKey<TestFocusState> key = GlobalKey(); await tester.pumpWidget( TestFocus(key: key, name: 'a'), ); 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); }); testWidgets('Setting canRequestFocus on focus node causes update.', (WidgetTester tester) async { final GlobalKey<TestFocusState> key = GlobalKey(); final TestFocus testFocus = TestFocus(key: key, name: 'a'); 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); }); testWidgets('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, 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(); // 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); }); testWidgets('skipTraversal works as expected.', (WidgetTester tester) async { final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1'); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); final FocusNode focus1 = FocusNode(debugLabel: 'focus1'); final FocusNode focus2 = FocusNode(debugLabel: 'focus2'); 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])); }); }