focus_manager_test.dart 83.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:math' as math;

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/gestures.dart';
9
import 'package:flutter/material.dart';
10
import 'package:flutter/rendering.dart';
11 12 13
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
14
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
15 16 17 18 19

void main() {
  final GlobalKey widgetKey = GlobalKey();
  Future<BuildContext> setupWidget(WidgetTester tester) async {
    await tester.pumpWidget(Container(key: widgetKey));
20
    return widgetKey.currentContext!;
21 22 23
  }

  group(FocusNode, () {
24
    testWidgetsWithLeakTracking('Can add children.', (WidgetTester tester) async {
25 26
      final BuildContext context = await setupWidget(tester);
      final FocusNode parent = FocusNode();
27
      addTearDown(parent.dispose);
28 29
      final FocusAttachment parentAttachment = parent.attach(context);
      final FocusNode child1 = FocusNode();
30
      addTearDown(child1.dispose);
31 32
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode();
33
      addTearDown(child2.dispose);
34 35 36 37 38 39 40 41 42 43 44 45
      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));
    });
46

47
    testWidgetsWithLeakTracking('Can remove children.', (WidgetTester tester) async {
48 49
      final BuildContext context = await setupWidget(tester);
      final FocusNode parent = FocusNode();
50
      addTearDown(parent.dispose);
51 52
      final FocusAttachment parentAttachment = parent.attach(context);
      final FocusNode child1 = FocusNode();
53
      addTearDown(child1.dispose);
54 55
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode();
56
      addTearDown(child2.dispose);
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
      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);
    });
75

76
    testWidgetsWithLeakTracking('Geometry is transformed properly.', (WidgetTester tester) async {
77
      final FocusNode focusNode1 = FocusNode(debugLabel: 'Test Node 1');
78
      addTearDown(focusNode1.dispose);
79
      final FocusNode focusNode2 = FocusNode(debugLabel: 'Test Node 2');
80 81
      addTearDown(focusNode2.dispose);

82 83 84 85 86
      await tester.pumpWidget(
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            children: <Widget>[
87 88 89 90
              Focus(
                focusNode: focusNode1,
                child: const SizedBox(width: 200, height: 100),
              ),
91 92 93 94 95 96
              Transform.translate(
                offset: const Offset(10, 20),
                child: Transform.scale(
                  scale: 0.33,
                  child: Transform.rotate(
                    angle: math.pi,
97
                    child: Focus(focusNode: focusNode2, child: const SizedBox(width: 200, height: 100)),
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
                  ),
                ),
              ),
            ],
          ),
        ),
      );
      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)));
    });
115

116
    testWidgetsWithLeakTracking('descendantsAreFocusable disables focus for descendants.', (WidgetTester tester) async {
117 118
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
119
      addTearDown(scope.dispose);
120 121
      final FocusAttachment scopeAttachment = scope.attach(context);
      final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
122
      addTearDown(parent1.dispose);
123 124
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
125
      addTearDown(parent2.dispose);
126 127
      final FocusAttachment parent2Attachment = parent2.attach(context);
      final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
128
      addTearDown(child1.dispose);
129 130
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
131
      addTearDown(child2.dispose);
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
      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);
    });
168

169
    testWidgetsWithLeakTracking('descendantsAreTraversable disables traversal for descendants.', (WidgetTester tester) async {
170 171
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
172
      addTearDown(scope.dispose);
173 174
      final FocusAttachment scopeAttachment = scope.attach(context);
      final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
175
      addTearDown(parent1.dispose);
176 177
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
178
      addTearDown(parent2.dispose);
179 180
      final FocusAttachment parent2Attachment = parent2.attach(context);
      final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
181
      addTearDown(child1.dispose);
182 183
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
184
      addTearDown(child2.dispose);
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
      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>[]));
    });

207
    testWidgetsWithLeakTracking("canRequestFocus doesn't affect traversalChildren", (WidgetTester tester) async {
208 209
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
210
      addTearDown(scope.dispose);
211 212
      final FocusAttachment scopeAttachment = scope.attach(context);
      final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
213
      addTearDown(parent1.dispose);
214 215
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
216
      addTearDown(parent2.dispose);
217 218
      final FocusAttachment parent2Attachment = parent2.attach(context);
      final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
219
      addTearDown(child1.dispose);
220 221
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
222
      addTearDown(child2.dispose);
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
      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);
    });

243
    testWidgetsWithLeakTracking('implements debugFillProperties', (WidgetTester tester) async {
244
      final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
245 246 247
      final FocusNode focusNode = FocusNode(debugLabel: 'Label');
      addTearDown(focusNode.dispose);
      focusNode.debugFillProperties(builder);
248
      final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
249
      expect(description, <String>[
250
        'context: null',
251
        'descendantsAreFocusable: true',
252
        'descendantsAreTraversable: true',
253 254
        'canRequestFocus: true',
        'hasFocus: false',
255
        'hasPrimaryFocus: false',
256 257
      ]);
    });
258

259 260 261 262 263 264 265
    testWidgetsWithLeakTracking('onKeyEvent and onKey correctly cooperate', (WidgetTester tester) async {
      final FocusNode focusNode1 = FocusNode(debugLabel: 'Test Node 1');
      addTearDown(focusNode1.dispose);
      final FocusNode focusNode2 = FocusNode(debugLabel: 'Test Node 2');
      addTearDown(focusNode2.dispose);
      final FocusNode focusNode3 = FocusNode(debugLabel: 'Test Node 3');
      addTearDown(focusNode3.dispose);
266 267 268 269 270 271 272 273 274
      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(
275
          focusNode: focusNode1,
276 277 278 279 280 281 282 283 284
          onKeyEvent: (_, KeyEvent event) {
            logs.add(0);
            return results[0][0];
          },
          onKey: (_, RawKeyEvent event) {
            logs.add(1);
            return results[0][1];
          },
          child: Focus(
285
            focusNode: focusNode2,
286 287 288 289 290 291 292 293 294
            onKeyEvent: (_, KeyEvent event) {
              logs.add(10);
              return results[1][0];
            },
            onKey: (_, RawKeyEvent event) {
              logs.add(11);
              return results[1][1];
            },
            child: Focus(
295
              focusNode: focusNode3,
296 297 298 299 300 301 302 303 304 305 306 307 308
              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),
            ),
          ),
        ),
      );
309
      focusNode3.requestFocus();
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
      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());
356
  });
357

358
  group(FocusScopeNode, () {
359

360
    testWidgetsWithLeakTracking('Can setFirstFocus on a scope with no manager.', (WidgetTester tester) async {
361 362
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
363
      addTearDown(scope.dispose);
364 365
      scope.attach(context);
      final FocusScopeNode parent = FocusScopeNode(debugLabel: 'Parent');
366
      addTearDown(parent.dispose);
367 368
      parent.attach(context);
      final FocusScopeNode child1 = FocusScopeNode(debugLabel: 'Child 1');
369
      addTearDown(child1.dispose);
370 371
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusScopeNode child2 = FocusScopeNode(debugLabel: 'Child 2');
372
      addTearDown(child2.dispose);
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
      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));
    });
388

389
    testWidgetsWithLeakTracking('Removing a node removes it from scope.', (WidgetTester tester) async {
390 391
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope = FocusScopeNode();
392
      addTearDown(scope.dispose);
393 394
      final FocusAttachment scopeAttachment = scope.attach(context);
      final FocusNode parent = FocusNode();
395
      addTearDown(parent.dispose);
396 397
      final FocusAttachment parentAttachment = parent.attach(context);
      final FocusNode child1 = FocusNode();
398
      addTearDown(child1.dispose);
399 400
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode();
401
      addTearDown(child2.dispose);
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
      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);
    });
417

418
    testWidgetsWithLeakTracking('Can add children to scope and focus', (WidgetTester tester) async {
419 420
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope = FocusScopeNode();
421
      addTearDown(scope.dispose);
422 423
      final FocusAttachment scopeAttachment = scope.attach(context);
      final FocusNode parent = FocusNode();
424
      addTearDown(parent.dispose);
425 426
      final FocusAttachment parentAttachment = parent.attach(context);
      final FocusNode child1 = FocusNode();
427
      addTearDown(child1.dispose);
428 429
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode();
430
      addTearDown(child2.dispose);
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
      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);
    });
461

462
    testWidgetsWithLeakTracking('Requesting focus before adding to tree results in a request after adding', (WidgetTester tester) async {
463 464
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope = FocusScopeNode();
465
      addTearDown(scope.dispose);
466 467
      final FocusAttachment scopeAttachment = scope.attach(context);
      final FocusNode child = FocusNode();
468
      addTearDown(child.dispose);
469 470 471 472 473 474 475 476 477 478 479 480 481 482
      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.
    });
483

484
    testWidgetsWithLeakTracking('Autofocus works.', (WidgetTester tester) async {
485 486
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
487
      addTearDown(scope.dispose);
488 489
      final FocusAttachment scopeAttachment = scope.attach(context);
      final FocusNode parent = FocusNode(debugLabel: 'Parent');
490
      addTearDown(parent.dispose);
491 492
      final FocusAttachment parentAttachment = parent.attach(context);
      final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
493
      addTearDown(child1.dispose);
494 495
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
496
      addTearDown(child2.dispose);
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
      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);
    });
524

525
    testWidgetsWithLeakTracking('Adding a focusedChild to a scope sets scope as focusedChild in parent scope', (WidgetTester tester) async {
526 527
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope1 = FocusScopeNode();
528
      addTearDown(scope1.dispose);
529 530
      final FocusAttachment scope1Attachment = scope1.attach(context);
      final FocusScopeNode scope2 = FocusScopeNode();
531
      addTearDown(scope2.dispose);
532 533
      final FocusAttachment scope2Attachment = scope2.attach(context);
      final FocusNode child1 = FocusNode();
534
      addTearDown(child1.dispose);
535 536
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode();
537
      addTearDown(child2.dispose);
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559
      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);
    });
560

561
    testWidgetsWithLeakTracking('Can move node with focus without losing focus', (WidgetTester tester) async {
562 563
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
564
      addTearDown(scope.dispose);
565 566
      final FocusAttachment scopeAttachment = scope.attach(context);
      final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
567
      addTearDown(parent1.dispose);
568 569
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
570
      addTearDown(parent2.dispose);
571 572
      final FocusAttachment parent2Attachment = parent2.attach(context);
      final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
573
      addTearDown(child1.dispose);
574 575
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
576
      addTearDown(child2.dispose);
577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
      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));
    });
602

603
    testWidgetsWithLeakTracking('canRequestFocus affects children.', (WidgetTester tester) async {
604
      final BuildContext context = await setupWidget(tester);
605
      final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
606
      addTearDown(scope.dispose);
607 608
      final FocusAttachment scopeAttachment = scope.attach(context);
      final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
609
      addTearDown(parent1.dispose);
610 611
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
612
      addTearDown(parent2.dispose);
613 614
      final FocusAttachment parent2Attachment = parent2.attach(context);
      final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
615
      addTearDown(child1.dispose);
616 617
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
618
      addTearDown(child2.dispose);
619 620 621 622 623 624 625 626 627 628 629 630 631
      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);
632 633
      expect(scope.traversalChildren.contains(parent1), isTrue);
      expect(parent1.traversalChildren.contains(child2), isTrue);
634 635 636 637 638 639 640

      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)));
641
      expect(scope.focusedChild, equals(child1));
642 643
      expect(scope.traversalDescendants.contains(child1), isFalse);
      expect(scope.traversalDescendants.contains(child2), isFalse);
644 645
      expect(scope.traversalChildren.contains(parent1), isFalse);
      expect(parent1.traversalChildren.contains(child2), isFalse);
646
    });
647

648
    testWidgetsWithLeakTracking("skipTraversal doesn't affect children.", (WidgetTester tester) async {
649
      final BuildContext context = await setupWidget(tester);
650
      final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
651
      addTearDown(scope.dispose);
652 653
      final FocusAttachment scopeAttachment = scope.attach(context);
      final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
654
      addTearDown(parent1.dispose);
655 656
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
657
      addTearDown(parent2.dispose);
658 659
      final FocusAttachment parent2Attachment = parent2.attach(context);
      final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
660
      addTearDown(child1.dispose);
661 662
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
663
      addTearDown(child2.dispose);
664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686
      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);
    });
687

688
    testWidgetsWithLeakTracking('Can move node between scopes and lose scope focus', (WidgetTester tester) async {
689
      final BuildContext context = await setupWidget(tester);
690
      final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
691
      addTearDown(scope1.dispose);
692
      final FocusAttachment scope1Attachment = scope1.attach(context);
693
      final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
694
      addTearDown(scope2.dispose);
695
      final FocusAttachment scope2Attachment = scope2.attach(context);
696
      final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
697
      addTearDown(parent1.dispose);
698
      final FocusAttachment parent1Attachment = parent1.attach(context);
699
      final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
700
      addTearDown(parent2.dispose);
701
      final FocusAttachment parent2Attachment = parent2.attach(context);
702
      final FocusNode child1 = FocusNode(debugLabel: 'child1');
703
      addTearDown(child1.dispose);
704
      final FocusAttachment child1Attachment = child1.attach(context);
705
      final FocusNode child2 = FocusNode(debugLabel: 'child2');
706
      addTearDown(child2.dispose);
707
      final FocusAttachment child2Attachment = child2.attach(context);
708
      final FocusNode child3 = FocusNode(debugLabel: 'child3');
709
      addTearDown(child3.dispose);
710
      final FocusAttachment child3Attachment = child3.attach(context);
711
      final FocusNode child4 = FocusNode(debugLabel: 'child4');
712
      addTearDown(child4.dispose);
713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732
      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);
    });
733

734
    testWidgetsWithLeakTracking('ancestors and descendants are computed and recomputed properly', (WidgetTester tester) async {
735 736
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1');
737
      addTearDown(scope1.dispose);
738 739
      final FocusAttachment scope1Attachment = scope1.attach(context);
      final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
740
      addTearDown(scope2.dispose);
741 742
      final FocusAttachment scope2Attachment = scope2.attach(context);
      final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
743
      addTearDown(parent1.dispose);
744 745
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
746
      addTearDown(parent2.dispose);
747 748
      final FocusAttachment parent2Attachment = parent2.attach(context);
      final FocusNode child1 = FocusNode(debugLabel: 'child1');
749
      addTearDown(child1.dispose);
750 751
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode(debugLabel: 'child2');
752
      addTearDown(child2.dispose);
753 754
      final FocusAttachment child2Attachment = child2.attach(context);
      final FocusNode child3 = FocusNode(debugLabel: 'child3');
755
      addTearDown(child3.dispose);
756 757
      final FocusAttachment child3Attachment = child3.attach(context);
      final FocusNode child4 = FocusNode(debugLabel: 'child4');
758
      addTearDown(child4.dispose);
759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776
      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]));
    });
777

778
    testWidgetsWithLeakTracking('Can move focus between scopes and keep focus', (WidgetTester tester) async {
779 780
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope1 = FocusScopeNode();
781
      addTearDown(scope1.dispose);
782 783
      final FocusAttachment scope1Attachment = scope1.attach(context);
      final FocusScopeNode scope2 = FocusScopeNode();
784
      addTearDown(scope2.dispose);
785 786
      final FocusAttachment scope2Attachment = scope2.attach(context);
      final FocusNode parent1 = FocusNode();
787
      addTearDown(parent1.dispose);
788 789
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode parent2 = FocusNode();
790
      addTearDown(parent2.dispose);
791 792
      final FocusAttachment parent2Attachment = parent2.attach(context);
      final FocusNode child1 = FocusNode();
793
      addTearDown(child1.dispose);
794 795
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode();
796
      addTearDown(child2.dispose);
797 798
      final FocusAttachment child2Attachment = child2.attach(context);
      final FocusNode child3 = FocusNode();
799
      addTearDown(child3.dispose);
800 801
      final FocusAttachment child3Attachment = child3.attach(context);
      final FocusNode child4 = FocusNode();
802
      addTearDown(child4.dispose);
803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842
      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));
    });
843

