// 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('descendantsAreTraversable disables traversal 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); expect(scope.traversalDescendants, equals(<FocusNode>[child1, parent1, child2, parent2])); parent2.descendantsAreTraversable = false; expect(scope.traversalDescendants, equals(<FocusNode>[child1, parent1, parent2])); parent1.descendantsAreTraversable = false; expect(scope.traversalDescendants, equals(<FocusNode>[parent1, parent2])); parent1.descendantsAreTraversable = true; parent2.descendantsAreTraversable = true; scope.descendantsAreTraversable = false; expect(scope.traversalDescendants, equals(<FocusNode>[])); }); testWidgets("canRequestFocus doesn't affect traversalChildren", (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(parent2.traversalChildren.contains(child2), isTrue); expect(scope.traversalChildren.contains(parent2), isTrue); parent2.canRequestFocus = false; await tester.pump(); expect(parent2.traversalChildren.contains(child2), isTrue); expect(scope.traversalChildren.contains(parent2), 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', 'descendantsAreTraversable: 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); expect(scope.traversalChildren.contains(parent1), isTrue); expect(parent1.traversalChildren.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); expect(scope.traversalChildren.contains(parent1), isFalse); expect(parent1.traversalChildren.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); 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); 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); 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', 'descendantsAreTraversable: 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: Notified 2 dirty nodes')); expect(messagesStr, contains(RegExp(r'FOCUS: Scheduling update, current focus is null, next focus will be FocusScopeNode#.*parent1'))); }); }