| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:ui'; |
| |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| void main() { |
| // 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( |
| pages: const <Page<void>>[_APage(), _BPage()], |
| onPopPage: (Route<dynamic> route, dynamic result) { |
| return false; |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('Dialog interaction', (WidgetTester tester) async { |
| expect(tester.testTextInput.isVisible, isFalse); |
| |
| final FocusNode focusNode = FocusNode(debugLabel: 'Editable Text Node'); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| focusNode: focusNode, |
| autofocus: true, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.testTextInput.isVisible, isTrue); |
| expect(focusNode.hasPrimaryFocus, isTrue); |
| |
| final BuildContext context = tester.element(find.byType(TextField)); |
| |
| showDialog<void>( |
| context: context, |
| builder: (BuildContext context) => const SimpleDialog(title: Text('Dialog')), |
| ); |
| |
| await tester.pump(); |
| |
| expect(tester.testTextInput.isVisible, isFalse); |
| |
| Navigator.of(tester.element(find.text('Dialog'))).pop(); |
| await tester.pump(); |
| |
| expect(focusNode.hasPrimaryFocus, isTrue); |
| expect(tester.testTextInput.isVisible, isTrue); |
| |
| await tester.pumpWidget(Container()); |
| |
| expect(tester.testTextInput.isVisible, isFalse); |
| }); |
| |
| testWidgets('Request focus shows keyboard', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| focusNode: focusNode, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.testTextInput.isVisible, isFalse); |
| |
| FocusScope.of(tester.element(find.byType(TextField))).requestFocus(focusNode); |
| await tester.idle(); |
| |
| expect(tester.testTextInput.isVisible, isTrue); |
| |
| await tester.pumpWidget(Container()); |
| |
| expect(tester.testTextInput.isVisible, isFalse); |
| }); |
| |
| testWidgets('Autofocus shows keyboard', (WidgetTester tester) async { |
| expect(tester.testTextInput.isVisible, isFalse); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| autofocus: true, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.testTextInput.isVisible, isTrue); |
| |
| await tester.pumpWidget(Container()); |
| |
| expect(tester.testTextInput.isVisible, isFalse); |
| }); |
| |
| testWidgets('Tap shows keyboard', (WidgetTester tester) async { |
| expect(tester.testTextInput.isVisible, isFalse); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField(), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.testTextInput.isVisible, isFalse); |
| |
| await tester.tap(find.byType(TextField)); |
| await tester.idle(); |
| |
| expect(tester.testTextInput.isVisible, isTrue); |
| // Prevent the gesture recognizer from recognizing the next tap as a |
| // double-tap. |
| await tester.pump(const Duration(seconds: 1)); |
| |
| tester.testTextInput.hide(); |
| final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); |
| state.connectionClosed(); |
| |
| expect(tester.testTextInput.isVisible, isFalse); |
| |
| await tester.tap(find.byType(TextField)); |
| await tester.idle(); |
| |
| expect(tester.testTextInput.isVisible, isTrue); |
| |
| await tester.pumpWidget(Container()); |
| |
| expect(tester.testTextInput.isVisible, isFalse); |
| }); |
| |
| testWidgets('Focus triggers keep-alive', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ListView( |
| children: <Widget>[ |
| TextField( |
| focusNode: focusNode, |
| ), |
| Container( |
| 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); |
| |
| await tester.drag(find.byType(TextField), const Offset(0.0, -1000.0)); |
| await tester.pump(); |
| expect(find.byType(TextField, skipOffstage: false), findsOneWidget); |
| 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 { |
| final FocusNode focusNode = FocusNode(); |
| |
| Widget makeTest(String? prefix) { |
| return MaterialApp( |
| home: Material( |
| child: ListView( |
| children: <Widget>[ |
| TextField( |
| focusNode: focusNode, |
| decoration: InputDecoration( |
| prefixText: prefix, |
| ), |
| ), |
| Container( |
| 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); |
| await tester.drag(find.byType(TextField), const Offset(0.0, -1000.0)); |
| await tester.pump(); |
| expect(find.byType(TextField, skipOffstage: false), findsOneWidget); |
| await tester.pumpWidget(makeTest('test')); |
| await tester.pump(); // in case the AutomaticKeepAlive widget thinks it needs a cleanup frame |
| expect(find.byType(TextField, skipOffstage: false), findsOneWidget); |
| }); |
| |
| testWidgets('TextField with decoration:null', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/16880 |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| decoration: null, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.testTextInput.isVisible, isFalse); |
| await tester.tap(find.byType(TextField)); |
| await tester.idle(); |
| expect(tester.testTextInput.isVisible, isTrue); |
| }); |
| |
| 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( |
| builder: (BuildContext context) => TextField(key: textField0), |
| ), |
| ), |
| 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); |
| }); |
| |
| 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()); |
| |
| 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()); |
| |
| 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 { |
| final FocusNode focusNodeA = FocusNode(debugLabel: 'A'); |
| final FocusNode focusNodeB = FocusNode(debugLabel: 'B'); |
| 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) { |
| 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(); |
| } |
| |
| expect(focusNodeA.hasFocus, true); |
| expect(focusNodeB.hasFocus, false); |
| |
| // Click on the container to not hit either text field. |
| await click(find.byKey(key)); |
| await tester.pump(); |
| |
| expect(focusNodeA.hasFocus, false); |
| expect(focusNodeB.hasFocus, false); |
| |
| // Second text field can still gain focus. |
| |
| await click(find.byType(TextField).last); |
| await tester.pump(); |
| |
| expect(focusNodeA.hasFocus, false); |
| expect(focusNodeB.hasFocus, true); |
| }, variant: TargetPlatformVariant.desktop()); |
| } |
| |
| class _APage extends Page<void> { |
| const _APage(); |
| |
| @override |
| Route<void> createRoute(BuildContext context) => PageRouteBuilder<void>( |
| settings: this, |
| pageBuilder: (_, __, ___) => const TextField(autofocus: true), |
| ); |
| } |
| |
| class _BPage extends Page<void> { |
| const _BPage(); |
| |
| @override |
| Route<void> createRoute(BuildContext context) => PageRouteBuilder<void>( |
| settings: this, |
| pageBuilder: (_, __, ___) => const Text('B'), |
| ); |
| } |