844
    testWidgetsWithLeakTracking('Unfocus with disposition previouslyFocusedChild works properly', (WidgetTester tester) async {
845
      final BuildContext context = await setupWidget(tester);
846
      final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
847
      addTearDown(scope1.dispose);
848
      final FocusAttachment scope1Attachment = scope1.attach(context);
849
      final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
850
      addTearDown(scope2.dispose);
851
      final FocusAttachment scope2Attachment = scope2.attach(context);
852
      final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
853
      addTearDown(parent1.dispose);
854
      final FocusAttachment parent1Attachment = parent1.attach(context);
855
      final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
856
      addTearDown(parent2.dispose);
857
      final FocusAttachment parent2Attachment = parent2.attach(context);
858
      final FocusNode child1 = FocusNode(debugLabel: 'child1');
859
      addTearDown(child1.dispose);
860
      final FocusAttachment child1Attachment = child1.attach(context);
861
      final FocusNode child2 = FocusNode(debugLabel: 'child2');
862
      addTearDown(child2.dispose);
863
      final FocusAttachment child2Attachment = child2.attach(context);
864
      final FocusNode child3 = FocusNode(debugLabel: 'child3');
865
      addTearDown(child3.dispose);
866
      final FocusAttachment child3Attachment = child3.attach(context);
867
      final FocusNode child4 = FocusNode(debugLabel: 'child4');
868
      addTearDown(child4.dispose);
869 870 871 872 873 874 875 876 877 878
      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);

879 880 881 882 883 884 885
      // Build up a history.
      child4.requestFocus();
      await tester.pump();
      child2.requestFocus();
      await tester.pump();
      child3.requestFocus();
      await tester.pump();
886 887 888
      child1.requestFocus();
      await tester.pump();
      expect(scope1.focusedChild, equals(child1));
889
      expect(scope2.focusedChild, equals(child3));
890

891
      child1.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
892
      await tester.pump();
893 894 895 896
      expect(scope1.focusedChild, equals(child2));
      expect(scope2.focusedChild, equals(child3));
      expect(scope1.hasFocus, isTrue);
      expect(scope2.hasFocus, isFalse);
897
      expect(child1.hasPrimaryFocus, isFalse);
898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926
      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));
927
      expect(scope1.hasFocus, isFalse);
928 929 930 931
      expect(scope2.hasFocus, isTrue);
      expect(child1.hasPrimaryFocus, isFalse);
      expect(child3.hasPrimaryFocus, isTrue);
    });
932

933
    testWidgetsWithLeakTracking('Unfocus with disposition scope works properly', (WidgetTester tester) async {
934 935
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
936
      addTearDown(scope1.dispose);
937 938
      final FocusAttachment scope1Attachment = scope1.attach(context);
      final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
939
      addTearDown(scope2.dispose);
940 941
      final FocusAttachment scope2Attachment = scope2.attach(context);
      final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
942
      addTearDown(parent1.dispose);
943 944
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
945
      addTearDown(parent2.dispose);
946 947
      final FocusAttachment parent2Attachment = parent2.attach(context);
      final FocusNode child1 = FocusNode(debugLabel: 'child1');
948
      addTearDown(child1.dispose);
949 950
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode(debugLabel: 'child2');
951
      addTearDown(child2.dispose);
952 953
      final FocusAttachment child2Attachment = child2.attach(context);
      final FocusNode child3 = FocusNode(debugLabel: 'child3');
954
      addTearDown(child3.dispose);
955 956
      final FocusAttachment child3Attachment = child3.attach(context);
      final FocusNode child4 = FocusNode(debugLabel: 'child4');
957
      addTearDown(child4.dispose);
958 959 960 961 962 963 964 965 966
      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);
967

968 969 970 971 972 973 974
      // Build up a history.
      child4.requestFocus();
      await tester.pump();
      child2.requestFocus();
      await tester.pump();
      child3.requestFocus();
      await tester.pump();
975 976 977
      child1.requestFocus();
      await tester.pump();
      expect(scope1.focusedChild, equals(child1));
978 979
      expect(scope2.focusedChild, equals(child3));

980
      child1.unfocus();
981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999
      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);
1000

1001
      // The same thing happens when unfocusing a second time.
1002
      child1.unfocus();
1003 1004
      await tester.pump();
      expect(scope1.focusedChild, isNull);
1005 1006 1007
      expect(scope2.focusedChild, equals(child3));
      expect(scope1.hasPrimaryFocus, isTrue);
      expect(scope2.hasFocus, isFalse);
1008
      expect(child1.hasPrimaryFocus, isFalse);
1009 1010 1011 1012 1013 1014
      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();
1015
      scope1.unfocus();
1016 1017 1018
      await tester.pump();
      expect(scope1.focusedChild, equals(child1));
      expect(scope2.focusedChild, equals(child3));
1019
      expect(scope1.hasFocus, isFalse);
1020 1021 1022 1023 1024
      expect(scope2.hasFocus, isFalse);
      expect(child1.hasPrimaryFocus, isFalse);
      expect(child3.hasPrimaryFocus, isFalse);
      expect(FocusManager.instance.rootScope.hasPrimaryFocus, isTrue);
    });
1025

