Unverified Commit 89512e46 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add/rewrite tests for FocusScope. (#28169)

In anticipation of changing a lot of the focus code, I'm adding some tests for the FocusScope.

As a result, I was able to find and fix a bug where there was an incorrect assert.

I also added some more documentation.

Several of the tests enforce what I think is incorrect behavior related to passing focus off when the widget tree gets rebuilt without focus nodes that were previously there, but I'm not going to change that behavior in this PR.

I also renamed focus_test.dart to focus_scope_test.dart to be more in line with our naming conventions.
parent 4be4830c
...@@ -121,6 +121,9 @@ class FocusNode extends ChangeNotifier { ...@@ -121,6 +121,9 @@ class FocusNode extends ChangeNotifier {
/// parent [FocusScopeNode] and [FocusScopeNode.isFirstFocus] is true for /// parent [FocusScopeNode] and [FocusScopeNode.isFirstFocus] is true for
/// that scope and all its ancestor scopes. /// that scope and all its ancestor scopes.
/// ///
/// If a [FocusScopeNode] is removed, then the next sibling node will be set as
/// the focused node by the [FocusManager].
///
/// See also: /// See also:
/// ///
/// * [FocusNode], which is a leaf node in the focus tree that can receive /// * [FocusNode], which is a leaf node in the focus tree that can receive
...@@ -326,7 +329,6 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { ...@@ -326,7 +329,6 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin {
/// the child. /// the child.
void setFirstFocus(FocusScopeNode child) { void setFirstFocus(FocusScopeNode child) {
assert(child != null); assert(child != null);
assert(child._parent == null || child._parent == this);
if (_firstChild == child) if (_firstChild == child)
return; return;
child.detach(); child.detach();
...@@ -348,11 +350,12 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { ...@@ -348,11 +350,12 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin {
assert(child != null); assert(child != null);
if (child._parent == null || child._parent == this) if (child._parent == null || child._parent == this)
return; return;
if (child.isFirstFocus) if (child.isFirstFocus) {
setFirstFocus(child); setFirstFocus(child);
else } else {
child.detach(); child.detach();
} }
}
/// Remove this scope from its parent child list. /// Remove this scope from its parent child list.
/// ///
...@@ -394,8 +397,9 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { ...@@ -394,8 +397,9 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin {
/// Manages the focus tree. /// Manages the focus tree.
/// ///
/// The focus tree keeps track of which widget is the user's current focus. The /// The focus tree keeps track of which [FocusNode] is the user's current
/// focused widget often listens for keyboard events. /// keyboard focus. The widget that owns the [FocusNode] often listens for
/// keyboard events.
/// ///
/// The focus manager is responsible for holding the [FocusScopeNode] that is /// The focus manager is responsible for holding the [FocusScopeNode] that is
/// the root of the focus tree and tracking which [FocusNode] has the overall /// the root of the focus tree and tracking which [FocusNode] has the overall
...@@ -406,6 +410,12 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { ...@@ -406,6 +410,12 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin {
/// directly. Instead, to find the [FocusScopeNode] for a given [BuildContext], /// directly. Instead, to find the [FocusScopeNode] for a given [BuildContext],
/// use [FocusScope.of]. /// use [FocusScope.of].
/// ///
/// The [FocusManager] knows nothing about [FocusNode]s other than the one that
/// is currently focused. If a [FocusScopeNode] is removed, then the
/// [FocusManager] will attempt to focus the next [FocusScopeNode] in the focus
/// tree that it maintains, but if the current focus in that [FocusScopeNode] is
/// null, it will stop there, and no [FocusNode] will have focus.
///
/// See also: /// See also:
/// ///
/// * [FocusNode], which is a leaf node in the focus tree that can receive /// * [FocusNode], which is a leaf node in the focus tree that can receive
......
...@@ -37,8 +37,12 @@ class _FocusScopeMarker extends InheritedWidget { ...@@ -37,8 +37,12 @@ class _FocusScopeMarker extends InheritedWidget {
/// FocusScope.of(context).setFirstFocus(node); /// FocusScope.of(context).setFirstFocus(node);
/// ``` /// ```
/// ///
/// When a [FocusScope] is removed from the tree, the previously active /// If a [FocusScope] is removed from the widget tree, then the previously
/// [FocusScope] becomes active again. /// focused node will be focused, but only if the [node] is the same [node]
/// object as in the previous frame. To assure this, you can use a GlobalKey to
/// keep the [FocusScope] widget from being rebuilt from one frame to the next,
/// or pass in the [node] from a parent that is not rebuilt. If there is no next
/// sibling, then the parent scope node will be focused.
/// ///
/// See also: /// See also:
/// ///
......
This diff is collapsed.
// Copyright 2015 The Chromium 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_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
class TestFocusable extends StatefulWidget {
const TestFocusable({
Key key,
this.no,
this.yes,
this.autofocus = true,
}) : super(key: key);
final String no;
final String yes;
final bool autofocus;
@override
TestFocusableState createState() => TestFocusableState();
}
class TestFocusableState extends State<TestFocusable> {
final FocusNode focusNode = FocusNode();
bool _didAutofocus = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_didAutofocus && widget.autofocus) {
_didAutofocus = true;
FocusScope.of(context).autofocus(focusNode);
}
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () { FocusScope.of(context).requestFocus(focusNode); },
child: AnimatedBuilder(
animation: focusNode,
builder: (BuildContext context, Widget child) {
return Text(focusNode.hasFocus ? widget.yes : widget.no, textDirection: TextDirection.ltr);
},
),
);
}
}
void main() {
testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async {
await tester.pumpWidget(
Column(
children: const <Widget>[
TestFocusable(
no: 'a',
yes: 'A FOCUSED',
),
TestFocusable(
no: 'b',
yes: 'B FOCUSED',
),
],
),
);
// Autofocus is delayed one frame.
await tester.pump();
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(find.text('b'), findsOneWidget);
expect(find.text('B FOCUSED'), findsNothing);
await tester.tap(find.text('A FOCUSED'));
await tester.idle();
await tester.pump();
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(find.text('b'), findsOneWidget);
expect(find.text('B FOCUSED'), findsNothing);
await tester.tap(find.text('A FOCUSED'));
await tester.idle();
await tester.pump();
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(find.text('b'), findsOneWidget);
expect(find.text('B FOCUSED'), findsNothing);
await tester.tap(find.text('b'));
await tester.idle();
await tester.pump();
expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing);
expect(find.text('b'), findsNothing);
expect(find.text('B FOCUSED'), findsOneWidget);
await tester.tap(find.text('a'));
await tester.idle();
await tester.pump();
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(find.text('b'), findsOneWidget);
expect(find.text('B FOCUSED'), findsNothing);
});
testWidgets('Can blur', (WidgetTester tester) async {
await tester.pumpWidget(
const TestFocusable(
no: 'a',
yes: 'A FOCUSED',
autofocus: false,
),
);
expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing);
final TestFocusableState state = tester.state(find.byType(TestFocusable));
FocusScope.of(state.context).requestFocus(state.focusNode);
await tester.idle();
await tester.pump();
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
state.focusNode.unfocus();
await tester.idle();
await tester.pump();
expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing);
});
testWidgets('Can move focus to scope', (WidgetTester tester) async {
final FocusScopeNode parentFocusScope = FocusScopeNode();
final FocusScopeNode childFocusScope = FocusScopeNode();
await tester.pumpWidget(
FocusScope(
node: parentFocusScope,
autofocus: true,
child: Row(
textDirection: TextDirection.ltr,
children: const <Widget>[
TestFocusable(
no: 'a',
yes: 'A FOCUSED',
autofocus: false,
),
],
),
),
);
expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing);
final TestFocusableState state = tester.state(find.byType(TestFocusable));
FocusScope.of(state.context).requestFocus(state.focusNode);
await tester.idle();
await tester.pump();
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(parentFocusScope, hasAGoodToStringDeep);
expect(
parentFocusScope.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes(
'FocusScopeNode#00000\n'
' focus: FocusNode#00000(FOCUSED)\n'
),
);
expect(WidgetsBinding.instance.focusManager.rootScope, hasAGoodToStringDeep);
expect(
WidgetsBinding.instance.focusManager.rootScope.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes(
'FocusScopeNode#00000\n'
' └─child 1: FocusScopeNode#00000\n'
' focus: FocusNode#00000(FOCUSED)\n'
),
);
parentFocusScope.setFirstFocus(childFocusScope);
await tester.idle();
await tester.pumpWidget(
FocusScope(
node: parentFocusScope,
child: Row(
textDirection: TextDirection.ltr,
children: <Widget>[
const TestFocusable(
no: 'a',
yes: 'A FOCUSED',
autofocus: false,
),
FocusScope(
node: childFocusScope,
child: Container(
width: 50.0,
height: 50.0,
),
),
],
),
),
);
expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing);
await tester.pumpWidget(
FocusScope(
node: parentFocusScope,
child: Row(
textDirection: TextDirection.ltr,
children: const <Widget>[
TestFocusable(
no: 'a',
yes: 'A FOCUSED',
autofocus: false,
),
],
),
),
);
// Focus has received the removal notification but we haven't rebuilt yet.
expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing);
await tester.pump();
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
parentFocusScope.detach();
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment