text_field_focus_test.dart 16.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:ui';

7
import 'package:flutter/material.dart';
8
import 'package:flutter/services.dart';
9
import 'package:flutter_test/flutter_test.dart';
10 11

void main() {
12 13 14 15 16 17
  // Regression test for https://github.com/flutter/flutter/issues/87099
  testWidgets('TextField.autofocus should skip the element that never layout', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Navigator(
18
            pages: const <Page<void>>[_APage(), _BPage()],
19 20 21 22 23 24 25 26 27 28 29
            onPopPage: (Route<dynamic> route, dynamic result) {
              return false;
            },
          ),
        ),
      ),
    );

    expect(tester.takeException(), isNull);
  });

30 31
  testWidgets('Dialog interaction', (WidgetTester tester) async {
    expect(tester.testTextInput.isVisible, isFalse);
32

33 34
    final FocusNode focusNode = FocusNode(debugLabel: 'Editable Text Node');

35
    await tester.pumpWidget(
36
      MaterialApp(
37 38 39
        home: Material(
          child: Center(
            child: TextField(
40
              focusNode: focusNode,
41
              autofocus: true,
42 43 44 45 46 47
            ),
          ),
        ),
      ),
    );

48
    expect(tester.testTextInput.isVisible, isTrue);
49
    expect(focusNode.hasPrimaryFocus, isTrue);
50 51 52 53 54 55 56 57 58 59

    final BuildContext context = tester.element(find.byType(TextField));

    showDialog<void>(
      context: context,
      builder: (BuildContext context) => const SimpleDialog(title: Text('Dialog')),
    );

    await tester.pump();

60 61
    expect(tester.testTextInput.isVisible, isFalse);

62
    Navigator.of(tester.element(find.text('Dialog'))).pop();
63 64
    await tester.pump();

65
    expect(focusNode.hasPrimaryFocus, isTrue);
66 67
    expect(tester.testTextInput.isVisible, isTrue);

68
    await tester.pumpWidget(Container());
69 70 71 72

    expect(tester.testTextInput.isVisible, isFalse);
  });

73 74
  testWidgets('Request focus shows keyboard', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();
75 76

    await tester.pumpWidget(
77
      MaterialApp(
78
        home: Material(
79 80
          child: Center(
            child: TextField(
81
              focusNode: focusNode,
82 83 84 85 86 87
            ),
          ),
        ),
      ),
    );

88 89 90 91 92
    expect(tester.testTextInput.isVisible, isFalse);

    FocusScope.of(tester.element(find.byType(TextField))).requestFocus(focusNode);
    await tester.idle();

93 94
    expect(tester.testTextInput.isVisible, isTrue);

95
    await tester.pumpWidget(Container());
96 97 98 99

    expect(tester.testTextInput.isVisible, isFalse);
  });

100
  testWidgets('Autofocus shows keyboard', (WidgetTester tester) async {
101 102 103
    expect(tester.testTextInput.isVisible, isFalse);

    await tester.pumpWidget(
104 105
      const MaterialApp(
        home: Material(
106
          child: Center(
107 108 109
            child: TextField(
              autofocus: true,
            ),
110 111 112 113 114 115 116
          ),
        ),
      ),
    );

    expect(tester.testTextInput.isVisible, isTrue);

117
    await tester.pumpWidget(Container());
118 119 120 121

    expect(tester.testTextInput.isVisible, isFalse);
  });

122
  testWidgets('Tap shows keyboard', (WidgetTester tester) async {
123 124 125
    expect(tester.testTextInput.isVisible, isFalse);

    await tester.pumpWidget(
126 127
      const MaterialApp(
        home: Material(
128
          child: Center(
129
            child: TextField(),
130 131 132 133 134
          ),
        ),
      ),
    );

135
    expect(tester.testTextInput.isVisible, isFalse);
136

137 138
    await tester.tap(find.byType(TextField));
    await tester.idle();
139

140
    expect(tester.testTextInput.isVisible, isTrue);
141 142 143
    // Prevent the gesture recognizer from recognizing the next tap as a
    // double-tap.
    await tester.pump(const Duration(seconds: 1));
144

145
    tester.testTextInput.hide();
146 147
    final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
    state.connectionClosed();
148 149 150 151

    expect(tester.testTextInput.isVisible, isFalse);

    await tester.tap(find.byType(TextField));
152
    await tester.idle();
153 154 155

    expect(tester.testTextInput.isVisible, isTrue);

156
    await tester.pumpWidget(Container());
157 158

    expect(tester.testTextInput.isVisible, isFalse);
159
  });
160

161
  testWidgets('Focus triggers keep-alive', (WidgetTester tester) async {
162
    final FocusNode focusNode = FocusNode();
163 164

    await tester.pumpWidget(
165 166 167
      MaterialApp(
        home: Material(
          child: ListView(
168
            children: <Widget>[
169
              TextField(
170 171
                focusNode: focusNode,
              ),
172
              Container(
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
                height: 1000.0,
              ),
            ],
          ),
        ),
      ),
    );

    expect(find.byType(TextField), findsOneWidget);
    expect(tester.testTextInput.isVisible, isFalse);

    FocusScope.of(tester.element(find.byType(TextField))).requestFocus(focusNode);
    await tester.pump();
    expect(find.byType(TextField), findsOneWidget);
    expect(tester.testTextInput.isVisible, isTrue);

189
    await tester.drag(find.byType(TextField), const Offset(0.0, -1000.0));
190
    await tester.pump();
191
    expect(find.byType(TextField, skipOffstage: false), findsOneWidget);
192 193 194 195 196 197 198 199 200 201
    expect(tester.testTextInput.isVisible, isTrue);

    focusNode.unfocus();
    await tester.pump();

    expect(find.byType(TextField), findsNothing);
    expect(tester.testTextInput.isVisible, isFalse);
  });

  testWidgets('Focus keep-alive works with GlobalKey reparenting', (WidgetTester tester) async {
202
    final FocusNode focusNode = FocusNode();
203

204
    Widget makeTest(String? prefix) {
205 206 207
      return MaterialApp(
        home: Material(
          child: ListView(
208
            children: <Widget>[
209
              TextField(
210
                focusNode: focusNode,
211
                decoration: InputDecoration(
212 213 214
                  prefixText: prefix,
                ),
              ),
215
              Container(
216 217 218 219 220 221 222 223 224 225 226 227
                height: 1000.0,
              ),
            ],
          ),
        ),
      );
    }

    await tester.pumpWidget(makeTest(null));
    FocusScope.of(tester.element(find.byType(TextField))).requestFocus(focusNode);
    await tester.pump();
    expect(find.byType(TextField), findsOneWidget);
228
    await tester.drag(find.byType(TextField), const Offset(0.0, -1000.0));
229
    await tester.pump();
230
    expect(find.byType(TextField, skipOffstage: false), findsOneWidget);
231 232
    await tester.pumpWidget(makeTest('test'));
    await tester.pump(); // in case the AutomaticKeepAlive widget thinks it needs a cleanup frame
233
    expect(find.byType(TextField, skipOffstage: false), findsOneWidget);
234
  });