1026
    testWidgetsWithLeakTracking('Unfocus works properly when some nodes are unfocusable', (WidgetTester tester) async {
1027 1028
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
1029
      addTearDown(scope1.dispose);
1030 1031
      final FocusAttachment scope1Attachment = scope1.attach(context);
      final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
1032
      addTearDown(scope2.dispose);
1033 1034
      final FocusAttachment scope2Attachment = scope2.attach(context);
      final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
1035
      addTearDown(parent1.dispose);
1036 1037
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
1038
      addTearDown(parent2.dispose);
1039 1040
      final FocusAttachment parent2Attachment = parent2.attach(context);
      final FocusNode child1 = FocusNode(debugLabel: 'child1');
1041
      addTearDown(child1.dispose);
1042 1043
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode(debugLabel: 'child2');
1044
      addTearDown(child2.dispose);
1045 1046
      final FocusAttachment child2Attachment = child2.attach(context);
      final FocusNode child3 = FocusNode(debugLabel: 'child3');
1047
      addTearDown(child3.dispose);
1048 1049
      final FocusAttachment child3Attachment = child3.attach(context);
      final FocusNode child4 = FocusNode(debugLabel: 'child4');
1050
      addTearDown(child4.dispose);
1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078
      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);

1079
      child1.unfocus();
1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098
      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);
    });
1099

1100
    testWidgetsWithLeakTracking('Requesting focus on a scope works properly when some focusedChild nodes are unfocusable', (WidgetTester tester) async {
1101 1102
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
1103
      addTearDown(scope1.dispose);
1104 1105
      final FocusAttachment scope1Attachment = scope1.attach(context);
      final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
1106
      addTearDown(scope2.dispose);
1107 1108
      final FocusAttachment scope2Attachment = scope2.attach(context);
      final FocusNode parent1 = FocusNode(debugLabel: 'parent1');
1109
      addTearDown(parent1.dispose);
1110 1111
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode parent2 = FocusNode(debugLabel: 'parent2');
1112
      addTearDown(parent2.dispose);
1113 1114
      final FocusAttachment parent2Attachment = parent2.attach(context);
      final FocusNode child1 = FocusNode(debugLabel: 'child1');
1115
      addTearDown(child1.dispose);
1116 1117
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode(debugLabel: 'child2');
1118
      addTearDown(child2.dispose);
1119 1120
      final FocusAttachment child2Attachment = child2.attach(context);
      final FocusNode child3 = FocusNode(debugLabel: 'child3');
1121
      addTearDown(child3.dispose);
1122 1123
      final FocusAttachment child3Attachment = child3.attach(context);
      final FocusNode child4 = FocusNode(debugLabel: 'child4');
1124
      addTearDown(child4.dispose);
1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159
      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);
1160
    });
1161

1162
    testWidgetsWithLeakTracking('Key handling bubbles up and terminates when handled.', (WidgetTester tester) async {
1163 1164
      final Set<FocusNode> receivedAnEvent = <FocusNode>{};
      final Set<FocusNode> shouldHandle = <FocusNode>{};
1165
      KeyEventResult handleEvent(FocusNode node, RawKeyEvent event) {
1166 1167
        if (shouldHandle.contains(node)) {
          receivedAnEvent.add(node);
1168
          return KeyEventResult.handled;
1169
        }
1170
        return KeyEventResult.ignored;
1171 1172
      }

1173
      Future<void> sendEvent() async {
1174
        receivedAnEvent.clear();
1175
        await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
1176 1177 1178 1179
      }

      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'Scope 1');
1180
      addTearDown(scope1.dispose);
1181 1182
      final FocusAttachment scope1Attachment = scope1.attach(context, onKey: handleEvent);
      final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'Scope 2');
1183
      addTearDown(scope2.dispose);
1184
      final FocusAttachment scope2Attachment = scope2.attach(context, onKey: handleEvent);
1185
      final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1', onKey: handleEvent);
1186
      addTearDown(parent1.dispose);
1187 1188
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2', onKey: handleEvent);
1189
      addTearDown(parent2.dispose);
1190
      final FocusAttachment parent2Attachment = parent2.attach(context);
1191
      final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
1192
      addTearDown(child1.dispose);
1193 1194
      final FocusAttachment child1Attachment = child1.attach(context, onKey: handleEvent);
      final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
1195
      addTearDown(child2.dispose);
1196 1197
      final FocusAttachment child2Attachment = child2.attach(context, onKey: handleEvent);
      final FocusNode child3 = FocusNode(debugLabel: 'Child 3');
1198
      addTearDown(child3.dispose);
1199 1200
      final FocusAttachment child3Attachment = child3.attach(context, onKey: handleEvent);
      final FocusNode child4 = FocusNode(debugLabel: 'Child 4');
1201
      addTearDown(child4.dispose);
1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213
      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});
1214
      await sendEvent();
1215 1216
      expect(receivedAnEvent, equals(<FocusNode>{child4}));
      shouldHandle.remove(child4);
1217
      await sendEvent();
1218 1219
      expect(receivedAnEvent, equals(<FocusNode>{parent2}));
      shouldHandle.remove(parent2);
1220
      await sendEvent();
1221 1222
      expect(receivedAnEvent, equals(<FocusNode>{scope2}));
      shouldHandle.clear();
1223
      await sendEvent();
1224 1225 1226 1227
      expect(receivedAnEvent, isEmpty);
      child1.requestFocus();
      await tester.pump();
      shouldHandle.addAll(<FocusNode>{scope2, parent2, child2, child4});
1228
      await sendEvent();
1229 1230 1231
      // Since none of the focused nodes handle this event, nothing should
      // receive it.
      expect(receivedAnEvent, isEmpty);
1232
    }, variant: KeySimulatorTransitModeVariant.all());
