blob: c7c1d3d655037ce4ebe1bc07fe586d4b22b06cad [file] [log] [blame]
// 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:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
final GlobalKey widgetKey = GlobalKey();
Future<BuildContext> setupWidget(WidgetTester tester) async {
await tester.pumpWidget(Container(key: widgetKey));
return widgetKey.currentContext!;
}
group(FocusNode, () {
testWidgets('Can add children.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusNode parent = FocusNode();
final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode();
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode();
final FocusAttachment child2Attachment = child2.attach(context);
parentAttachment.reparent(parent: tester.binding.focusManager.rootScope);
child1Attachment.reparent(parent: parent);
expect(child1.parent, equals(parent));
expect(parent.children.first, equals(child1));
expect(parent.children.last, equals(child1));
child2Attachment.reparent(parent: parent);
expect(child1.parent, equals(parent));
expect(child2.parent, equals(parent));
expect(parent.children.first, equals(child1));
expect(parent.children.last, equals(child2));
});
testWidgets('Can remove children.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusNode parent = FocusNode();
final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode();
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode();
final FocusAttachment child2Attachment = child2.attach(context);
parentAttachment.reparent(parent: tester.binding.focusManager.rootScope);
child1Attachment.reparent(parent: parent);
child2Attachment.reparent(parent: parent);
expect(child1.parent, equals(parent));
expect(child2.parent, equals(parent));
expect(parent.children.first, equals(child1));
expect(parent.children.last, equals(child2));
child1Attachment.detach();
expect(child1.parent, isNull);
expect(child2.parent, equals(parent));
expect(parent.children.first, equals(child2));
expect(parent.children.last, equals(child2));
child2Attachment.detach();
expect(child1.parent, isNull);
expect(child2.parent, isNull);
expect(parent.children, isEmpty);
});
testWidgets('Geometry is transformed properly.', (WidgetTester tester) async {
final FocusNode focusNode1 = FocusNode(debugLabel: 'Test Node 1');
final FocusNode focusNode2 = FocusNode(debugLabel: 'Test Node 2');
await tester.pumpWidget(
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Focus(
focusNode: focusNode1,
child: const SizedBox(width: 200, height: 100),
),
Transform.translate(
offset: const Offset(10, 20),
child: Transform.scale(
scale: 0.33,
child: Transform.rotate(
angle: math.pi,
child: Focus(focusNode: focusNode2, child: const SizedBox(width: 200, height: 100)),
),
),
),
],
),
),
);
focusNode2.requestFocus();
await tester.pump();
expect(focusNode1.rect, equals(const Rect.fromLTRB(300.0, 8.0, 500.0, 108.0)));
expect(focusNode2.rect, equals(const Rect.fromLTRB(443.0, 194.5, 377.0, 161.5)));
expect(focusNode1.size, equals(const Size(200.0, 100.0)));
expect(focusNode2.size, equals(const Size(-66.0, -33.0)));
expect(focusNode1.offset, equals(const Offset(300.0, 8.0)));
expect(focusNode2.offset, equals(const Offset(443.0, 194.5)));
});
testWidgets('descendantsAreFocusable disables focus for descendants.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope);
parent2Attachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent2);
child1.requestFocus();
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, equals(child1));
expect(scope.focusedChild, equals(child1));
expect(scope.traversalDescendants.contains(child1), isTrue);
expect(scope.traversalDescendants.contains(child2), isTrue);
parent2.descendantsAreFocusable = false;
// Node should still be focusable, even if descendants are not.
parent2.requestFocus();
await tester.pump();
expect(parent2.hasPrimaryFocus, isTrue);
child2.requestFocus();
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, isNot(equals(child2)));
expect(tester.binding.focusManager.primaryFocus, equals(parent2));
expect(scope.focusedChild, equals(parent2));
expect(scope.traversalDescendants.contains(child1), isTrue);
expect(scope.traversalDescendants.contains(child2), isFalse);
parent1.descendantsAreFocusable = false;
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, isNot(equals(child2)));
expect(tester.binding.focusManager.primaryFocus, isNot(equals(child1)));
expect(scope.focusedChild, equals(parent2));
expect(scope.traversalDescendants.contains(child1), isFalse);
expect(scope.traversalDescendants.contains(child2), isFalse);
});
testWidgets('implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
FocusNode(
debugLabel: 'Label',
).debugFillProperties(builder);
final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[
'context: null',
'descendantsAreFocusable: true',
'canRequestFocus: true',
'hasFocus: false',
'hasPrimaryFocus: false',
]);
});
testWidgets('onKeyEvent and onKey correctly cooperate', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node 3');
List<List<KeyEventResult>> results = <List<KeyEventResult>>[
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
];
final List<int> logs = <int>[];
await tester.pumpWidget(
Focus(
focusNode: FocusNode(debugLabel: 'Test Node 1'),
onKeyEvent: (_, KeyEvent event) {
logs.add(0);
return results[0][0];
},
onKey: (_, RawKeyEvent event) {
logs.add(1);
return results[0][1];
},
child: Focus(
focusNode: FocusNode(debugLabel: 'Test Node 2'),
onKeyEvent: (_, KeyEvent event) {
logs.add(10);
return results[1][0];
},
onKey: (_, RawKeyEvent event) {
logs.add(11);
return results[1][1];
},
child: Focus(
focusNode: focusNode,
onKeyEvent: (_, KeyEvent event) {
logs.add(20);
return results[2][0];
},
onKey: (_, RawKeyEvent event) {
logs.add(21);
return results[2][1];
},
child: const SizedBox(width: 200, height: 100),
),
),
),
);
focusNode.requestFocus();
await tester.pump();
// All ignored.
results = <List<KeyEventResult>>[
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
];
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1),
false);
expect(logs, <int>[20, 21, 10, 11, 0, 1]);
logs.clear();
// The onKeyEvent should be able to stop propagation.
results = <List<KeyEventResult>>[
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
<KeyEventResult>[KeyEventResult.handled, KeyEventResult.ignored],
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
];
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1),
true);
expect(logs, <int>[20, 21, 10, 11]);
logs.clear();
// The onKey should be able to stop propagation.
results = <List<KeyEventResult>>[
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.handled],
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
];
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1),
true);
expect(logs, <int>[20, 21, 10, 11]);
logs.clear();
// KeyEventResult.skipRemainingHandlers works.
results = <List<KeyEventResult>>[
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
<KeyEventResult>[KeyEventResult.skipRemainingHandlers, KeyEventResult.ignored],
<KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
];
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1),
false);
expect(logs, <int>[20, 21, 10, 11]);
logs.clear();
}, variant: KeySimulatorTransitModeVariant.all());
});
group(FocusScopeNode, () {
testWidgets('Can setFirstFocus on a scope with no manager.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
scope.attach(context);
final FocusScopeNode parent = FocusScopeNode(debugLabel: 'Parent');
parent.attach(context);
final FocusScopeNode child1 = FocusScopeNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusScopeNode child2 = FocusScopeNode(debugLabel: 'Child 2');
child2.attach(context);
scope.setFirstFocus(parent);
parent.setFirstFocus(child1);
parent.setFirstFocus(child2);
child1.requestFocus();
await tester.pump();
expect(scope.hasFocus, isFalse);
expect(child1.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(scope.focusedChild, equals(parent));
expect(parent.focusedChild, equals(child1));
child1Attachment.detach();
expect(scope.hasFocus, isFalse);
expect(scope.focusedChild, equals(parent));
});
testWidgets('Removing a node removes it from scope.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode();
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent = FocusNode();
final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode();
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode();
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parentAttachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent);
child2Attachment.reparent(parent: parent);
child1.requestFocus();
await tester.pump();
expect(scope.hasFocus, isTrue);
expect(child1.hasFocus, isTrue);
expect(child1.hasPrimaryFocus, isTrue);
expect(scope.focusedChild, equals(child1));
child1Attachment.detach();
expect(scope.hasFocus, isFalse);
expect(scope.focusedChild, isNull);
});
testWidgets('Can add children to scope and focus', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode();
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent = FocusNode();
final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode();
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode();
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parentAttachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent);
child2Attachment.reparent(parent: parent);
expect(scope.children.first, equals(parent));
expect(parent.parent, equals(scope));
expect(child1.parent, equals(parent));
expect(child2.parent, equals(parent));
expect(parent.children.first, equals(child1));
expect(parent.children.last, equals(child2));
child1.requestFocus();
await tester.pump();
expect(scope.focusedChild, equals(child1));
expect(parent.hasFocus, isTrue);
expect(parent.hasPrimaryFocus, isFalse);
expect(child1.hasFocus, isTrue);
expect(child1.hasPrimaryFocus, isTrue);
expect(child2.hasFocus, isFalse);
expect(child2.hasPrimaryFocus, isFalse);
child2.requestFocus();
await tester.pump();
expect(scope.focusedChild, equals(child2));
expect(parent.hasFocus, isTrue);
expect(parent.hasPrimaryFocus, isFalse);
expect(child1.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(child2.hasFocus, isTrue);
expect(child2.hasPrimaryFocus, isTrue);
});
testWidgets('Requesting focus before adding to tree results in a request after adding', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode();
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode child = FocusNode();
child.requestFocus();
expect(child.hasPrimaryFocus, isFalse); // not attached yet.
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
await tester.pump();
expect(scope.focusedChild, isNull);
expect(child.hasPrimaryFocus, isFalse); // not attached yet.
final FocusAttachment childAttachment = child.attach(context);
expect(child.hasPrimaryFocus, isFalse); // not parented yet.
childAttachment.reparent(parent: scope);
await tester.pump();
expect(child.hasPrimaryFocus, isTrue); // now attached and parented, so focus finally happened.
});
testWidgets('Autofocus works.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent = FocusNode(debugLabel: 'Parent');
final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parentAttachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent);
child2Attachment.reparent(parent: parent);
scope.autofocus(child2);
await tester.pump();
expect(scope.focusedChild, equals(child2));
expect(parent.hasFocus, isTrue);
expect(child1.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(child2.hasFocus, isTrue);
expect(child2.hasPrimaryFocus, isTrue);
child1.requestFocus();
scope.autofocus(child2);
await tester.pump();
expect(scope.focusedChild, equals(child1));
expect(parent.hasFocus, isTrue);
expect(child1.hasFocus, isTrue);
expect(child1.hasPrimaryFocus, isTrue);
expect(child2.hasFocus, isFalse);
expect(child2.hasPrimaryFocus, isFalse);
});
testWidgets('Adding a focusedChild to a scope sets scope as focusedChild in parent scope', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode();
final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode();
final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode child1 = FocusNode();
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode();
final FocusAttachment child2Attachment = child2.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: scope1);
child1Attachment.reparent(parent: scope1);
child2Attachment.reparent(parent: scope2);
child2.requestFocus();
await tester.pump();
expect(scope2.focusedChild, equals(child2));
expect(scope1.focusedChild, equals(scope2));
expect(child1.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(child2.hasFocus, isTrue);
expect(child2.hasPrimaryFocus, isTrue);
child1.requestFocus();
await tester.pump();
expect(scope2.focusedChild, equals(child2));
expect(scope1.focusedChild, equals(child1));
expect(child1.hasFocus, isTrue);
expect(child1.hasPrimaryFocus, isTrue);
expect(child2.hasFocus, isFalse);
expect(child2.hasPrimaryFocus, isFalse);
});
testWidgets('Can move node with focus without losing focus', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope);
parent2Attachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
expect(scope.children.first, equals(parent1));
expect(scope.children.last, equals(parent2));
expect(parent1.parent, equals(scope));
expect(parent2.parent, equals(scope));
expect(child1.parent, equals(parent1));
expect(child2.parent, equals(parent1));
expect(parent1.children.first, equals(child1));
expect(parent1.children.last, equals(child2));
child1.requestFocus();
await tester.pump();
child1Attachment.reparent(parent: parent2);
await tester.pump();
expect(scope.focusedChild, equals(child1));
expect(child1.parent, equals(parent2));
expect(child2.parent, equals(parent1));
expect(parent1.children.first, equals(child2));
expect(parent2.children.first, equals(child1));
});
testWidgets('canRequestFocus affects children.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope);
parent2Attachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child1.requestFocus();
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, equals(child1));
expect(scope.focusedChild, equals(child1));
expect(scope.traversalDescendants.contains(child1), isTrue);
expect(scope.traversalDescendants.contains(child2), isTrue);
scope.canRequestFocus = false;
await tester.pump();
child2.requestFocus();
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, isNot(equals(child2)));
expect(tester.binding.focusManager.primaryFocus, isNot(equals(child1)));
expect(scope.focusedChild, equals(child1));
expect(scope.traversalDescendants.contains(child1), isFalse);
expect(scope.traversalDescendants.contains(child2), isFalse);
});
testWidgets("skipTraversal doesn't affect children.", (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope);
parent2Attachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child1.requestFocus();
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, equals(child1));
expect(scope.focusedChild, equals(child1));
expect(tester.binding.focusManager.rootScope.traversalDescendants.contains(scope), isTrue);
expect(scope.traversalDescendants.contains(child1), isTrue);
expect(scope.traversalDescendants.contains(child2), isTrue);
scope.skipTraversal = true;
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, equals(child1));
expect(scope.focusedChild, equals(child1));
expect(tester.binding.focusManager.rootScope.traversalDescendants.contains(scope), isFalse);
expect(scope.traversalDescendants.contains(child1), isTrue);
expect(scope.traversalDescendants.contains(child2), isTrue);
});
testWidgets('Can move node between scopes and lose scope focus', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2');
final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'child3');
final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'child4');
final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope1);
parent2Attachment.reparent(parent: scope2);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child3Attachment.reparent(parent: parent2);
child4Attachment.reparent(parent: parent2);
child1.requestFocus();
await tester.pump();
expect(scope1.focusedChild, equals(child1));
expect(parent2.children.contains(child1), isFalse);
child1Attachment.reparent(parent: parent2);
await tester.pump();
expect(scope1.focusedChild, isNull);
expect(parent2.children.contains(child1), isTrue);
});
testWidgets('ancestors and descendants are computed and recomputed properly', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1');
final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2');
final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'child3');
final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'child4');
final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope1);
parent2Attachment.reparent(parent: scope2);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child3Attachment.reparent(parent: parent2);
child4Attachment.reparent(parent: parent2);
child4.requestFocus();
await tester.pump();
expect(child4.ancestors, equals(<FocusNode>[parent2, scope2, tester.binding.focusManager.rootScope]));
expect(tester.binding.focusManager.rootScope.descendants, equals(<FocusNode>[child1, child2, parent1, scope1, child3, child4, parent2, scope2]));
scope2Attachment.reparent(parent: child2);
await tester.pump();
expect(child4.ancestors, equals(<FocusNode>[parent2, scope2, child2, parent1, scope1, tester.binding.focusManager.rootScope]));
expect(tester.binding.focusManager.rootScope.descendants, equals(<FocusNode>[child1, child3, child4, parent2, scope2, child2, parent1, scope1]));
});
testWidgets('Can move focus between scopes and keep focus', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode();
final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode();
final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode();
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode();
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode();
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode();
final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode();
final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode();
final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope1);
parent2Attachment.reparent(parent: scope2);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child3Attachment.reparent(parent: parent2);
child4Attachment.reparent(parent: parent2);
child4.requestFocus();
await tester.pump();
child1.requestFocus();
await tester.pump();
expect(child4.hasFocus, isFalse);
expect(child4.hasPrimaryFocus, isFalse);
expect(child1.hasFocus, isTrue);
expect(child1.hasPrimaryFocus, isTrue);
expect(scope1.hasFocus, isTrue);
expect(scope1.hasPrimaryFocus, isFalse);
expect(scope2.hasFocus, isFalse);
expect(scope2.hasPrimaryFocus, isFalse);
expect(parent1.hasFocus, isTrue);
expect(parent2.hasFocus, isFalse);
expect(scope1.focusedChild, equals(child1));
expect(scope2.focusedChild, equals(child4));
scope2.requestFocus();
await tester.pump();
expect(child4.hasFocus, isTrue);
expect(child4.hasPrimaryFocus, isTrue);
expect(child1.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(scope1.hasFocus, isFalse);
expect(scope1.hasPrimaryFocus, isFalse);
expect(scope2.hasFocus, isTrue);
expect(scope2.hasPrimaryFocus, isFalse);
expect(parent1.hasFocus, isFalse);
expect(parent2.hasFocus, isTrue);
expect(scope1.focusedChild, equals(child1));
expect(scope2.focusedChild, equals(child4));
});
testWidgets('Unfocus with disposition previouslyFocusedChild works properly', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2');
final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'child3');
final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'child4');
final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope1);
parent2Attachment.reparent(parent: scope2);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child3Attachment.reparent(parent: parent2);
child4Attachment.reparent(parent: parent2);
// Build up a history.
child4.requestFocus();
await tester.pump();
child2.requestFocus();
await tester.pump();
child3.requestFocus();
await tester.pump();
child1.requestFocus();
await tester.pump();
expect(scope1.focusedChild, equals(child1));
expect(scope2.focusedChild, equals(child3));
child1.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
await tester.pump();
expect(scope1.focusedChild, equals(child2));
expect(scope2.focusedChild, equals(child3));
expect(scope1.hasFocus, isTrue);
expect(scope2.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(child2.hasPrimaryFocus, isTrue);
// Can re-focus child.
child1.requestFocus();
await tester.pump();
expect(scope1.focusedChild, equals(child1));
expect(scope2.focusedChild, equals(child3));
expect(scope1.hasFocus, isTrue);
expect(scope2.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isTrue);
expect(child3.hasPrimaryFocus, isFalse);
// The same thing happens when unfocusing a second time.
child1.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
await tester.pump();
expect(scope1.focusedChild, equals(child2));
expect(scope2.focusedChild, equals(child3));
expect(scope1.hasFocus, isTrue);
expect(scope2.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(child2.hasPrimaryFocus, isTrue);
// When the scope gets unfocused, then the sibling scope gets focus.
child1.requestFocus();
await tester.pump();
scope1.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
await tester.pump();
expect(scope1.focusedChild, equals(child1));
expect(scope2.focusedChild, equals(child3));
expect(scope1.hasFocus, isFalse);
expect(scope2.hasFocus, isTrue);
expect(child1.hasPrimaryFocus, isFalse);
expect(child3.hasPrimaryFocus, isTrue);
});
testWidgets('Unfocus with disposition scope works properly', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2');
final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'child3');
final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'child4');
final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope1);
parent2Attachment.reparent(parent: scope2);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child3Attachment.reparent(parent: parent2);
child4Attachment.reparent(parent: parent2);
// Build up a history.
child4.requestFocus();
await tester.pump();
child2.requestFocus();
await tester.pump();
child3.requestFocus();
await tester.pump();
child1.requestFocus();
await tester.pump();
expect(scope1.focusedChild, equals(child1));
expect(scope2.focusedChild, equals(child3));
child1.unfocus();
await tester.pump();
// Focused child doesn't change.
expect(scope1.focusedChild, isNull);
expect(scope2.focusedChild, equals(child3));
// Focus does change.
expect(scope1.hasPrimaryFocus, isTrue);
expect(scope2.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(child2.hasPrimaryFocus, isFalse);
// Can re-focus child.
child1.requestFocus();
await tester.pump();
expect(scope1.focusedChild, equals(child1));
expect(scope2.focusedChild, equals(child3));
expect(scope1.hasFocus, isTrue);
expect(scope2.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isTrue);
expect(child3.hasPrimaryFocus, isFalse);
// The same thing happens when unfocusing a second time.
child1.unfocus();
await tester.pump();
expect(scope1.focusedChild, isNull);
expect(scope2.focusedChild, equals(child3));
expect(scope1.hasPrimaryFocus, isTrue);
expect(scope2.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(child2.hasPrimaryFocus, isFalse);
// When the scope gets unfocused, then its parent scope (the root scope)
// gets focus, but it doesn't mess with the focused children.
child1.requestFocus();
await tester.pump();
scope1.unfocus();
await tester.pump();
expect(scope1.focusedChild, equals(child1));
expect(scope2.focusedChild, equals(child3));
expect(scope1.hasFocus, isFalse);
expect(scope2.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(child3.hasPrimaryFocus, isFalse);
expect(FocusManager.instance.rootScope.hasPrimaryFocus, isTrue);
});
testWidgets('Unfocus works properly when some nodes are unfocusable', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2');
final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'child3');
final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'child4');
final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope1);
parent2Attachment.reparent(parent: scope2);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child3Attachment.reparent(parent: parent2);
child4Attachment.reparent(parent: parent2);
// Build up a history.
child4.requestFocus();
await tester.pump();
child2.requestFocus();
await tester.pump();
child3.requestFocus();
await tester.pump();
child1.requestFocus();
await tester.pump();
expect(child1.hasPrimaryFocus, isTrue);
scope1.canRequestFocus = false;
await tester.pump();
expect(scope1.focusedChild, equals(child1));
expect(scope2.focusedChild, equals(child3));
expect(child3.hasPrimaryFocus, isTrue);
child1.unfocus();
await tester.pump();
expect(child3.hasPrimaryFocus, isTrue);
expect(scope1.focusedChild, equals(child1));
expect(scope2.focusedChild, equals(child3));
expect(scope1.hasPrimaryFocus, isFalse);
expect(scope2.hasFocus, isTrue);
expect(child1.hasPrimaryFocus, isFalse);
expect(child2.hasPrimaryFocus, isFalse);
child1.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
await tester.pump();
expect(child3.hasPrimaryFocus, isTrue);
expect(scope1.focusedChild, equals(child1));
expect(scope2.focusedChild, equals(child3));
expect(scope1.hasPrimaryFocus, isFalse);
expect(scope2.hasFocus, isTrue);
expect(child1.hasPrimaryFocus, isFalse);
expect(child2.hasPrimaryFocus, isFalse);
});
testWidgets('Requesting focus on a scope works properly when some focusedChild nodes are unfocusable', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2');
final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'child3');
final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'child4');
final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope1);
parent2Attachment.reparent(parent: scope2);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child3Attachment.reparent(parent: parent2);
child4Attachment.reparent(parent: parent2);
// Build up a history.
child4.requestFocus();
await tester.pump();
child2.requestFocus();
await tester.pump();
child3.requestFocus();
await tester.pump();
child1.requestFocus();
await tester.pump();
expect(child1.hasPrimaryFocus, isTrue);
child1.canRequestFocus = false;
child3.canRequestFocus = false;
await tester.pump();
scope1.requestFocus();
await tester.pump();
expect(scope1.focusedChild, equals(child2));
expect(child2.hasPrimaryFocus, isTrue);
scope2.requestFocus();
await tester.pump();
expect(scope2.focusedChild, equals(child4));
expect(child4.hasPrimaryFocus, isTrue);
});
testWidgets('Key handling bubbles up and terminates when handled.', (WidgetTester tester) async {
final Set<FocusNode> receivedAnEvent = <FocusNode>{};
final Set<FocusNode> shouldHandle = <FocusNode>{};
KeyEventResult handleEvent(FocusNode node, RawKeyEvent event) {
if (shouldHandle.contains(node)) {
receivedAnEvent.add(node);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
Future<void> sendEvent() async {
receivedAnEvent.clear();
await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
}
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'Scope 1');
final FocusAttachment scope1Attachment = scope1.attach(context, onKey: handleEvent);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'Scope 2');
final FocusAttachment scope2Attachment = scope2.attach(context, onKey: handleEvent);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1', onKey: handleEvent);
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2', onKey: handleEvent);
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context, onKey: handleEvent);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
final FocusAttachment child2Attachment = child2.attach(context, onKey: handleEvent);
final FocusNode child3 = FocusNode(debugLabel: 'Child 3');
final FocusAttachment child3Attachment = child3.attach(context, onKey: handleEvent);
final FocusNode child4 = FocusNode(debugLabel: 'Child 4');
final FocusAttachment child4Attachment = child4.attach(context, onKey: handleEvent);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope1);
parent2Attachment.reparent(parent: scope2);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child3Attachment.reparent(parent: parent2);
child4Attachment.reparent(parent: parent2);
child4.requestFocus();
await tester.pump();
shouldHandle.addAll(<FocusNode>{scope2, parent2, child2, child4});
await sendEvent();
expect(receivedAnEvent, equals(<FocusNode>{child4}));
shouldHandle.remove(child4);
await sendEvent();
expect(receivedAnEvent, equals(<FocusNode>{parent2}));
shouldHandle.remove(parent2);
await sendEvent();
expect(receivedAnEvent, equals(<FocusNode>{scope2}));
shouldHandle.clear();
await sendEvent();
expect(receivedAnEvent, isEmpty);
child1.requestFocus();
await tester.pump();
shouldHandle.addAll(<FocusNode>{scope2, parent2, child2, child4});
await sendEvent();
// Since none of the focused nodes handle this event, nothing should
// receive it.
expect(receivedAnEvent, isEmpty);
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Initial highlight mode guesses correctly.', (WidgetTester tester) async {
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.automatic;
switch (defaultTargetPlatform) {
case TargetPlatform.fuchsia:
case TargetPlatform.android:
case TargetPlatform.iOS:
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
break;
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
break;
}
}, variant: TargetPlatformVariant.all());
testWidgets('Mouse events change initial focus highlight mode on mobile.', (WidgetTester tester) async {
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
RendererBinding.instance!.initMouseTracker(); // Clear out the mouse state.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 0);
addTearDown(gesture.removePointer);
await gesture.moveTo(Offset.zero);
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
}, variant: TargetPlatformVariant.mobile());
testWidgets('Mouse events change initial focus highlight mode on desktop.', (WidgetTester tester) async {
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
RendererBinding.instance!.initMouseTracker(); // Clear out the mouse state.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 0);
addTearDown(gesture.removePointer);
await gesture.moveTo(Offset.zero);
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
}, variant: TargetPlatformVariant.desktop());
testWidgets('Keyboard events change initial focus highlight mode.', (WidgetTester tester) async {
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
}, variant: TargetPlatformVariant.all());
testWidgets('Events change focus highlight mode.', (WidgetTester tester) async {
await setupWidget(tester);
int callCount = 0;
FocusHighlightMode? lastMode;
void handleModeChange(FocusHighlightMode mode) {
lastMode = mode;
callCount++;
}
FocusManager.instance.addHighlightModeListener(handleModeChange);
addTearDown(() => FocusManager.instance.removeHighlightModeListener(handleModeChange));
expect(callCount, equals(0));
expect(lastMode, isNull);
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.automatic;
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
expect(callCount, equals(1));
expect(lastMode, FocusHighlightMode.traditional);
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
await tester.tap(find.byType(Container), warnIfMissed: false);
expect(callCount, equals(2));
expect(lastMode, FocusHighlightMode.touch);
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
final TestGesture gesture = await tester.startGesture(Offset.zero, kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.up();
expect(callCount, equals(3));
expect(lastMode, FocusHighlightMode.traditional);
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
await tester.tap(find.byType(Container), warnIfMissed: false);
expect(callCount, equals(4));
expect(lastMode, FocusHighlightMode.touch);
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
expect(callCount, equals(5));
expect(lastMode, FocusHighlightMode.traditional);
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
expect(callCount, equals(6));
expect(lastMode, FocusHighlightMode.touch);
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
});
testWidgets('implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
FocusScopeNode(
debugLabel: 'Scope Label',
).debugFillProperties(builder);
final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[
'context: null',
'descendantsAreFocusable: true',
'canRequestFocus: true',
'hasFocus: false',
'hasPrimaryFocus: false',
]);
});
testWidgets('debugDescribeFocusTree produces correct output', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'Scope 1');
final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(); // No label, Just to test that it works.
final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(); // No label, Just to test that it works.
final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'Child 3');
final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'Child 4');
final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope1);
parent2Attachment.reparent(parent: scope2);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child3Attachment.reparent(parent: parent2);
child4Attachment.reparent(parent: parent2);
child4.requestFocus();
await tester.pump();
final String description = debugDescribeFocusTree();
expect(
description,
equalsIgnoringHashCodes(
'FocusManager#00000\n'
' │ primaryFocus: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n'
' │ primaryFocusCreator: Container-[GlobalKey#00000] ← [root]\n'
' │\n'
' └─rootScope: FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n'
' │ IN FOCUS PATH\n'
' │ focusedChildren: FocusScopeNode#00000([IN FOCUS PATH])\n'
' │\n'
' ├─Child 1: FocusScopeNode#00000(Scope 1)\n'
' │ │ context: Container-[GlobalKey#00000]\n'
' │ │\n'
' │ └─Child 1: FocusNode#00000(Parent 1)\n'
' │ │ context: Container-[GlobalKey#00000]\n'
' │ │\n'
' │ ├─Child 1: FocusNode#00000(Child 1)\n'
' │ │ context: Container-[GlobalKey#00000]\n'
' │ │\n'
' │ └─Child 2: FocusNode#00000\n'
' │ context: Container-[GlobalKey#00000]\n'
' │\n'
' └─Child 2: FocusScopeNode#00000([IN FOCUS PATH])\n'
' │ context: Container-[GlobalKey#00000]\n'
' │ IN FOCUS PATH\n'
' │ focusedChildren: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n'
' │\n'
' └─Child 1: FocusNode#00000(Parent 2 [IN FOCUS PATH])\n'
' │ context: Container-[GlobalKey#00000]\n'
' │ IN FOCUS PATH\n'
' │\n'
' ├─Child 1: FocusNode#00000(Child 3)\n'
' │ context: Container-[GlobalKey#00000]\n'
' │\n'
' └─Child 2: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n'
' context: Container-[GlobalKey#00000]\n'
' PRIMARY FOCUS\n',
),
);
});
});
group('Autofocus', () {
testWidgets(
'works when the previous focused node is detached',
(WidgetTester tester) async {
final FocusNode node1 = FocusNode();
final FocusNode node2 = FocusNode();
await tester.pumpWidget(
FocusScope(
child: Focus(autofocus: true, focusNode: node1, child: const Placeholder()),
),
);
await tester.pump();
expect(node1.hasPrimaryFocus, isTrue);
await tester.pumpWidget(
FocusScope(
child: SizedBox(
child: Focus(autofocus: true, focusNode: node2, child: const Placeholder()),
),
),
);
await tester.pump();
expect(node2.hasPrimaryFocus, isTrue);
});
testWidgets(
'node detached before autofocus is applied',
(WidgetTester tester) async {
final FocusScopeNode scopeNode = FocusScopeNode();
final FocusNode node1 = FocusNode();
await tester.pumpWidget(
FocusScope(
node: scopeNode,
child: Focus(
autofocus: true,
focusNode: node1,
child: const Placeholder(),
),
),
);
await tester.pumpWidget(
FocusScope(
node: scopeNode,
child: const Focus(child: Placeholder()),
),
);
await tester.pump();
expect(node1.hasPrimaryFocus, isFalse);
expect(scopeNode.hasPrimaryFocus, isTrue);
});
testWidgets('autofocus the first candidate', (WidgetTester tester) async {
final FocusNode node1 = FocusNode();
final FocusNode node2 = FocusNode();
final FocusNode node3 = FocusNode();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Column(
children: <Focus>[
Focus(
autofocus: true,
focusNode: node1,
child: const SizedBox(),
),
Focus(
autofocus: true,
focusNode: node2,
child: const SizedBox(),
),
Focus(
autofocus: true,
focusNode: node3,
child: const SizedBox(),
),
],
),
),
);
expect(node1.hasPrimaryFocus, isTrue);
});
testWidgets('Autofocus works with global key reparenting', (WidgetTester tester) async {
final FocusNode node = FocusNode();
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1');
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Column(
children: <Focus>[
FocusScope(
node: scope1,
child: Focus(
key: key,
focusNode: node,
child: const SizedBox(),
),
),
FocusScope(node: scope2, child: const SizedBox()),
],
),
),
);
// _applyFocusChange will be called before persistentCallbacks,
// guaranteeing the focus changes are applied before the BuildContext
// `node` attaches to gets reparented.
scope1.autofocus(node);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Column(
children: <Focus>[
FocusScope(node: scope1, child: const SizedBox()),
FocusScope(
node: scope2,
child: Focus(
key: key,
focusNode: node,
child: const SizedBox(),
),
),
],
),
),
);
expect(node.hasPrimaryFocus, isTrue);
expect(scope2.hasFocus, isTrue);
});
});
testWidgets("Doesn't lose focused child when reparenting if the nearestScope doesn't change.", (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
final FocusScopeNode parent2 = FocusScopeNode(debugLabel: 'parent2');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2');
final FocusAttachment child2Attachment = child2.attach(context);
parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: child1);
parent1.autofocus(child2);
await tester.pump();
parent2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent2.requestFocus();
await tester.pump();
expect(parent1.focusedChild, equals(child2));
child2Attachment.reparent(parent: parent1);
expect(parent1.focusedChild, equals(child2));
parent1.requestFocus();
await tester.pump();
expect(parent1.focusedChild, equals(child2));
});
testWidgets('Ancestors get notified exactly as often as needed if focused child changes focus.', (WidgetTester tester) async {
bool topFocus = false;
bool parent1Focus = false;
bool parent2Focus = false;
bool child1Focus = false;
bool child2Focus = false;
int topNotify = 0;
int parent1Notify = 0;
int parent2Notify = 0;
int child1Notify = 0;
int child2Notify = 0;
void clear() {
topFocus = false;
parent1Focus = false;
parent2Focus = false;
child1Focus = false;
child2Focus = false;
topNotify = 0;
parent1Notify = 0;
parent2Notify = 0;
child1Notify = 0;
child2Notify = 0;
}
final BuildContext context = await setupWidget(tester);
final FocusScopeNode top = FocusScopeNode(debugLabel: 'top');
final FocusAttachment topAttachment = top.attach(context);
final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusScopeNode parent2 = FocusScopeNode(debugLabel: 'parent2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2');
final FocusAttachment child2Attachment = child2.attach(context);
topAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: top);
parent2Attachment.reparent(parent: top);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent2);
top.addListener(() {
topNotify++;
topFocus = top.hasFocus;
});
parent1.addListener(() {
parent1Notify++;
parent1Focus = parent1.hasFocus;
});
parent2.addListener(() {
parent2Notify++;
parent2Focus = parent2.hasFocus;
});
child1.addListener(() {
child1Notify++;
child1Focus = child1.hasFocus;
});
child2.addListener(() {
child2Notify++;
child2Focus = child2.hasFocus;
});
child1.requestFocus();
await tester.pump();
expect(topFocus, isTrue);
expect(parent1Focus, isTrue);
expect(child1Focus, isTrue);
expect(parent2Focus, isFalse);
expect(child2Focus, isFalse);
expect(topNotify, equals(1));
expect(parent1Notify, equals(1));
expect(child1Notify, equals(1));
expect(parent2Notify, equals(0));
expect(child2Notify, equals(0));
clear();
child1.unfocus();
await tester.pump();
expect(topFocus, isFalse);
expect(parent1Focus, isTrue);
expect(child1Focus, isFalse);
expect(parent2Focus, isFalse);
expect(child2Focus, isFalse);
expect(topNotify, equals(0));
expect(parent1Notify, equals(1));
expect(child1Notify, equals(1));
expect(parent2Notify, equals(0));
expect(child2Notify, equals(0));
clear();
child1.requestFocus();
await tester.pump();
expect(topFocus, isFalse);
expect(parent1Focus, isTrue);
expect(child1Focus, isTrue);
expect(parent2Focus, isFalse);
expect(child2Focus, isFalse);
expect(topNotify, equals(0));
expect(parent1Notify, equals(1));
expect(child1Notify, equals(1));
expect(parent2Notify, equals(0));
expect(child2Notify, equals(0));
clear();
child2.requestFocus();
await tester.pump();
expect(topFocus, isFalse);
expect(parent1Focus, isFalse);
expect(child1Focus, isFalse);
expect(parent2Focus, isTrue);
expect(child2Focus, isTrue);
expect(topNotify, equals(0));
expect(parent1Notify, equals(1));
expect(child1Notify, equals(1));
expect(parent2Notify, equals(1));
expect(child2Notify, equals(1));
// Changing the focus back before the pump shouldn't cause notifications.
clear();
child1.requestFocus();
child2.requestFocus();
await tester.pump();
expect(topFocus, isFalse);
expect(parent1Focus, isFalse);
expect(child1Focus, isFalse);
expect(parent2Focus, isFalse);
expect(child2Focus, isFalse);
expect(topNotify, equals(0));
expect(parent1Notify, equals(0));
expect(child1Notify, equals(0));
expect(parent2Notify, equals(0));
expect(child2Notify, equals(0));
});
testWidgets('Focus changes notify listeners.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'child2');
final FocusAttachment child2Attachment = child2.attach(context);
parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: child1);
int notifyCount = 0;
void handleFocusChange() {
notifyCount++;
}
tester.binding.focusManager.addListener(handleFocusChange);
parent1.autofocus(child2);
expect(notifyCount, equals(0));
await tester.pump();
expect(notifyCount, equals(1));
notifyCount = 0;
child1.requestFocus();
child2.requestFocus();
child1.requestFocus();
await tester.pump();
expect(notifyCount, equals(1));
notifyCount = 0;
child2.requestFocus();
await tester.pump();
expect(notifyCount, equals(1));
notifyCount = 0;
child2.unfocus();
await tester.pump();
expect(notifyCount, equals(1));
notifyCount = 0;
tester.binding.focusManager.removeListener(handleFocusChange);
});
testWidgets('FocusManager notifies listeners when a widget loses focus because it was removed.', (WidgetTester tester) async {
final FocusNode nodeA = FocusNode(debugLabel: 'a');
final FocusNode nodeB = FocusNode(debugLabel: 'b');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: Column(
children: <Widget>[
Focus(focusNode: nodeA , child: const Text('a')),
Focus(focusNode: nodeB, child: const Text('b')),
],
),
),
);
int notifyCount = 0;
void handleFocusChange() {
notifyCount++;
}
tester.binding.focusManager.addListener(handleFocusChange);
nodeA.requestFocus();
await tester.pump();
expect(nodeA.hasPrimaryFocus, isTrue);
expect(notifyCount, equals(1));
notifyCount = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: Column(
children: <Widget>[
Focus(focusNode: nodeB, child: const Text('b')),
],
),
),
);
await tester.pump();
expect(nodeA.hasPrimaryFocus, isFalse);
expect(nodeB.hasPrimaryFocus, isFalse);
expect(notifyCount, equals(1));
notifyCount = 0;
tester.binding.focusManager.removeListener(handleFocusChange);
});
testWidgets('debugFocusChanges causes logging of focus changes', (WidgetTester tester) async {
final bool oldDebugFocusChanges = debugFocusChanges;
final DebugPrintCallback oldDebugPrint = debugPrint;
final StringBuffer messages = StringBuffer();
debugPrint = (String? message, {int? wrapWidth}) {
messages.writeln(message ?? '');
};
debugFocusChanges = true;
try {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'child1');
final FocusAttachment child1Attachment = child1.attach(context);
parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
child1Attachment.reparent(parent: parent1);
int notifyCount = 0;
void handleFocusChange() {
notifyCount++;
}
tester.binding.focusManager.addListener(handleFocusChange);
parent1.requestFocus();
expect(notifyCount, equals(0));
await tester.pump();
expect(notifyCount, equals(1));
notifyCount = 0;
child1.requestFocus();
await tester.pump();
expect(notifyCount, equals(1));
notifyCount = 0;
tester.binding.focusManager.removeListener(handleFocusChange);
} finally {
debugFocusChanges = oldDebugFocusChanges;
debugPrint = oldDebugPrint;
}
final String messagesStr = messages.toString();
expect(messagesStr.split('\n').length, equals(58));
expect(messagesStr, contains(RegExp(r' └─Child 1: FocusScopeNode#[a-f0-9]{5}\(parent1 \[PRIMARY FOCUS\]\)')));
expect(messagesStr, contains('FOCUS: Notifying 2 dirty nodes'));
expect(messagesStr, contains(RegExp(r'FOCUS: Scheduling update, current focus is null, next focus will be FocusScopeNode#.*parent1')));
});
}