Hans Muller's avatar
Hans Muller committed
235 236 237 238 239

  testWidgets('TextField with decoration:null', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/16880

    await tester.pumpWidget(
240 241
      const MaterialApp(
        home: Material(
242 243
          child: Center(
            child: TextField(
244
              decoration: null,
Hans Muller's avatar
Hans Muller committed
245 246 247 248 249 250 251 252
            ),
          ),
        ),
      ),
    );

    expect(tester.testTextInput.isVisible, isFalse);
    await tester.tap(find.byType(TextField));
253
    await tester.idle();
Hans Muller's avatar
Hans Muller committed
254 255
    expect(tester.testTextInput.isVisible, isTrue);
  });
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274

  testWidgets('Sibling FocusScopes', (WidgetTester tester) async {
    expect(tester.testTextInput.isVisible, isFalse);

    final FocusScopeNode focusScopeNode0 = FocusScopeNode();
    final FocusScopeNode focusScopeNode1 = FocusScopeNode();
    final Key textField0 = UniqueKey();
    final Key textField1 = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                FocusScope(
                  node: focusScopeNode0,
                  child: Builder(
275
                    builder: (BuildContext context) => TextField(key: textField0),
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 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 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
                  ),
                ),
                FocusScope(
                  node: focusScopeNode1,
                  child: Builder(
                    builder: (BuildContext context) => TextField(key: textField1),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );

    expect(tester.testTextInput.isVisible, isFalse);

    await tester.tap(find.byKey(textField0));
    await tester.idle();
    expect(tester.testTextInput.isVisible, isTrue);

    tester.testTextInput.hide();
    expect(tester.testTextInput.isVisible, isFalse);

    await tester.tap(find.byKey(textField1));
    await tester.idle();
    expect(tester.testTextInput.isVisible, isTrue);

    await tester.tap(find.byKey(textField0));
    await tester.idle();
    expect(tester.testTextInput.isVisible, isTrue);

    await tester.tap(find.byKey(textField1));
    await tester.idle();
    expect(tester.testTextInput.isVisible, isTrue);

    tester.testTextInput.hide();
    expect(tester.testTextInput.isVisible, isFalse);

    await tester.tap(find.byKey(textField0));
    await tester.idle();
    expect(tester.testTextInput.isVisible, isTrue);

    await tester.pumpWidget(Container());
    expect(tester.testTextInput.isVisible, isFalse);
  });

  testWidgets('Sibling Navigators', (WidgetTester tester) async {
    expect(tester.testTextInput.isVisible, isFalse);

    final Key textField0 = UniqueKey();
    final Key textField1 = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: Column(
              children: <Widget>[
                Expanded(
                  child: Navigator(
                    onGenerateRoute: (RouteSettings settings) {
                      return MaterialPageRoute<void>(
                        builder: (BuildContext context) {
                          return TextField(key: textField0);
                        },
                        settings: settings,
                      );
                    },
                  ),
                ),
                Expanded(
                  child: Navigator(
                    onGenerateRoute: (RouteSettings settings) {
                      return MaterialPageRoute<void>(
                        builder: (BuildContext context) {
                          return TextField(key: textField1);
                        },
                        settings: settings,
                      );
                    },
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );

    expect(tester.testTextInput.isVisible, isFalse);

    await tester.tap(find.byKey(textField0));
    await tester.idle();
    expect(tester.testTextInput.isVisible, isTrue);

    tester.testTextInput.hide();
    expect(tester.testTextInput.isVisible, isFalse);

    await tester.tap(find.byKey(textField1));
    await tester.idle();
    expect(tester.testTextInput.isVisible, isTrue);

    await tester.tap(find.byKey(textField0));
    await tester.idle();
    expect(tester.testTextInput.isVisible, isTrue);

    await tester.tap(find.byKey(textField1));
    await tester.idle();
    expect(tester.testTextInput.isVisible, isTrue);

    tester.testTextInput.hide();
    expect(tester.testTextInput.isVisible, isFalse);

    await tester.tap(find.byKey(textField0));
    await tester.idle();
    expect(tester.testTextInput.isVisible, isTrue);

    await tester.pumpWidget(Container());
    expect(tester.testTextInput.isVisible, isFalse);
  });
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453

  testWidgets('A Focused text-field will lose focus when clicking outside of its hitbox with a mouse on desktop', (WidgetTester tester) async {
    final FocusNode focusNodeA = FocusNode();
    final FocusNode focusNodeB = FocusNode();
    final Key key = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: ListView(
            children: <Widget>[
              TextField(
                focusNode: focusNodeA,
              ),
              Container(
                key: key,
                height: 200,
              ),
              TextField(
                focusNode: focusNodeB,
              ),
            ],
          ),
        ),
      ),
    );

    final TestGesture down1 = await tester.startGesture(tester.getCenter(find.byType(TextField).first), kind: PointerDeviceKind.mouse);
    await tester.pump();
    await tester.pumpAndSettle();
    await down1.up();
    await down1.removePointer();

    expect(focusNodeA.hasFocus, true);
    expect(focusNodeB.hasFocus, false);

    // Click on the container to not hit either text field.
    final TestGesture down2 = await tester.startGesture(tester.getCenter(find.byKey(key)), kind: PointerDeviceKind.mouse);
    await tester.pump();
    await tester.pumpAndSettle();
    await down2.up();
    await down2.removePointer();

    expect(focusNodeA.hasFocus, false);
    expect(focusNodeB.hasFocus, false);

    // Second text field can still gain focus.

    final TestGesture down3 = await tester.startGesture(tester.getCenter(find.byType(TextField).last), kind: PointerDeviceKind.mouse);
    await tester.pump();
    await tester.pumpAndSettle();
    await down3.up();
    await down3.removePointer();

    expect(focusNodeA.hasFocus, false);
    expect(focusNodeB.hasFocus, true);
  }, variant: TargetPlatformVariant.desktop());
454

455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
  testWidgets('A Focused text-field will not lose focus when clicking on its decoration', (WidgetTester tester) async {
    final FocusNode focusNodeA = FocusNode();
    final Key iconKey = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: ListView(
            children: <Widget>[
              TextField(
                focusNode: focusNodeA,
                decoration: InputDecoration(
                  icon: Icon(Icons.copy_all, key: iconKey),
                ),
              ),
            ],
          ),
        ),
      ),
    );

    final TestGesture down1 = await tester.startGesture(tester.getCenter(find.byType(TextField).first), kind: PointerDeviceKind.mouse);
    await tester.pump();
    await down1.removePointer();

    expect(focusNodeA.hasFocus, true);

    // Click on the icon which has a different RO than the text field's focus node context
    final TestGesture down2 = await tester.startGesture(tester.getCenter(find.byKey(iconKey)), kind: PointerDeviceKind.mouse);
    await tester.pump();
    await tester.pumpAndSettle();
    await down2.up();
    await down2.removePointer();

    expect(focusNodeA.hasFocus, true);
  }, variant: TargetPlatformVariant.desktop());