1233

1234
    testWidgetsWithLeakTracking('Initial highlight mode guesses correctly.', (WidgetTester tester) async {
1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246
      FocusManager.instance.highlightStrategy = FocusHighlightStrategy.automatic;
      switch (defaultTargetPlatform) {
        case TargetPlatform.fuchsia:
        case TargetPlatform.android:
        case TargetPlatform.iOS:
          expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
        case TargetPlatform.linux:
        case TargetPlatform.macOS:
        case TargetPlatform.windows:
          expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
      }
    }, variant: TargetPlatformVariant.all());
1247

1248
    testWidgetsWithLeakTracking('Mouse events change initial focus highlight mode on mobile.', (WidgetTester tester) async {
1249
      expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
1250
      RendererBinding.instance.initMouseTracker(); // Clear out the mouse state.
1251 1252 1253 1254
      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());
1255

1256
    testWidgetsWithLeakTracking('Mouse events change initial focus highlight mode on desktop.', (WidgetTester tester) async {
1257
      expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
1258
      RendererBinding.instance.initMouseTracker(); // Clear out the mouse state.
1259 1260 1261 1262
      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());
1263

1264
    testWidgetsWithLeakTracking('Keyboard events change initial focus highlight mode.', (WidgetTester tester) async {
1265 1266 1267
      await tester.sendKeyEvent(LogicalKeyboardKey.enter);
      expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
    }, variant: TargetPlatformVariant.all());
1268

1269
    testWidgetsWithLeakTracking('Events change focus highlight mode.', (WidgetTester tester) async {
1270 1271
      await setupWidget(tester);
      int callCount = 0;
1272
      FocusHighlightMode? lastMode;
1273 1274 1275 1276
      void handleModeChange(FocusHighlightMode mode) {
        lastMode = mode;
        callCount++;
      }
1277 1278
      FocusManager.instance.addHighlightModeListener(handleModeChange);
      addTearDown(() => FocusManager.instance.removeHighlightModeListener(handleModeChange));
1279 1280
      expect(callCount, equals(0));
      expect(lastMode, isNull);
1281 1282
      FocusManager.instance.highlightStrategy = FocusHighlightStrategy.automatic;
      expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
1283
      await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
1284 1285
      expect(callCount, equals(1));
      expect(lastMode, FocusHighlightMode.traditional);
1286
      expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
1287
      await tester.tap(find.byType(Container), warnIfMissed: false);
1288 1289
      expect(callCount, equals(2));
      expect(lastMode, FocusHighlightMode.touch);
1290
      expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
1291 1292 1293 1294
      final TestGesture gesture = await tester.startGesture(Offset.zero, kind: PointerDeviceKind.mouse);
      await gesture.up();
      expect(callCount, equals(3));
      expect(lastMode, FocusHighlightMode.traditional);
1295
      expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
1296
      await tester.tap(find.byType(Container), warnIfMissed: false);
1297 1298
      expect(callCount, equals(4));
      expect(lastMode, FocusHighlightMode.touch);
1299 1300
      expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
      FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
1301 1302
      expect(callCount, equals(5));
      expect(lastMode, FocusHighlightMode.traditional);
1303 1304
      expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
      FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
1305 1306
      expect(callCount, equals(6));
      expect(lastMode, FocusHighlightMode.touch);
1307
      expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
1308
    });
1309

1310
    testWidgetsWithLeakTracking('implements debugFillProperties', (WidgetTester tester) async {
1311
      final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
1312 1313 1314
      final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope Label');
      addTearDown(scope.dispose);
      scope.debugFillProperties(builder);
1315
      final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
1316
      expect(description, <String>[
1317
        'context: null',
1318
        'descendantsAreFocusable: true',
1319
        'descendantsAreTraversable: true',
1320 1321
        'canRequestFocus: true',
        'hasFocus: false',
1322
        'hasPrimaryFocus: false',
1323 1324
      ]);
    });
1325

1326
    testWidgetsWithLeakTracking('debugDescribeFocusTree produces correct output', (WidgetTester tester) async {
1327 1328
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'Scope 1');
1329
      addTearDown(scope1.dispose);
1330 1331
      final FocusAttachment scope1Attachment = scope1.attach(context);
      final FocusScopeNode scope2 = FocusScopeNode(); // No label, Just to test that it works.
1332
      addTearDown(scope2.dispose);
1333 1334
      final FocusAttachment scope2Attachment = scope2.attach(context);
      final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
1335
      addTearDown(parent1.dispose);
1336 1337
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
1338
      addTearDown(parent2.dispose);
1339 1340
      final FocusAttachment parent2Attachment = parent2.attach(context);
      final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
1341
      addTearDown(child1.dispose);
1342 1343
      final FocusAttachment child1Attachment = child1.attach(context);
      final FocusNode child2 = FocusNode(); // No label, Just to test that it works.
1344
      addTearDown(child2.dispose);
1345 1346
      final FocusAttachment child2Attachment = child2.attach(context);
      final FocusNode child3 = FocusNode(debugLabel: 'Child 3');
1347
      addTearDown(child3.dispose);
1348 1349
      final FocusAttachment child3Attachment = child3.attach(context);
      final FocusNode child4 = FocusNode(debugLabel: 'Child 4');
1350
      addTearDown(child4.dispose);
1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366
      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'
1367
          ' │ primaryFocus: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n'
1368
          ' │ primaryFocusCreator: Container-[GlobalKey#00000] ← MediaQuery ←\n'
1369 1370 1371
          ' │   _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
          ' │   _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
          ' │   [root]\n'
1372
          ' │\n'
1373
          ' └─rootScope: FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n'
1374
          '   │ IN FOCUS PATH\n'
1375
          '   │ focusedChildren: FocusScopeNode#00000([IN FOCUS PATH])\n'
1376
          '   │\n'
1377
          '   ├─Child 1: FocusScopeNode#00000(Scope 1)\n'
1378 1379
          '   │ │ context: Container-[GlobalKey#00000]\n'
          '   │ │\n'
1380
          '   │ └─Child 1: FocusNode#00000(Parent 1)\n'
1381 1382
          '   │   │ context: Container-[GlobalKey#00000]\n'
          '   │   │\n'
1383
          '   │   ├─Child 1: FocusNode#00000(Child 1)\n'
1384 1385 1386 1387 1388
          '   │   │   context: Container-[GlobalKey#00000]\n'
          '   │   │\n'
          '   │   └─Child 2: FocusNode#00000\n'
          '   │       context: Container-[GlobalKey#00000]\n'
          '   │\n'
1389
          '   └─Child 2: FocusScopeNode#00000([IN FOCUS PATH])\n'
1390
          '     │ context: Container-[GlobalKey#00000]\n'
1391
          '     │ IN FOCUS PATH\n'
1392
          '     │ focusedChildren: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n'
1393
          '     │\n'
1394
          '     └─Child 1: FocusNode#00000(Parent 2 [IN FOCUS PATH])\n'
1395
          '       │ context: Container-[GlobalKey#00000]\n'
1396
          '       │ IN FOCUS PATH\n'
1397
          '       │\n'
1398
          '       ├─Child 1: FocusNode#00000(Child 3)\n'
1399 1400
          '       │   context: Container-[GlobalKey#00000]\n'
          '       │\n'
1401
          '       └─Child 2: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n'
1402
          '           context: Container-[GlobalKey#00000]\n'
1403 1404 1405
          '           PRIMARY FOCUS\n',
        ),
      );
1406 1407
    });
  });