492
  testWidgets('A Focused text-field will lose focus when clicking outside of its hitbox with a mouse on desktop after tab navigation', (WidgetTester tester) async {
493 494
    final FocusNode focusNodeA = FocusNode(debugLabel: 'A');
    final FocusNode focusNodeB = FocusNode(debugLabel: 'B');
495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
    final Key key = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: ListView(
            children: <Widget>[
              const TextField(),
              const TextField(),
              TextField(
                focusNode: focusNodeA,
              ),
              Container(
                key: key,
                height: 200,
              ),
              TextField(
                focusNode: focusNodeB,
              ),
            ],
          ),
        ),
      ),
    );
    // Tab over to the 3rd text field.
    for (int i = 0; i < 3; i += 1) {
521 522 523 524 525 526 527 528 529 530 531
      await tester.sendKeyEvent(LogicalKeyboardKey.tab);
      await tester.pump();
    }

    Future<void> click(Finder finder) async {
      final TestGesture gesture = await tester.startGesture(
        tester.getCenter(finder),
        kind: PointerDeviceKind.mouse,
      );
      await gesture.up();
      await gesture.removePointer();
532 533 534 535 536 537
    }

    expect(focusNodeA.hasFocus, true);
    expect(focusNodeB.hasFocus, false);

    // Click on the container to not hit either text field.
538
    await click(find.byKey(key));
539 540 541 542 543 544 545
    await tester.pump();

    expect(focusNodeA.hasFocus, false);
    expect(focusNodeB.hasFocus, false);

    // Second text field can still gain focus.

546
    await click(find.byType(TextField).last);
547 548 549 550 551
    await tester.pump();

    expect(focusNodeA.hasFocus, false);
    expect(focusNodeB.hasFocus, true);
  }, variant: TargetPlatformVariant.desktop());
552
}
553 554

class _APage extends Page<void> {
555 556
  const _APage();

557 558 559 560 561 562 563 564
  @override
  Route<void> createRoute(BuildContext context) => PageRouteBuilder<void>(
    settings: this,
    pageBuilder: (_, __, ___) => const TextField(autofocus: true),
  );
}

class _BPage extends Page<void> {
565 566
  const _BPage();

567 568 569 570 571 572
  @override
  Route<void> createRoute(BuildContext context) => PageRouteBuilder<void>(
    settings: this,
    pageBuilder: (_, __, ___) => const Text('B'),
  );
}