1408

1409
  group('Autofocus', () {
1410
    testWidgetsWithLeakTracking(
1411 1412 1413
      'works when the previous focused node is detached',
      (WidgetTester tester) async {
        final FocusNode node1 = FocusNode();
1414
        addTearDown(node1.dispose);
1415
        final FocusNode node2 = FocusNode();
1416
        addTearDown(node2.dispose);
1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436

        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);
    });

1437
    testWidgetsWithLeakTracking(
1438 1439 1440
      'node detached before autofocus is applied',
      (WidgetTester tester) async {
        final FocusScopeNode scopeNode = FocusScopeNode();
1441
        addTearDown(scopeNode.dispose);
1442
        final FocusNode node1 = FocusNode();
1443
        addTearDown(node1.dispose);
1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466

        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);
    });

1467
    testWidgetsWithLeakTracking('autofocus the first candidate', (WidgetTester tester) async {
1468
      final FocusNode node1 = FocusNode();
1469
      addTearDown(node1.dispose);
1470
      final FocusNode node2 = FocusNode();
1471
      addTearDown(node2.dispose);
1472
      final FocusNode node3 = FocusNode();
1473
      addTearDown(node3.dispose);
1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502

      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);
    });

1503
    testWidgetsWithLeakTracking('Autofocus works with global key reparenting', (WidgetTester tester) async {
1504
      final FocusNode node = FocusNode();
1505
      addTearDown(node.dispose);
1506
      final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1');
1507
      addTearDown(scope1.dispose);
1508
      final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
1509
      addTearDown(scope2.dispose);
1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559
      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);
    });
  });

1560
  testWidgetsWithLeakTracking("Doesn't lose focused child when reparenting if the nearestScope doesn't change.", (WidgetTester tester) async {
1561 1562
    final BuildContext context = await setupWidget(tester);
    final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
1563
    addTearDown(parent1.dispose);
1564
    final FocusScopeNode parent2 = FocusScopeNode(debugLabel: 'parent2');
1565
    addTearDown(parent2.dispose);
1566 1567 1568
    final FocusAttachment parent1Attachment = parent1.attach(context);
    final FocusAttachment parent2Attachment = parent2.attach(context);
    final FocusNode child1 = FocusNode(debugLabel: 'child1');
1569
    addTearDown(child1.dispose);
1570 1571
    final FocusAttachment child1Attachment = child1.attach(context);
    final FocusNode child2 = FocusNode(debugLabel: 'child2');
1572
    addTearDown(child2.dispose);
1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588
    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));
  });
1589

1590
  testWidgetsWithLeakTracking('Ancestors get notified exactly as often as needed if focused child changes focus.', (WidgetTester tester) async {
1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614
    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');
1615
    addTearDown(top.dispose);
1616 1617
    final FocusAttachment topAttachment = top.attach(context);
    final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
1618
    addTearDown(parent1.dispose);
1619 1620
    final FocusAttachment parent1Attachment = parent1.attach(context);
    final FocusScopeNode parent2 = FocusScopeNode(debugLabel: 'parent2');
1621
    addTearDown(parent2.dispose);
1622 1623
    final FocusAttachment parent2Attachment = parent2.attach(context);
    final FocusNode child1 = FocusNode(debugLabel: 'child1');
1624
    addTearDown(child1.dispose);
1625 1626
    final FocusAttachment child1Attachment = child1.attach(context);
    final FocusNode child2 = FocusNode(debugLabel: 'child2');
1627
    addTearDown(child2.dispose);
1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670
    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);
1671
    expect(parent1Focus, isTrue);
1672 1673 1674
    expect(child1Focus, isFalse);
    expect(parent2Focus, isFalse);
    expect(child2Focus, isFalse);
1675
    expect(topNotify, equals(0));
1676 1677 1678 1679 1680 1681 1682 1683
    expect(parent1Notify, equals(1));
    expect(child1Notify, equals(1));
    expect(parent2Notify, equals(0));
    expect(child2Notify, equals(0));

    clear();
    child1.requestFocus();
    await tester.pump();
1684
    expect(topFocus, isFalse);
1685 1686 1687 1688
    expect(parent1Focus, isTrue);
    expect(child1Focus, isTrue);
    expect(parent2Focus, isFalse);
    expect(child2Focus, isFalse);
1689
    expect(topNotify, equals(0));
1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724
    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));
  });
1725

1726
  testWidgetsWithLeakTracking('Focus changes notify listeners.', (WidgetTester tester) async {
1727 1728
    final BuildContext context = await setupWidget(tester);
    final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
1729
    addTearDown(parent1.dispose);
1730 1731
    final FocusAttachment parent1Attachment = parent1.attach(context);
    final FocusNode child1 = FocusNode(debugLabel: 'child1');
1732
    addTearDown(child1.dispose);
1733 1734
    final FocusAttachment child1Attachment = child1.attach(context);
    final FocusNode child2 = FocusNode(debugLabel: 'child2');
1735
    addTearDown(child2.dispose);
1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771
    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);
  });
1772

1773 1774 1775 1776 1777
  test('$FocusManager dispatches object creation in constructor', () async {
    await expectLater(
      await memoryEvents(() => FocusManager().dispose(), FocusManager),
      areCreateAndDispose,
    );
1778 1779
  });

1780 1781 1782 1783 1784
  test('$FocusNode dispatches object creation in constructor', () async {
    await expectLater(
      await memoryEvents(() => FocusNode().dispose(), FocusNode),
      areCreateAndDispose,
    );
1785 1786
  });

1787
  testWidgetsWithLeakTracking('FocusManager notifies listeners when a widget loses focus because it was removed.', (WidgetTester tester) async {
1788
    final FocusNode nodeA = FocusNode(debugLabel: 'a');
1789
    addTearDown(nodeA.dispose);
1790
    final FocusNode nodeB = FocusNode(debugLabel: 'b');
1791 1792
    addTearDown(nodeB.dispose);

1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834
    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);
  });
1835

1836
  testWidgetsWithLeakTracking('debugFocusChanges causes logging of focus changes', (WidgetTester tester) async {
1837 1838 1839 1840 1841 1842 1843 1844 1845 1846
    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');
1847
      addTearDown(parent1.dispose);
1848 1849
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode child1 = FocusNode(debugLabel: 'child1');
1850
      addTearDown(child1.dispose);
1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878
      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, contains(RegExp(r'   └─Child 1: FocusScopeNode#[a-f0-9]{5}\(parent1 \[PRIMARY FOCUS\]\)')));
1879
    expect(messagesStr, contains('FOCUS: Notified 2 dirty nodes'));
1880 1881
    expect(messagesStr, contains(RegExp(r'FOCUS: Scheduling update, current focus is null, next focus will be FocusScopeNode#.*parent1')));
  });
1882

1883
  testWidgetsWithLeakTracking("doesn't call toString on a focus node when debugFocusChanges is false", (WidgetTester tester) async {
1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938
    final bool oldDebugFocusChanges = debugFocusChanges;
    final DebugPrintCallback oldDebugPrint = debugPrint;
    final StringBuffer messages = StringBuffer();
    debugPrint = (String? message, {int? wrapWidth}) {
      messages.writeln(message ?? '');
    };
    Future<void> testDebugFocusChanges() async {
      final BuildContext context = await setupWidget(tester);
      final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
      final FocusAttachment parent1Attachment = parent1.attach(context);
      final FocusNode child1 = debugFocusChanges ? FocusNode(debugLabel: 'child1') : _LoggingTestFocusNode(debugLabel: 'child1');
      final FocusAttachment child1Attachment = child1.attach(context);
      parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
      child1Attachment.reparent(parent: parent1);

      child1.requestFocus();
      await tester.pump();
      child1.dispose();
      parent1.dispose();
      await tester.pump();
    }
    try {
      debugFocusChanges = false;
      await testDebugFocusChanges();
      expect(messages, isEmpty);
      expect(tester.takeException(), isNull);
      debugFocusChanges = true;
      await testDebugFocusChanges();
      expect(messages.toString(), contains('FOCUS: Notified 3 dirty nodes:'));
      expect(tester.takeException(), isNull);
    } finally {
      debugFocusChanges = oldDebugFocusChanges;
      debugPrint = oldDebugPrint;
    }
  });
}

class _LoggingTestFocusNode extends FocusNode {
  _LoggingTestFocusNode({super.debugLabel});

  @override
  String toString({
    DiagnosticLevel minLevel = DiagnosticLevel.debug,
  }) {
    throw StateError("Shouldn't call toString here");
  }

  @override
  String toStringDeep({
    String prefixLineOne = '',
    String? prefixOtherLines,
    DiagnosticLevel minLevel = DiagnosticLevel.debug,
  }) {
    throw StateError("Shouldn't call toStringDeep here");
  }
1939
}