| // 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. |
| |
| // reduced-test-set: |
| // This file is run as part of a reduced test set in CI on Mac and Windows |
| // machines. |
| // no-shuffle: |
| // TODO(122950): Remove this tag once this test's state leaks/test |
| // dependencies have been fixed. |
| // https://github.com/flutter/flutter/issues/122950 |
| // Fails with "flutter test --test-randomize-ordering-seed=20230318" |
| @Tags(<String>['reduced-test-set', 'no-shuffle']) |
| library; |
| |
| import 'dart:math' as math; |
| import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../widgets/clipboard_utils.dart'; |
| import '../widgets/editable_text_utils.dart'; |
| import '../widgets/semantics_tester.dart'; |
| import 'feedback_tester.dart'; |
| |
| typedef FormatEditUpdateCallback = void Function(TextEditingValue, TextEditingValue); |
| |
| // On web, key events in text fields are handled by the browser. |
| const bool areKeyEventsHandledByPlatform = isBrowser; |
| |
| class CupertinoLocalizationsDelegate extends LocalizationsDelegate<CupertinoLocalizations> { |
| @override |
| bool isSupported(Locale locale) => true; |
| |
| @override |
| Future<CupertinoLocalizations> load(Locale locale) => |
| DefaultCupertinoLocalizations.load(locale); |
| |
| @override |
| bool shouldReload(CupertinoLocalizationsDelegate old) => false; |
| } |
| |
| class MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> { |
| @override |
| bool isSupported(Locale locale) => true; |
| |
| @override |
| Future<MaterialLocalizations> load(Locale locale) => DefaultMaterialLocalizations.load(locale); |
| |
| @override |
| bool shouldReload(MaterialLocalizationsDelegate old) => false; |
| } |
| |
| class WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> { |
| @override |
| bool isSupported(Locale locale) => true; |
| |
| @override |
| Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale); |
| |
| @override |
| bool shouldReload(WidgetsLocalizationsDelegate old) => false; |
| } |
| |
| Widget overlay({ required Widget child }) { |
| final OverlayEntry entry = OverlayEntry( |
| builder: (BuildContext context) { |
| return Center( |
| child: Material( |
| child: child, |
| ), |
| ); |
| }, |
| ); |
| return overlayWithEntry(entry); |
| } |
| |
| Widget overlayWithEntry(OverlayEntry entry) { |
| return Localizations( |
| locale: const Locale('en', 'US'), |
| delegates: <LocalizationsDelegate<dynamic>>[ |
| WidgetsLocalizationsDelegate(), |
| MaterialLocalizationsDelegate(), |
| CupertinoLocalizationsDelegate(), |
| ], |
| child: DefaultTextEditingShortcuts( |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(size: Size(800.0, 600.0)), |
| child: Overlay( |
| initialEntries: <OverlayEntry>[ |
| entry, |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| Widget boilerplate({ required Widget child, ThemeData? theme }) { |
| return MaterialApp( |
| theme: theme, |
| home: Localizations( |
| locale: const Locale('en', 'US'), |
| delegates: <LocalizationsDelegate<dynamic>>[ |
| WidgetsLocalizationsDelegate(), |
| MaterialLocalizationsDelegate(), |
| ], |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(size: Size(800.0, 600.0)), |
| child: Center( |
| child: Material( |
| child: child, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| Future<void> skipPastScrollingAnimation(WidgetTester tester) async { |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); |
| } |
| |
| double getOpacity(WidgetTester tester, Finder finder) { |
| return tester.widget<FadeTransition>( |
| find.ancestor( |
| of: finder, |
| matching: find.byType(FadeTransition), |
| ), |
| ).opacity.value; |
| } |
| |
| class TestFormatter extends TextInputFormatter { |
| TestFormatter(this.onFormatEditUpdate); |
| FormatEditUpdateCallback onFormatEditUpdate; |
| @override |
| TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { |
| onFormatEditUpdate(oldValue, newValue); |
| return newValue; |
| } |
| } |
| |
| void main() { |
| TestWidgetsFlutterBinding.ensureInitialized(); |
| final MockClipboard mockClipboard = MockClipboard(); |
| |
| const String kThreeLines = |
| 'First line of text is\n' |
| 'Second line goes until\n' |
| 'Third line of stuff'; |
| const String kMoreThanFourLines = |
| '$kThreeLines\n' |
| "Fourth line won't display and ends at"; |
| // Gap between caret and edge of input, defined in editable.dart. |
| const int kCaretGap = 1; |
| |
| setUp(() async { |
| debugResetSemanticsIdCounter(); |
| TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( |
| SystemChannels.platform, |
| mockClipboard.handleMethodCall, |
| ); |
| // Fill the clipboard so that the Paste option is available in the text |
| // selection menu. |
| await Clipboard.setData(const ClipboardData(text: 'Clipboard data')); |
| }); |
| |
| tearDown(() { |
| TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( |
| SystemChannels.platform, |
| null, |
| ); |
| }); |
| |
| final Key textFieldKey = UniqueKey(); |
| Widget textFieldBuilder({ |
| int? maxLines = 1, |
| int? minLines, |
| }) { |
| return boilerplate( |
| child: TextField( |
| key: textFieldKey, |
| style: const TextStyle(color: Colors.black, fontSize: 34.0), |
| maxLines: maxLines, |
| minLines: minLines, |
| decoration: const InputDecoration( |
| hintText: 'Placeholder', |
| ), |
| ), |
| ); |
| } |
| |
| testWidgets('text field selection toolbar should hide when the user starts typing', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Scaffold( |
| body: Center( |
| child: SizedBox( |
| width: 100, |
| height: 100, |
| child: TextField( |
| decoration: InputDecoration(hintText: 'Placeholder'), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.showKeyboard(find.byType(TextField)); |
| |
| const String testValue = 'A B C'; |
| tester.testTextInput.updateEditingValue( |
| const TextEditingValue( |
| text: testValue, |
| ), |
| ); |
| await tester.pump(); |
| |
| // The selectWordsInRange with SelectionChangedCause.tap seems to be needed to show the toolbar. |
| // (This is true even if we provide selection parameter to the TextEditingValue above.) |
| final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); |
| state.renderEditable.selectWordsInRange(from: Offset.zero, cause: SelectionChangedCause.tap); |
| |
| expect(state.showToolbar(), true); |
| |
| // This is needed for the AnimatedOpacity to turn from 0 to 1 so the toolbar is visible. |
| await tester.pumpAndSettle(); |
| |
| // Sanity check that the toolbar widget exists. |
| expect(find.text('Paste'), findsOneWidget); |
| |
| const String newValue = 'A B C D'; |
| tester.testTextInput.updateEditingValue( |
| const TextEditingValue( |
| text: newValue, |
| ), |
| ); |
| await tester.pump(); |
| |
| expect(state.selectionOverlay!.toolbarIsVisible, isFalse); |
| }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. |
| |
| testWidgets('Composing change does not hide selection handle caret', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/108673 |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'I Love Flutter!'; |
| await tester.enterText(find.byType(TextField), testValue); |
| expect(controller.value.text, testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| // Handle not shown. |
| expect(controller.selection.isCollapsed, true); |
| final Finder fadeFinder = find.byType(FadeTransition); |
| FadeTransition handle = tester.widget(fadeFinder.at(0)); |
| expect(handle.opacity.value, equals(0.0)); |
| |
| // Tap on the text field to show the handle. |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| expect(fadeFinder, findsNWidgets(1)); |
| handle = tester.widget(fadeFinder.at(0)); |
| expect(handle.opacity.value, equals(1.0)); |
| final RenderObject handleRenderObjectBegin = tester.renderObject(fadeFinder.at(0)); |
| |
| expect( |
| controller.value, |
| const TextEditingValue( |
| text: 'I Love Flutter!', |
| selection: TextSelection.collapsed(offset: 15, affinity: TextAffinity.upstream), |
| ), |
| ); |
| |
| // Simulate text composing change. |
| tester.testTextInput.updateEditingValue( |
| controller.value.copyWith( |
| composing: const TextRange(start: 7, end: 15), |
| ), |
| ); |
| await skipPastScrollingAnimation(tester); |
| |
| expect( |
| controller.value, |
| const TextEditingValue( |
| text: 'I Love Flutter!', |
| selection: TextSelection.collapsed(offset: 15, affinity: TextAffinity.upstream), |
| composing: TextRange(start: 7, end: 15), |
| ), |
| ); |
| |
| // Handle still shown. |
| expect(controller.selection.isCollapsed, true); |
| handle = tester.widget(fadeFinder.at(0)); |
| expect(handle.opacity.value, equals(1.0)); |
| |
| // Simulate text composing and affinity change. |
| tester.testTextInput.updateEditingValue( |
| controller.value.copyWith( |
| selection: controller.value.selection.copyWith(affinity: TextAffinity.downstream), |
| composing: const TextRange(start: 8, end: 15), |
| ), |
| ); |
| await skipPastScrollingAnimation(tester); |
| |
| expect( |
| controller.value, |
| const TextEditingValue( |
| text: 'I Love Flutter!', |
| selection: TextSelection.collapsed(offset: 15, affinity: TextAffinity.upstream), |
| composing: TextRange(start: 8, end: 15), |
| ), |
| ); |
| |
| // Handle still shown. |
| expect(controller.selection.isCollapsed, true); |
| handle = tester.widget(fadeFinder.at(0)); |
| expect(handle.opacity.value, equals(1.0)); |
| |
| final RenderObject handleRenderObjectEnd = tester.renderObject(fadeFinder.at(0)); |
| // The RenderObject sub-tree should not be unmounted. |
| expect(identical(handleRenderObjectBegin, handleRenderObjectEnd), true); |
| }); |
| |
| testWidgets('can use the desktop cut/copy/paste buttons on Mac', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'blah1 blah2', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| // Initially, the menu is not shown and there is no selection. |
| expect(find.byType(CupertinoButton), findsNothing); |
| expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1)); |
| |
| final Offset midBlah1 = textOffsetToPosition(tester, 2); |
| |
| // Right clicking shows the menu. |
| final TestGesture gesture = await tester.startGesture( |
| midBlah1, |
| kind: PointerDeviceKind.mouse, |
| buttons: kSecondaryMouseButton, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); |
| expect(find.text('Cut'), findsOneWidget); |
| expect(find.text('Copy'), findsOneWidget); |
| expect(find.text('Paste'), findsOneWidget); |
| |
| // Copy the first word. |
| await tester.tap(find.text('Copy')); |
| await tester.pumpAndSettle(); |
| expect(controller.text, 'blah1 blah2'); |
| expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| // Paste it at the end. |
| await gesture.down(textOffsetToPosition(tester, controller.text.length)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream)); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), findsOneWidget); |
| await tester.tap(find.text('Paste')); |
| await tester.pumpAndSettle(); |
| expect(controller.text, 'blah1 blah2blah1'); |
| expect(controller.selection, const TextSelection.collapsed(offset: 16)); |
| |
| // Cut the first word. |
| await gesture.down(midBlah1); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(find.text('Cut'), findsOneWidget); |
| expect(find.text('Copy'), findsOneWidget); |
| expect(find.text('Paste'), findsOneWidget); |
| expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); |
| await tester.tap(find.text('Cut')); |
| await tester.pumpAndSettle(); |
| expect(controller.text, ' blah2blah1'); |
| expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0)); |
| expect(find.byType(CupertinoButton), findsNothing); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }), |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. |
| ); |
| |
| testWidgets('can use the desktop cut/copy/paste buttons on Windows and Linux', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'blah1 blah2', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| // Initially, the menu is not shown and there is no selection. |
| expect(find.byType(CupertinoButton), findsNothing); |
| expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1)); |
| |
| final Offset midBlah1 = textOffsetToPosition(tester, 2); |
| |
| // Right clicking shows the menu. |
| TestGesture gesture = await tester.startGesture( |
| midBlah1, |
| kind: PointerDeviceKind.mouse, |
| buttons: kSecondaryMouseButton, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await gesture.removePointer(); |
| await tester.pumpAndSettle(); |
| expect(controller.selection, const TextSelection.collapsed(offset: 2)); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), findsOneWidget); |
| expect(find.text('Select all'), findsOneWidget); |
| |
| // Double tap to select the first word, then right click to show the menu. |
| final Offset startBlah1 = textOffsetToPosition(tester, 0); |
| gesture = await tester.startGesture( |
| startBlah1, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 100)); |
| await gesture.down(startBlah1); |
| await tester.pump(); |
| await gesture.up(); |
| await gesture.removePointer(); |
| await tester.pumpAndSettle(); |
| expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Select all'), findsNothing); |
| gesture = await tester.startGesture( |
| midBlah1, |
| kind: PointerDeviceKind.mouse, |
| buttons: kSecondaryMouseButton, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await gesture.removePointer(); |
| await tester.pumpAndSettle(); |
| expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); |
| expect(find.text('Cut'), findsOneWidget); |
| expect(find.text('Copy'), findsOneWidget); |
| expect(find.text('Paste'), findsOneWidget); |
| |
| // Copy the first word. |
| await tester.tap(find.text('Copy')); |
| await tester.pumpAndSettle(); |
| expect(controller.text, 'blah1 blah2'); |
| expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| // Paste it at the end. |
| gesture = await tester.startGesture( |
| textOffsetToPosition(tester, controller.text.length), |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await gesture.removePointer(); |
| expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream)); |
| gesture = await tester.startGesture( |
| textOffsetToPosition(tester, controller.text.length), |
| kind: PointerDeviceKind.mouse, |
| buttons: kSecondaryMouseButton, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await gesture.removePointer(); |
| await tester.pumpAndSettle(); |
| expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream)); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), findsOneWidget); |
| await tester.tap(find.text('Paste')); |
| await tester.pumpAndSettle(); |
| expect(controller.text, 'blah1 blah2blah1'); |
| expect(controller.selection, const TextSelection.collapsed(offset: 16)); |
| |
| // Cut the first word. |
| gesture = await tester.startGesture( |
| midBlah1, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 100)); |
| await gesture.down(startBlah1); |
| await tester.pump(); |
| await gesture.up(); |
| await gesture.removePointer(); |
| await tester.pumpAndSettle(); |
| expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Select all'), findsNothing); |
| gesture = await tester.startGesture( |
| textOffsetToPosition(tester, controller.text.length), |
| kind: PointerDeviceKind.mouse, |
| buttons: kSecondaryMouseButton, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await gesture.removePointer(); |
| await tester.pumpAndSettle(); |
| expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); |
| expect(find.text('Cut'), findsOneWidget); |
| expect(find.text('Copy'), findsOneWidget); |
| expect(find.text('Paste'), findsOneWidget); |
| await tester.tap(find.text('Cut')); |
| await tester.pumpAndSettle(); |
| expect(controller.text, ' blah2blah1'); |
| expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0)); |
| expect(find.byType(CupertinoButton), findsNothing); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.windows }), |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. |
| ); |
| |
| testWidgets('uses DefaultSelectionStyle for selection and cursor colors if provided', (WidgetTester tester) async { |
| const Color selectionColor = Colors.orange; |
| const Color cursorColor = Colors.red; |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: DefaultSelectionStyle( |
| selectionColor: selectionColor, |
| cursorColor: cursorColor, |
| child: TextField(autofocus: true), |
| ), |
| ), |
| ), |
| ); |
| await tester.pump(); |
| final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); |
| expect(state.widget.selectionColor, selectionColor); |
| expect(state.widget.cursorColor, cursorColor); |
| }); |
| |
| testWidgets('sets cursorOpacityAnimates on EditableText correctly', (WidgetTester tester) async { |
| |
| // True |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: TextField(autofocus: true, cursorOpacityAnimates: true), |
| ), |
| ), |
| ); |
| await tester.pump(); |
| EditableText editableText = tester.widget(find.byType(EditableText)); |
| expect(editableText.cursorOpacityAnimates, true); |
| |
| // False |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: TextField(autofocus: true, cursorOpacityAnimates: false), |
| ), |
| ), |
| ); |
| await tester.pump(); |
| editableText = tester.widget(find.byType(EditableText)); |
| expect(editableText.cursorOpacityAnimates, false); |
| }); |
| |
| testWidgets('Activates the text field when receives semantics focus on Mac, Windows', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; |
| final FocusNode focusNode = FocusNode(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(focusNode: focusNode), |
| ), |
| ), |
| ); |
| expect(semantics, hasSemantics( |
| TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 3, |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 4, |
| flags: <SemanticsFlag>[SemanticsFlag.isTextField], |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| SemanticsAction.didGainAccessibilityFocus, |
| ], |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ignoreRect: true, |
| ignoreTransform: true, |
| )); |
| |
| expect(focusNode.hasFocus, isFalse); |
| semanticsOwner.performAction(4, SemanticsAction.didGainAccessibilityFocus); |
| await tester.pumpAndSettle(); |
| expect(focusNode.hasFocus, isTrue); |
| semantics.dispose(); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows })); |
| |
| testWidgets('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async { |
| void onEditingComplete() { } |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| onEditingComplete: onEditingComplete, |
| ), |
| ), |
| ), |
| ); |
| |
| final Finder editableTextFinder = find.byType(EditableText); |
| expect(editableTextFinder, findsOneWidget); |
| |
| final EditableText editableTextWidget = tester.widget(editableTextFinder); |
| expect(editableTextWidget.onEditingComplete, onEditingComplete); |
| }); |
| |
| testWidgets('TextField has consistent size', (WidgetTester tester) async { |
| final Key textFieldKey = UniqueKey(); |
| String? textFieldValue; |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: textFieldKey, |
| decoration: const InputDecoration( |
| hintText: 'Placeholder', |
| ), |
| onChanged: (String value) { |
| textFieldValue = value; |
| }, |
| ), |
| ), |
| ); |
| |
| RenderBox findTextFieldBox() => tester.renderObject(find.byKey(textFieldKey)); |
| |
| final RenderBox inputBox = findTextFieldBox(); |
| final Size emptyInputSize = inputBox.size; |
| |
| Future<void> checkText(String testValue) async { |
| return TestAsyncUtils.guard(() async { |
| expect(textFieldValue, isNull); |
| await tester.enterText(find.byType(TextField), testValue); |
| // Check that the onChanged event handler fired. |
| expect(textFieldValue, equals(testValue)); |
| textFieldValue = null; |
| await skipPastScrollingAnimation(tester); |
| }); |
| } |
| |
| await checkText(' '); |
| expect(findTextFieldBox(), equals(inputBox)); |
| expect(inputBox.size, equals(emptyInputSize)); |
| |
| await checkText('Test'); |
| expect(findTextFieldBox(), equals(inputBox)); |
| expect(inputBox.size, equals(emptyInputSize)); |
| }); |
| |
| testWidgets('Cursor blinks', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: const TextField( |
| decoration: InputDecoration( |
| hintText: 'Placeholder', |
| ), |
| ), |
| ), |
| ); |
| await tester.showKeyboard(find.byType(TextField)); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| |
| // Check that the cursor visibility toggles after each blink interval. |
| Future<void> checkCursorToggle() async { |
| final bool initialShowCursor = editableText.cursorCurrentlyVisible; |
| await tester.pump(editableText.cursorBlinkInterval); |
| expect(editableText.cursorCurrentlyVisible, equals(!initialShowCursor)); |
| await tester.pump(editableText.cursorBlinkInterval); |
| expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor)); |
| await tester.pump(editableText.cursorBlinkInterval ~/ 10); |
| expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor)); |
| await tester.pump(editableText.cursorBlinkInterval); |
| expect(editableText.cursorCurrentlyVisible, equals(!initialShowCursor)); |
| await tester.pump(editableText.cursorBlinkInterval); |
| expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor)); |
| } |
| |
| await checkCursorToggle(); |
| await tester.showKeyboard(find.byType(TextField)); |
| |
| // Try the test again with a nonempty EditableText. |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: 'X', |
| selection: TextSelection.collapsed(offset: 1), |
| )); |
| await tester.idle(); |
| expect(tester.state(find.byType(EditableText)), editableText); |
| await checkCursorToggle(); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/78918. |
| testWidgets('RenderEditable sets correct text editing value', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(text: 'how are you'); |
| final UniqueKey icon = UniqueKey(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| decoration: InputDecoration( |
| suffixIcon: IconButton( |
| key: icon, |
| icon: const Icon(Icons.cancel), |
| onPressed: () => controller.clear(), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byKey(icon)); |
| await tester.pump(); |
| expect(controller.text, ''); |
| expect(controller.selection, const TextSelection.collapsed(offset: 0)); |
| }); |
| |
| testWidgets('Cursor radius is 2.0', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: TextField(), |
| ), |
| ), |
| ); |
| |
| final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); |
| final RenderEditable renderEditable = editableTextState.renderEditable; |
| |
| expect(renderEditable.cursorRadius, const Radius.circular(2.0)); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('cursor has expected defaults', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: const TextField(), |
| ), |
| ); |
| |
| final TextField textField = tester.firstWidget(find.byType(TextField)); |
| expect(textField.cursorWidth, 2.0); |
| expect(textField.cursorHeight, null); |
| expect(textField.cursorRadius, null); |
| }); |
| |
| testWidgets('cursor has expected radius value', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: const TextField( |
| cursorRadius: Radius.circular(3.0), |
| ), |
| ), |
| ); |
| |
| final TextField textField = tester.firstWidget(find.byType(TextField)); |
| expect(textField.cursorWidth, 2.0); |
| expect(textField.cursorRadius, const Radius.circular(3.0)); |
| }); |
| |
| testWidgets('clipBehavior has expected defaults', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: const TextField(), |
| ), |
| ); |
| |
| final TextField textField = tester.firstWidget(find.byType(TextField)); |
| expect(textField.clipBehavior, Clip.hardEdge); |
| }); |
| |
| testWidgets('Overflow clipBehavior none golden', (WidgetTester tester) async { |
| final Widget widget = Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: RepaintBoundary( |
| key: const ValueKey<int>(1), |
| child: SizedBox( |
| height: 200, |
| width: 200, |
| child: Center( |
| child: SizedBox( |
| // Make sure the input field is not high enough for the WidgetSpan. |
| height: 50, |
| child: TextField( |
| controller: OverflowWidgetTextEditingController(), |
| clipBehavior: Clip.none, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpWidget(widget); |
| |
| final TextField textField = tester.firstWidget(find.byType(TextField)); |
| expect(textField.clipBehavior, Clip.none); |
| |
| final EditableText editableText = tester.firstWidget(find.byType(EditableText)); |
| expect(editableText.clipBehavior, Clip.none); |
| |
| await expectLater( |
| find.byKey(const ValueKey<int>(1)), |
| matchesGoldenFile('overflow_clipbehavior_none.material.0.png'), |
| ); |
| }); |
| |
| testWidgets('Material cursor android golden', (WidgetTester tester) async { |
| final Widget widget = Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: const RepaintBoundary( |
| key: ValueKey<int>(1), |
| child: TextField( |
| cursorColor: Colors.blue, |
| cursorWidth: 15, |
| cursorRadius: Radius.circular(3.0), |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpWidget(widget); |
| |
| const String testValue = 'A short phrase'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| await tester.tapAt(textOffsetToPosition(tester, testValue.length)); |
| await tester.pump(); |
| |
| await expectLater( |
| find.byKey(const ValueKey<int>(1)), |
| matchesGoldenFile('text_field_cursor_test.material.0.png'), |
| ); |
| }); |
| |
| testWidgets('Material cursor golden', (WidgetTester tester) async { |
| final Widget widget = Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: const RepaintBoundary( |
| key: ValueKey<int>(1), |
| child: TextField( |
| cursorColor: Colors.blue, |
| cursorWidth: 15, |
| cursorRadius: Radius.circular(3.0), |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpWidget(widget); |
| |
| const String testValue = 'A short phrase'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| await tester.tapAt(textOffsetToPosition(tester, testValue.length)); |
| await tester.pump(); |
| |
| await expectLater( |
| find.byKey(const ValueKey<int>(1)), |
| matchesGoldenFile( |
| 'text_field_cursor_test_${debugDefaultTargetPlatformOverride!.name.toLowerCase()}.material.1.png', |
| ), |
| ); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('TextInputFormatter gets correct selection value', (WidgetTester tester) async { |
| late TextEditingValue actualOldValue; |
| late TextEditingValue actualNewValue; |
| void callBack(TextEditingValue oldValue, TextEditingValue newValue) { |
| actualOldValue = oldValue; |
| actualNewValue = newValue; |
| } |
| final FocusNode focusNode = FocusNode(); |
| final TextEditingController controller = TextEditingController(text: '123'); |
| await tester.pumpWidget( |
| boilerplate( |
| child: TextField( |
| controller: controller, |
| focusNode: focusNode, |
| inputFormatters: <TextInputFormatter>[TestFormatter(callBack)], |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.backspace); |
| await tester.pumpAndSettle(); |
| |
| expect( |
| actualOldValue, |
| const TextEditingValue( |
| text: '123', |
| selection: TextSelection.collapsed(offset: 3, affinity: TextAffinity.upstream), |
| ), |
| ); |
| expect( |
| actualNewValue, |
| const TextEditingValue( |
| text: '12', |
| selection: TextSelection.collapsed(offset: 2), |
| ), |
| ); |
| }, skip: areKeyEventsHandledByPlatform); // [intended] only applies to platforms where we handle key events. |
| |
| testWidgets('text field selection toolbar renders correctly inside opacity', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: const Scaffold( |
| body: Center( |
| child: SizedBox( |
| width: 100, |
| height: 100, |
| child: Opacity( |
| opacity: 0.5, |
| child: TextField( |
| decoration: InputDecoration(hintText: 'Placeholder'), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.showKeyboard(find.byType(TextField)); |
| |
| const String testValue = 'A B C'; |
| tester.testTextInput.updateEditingValue( |
| const TextEditingValue( |
| text: testValue, |
| ), |
| ); |
| await tester.pump(); |
| |
| // The selectWordsInRange with SelectionChangedCause.tap seems to be needed to show the toolbar. |
| // (This is true even if we provide selection parameter to the TextEditingValue above.) |
| final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); |
| state.renderEditable.selectWordsInRange(from: Offset.zero, cause: SelectionChangedCause.tap); |
| |
| expect(state.showToolbar(), true); |
| |
| // This is needed for the AnimatedOpacity to turn from 0 to 1 so the toolbar is visible. |
| await tester.pumpAndSettle(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| // Sanity check that the toolbar widget exists. |
| expect(find.text('Paste'), findsOneWidget); |
| |
| await expectLater( |
| // The toolbar exists in the Overlay above the MaterialApp. |
| find.byType(Overlay), |
| matchesGoldenFile('text_field_opacity_test.0.png'), |
| ); |
| }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. |
| |
| testWidgets('text field toolbar options correctly changes options', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. |
| // On macOS, we select the precise position of the tap. |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| toolbarOptions: const ToolbarOptions(copy: true), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // This tap just puts the cursor somewhere different than where the double |
| // tap will occur to test that the double tap moves the existing cursor first. |
| await tester.tapAt(textOffsetToPosition(tester, 3)); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| await tester.tapAt(textOffsetToPosition(tester, 8)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap moved the cursor. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 8), |
| ); |
| await tester.tapAt(textOffsetToPosition(tester, 8)); |
| await tester.pump(); |
| |
| // Second tap selects the word around the cursor. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 8, extentOffset: 12), |
| ); |
| |
| // Selected text shows 'Copy', and not 'Paste', 'Cut', 'Select All'. |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Copy'), findsOneWidget); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Select All'), findsNothing); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. |
| ); |
| |
| testWidgets('text selection style 1', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwasssup!', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: Center( |
| child: RepaintBoundary( |
| child: Container( |
| width: 650.0, |
| height: 600.0, |
| decoration: const BoxDecoration( |
| color: Color(0xff00ff00), |
| ), |
| child: Column( |
| children: <Widget>[ |
| TextField( |
| key: const Key('field0'), |
| controller: controller, |
| style: const TextStyle(height: 4, color: Colors.black45), |
| toolbarOptions: const ToolbarOptions(copy: true, selectAll: true), |
| selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingTop, |
| selectionWidthStyle: ui.BoxWidthStyle.max, |
| maxLines: 3, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byKey(const Key('field0'))); |
| |
| await tester.longPressAt(textfieldStart + const Offset(50.0, 2.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.tapAt(textfieldStart + const Offset(100.0, 107.0)); |
| await tester.pump(const Duration(milliseconds: 300)); |
| |
| await expectLater( |
| find.byType(MaterialApp), |
| matchesGoldenFile('text_field_golden.TextSelectionStyle.1.png'), |
| ); |
| }); |
| |
| testWidgets('text selection style 2', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwasssup!', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: Center( |
| child: RepaintBoundary( |
| child: Container( |
| width: 650.0, |
| height: 600.0, |
| decoration: const BoxDecoration( |
| color: Color(0xff00ff00), |
| ), |
| child: Column( |
| children: <Widget>[ |
| TextField( |
| key: const Key('field0'), |
| controller: controller, |
| style: const TextStyle(height: 4, color: Colors.black45), |
| toolbarOptions: const ToolbarOptions(copy: true, selectAll: true), |
| selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingBottom, |
| maxLines: 3, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| final EditableTextState editableTextState = tester.state(find.byType(EditableText)); |
| |
| // Double tap to select the first word. |
| const int index = 4; |
| await tester.tapAt(textOffsetToPosition(tester, index)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.tapAt(textOffsetToPosition(tester, index)); |
| await tester.pumpAndSettle(); |
| expect(editableTextState.selectionOverlay!.handlesAreVisible, isTrue); |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 7); |
| |
| // Select all text. Use the toolbar if possible. iOS only shows the toolbar |
| // when the selection is collapsed. |
| if (isContextMenuProvidedByPlatform || defaultTargetPlatform == TargetPlatform.iOS) { |
| controller.selection = TextSelection(baseOffset: 0, extentOffset: controller.text.length); |
| expect(controller.selection.extentOffset, controller.text.length); |
| } else { |
| await tester.tap(find.text('Select all')); |
| await tester.pump(); |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, controller.text.length); |
| } |
| |
| await expectLater( |
| find.byType(MaterialApp), |
| matchesGoldenFile('text_field_golden.TextSelectionStyle.2.png'), |
| ); |
| // Text selection styles are not fully supported on web. |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/93723 |
| |
| testWidgets( |
| 'text field toolbar options correctly changes options', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| toolbarOptions: const ToolbarOptions(copy: true), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset pos = textOffsetToPosition(tester, 9); // Index of 'P|eel' |
| |
| await tester.tapAt(pos); |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| await tester.tapAt(pos); |
| await tester.pump(); |
| |
| // Selected text shows 'Copy', and not 'Paste', 'Cut', 'Select all'. |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Copy'), findsOneWidget); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Select all'), findsNothing); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.android, |
| TargetPlatform.fuchsia, |
| TargetPlatform.linux, |
| TargetPlatform.windows, |
| }), |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. |
| ); |
| |
| testWidgets('cursor layout has correct width', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController.fromValue( |
| const TextEditingValue(selection: TextSelection.collapsed(offset: 0)), |
| ); |
| final FocusNode focusNode = FocusNode(); |
| EditableText.debugDeterministicCursor = true; |
| await tester.pumpWidget( |
| Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: RepaintBoundary( |
| child: TextField( |
| cursorWidth: 15.0, |
| controller: controller, |
| focusNode: focusNode, |
| ), |
| ), |
| ), |
| ), |
| ); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| |
| await expectLater( |
| find.byType(TextField), |
| matchesGoldenFile('text_field_cursor_width_test.0.png'), |
| ); |
| EditableText.debugDeterministicCursor = false; |
| }); |
| |
| testWidgets('cursor layout has correct radius', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController.fromValue( |
| const TextEditingValue(selection: TextSelection.collapsed(offset: 0)), |
| ); |
| final FocusNode focusNode = FocusNode(); |
| EditableText.debugDeterministicCursor = true; |
| await tester.pumpWidget( |
| Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: RepaintBoundary( |
| child: TextField( |
| cursorWidth: 15.0, |
| cursorRadius: const Radius.circular(3.0), |
| controller: controller, |
| focusNode: focusNode, |
| ), |
| ), |
| ), |
| ), |
| ); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| |
| await expectLater( |
| find.byType(TextField), |
| matchesGoldenFile('text_field_cursor_width_test.1.png'), |
| ); |
| EditableText.debugDeterministicCursor = false; |
| }); |
| |
| testWidgets('cursor layout has correct height', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController.fromValue( |
| const TextEditingValue(selection: TextSelection.collapsed(offset: 0)), |
| ); |
| final FocusNode focusNode = FocusNode(); |
| EditableText.debugDeterministicCursor = true; |
| await tester.pumpWidget( |
| Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: RepaintBoundary( |
| child: TextField( |
| cursorWidth: 15.0, |
| cursorHeight: 30.0, |
| controller: controller, |
| focusNode: focusNode, |
| ), |
| ), |
| ), |
| ), |
| ); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| |
| await expectLater( |
| find.byType(TextField), |
| matchesGoldenFile('text_field_cursor_width_test.2.png'), |
| ); |
| EditableText.debugDeterministicCursor = false; |
| }); |
| |
| testWidgets('Overflowing a line with spaces stops the cursor at the end', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: TextField( |
| key: textFieldKey, |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ); |
| expect(controller.selection.baseOffset, -1); |
| expect(controller.selection.extentOffset, -1); |
| |
| const String testValueOneLine = 'enough text to be exactly at the end of the line.'; |
| await tester.enterText(find.byType(TextField), testValueOneLine); |
| await skipPastScrollingAnimation(tester); |
| |
| RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); |
| |
| RenderBox inputBox = findInputBox(); |
| final Size oneLineInputSize = inputBox.size; |
| |
| await tester.tapAt(textOffsetToPosition(tester, testValueOneLine.length)); |
| await tester.pump(); |
| |
| const String testValueTwoLines = 'enough text to overflow the first line and go to the second'; |
| await tester.enterText(find.byType(TextField), testValueTwoLines); |
| await skipPastScrollingAnimation(tester); |
| |
| expect(inputBox, findInputBox()); |
| inputBox = findInputBox(); |
| expect(inputBox.size.height, greaterThan(oneLineInputSize.height)); |
| final Size twoLineInputSize = inputBox.size; |
| |
| // Enter a string with the same number of characters as testValueTwoLines, |
| // but where the overflowing part is all spaces. Assert that it only renders |
| // on one line. |
| const String testValueSpaces = '$testValueOneLine '; |
| expect(testValueSpaces.length, testValueTwoLines.length); |
| await tester.enterText(find.byType(TextField), testValueSpaces); |
| await skipPastScrollingAnimation(tester); |
| |
| expect(inputBox, findInputBox()); |
| inputBox = findInputBox(); |
| expect(inputBox.size.height, oneLineInputSize.height); |
| |
| // Swapping the final space for a letter causes it to wrap to 2 lines. |
| const String testValueSpacesOverflow = '$testValueOneLine a'; |
| expect(testValueSpacesOverflow.length, testValueTwoLines.length); |
| await tester.enterText(find.byType(TextField), testValueSpacesOverflow); |
| await skipPastScrollingAnimation(tester); |
| |
| expect(inputBox, findInputBox()); |
| inputBox = findInputBox(); |
| expect(inputBox.size.height, twoLineInputSize.height); |
| |
| // Positioning the cursor at the end of a line overflowing with spaces puts |
| // it inside the input still. |
| await tester.enterText(find.byType(TextField), testValueSpaces); |
| await skipPastScrollingAnimation(tester); |
| await tester.tapAt(textOffsetToPosition(tester, testValueSpaces.length)); |
| await tester.pump(); |
| |
| final double inputWidth = findRenderEditable(tester).size.width; |
| final Offset cursorOffsetSpaces = findRenderEditable(tester).getLocalRectForCaret( |
| const TextPosition(offset: testValueSpaces.length), |
| ).bottomRight; |
| |
| expect(cursorOffsetSpaces.dx, inputWidth - kCaretGap); |
| }); |
| |
| testWidgets('Overflowing a line with spaces stops the cursor at the end (rtl direction)', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: const TextField( |
| textDirection: TextDirection.rtl, |
| maxLines: null, |
| ), |
| ), |
| ); |
| |
| const String testValueOneLine = 'enough text to be exactly at the end of the line.'; |
| const String testValueSpaces = '$testValueOneLine '; |
| |
| // Positioning the cursor at the end of a line overflowing with spaces puts |
| // it inside the input still. |
| await tester.enterText(find.byType(TextField), testValueSpaces); |
| await skipPastScrollingAnimation(tester); |
| await tester.tapAt(textOffsetToPosition(tester, testValueSpaces.length)); |
| await tester.pump(); |
| |
| final Offset cursorOffsetSpaces = findRenderEditable(tester).getLocalRectForCaret( |
| const TextPosition(offset: testValueSpaces.length), |
| ).topLeft; |
| |
| expect(cursorOffsetSpaces.dx >= 0, isTrue); |
| }); |
| |
| testWidgets('mobile obscureText control test', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: const TextField( |
| obscureText: true, |
| decoration: InputDecoration( |
| hintText: 'Placeholder', |
| ), |
| ), |
| ), |
| ); |
| await tester.showKeyboard(find.byType(TextField)); |
| |
| const String testValue = 'ABC'; |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: testValue, |
| selection: TextSelection.collapsed(offset: testValue.length), |
| )); |
| |
| await tester.pump(); |
| |
| // Enter a character into the obscured field and verify that the character |
| // is temporarily shown to the user and then changed to a bullet. |
| const String newChar = 'X'; |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: testValue + newChar, |
| selection: TextSelection.collapsed(offset: testValue.length + 1), |
| )); |
| |
| await tester.pump(); |
| |
| String editText = (findRenderEditable(tester).text! as TextSpan).text!; |
| expect(editText.substring(editText.length - 1), newChar); |
| |
| await tester.pump(const Duration(seconds: 2)); |
| |
| editText = (findRenderEditable(tester).text! as TextSpan).text!; |
| expect(editText.substring(editText.length - 1), '\u2022'); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android })); |
| |
| testWidgets('desktop obscureText control test', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: const TextField( |
| obscureText: true, |
| decoration: InputDecoration( |
| hintText: 'Placeholder', |
| ), |
| ), |
| ), |
| ); |
| await tester.showKeyboard(find.byType(TextField)); |
| |
| const String testValue = 'ABC'; |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: testValue, |
| selection: TextSelection.collapsed(offset: testValue.length), |
| )); |
| |
| await tester.pump(); |
| |
| // Enter a character into the obscured field and verify that the character |
| // isn't shown to the user. |
| const String newChar = 'X'; |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: testValue + newChar, |
| selection: TextSelection.collapsed(offset: testValue.length + 1), |
| )); |
| |
| await tester.pump(); |
| |
| final String editText = (findRenderEditable(tester).text! as TextSpan).text!; |
| expect(editText.substring(editText.length - 1), '\u2022'); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.macOS, |
| TargetPlatform.linux, |
| TargetPlatform.windows, |
| })); |
| |
| testWidgets('Caret position is updated on tap', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ); |
| expect(controller.selection.baseOffset, -1); |
| expect(controller.selection.extentOffset, -1); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| // Tap to reposition the caret. |
| final int tapIndex = testValue.indexOf('e'); |
| final Offset ePos = textOffsetToPosition(tester, tapIndex); |
| await tester.tapAt(ePos); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, tapIndex); |
| expect(controller.selection.extentOffset, tapIndex); |
| }); |
| |
| testWidgets('enableInteractiveSelection = false, tap', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| enableInteractiveSelection: false, |
| ), |
| ), |
| ); |
| expect(controller.selection.baseOffset, -1); |
| expect(controller.selection.extentOffset, -1); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| // Tap would ordinarily reposition the caret. |
| final int tapIndex = testValue.indexOf('e'); |
| final Offset ePos = textOffsetToPosition(tester, tapIndex); |
| await tester.tapAt(ePos); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, testValue.length); |
| expect(controller.selection.isCollapsed, isTrue); |
| }); |
| |
| testWidgets('Can long press to select', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| expect(controller.value.text, testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| expect(controller.selection.isCollapsed, true); |
| |
| // Long press the 'e' to select 'def'. |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| await tester.longPressAt(ePos, pointer: 7); |
| await tester.pump(); |
| |
| // 'def' is selected. |
| expect(controller.selection.baseOffset, testValue.indexOf('d')); |
| expect(controller.selection.extentOffset, testValue.indexOf('f')+1); |
| |
| // Tapping elsewhere immediately collapses and moves the cursor. |
| await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('h'))); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('h')); |
| }); |
| |
| testWidgets("Slight movements in longpress don't hide/show handles", (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| expect(controller.value.text, testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| expect(controller.selection.isCollapsed, true); |
| |
| // Long press the 'e' to select 'def', but don't release the gesture. |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| final TestGesture gesture = await tester.startGesture(ePos, pointer: 7); |
| await tester.pump(const Duration(seconds: 2)); |
| await tester.pumpAndSettle(); |
| |
| // Handles are shown |
| final Finder fadeFinder = find.byType(FadeTransition); |
| expect(fadeFinder, findsNWidgets(2)); // 2 handles, 1 toolbar |
| FadeTransition handle = tester.widget(fadeFinder.at(0)); |
| expect(handle.opacity.value, equals(1.0)); |
| |
| // Move the gesture very slightly |
| await gesture.moveBy(const Offset(1.0, 1.0)); |
| await tester.pump(SelectionOverlay.fadeDuration * 0.5); |
| handle = tester.widget(fadeFinder.at(0)); |
| |
| // The handle should still be fully opaque. |
| expect(handle.opacity.value, equals(1.0)); |
| }); |
| |
| testWidgets('Long pressing a field with selection 0,0 shows the selection menu', (WidgetTester tester) async { |
| await tester.pumpWidget(overlay( |
| child: TextField( |
| controller: TextEditingController.fromValue( |
| const TextEditingValue( |
| selection: TextSelection(baseOffset: 0, extentOffset: 0), |
| ), |
| ), |
| ), |
| )); |
| |
| expect(find.text('Paste'), findsNothing); |
| final Offset emptyPos = textOffsetToPosition(tester, 0); |
| await tester.longPressAt(emptyPos, pointer: 7); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), findsOneWidget); |
| }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. |
| |
| testWidgets('Entering text hides selection handle caret', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abcdefghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| expect(controller.value.text, testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| // Handle not shown. |
| expect(controller.selection.isCollapsed, true); |
| final Finder fadeFinder = find.byType(FadeTransition); |
| FadeTransition handle = tester.widget(fadeFinder.at(0)); |
| expect(handle.opacity.value, equals(0.0)); |
| |
| // Tap on the text field to show the handle. |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| expect(controller.selection.isCollapsed, true); |
| expect(fadeFinder, findsNWidgets(1)); |
| handle = tester.widget(fadeFinder.at(0)); |
| expect(handle.opacity.value, equals(1.0)); |
| |
| // Enter more text. |
| const String testValueAddition = 'jklmni'; |
| await tester.enterText(find.byType(TextField), testValueAddition); |
| expect(controller.value.text, testValueAddition); |
| await skipPastScrollingAnimation(tester); |
| |
| // Handle not shown. |
| expect(controller.selection.isCollapsed, true); |
| handle = tester.widget(fadeFinder.at(0)); |
| expect(handle.opacity.value, equals(0.0)); |
| }); |
| |
| testWidgets('selection handles are excluded from the semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abcdefghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| expect(controller.value.text, testValue); |
| await skipPastScrollingAnimation(tester); |
| // Tap on the text field to show the handle. |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| // The semantics should only have the text field. |
| expect(semantics, hasSemantics( |
| TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 1, |
| flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isFocused], |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| SemanticsAction.moveCursorBackwardByCharacter, |
| SemanticsAction.setSelection, |
| SemanticsAction.paste, |
| SemanticsAction.setText, |
| SemanticsAction.moveCursorBackwardByWord, |
| ], |
| value: 'abcdefghi', |
| textDirection: TextDirection.ltr, |
| textSelection: const TextSelection.collapsed(offset: 9), |
| ), |
| ], |
| ), |
| ignoreRect: true, |
| ignoreTransform: true, |
| )); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Mouse long press is just like a tap', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| expect(controller.value.text, testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| expect(controller.selection.isCollapsed, true); |
| |
| // Long press the 'e' using a mouse device. |
| final int eIndex = testValue.indexOf('e'); |
| final Offset ePos = textOffsetToPosition(tester, eIndex); |
| final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pump(); |
| |
| // The cursor is placed just like a regular tap. |
| expect(controller.selection.baseOffset, eIndex); |
| expect(controller.selection.extentOffset, eIndex); |
| }); |
| |
| testWidgets('Read only text field basic', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(text: 'readonly'); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| readOnly: true, |
| ), |
| ), |
| ); |
| // Read only text field cannot open keyboard. |
| await tester.showKeyboard(find.byType(TextField)); |
| // On web, we always create a client connection to the engine. |
| expect(tester.testTextInput.hasAnyClients, isBrowser ? isTrue : isFalse); |
| await skipPastScrollingAnimation(tester); |
| |
| expect(controller.selection.isCollapsed, true); |
| |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| // On web, we always create a client connection to the engine. |
| expect(tester.testTextInput.hasAnyClients, isBrowser ? isTrue : isFalse); |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| // Collapse selection should not paint. |
| expect(editableText.selectionOverlay!.handlesAreVisible, isFalse); |
| // Long press on the 'd' character of text 'readOnly' to show context menu. |
| const int dIndex = 3; |
| final Offset dPos = textOffsetToPosition(tester, dIndex); |
| await tester.longPressAt(dPos); |
| await tester.pumpAndSettle(); |
| |
| // Context menu should not have paste and cut. |
| expect(find.text('Copy'), isContextMenuProvidedByPlatform ? findsNothing : findsOneWidget); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Cut'), findsNothing); |
| }); |
| |
| testWidgets('does not paint toolbar when no options available', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: TextField( |
| readOnly: true, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| await tester.tap(find.byType(TextField)); |
| // Wait for context menu to be built. |
| await tester.pumpAndSettle(); |
| |
| expect(find.byType(CupertinoTextSelectionToolbar), findsNothing); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('text field build empty toolbar when no options available', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: TextField( |
| readOnly: true, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| await tester.tap(find.byType(TextField)); |
| // Wait for context menu to be built. |
| await tester.pumpAndSettle(); |
| final RenderBox container = tester.renderObject(find.descendant( |
| of: find.byType(SnapshotWidget), |
| matching: find.byType(SizedBox), |
| ).first); |
| expect(container.size, Size.zero); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows })); |
| |
| testWidgets('Swapping controllers should update selection', (WidgetTester tester) async { |
| TextEditingController controller = TextEditingController(text: 'readonly'); |
| final OverlayEntry entry = OverlayEntry( |
| builder: (BuildContext context) { |
| return Center( |
| child: Material( |
| child: TextField( |
| controller: controller, |
| readOnly: true, |
| ), |
| ), |
| ); |
| }, |
| ); |
| await tester.pumpWidget(overlayWithEntry(entry)); |
| const int dIndex = 3; |
| final Offset dPos = textOffsetToPosition(tester, dIndex); |
| await tester.longPressAt(dPos); |
| await tester.pumpAndSettle(); |
| final EditableTextState state = tester.state(find.byType(EditableText)); |
| TextSelection currentOverlaySelection = |
| state.selectionOverlay!.value.selection; |
| expect(currentOverlaySelection.baseOffset, 0); |
| expect(currentOverlaySelection.extentOffset, 8); |
| |
| // Update selection from [0 to 8] to [1 to 7]. |
| controller = TextEditingController.fromValue( |
| controller.value.copyWith(selection: const TextSelection( |
| baseOffset: 1, |
| extentOffset: 7, |
| )), |
| ); |
| |
| // Mark entry to be dirty in order to trigger overlay update. |
| entry.markNeedsBuild(); |
| |
| await tester.pump(); |
| currentOverlaySelection = state.selectionOverlay!.value.selection; |
| expect(currentOverlaySelection.baseOffset, 1); |
| expect(currentOverlaySelection.extentOffset, 7); |
| }); |
| |
| testWidgets('Read only text should not compose', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController.fromValue( |
| const TextEditingValue( |
| text: 'readonly', |
| composing: TextRange(start: 0, end: 8), // Simulate text composing. |
| ), |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| readOnly: true, |
| ), |
| ), |
| ); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| // There should be no composing. |
| expect(renderEditable.text, TextSpan(text:'readonly', style: renderEditable.text!.style)); |
| }); |
| |
| testWidgets('Dynamically switching between read only and not read only should hide or show collapse cursor', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(text: 'readonly'); |
| bool readOnly = true; |
| final OverlayEntry entry = OverlayEntry( |
| builder: (BuildContext context) { |
| return Center( |
| child: Material( |
| child: TextField( |
| controller: controller, |
| readOnly: readOnly, |
| ), |
| ), |
| ); |
| }, |
| ); |
| await tester.pumpWidget(overlayWithEntry(entry)); |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| // Collapse selection should not paint. |
| expect(editableText.selectionOverlay!.handlesAreVisible, isFalse); |
| |
| readOnly = false; |
| // Mark entry to be dirty in order to trigger overlay update. |
| entry.markNeedsBuild(); |
| await tester.pumpAndSettle(); |
| expect(editableText.selectionOverlay!.handlesAreVisible, isTrue); |
| |
| readOnly = true; |
| entry.markNeedsBuild(); |
| await tester.pumpAndSettle(); |
| expect(editableText.selectionOverlay!.handlesAreVisible, isFalse); |
| }); |
| |
| testWidgets('Dynamically switching to read only should close input connection', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(text: 'readonly'); |
| bool readOnly = false; |
| final OverlayEntry entry = OverlayEntry( |
| builder: (BuildContext context) { |
| return Center( |
| child: Material( |
| child: TextField( |
| controller: controller, |
| readOnly: readOnly, |
| ), |
| ), |
| ); |
| }, |
| ); |
| await tester.pumpWidget(overlayWithEntry(entry)); |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| expect(tester.testTextInput.hasAnyClients, true); |
| |
| readOnly = true; |
| // Mark entry to be dirty in order to trigger overlay update. |
| entry.markNeedsBuild(); |
| await tester.pump(); |
| // On web, we always have a client connection to the engine. |
| expect(tester.testTextInput.hasAnyClients, isBrowser ? isTrue : isFalse); |
| }); |
| |
| testWidgets('Dynamically switching to non read only should open input connection', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(text: 'readonly'); |
| bool readOnly = true; |
| final OverlayEntry entry = OverlayEntry( |
| builder: (BuildContext context) { |
| return Center( |
| child: Material( |
| child: TextField( |
| controller: controller, |
| readOnly: readOnly, |
| ), |
| ), |
| ); |
| }, |
| ); |
| await tester.pumpWidget(overlayWithEntry(entry)); |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| // On web, we always have a client connection to the engine. |
| expect(tester.testTextInput.hasAnyClients, isBrowser ? isTrue : isFalse); |
| |
| readOnly = false; |
| // Mark entry to be dirty in order to trigger overlay update. |
| entry.markNeedsBuild(); |
| await tester.pump(); |
| expect(tester.testTextInput.hasAnyClients, true); |
| }); |
| |
| testWidgets('enableInteractiveSelection = false, long-press', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| enableInteractiveSelection: false, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| expect(controller.value.text, testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| expect(controller.selection.isCollapsed, true); |
| |
| // Long press the 'e' to select 'def'. |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| await tester.longPressAt(ePos, pointer: 7); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.length); |
| }); |
| |
| testWidgets('Selection updates on tap down (Desktop platforms)', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset ePos = textOffsetToPosition(tester, 5); |
| final Offset gPos = textOffsetToPosition(tester, 8); |
| |
| final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); |
| await tester.pumpAndSettle(); |
| expect(controller.selection.baseOffset, 5); |
| expect(controller.selection.extentOffset, 5); |
| |
| await gesture.up(); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| |
| await gesture.down(gPos); |
| await tester.pumpAndSettle(); |
| expect(controller.selection.baseOffset, 8); |
| expect(controller.selection.extentOffset, 8); |
| |
| // This should do nothing. The selection is set on tap down on desktop platforms. |
| await gesture.up(); |
| expect(controller.selection.baseOffset, 8); |
| expect(controller.selection.extentOffset, 8); |
| }, |
| variant: TargetPlatformVariant.desktop(), |
| ); |
| |
| testWidgets('Selection updates on tap up (Mobile platforms)', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset ePos = textOffsetToPosition(tester, 5); |
| final Offset gPos = textOffsetToPosition(tester, 8); |
| |
| final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); |
| await gesture.up(); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| |
| await gesture.down(gPos); |
| await tester.pumpAndSettle(); |
| expect(controller.selection.baseOffset, 5); |
| expect(controller.selection.extentOffset, 5); |
| |
| await gesture.up(); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| expect(controller.selection.baseOffset, 8); |
| expect(controller.selection.extentOffset, 8); |
| |
| final TestGesture touchGesture = await tester.startGesture(ePos); |
| await touchGesture.up(); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| // On iOS a tap to select, selects the word edge instead of the exact tap position. |
| expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5); |
| expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5); |
| |
| // Selection should stay the same since it is set on tap up for mobile platforms. |
| await touchGesture.down(gPos); |
| await tester.pump(); |
| expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5); |
| expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5); |
| |
| await touchGesture.up(); |
| await tester.pumpAndSettle(); |
| expect(controller.selection.baseOffset, 8); |
| expect(controller.selection.extentOffset, 8); |
| }, |
| variant: TargetPlatformVariant.mobile(), |
| ); |
| |
| testWidgets('Can select text with a mouse when wrapped in a GestureDetector with tap/double tap callbacks', (WidgetTester tester) async { |
| // This is a regression test for https://github.com/flutter/flutter/issues/129161. |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: GestureDetector( |
| onTap: () {}, |
| onDoubleTap: () {}, |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); |
| |
| final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); |
| await tester.pump(); |
| await gesture.up(); |
| // This is to allow the GestureArena to decide a winner between TapGestureRecognizer, |
| // DoubleTapGestureRecognizer, and BaseTapAndDragGestureRecognizer. |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('e')); |
| |
| await gesture.down(ePos); |
| await tester.pump(); |
| await gesture.moveTo(gPos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, testValue.indexOf('e')); |
| expect(controller.selection.extentOffset, testValue.indexOf('g')); |
| }, variant: TargetPlatformVariant.desktop()); |
| |
| testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); |
| |
| final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); |
| await tester.pump(); |
| await gesture.moveTo(gPos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, testValue.indexOf('e')); |
| expect(controller.selection.extentOffset, testValue.indexOf('g')); |
| }); |
| |
| testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS)', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i')); |
| |
| // Tap on text field to gain focus, and set selection to '|g'. On iOS |
| // the selection is set to the word edge closest to the tap position. |
| // We await for kDoubleTapTimeout after the up event, so our next down event |
| // does not register as a double tap. |
| final TestGesture gesture = await tester.startGesture(ePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 7); |
| |
| // If the position we tap during a drag start is on the collapsed selection, then |
| // we can move the cursor with a drag. |
| // Here we tap on '|g', where our selection was previously, and move to '|i'. |
| await gesture.down(textOffsetToPosition(tester, 7)); |
| await tester.pump(); |
| await gesture.moveTo(iPos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('i')); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets('Cursor should not move on a quick touch drag when touch does not begin on previous selection (iOS)', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a')); |
| final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i')); |
| |
| // Tap on text field to gain focus, and set selection to '|a'. On iOS |
| // the selection is set to the word edge closest to the tap position. |
| // We await for kDoubleTapTimeout after the up event, so our next down event |
| // does not register as a double tap. |
| final TestGesture gesture = await tester.startGesture(aPos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 0); |
| |
| // The position we tap during a drag start is not on the collapsed selection, |
| // so the cursor should not move. |
| await gesture.down(textOffsetToPosition(tester, 7)); |
| await gesture.moveTo(iPos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 0); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - multiline', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc\ndef\nghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a')); |
| final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i')); |
| |
| // Tap on text field to gain focus, and set selection to '|a'. On iOS |
| // the selection is set to the word edge closest to the tap position. |
| // We await for kDoubleTapTimeout after the up event, so our next down event |
| // does not register as a double tap. |
| final TestGesture gesture = await tester.startGesture(aPos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 0); |
| |
| // If the position we tap during a drag start is on the collapsed selection, then |
| // we can move the cursor with a drag. |
| // Here we tap on '|a', where our selection was previously, and move to '|i'. |
| await gesture.down(aPos); |
| await tester.pump(); |
| await gesture.moveTo(iPos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('i')); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - ListView', (WidgetTester tester) async { |
| // This is a regression test for |
| // https://github.com/flutter/flutter/issues/122519 |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ListView( |
| children: <Widget>[ |
| TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| maxLines: null, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc\ndef\nghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a')); |
| final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); |
| final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i')); |
| |
| // Tap on text field to gain focus, and set selection to '|a'. On iOS |
| // the selection is set to the word edge closest to the tap position. |
| // We await for kDoubleTapTimeout after the up event, so our next down event |
| // does not register as a double tap. |
| final TestGesture gesture = await tester.startGesture(aPos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 0); |
| |
| // If the position we tap during a drag start is on the collapsed selection, then |
| // we can move the cursor with a drag. |
| // Here we tap on '|a', where our selection was previously, and attempt move |
| // to '|g'. The cursor will not move because the `VerticalDragGestureRecognizer` |
| // in the scrollable will beat the `TapAndHorizontalDragGestureRecognizer` |
| // in the TextField. This is because moving from `|a` to `|g` is a completely |
| // vertical movement. |
| await gesture.down(aPos); |
| await tester.pump(); |
| await gesture.moveTo(gPos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 0); |
| |
| // Release the pointer. |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // If the position we tap during a drag start is on the collapsed selection, then |
| // we can move the cursor with a drag. |
| // Here we tap on '|a', where our selection was previously, and move to '|i'. |
| // Unlike our previous attempt to drag to `|g`, this works because moving |
| // to `|i` includes a horizontal movement so the `TapAndHorizontalDragGestureRecognizer` |
| // in TextField can beat the `VerticalDragGestureRecognizer` in the scrollable. |
| await gesture.down(aPos); |
| await tester.pump(); |
| await gesture.moveTo(iPos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('i')); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets('Can move cursor when dragging (Android)', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); |
| |
| // Tap on text field to gain focus, and set selection to '|e'. |
| // We await for kDoubleTapTimeout after the up event, so our next down event |
| // does not register as a double tap. |
| final TestGesture gesture = await tester.startGesture(ePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('e')); |
| |
| // Here we tap on '|d', and move to '|g'. |
| await gesture.down(textOffsetToPosition(tester, testValue.indexOf('d'))); |
| await tester.pump(); |
| await gesture.moveTo(gPos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('g')); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }), |
| ); |
| |
| testWidgets('Can move cursor when dragging (Android) - multiline', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc\ndef\nghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a')); |
| final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); |
| |
| // Tap on text field to gain focus, and set selection to '|a'. |
| // We await for kDoubleTapTimeout after the up event, so our next down event |
| // does not register as a double tap. |
| final TestGesture gesture = await tester.startGesture(aPos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('a')); |
| |
| // Here we tap on '|c', and move down to '|g'. |
| await gesture.down(textOffsetToPosition(tester, testValue.indexOf('c'))); |
| await tester.pump(); |
| await gesture.moveTo(gPos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('g')); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }), |
| ); |
| |
| testWidgets('Can move cursor when dragging (Android) - ListView', (WidgetTester tester) async { |
| // This is a regression test for |
| // https://github.com/flutter/flutter/issues/122519 |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ListView( |
| children: <Widget>[ |
| TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| maxLines: null, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc\ndef\nghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a')); |
| final Offset cPos = textOffsetToPosition(tester, testValue.indexOf('c')); |
| final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); |
| |
| // Tap on text field to gain focus, and set selection to '|c'. |
| // We await for kDoubleTapTimeout after the up event, so our next down event |
| // does not register as a double tap. |
| final TestGesture gesture = await tester.startGesture(cPos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('c')); |
| |
| // Here we tap on '|a', and attempt move to '|g'. The cursor will not move |
| // because the `VerticalDragGestureRecognizer` in the scrollable will beat |
| // the `TapAndHorizontalDragGestureRecognizer` in the TextField. This is |
| // because moving from `|a` to `|g` is a completely vertical movement. |
| await gesture.down(aPos); |
| await tester.pump(); |
| await gesture.moveTo(gPos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('c')); |
| |
| // Release the pointer. |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // Here we tap on '|c', and move to '|g'. Unlike our previous attempt to |
| // drag to `|g`, this works because moving from `|c` to `|g` includes a |
| // horizontal movement so the `TapAndHorizontalDragGestureRecognizer` |
| // in TextField can beat the `VerticalDragGestureRecognizer` in the scrollable. |
| await gesture.down(cPos); |
| await tester.pump(); |
| await gesture.moveTo(gPos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('g')); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }), |
| ); |
| |
| testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async { |
| int selectionChangedCount = 0; |
| const String testValue = 'abc def ghi'; |
| final TextEditingController controller = TextEditingController(text: testValue); |
| |
| controller.addListener(() { |
| selectionChangedCount++; |
| }); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| style: const TextStyle(fontSize: 10.0), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset cPos = textOffsetToPosition(tester, 2); // Index of 'c'. |
| final Offset gPos = textOffsetToPosition(tester, 8); // Index of 'g'. |
| final Offset hPos = textOffsetToPosition(tester, 9); // Index of 'h'. |
| |
| // Drag from 'c' to 'g'. |
| final TestGesture gesture = await tester.startGesture(cPos, kind: PointerDeviceKind.mouse); |
| await tester.pump(); |
| await gesture.moveTo(gPos); |
| await tester.pumpAndSettle(); |
| |
| expect(selectionChangedCount, isNonZero); |
| selectionChangedCount = 0; |
| expect(controller.selection.baseOffset, 2); |
| expect(controller.selection.extentOffset, 8); |
| |
| // Tiny movement shouldn't cause text selection to change. |
| await gesture.moveTo(gPos + const Offset(2.0, 0.0)); |
| await tester.pumpAndSettle(); |
| expect(selectionChangedCount, 0); |
| |
| // Now a text selection change will occur after a significant movement. |
| await gesture.moveTo(hPos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(selectionChangedCount, 1); |
| expect(controller.selection.baseOffset, 2); |
| expect(controller.selection.extentOffset, 9); |
| }); |
| |
| testWidgets('Dragging in opposite direction also works', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); |
| |
| final TestGesture gesture = await tester.startGesture(gPos, kind: PointerDeviceKind.mouse); |
| await tester.pump(); |
| await gesture.moveTo(ePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, testValue.indexOf('g')); |
| expect(controller.selection.extentOffset, testValue.indexOf('e')); |
| }); |
| |
| testWidgets('Slow mouse dragging also selects text', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); |
| |
| final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.moveTo(gPos); |
| await tester.pump(); |
| await gesture.up(); |
| |
| expect(controller.selection.baseOffset, testValue.indexOf('e')); |
| expect(controller.selection.extentOffset, testValue.indexOf('g')); |
| }); |
| |
| testWidgets('Can drag handles to change selection on Apple platforms', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| // Double tap the 'e' to select 'def'. |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| // The first tap. |
| TestGesture gesture = await tester.startGesture(ePos, pointer: 7); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| |
| // The second tap. |
| await gesture.down(ePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| final TextSelection selection = controller.selection; |
| expect(selection.baseOffset, 4); |
| expect(selection.extentOffset, 7); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 2); |
| |
| // Drag the right handle 2 letters to the right. |
| // We use a small offset because the endpoint is on the very corner |
| // of the handle. |
| Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0); |
| Offset newHandlePos = textOffsetToPosition(tester, testValue.length); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, 11); |
| |
| // Drag the left handle 2 letters to the left. |
| handlePos = endpoints[0].point + const Offset(-1.0, 1.0); |
| newHandlePos = textOffsetToPosition(tester, 2); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| switch (defaultTargetPlatform) { |
| // On Apple platforms, dragging the base handle makes it the extent. |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| expect(controller.selection.baseOffset, 11); |
| expect(controller.selection.extentOffset, 2); |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| expect(controller.selection.baseOffset, 2); |
| expect(controller.selection.extentOffset, 11); |
| } |
| |
| // Drag the left handle 2 letters to the left again. |
| endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| handlePos = endpoints[0].point + const Offset(-1.0, 1.0); |
| newHandlePos = textOffsetToPosition(tester, 0); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| // The left handle was already the extent, and it remains so. |
| expect(controller.selection.baseOffset, 11); |
| expect(controller.selection.extentOffset, 0); |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 11); |
| } |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets('Can drag handles to change selection on non-Apple platforms', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| // Long press the 'e' to select 'def'. |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| TestGesture gesture = await tester.startGesture(ePos, pointer: 7); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| |
| final TextSelection selection = controller.selection; |
| expect(selection.baseOffset, 4); |
| expect(selection.extentOffset, 7); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 2); |
| |
| // Drag the right handle 2 letters to the right. |
| // We use a small offset because the endpoint is on the very corner |
| // of the handle. |
| Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0); |
| Offset newHandlePos = textOffsetToPosition(tester, testValue.length); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, 11); |
| |
| // Drag the left handle 2 letters to the left. |
| handlePos = endpoints[0].point + const Offset(-1.0, 1.0); |
| newHandlePos = textOffsetToPosition(tester, 2); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| switch (defaultTargetPlatform) { |
| // On Apple platforms, dragging the base handle makes it the extent. |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| expect(controller.selection.baseOffset, 11); |
| expect(controller.selection.extentOffset, 2); |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| expect(controller.selection.baseOffset, 2); |
| expect(controller.selection.extentOffset, 11); |
| } |
| |
| // Drag the left handle 2 letters to the left again. |
| endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| handlePos = endpoints[0].point + const Offset(-1.0, 1.0); |
| newHandlePos = textOffsetToPosition(tester, 0); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| // The left handle was already the extent, and it remains so. |
| expect(controller.selection.baseOffset, 11); |
| expect(controller.selection.extentOffset, 0); |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 11); |
| } |
| }, |
| variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'Can drag the left handle while the right handle remains off-screen', |
| (WidgetTester tester) async { |
| // Text is longer than textfield width. |
| const String testValue = |
| 'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb'; |
| final TextEditingController controller = TextEditingController(text: testValue); |
| final ScrollController scrollController = ScrollController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: MediaQuery( |
| data: const MediaQueryData(size: Size(800.0, 600.0)), |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| scrollController: scrollController, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Double tap 'b' to show handles. |
| final Offset bPos = textOffsetToPosition(tester, testValue.indexOf('b')); |
| await tester.tapAt(bPos); |
| await tester.pump(kDoubleTapTimeout ~/ 2); |
| await tester.tapAt(bPos); |
| await tester.pumpAndSettle(); |
| |
| final TextSelection selection = controller.selection; |
| expect(selection.baseOffset, 28); |
| expect(selection.extentOffset, testValue.length); |
| |
| // Move to the left edge. |
| scrollController.jumpTo(0); |
| await tester.pumpAndSettle(); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 2); |
| |
| // Left handle should appear between textfield's left and right position. |
| final Offset textFieldLeftPosition = |
| tester.getTopLeft(find.byType(TextField)); |
| expect(endpoints[0].point.dx - textFieldLeftPosition.dx, isPositive); |
| final Offset textFieldRightPosition = |
| tester.getTopRight(find.byType(TextField)); |
| expect(textFieldRightPosition.dx - endpoints[0].point.dx, isPositive); |
| // Right handle should remain off-screen. |
| expect(endpoints[1].point.dx - textFieldRightPosition.dx, isPositive); |
| |
| // Drag the left handle to the right by 25 offset. |
| const int toOffset = 25; |
| final double beforeScrollOffset = scrollController.offset; |
| final Offset handlePos = endpoints[0].point + const Offset(-1.0, 1.0); |
| final Offset newHandlePos = textOffsetToPosition(tester, toOffset); |
| final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| // On Apple platforms, dragging the base handle makes it the extent. |
| expect(controller.selection.baseOffset, testValue.length); |
| expect(controller.selection.extentOffset, toOffset); |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| expect(controller.selection.baseOffset, toOffset); |
| expect(controller.selection.extentOffset, testValue.length); |
| } |
| |
| // The scroll area of text field should not move. |
| expect(scrollController.offset, beforeScrollOffset); |
| }, |
| ); |
| |
| testWidgets( |
| 'Can drag the right handle while the left handle remains off-screen', |
| (WidgetTester tester) async { |
| // Text is longer than textfield width. |
| const String testValue = |
| 'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb'; |
| final TextEditingController controller = TextEditingController(text: testValue); |
| final ScrollController scrollController = ScrollController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: MediaQuery( |
| data: const MediaQueryData(size: Size(800.0, 600.0)), |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| scrollController: scrollController, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Double tap 'a' to show handles. |
| final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a')); |
| await tester.tapAt(aPos); |
| await tester.pump(kDoubleTapTimeout ~/ 2); |
| await tester.tapAt(aPos); |
| await tester.pumpAndSettle(); |
| |
| final TextSelection selection = controller.selection; |
| expect(selection.baseOffset, 0); |
| expect(selection.extentOffset, 27); |
| |
| // Move to the right edge. |
| scrollController.jumpTo(800); |
| await tester.pumpAndSettle(); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 2); |
| |
| // Right handle should appear between textfield's left and right position. |
| final Offset textFieldLeftPosition = |
| tester.getTopLeft(find.byType(TextField)); |
| expect(endpoints[1].point.dx - textFieldLeftPosition.dx, isPositive); |
| final Offset textFieldRightPosition = |
| tester.getTopRight(find.byType(TextField)); |
| expect(textFieldRightPosition.dx - endpoints[1].point.dx, isPositive); |
| // Left handle should remain off-screen. |
| expect(endpoints[0].point.dx, isNegative); |
| |
| // Drag the right handle to the left by 50 offset. |
| const int toOffset = 50; |
| final double beforeScrollOffset = scrollController.offset; |
| final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0); |
| final Offset newHandlePos = textOffsetToPosition(tester, toOffset); |
| final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, toOffset); |
| |
| // The scroll area of text field should not move. |
| expect(scrollController.offset, beforeScrollOffset); |
| }, |
| ); |
| |
| testWidgets('Drag handles trigger feedback', (WidgetTester tester) async { |
| final FeedbackTester feedback = FeedbackTester(); |
| addTearDown(feedback.dispose); |
| final TextEditingController controller = TextEditingController(); |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| expect(feedback.hapticCount, 0); |
| await skipPastScrollingAnimation(tester); |
| |
| // Long press the 'e' to select 'def'. |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| TestGesture gesture = await tester.startGesture(ePos, pointer: 7); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| |
| final TextSelection selection = controller.selection; |
| expect(selection.baseOffset, 4); |
| expect(selection.extentOffset, 7); |
| expect(feedback.hapticCount, 1); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 2); |
| |
| // Drag the right handle 2 letters to the right. |
| // Use a small offset because the endpoint is on the very corner |
| // of the handle. |
| final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0); |
| final Offset newHandlePos = textOffsetToPosition(tester, testValue.length); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, 11); |
| expect(feedback.hapticCount, 2); |
| }); |
| |
| testWidgets('Dragging a collapsed handle should trigger feedback.', (WidgetTester tester) async { |
| final FeedbackTester feedback = FeedbackTester(); |
| addTearDown(feedback.dispose); |
| final TextEditingController controller = TextEditingController(); |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| expect(feedback.hapticCount, 0); |
| await skipPastScrollingAnimation(tester); |
| |
| // Tap the 'e' to bring up a collapsed handle. |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| TestGesture gesture = await tester.startGesture(ePos, pointer: 7); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| |
| final TextSelection selection = controller.selection; |
| expect(selection.baseOffset, 5); |
| expect(selection.extentOffset, 5); |
| expect(feedback.hapticCount, 0); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 1); |
| |
| // Drag the right handle 3 letters to the right. |
| // Use a small offset because the endpoint is on the very corner |
| // of the handle. |
| final Offset handlePos = endpoints[0].point + const Offset(1.0, 1.0); |
| final Offset newHandlePos = textOffsetToPosition(tester, testValue.indexOf('g')); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 8); |
| expect(controller.selection.extentOffset, 8); |
| expect(feedback.hapticCount, 1); |
| }); |
| |
| testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| // Long press the 'e' to select 'def'. |
| final Offset ePos = textOffsetToPosition(tester, 5); // Position before 'e'. |
| TestGesture gesture = await tester.startGesture(ePos, pointer: 7); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| |
| final TextSelection selection = controller.selection; |
| expect(selection.baseOffset, 4); |
| expect(selection.extentOffset, 7); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 2); |
| |
| // Drag the right handle until there's only 1 char selected. |
| // We use a small offset because the endpoint is on the very corner |
| // of the handle. |
| final Offset handlePos = endpoints[1].point + const Offset(4.0, 0.0); |
| Offset newHandlePos = textOffsetToPosition(tester, 5); // Position before 'e'. |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, 5); |
| |
| newHandlePos = textOffsetToPosition(tester, 2); // Position before 'c'. |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 4); |
| // The selection doesn't move beyond the left handle. There's always at |
| // least 1 char selected. |
| expect(controller.selection.extentOffset, 5); |
| }); |
| |
| testWidgets('Dragging between multiple lines keeps the contact point at the same place on the handle on Android', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| // 11 first line, 19 second line, 17 third line = length 49 |
| text: 'a big house\njumped over a mouse\nOne more line yay', |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| maxLines: 3, |
| minLines: 3, |
| ), |
| ), |
| ); |
| |
| // Double tap to select 'over'. |
| final Offset pos = textOffsetToPosition(tester, controller.text.indexOf('v')); |
| // The first tap. |
| TestGesture gesture = await tester.startGesture(pos, pointer: 7); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| |
| // The second tap. |
| await gesture.down(pos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| final TextSelection selection = controller.selection; |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 19, |
| extentOffset: 23, |
| ), |
| ); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 2); |
| |
| // Drag the right handle 4 letters to the right. |
| // The adjustment moves the tap from the text position to the handle. |
| const Offset endHandleAdjustment = Offset(1.0, 6.0); |
| Offset handlePos = endpoints[1].point + endHandleAdjustment; |
| Offset newHandlePos = textOffsetToPosition(tester, 27) + endHandleAdjustment; |
| await tester.pump(); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 19, |
| extentOffset: 27, |
| ), |
| ); |
| |
| // Drag the right handle 1 line down. |
| endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| handlePos = endpoints[1].point + endHandleAdjustment; |
| final Offset toNextLine = Offset( |
| 0.0, |
| findRenderEditable(tester).preferredLineHeight + 3.0, |
| ); |
| newHandlePos = handlePos + toNextLine; |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 19, |
| extentOffset: 47, |
| ), |
| ); |
| |
| // Drag the right handle back up 1 line. |
| endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| handlePos = endpoints[1].point + endHandleAdjustment; |
| newHandlePos = handlePos - toNextLine; |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 19, |
| extentOffset: 27, |
| ), |
| ); |
| |
| // Drag the left handle 4 letters to the left. |
| // The adjustment moves the tap from the text position to the handle. |
| const Offset startHandleAdjustment = Offset(-1.0, 6.0); |
| endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| handlePos = endpoints[0].point + startHandleAdjustment; |
| newHandlePos = textOffsetToPosition(tester, 15) + startHandleAdjustment; |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 15, |
| extentOffset: 27, |
| ), |
| ); |
| |
| // Drag the left handle 1 line up. |
| endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| handlePos = endpoints[0].point + startHandleAdjustment; |
| newHandlePos = handlePos - toNextLine; |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 3, |
| extentOffset: 27, |
| ), |
| ); |
| |
| // Drag the left handle 1 line back down. |
| endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| handlePos = endpoints[0].point + startHandleAdjustment; |
| newHandlePos = handlePos + toNextLine; |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 15, |
| extentOffset: 27, |
| ), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), |
| ); |
| |
| testWidgets('Dragging between multiple lines keeps the contact point at the same place on the handle on iOS', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| // 11 first line, 19 second line, 17 third line = length 49 |
| text: 'a big house\njumped over a mouse\nOne more line yay', |
| ); |
| |
| await tester.pumpWidget( |
| Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| maxLines: 3, |
| minLines: 3, |
| ), |
| ), |
| ), |
| ); |
| |
| // Double tap to select 'over'. |
| final Offset pos = textOffsetToPosition(tester, controller.text.indexOf('v')); |
| // The first tap. |
| TestGesture gesture = await tester.startGesture(pos, pointer: 7); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| |
| // The second tap. |
| await gesture.down(pos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| final TextSelection selection = controller.selection; |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 19, |
| extentOffset: 23, |
| ), |
| ); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 2); |
| |
| // Drag the right handle 4 letters to the right. |
| // The adjustment moves the tap from the text position to the handle. |
| const Offset endHandleAdjustment = Offset(1.0, 6.0); |
| Offset handlePos = endpoints[1].point + endHandleAdjustment; |
| Offset newHandlePos = textOffsetToPosition(tester, 27) + endHandleAdjustment; |
| await tester.pump(); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 19, |
| extentOffset: 27, |
| ), |
| ); |
| |
| // Drag the right handle 1 line down. |
| endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| handlePos = endpoints[1].point + endHandleAdjustment; |
| final double lineHeight = findRenderEditable(tester).preferredLineHeight; |
| final Offset toNextLine = Offset(0.0, lineHeight + 3.0); |
| newHandlePos = handlePos + toNextLine; |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 19, |
| extentOffset: 47, |
| ), |
| ); |
| |
| // Drag the right handle back up 1 line. |
| endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| handlePos = endpoints[1].point + endHandleAdjustment; |
| newHandlePos = handlePos - toNextLine; |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 19, |
| extentOffset: 27, |
| ), |
| ); |
| |
| // Drag the left handle 4 letters to the left. |
| // The adjustment moves the tap from the text position to the handle. |
| final Offset startHandleAdjustment = Offset(-1.0, -lineHeight + 6.0); |
| endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| handlePos = endpoints[0].point + startHandleAdjustment; |
| newHandlePos = textOffsetToPosition(tester, 15) + startHandleAdjustment; |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| // On Apple platforms, dragging the base handle makes it the extent. |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 27, |
| extentOffset: 15, |
| ), |
| ); |
| |
| // Drag the left handle 1 line up. |
| endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| handlePos = endpoints[0].point + startHandleAdjustment; |
| // Move handle a sufficient global distance so it can be considered a drag |
| // by the selection handle's [PanGestureRecognizer]. |
| newHandlePos = handlePos - (toNextLine * 2); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 27, |
| extentOffset: 3, |
| ), |
| ); |
| |
| // Drag the left handle 1 line back down. |
| endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| handlePos = endpoints[0].point + startHandleAdjustment; |
| newHandlePos = handlePos + toNextLine; |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| // Move handle up a small amount before dragging it down so the total global |
| // distance travelled can be accepted by the selection handle's [PanGestureRecognizer] as a drag. |
| // This way it can declare itself the winner before the [TapAndDragGestureRecognizer] that |
| // is on the selection overlay. |
| await gesture.moveTo(handlePos - toNextLine); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 27, |
| extentOffset: 15, |
| ), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets("dragging caret within a word doesn't affect composing region", (WidgetTester tester) async { |
| const String testValue = 'abc def ghi'; |
| final TextEditingController controller = TextEditingController.fromValue( |
| const TextEditingValue( |
| text: testValue, |
| selection: TextSelection( |
| baseOffset: 4, |
| extentOffset: 4, |
| affinity: TextAffinity.upstream, |
| ), |
| composing: TextRange( |
| start: 4, |
| end: 7, |
| ), |
| ), |
| ); |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.value.composing.start, 4); |
| expect(controller.value.composing.end, 7); |
| |
| // Tap the caret to show the handle. |
| final Offset ePos = textOffsetToPosition(tester, 4); |
| await tester.tapAt(ePos); |
| await tester.pumpAndSettle(); |
| |
| final TextSelection selection = controller.selection; |
| expect(controller.selection.isCollapsed, true); |
| expect(selection.baseOffset, 4); |
| expect(controller.value.composing.start, 4); |
| expect(controller.value.composing.end, 7); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 1); |
| |
| // Drag the right handle 2 letters to the right. |
| // We use a small offset because the endpoint is on the very corner |
| // of the handle. |
| final Offset handlePos = endpoints[0].point + const Offset(1.0, 1.0); |
| final Offset newHandlePos = textOffsetToPosition(tester, 7); |
| final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 7); |
| expect(controller.value.composing.start, 4); |
| expect(controller.value.composing.end, 7); |
| }, |
| skip: kIsWeb, // [intended] text selection is handled by the browser |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }) |
| ); |
| |
| testWidgets('Can use selection toolbar', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| // Tap the selection handle to bring up the "paste / select all" menu. |
| await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| RenderEditable renderEditable = findRenderEditable(tester); |
| List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| // Tapping on the part of the handle's GestureDetector where it overlaps |
| // with the text itself does not show the menu, so add a small vertical |
| // offset to tap below the text. |
| await tester.tapAt(endpoints[0].point + const Offset(1.0, 13.0)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| |
| // Select all should select all the text. |
| await tester.tap(find.text('Select all')); |
| await tester.pump(); |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, testValue.length); |
| |
| // Copy should reset the selection. |
| await tester.tap(find.text('Copy')); |
| await skipPastScrollingAnimation(tester); |
| expect(controller.selection.isCollapsed, true); |
| |
| // Tap again to bring back the menu. |
| await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); |
| await tester.pump(); |
| // Allow time for handle to appear and double tap to time out. |
| await tester.pump(const Duration(milliseconds: 300)); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('e')); |
| expect(controller.selection.extentOffset, testValue.indexOf('e')); |
| renderEditable = findRenderEditable(tester); |
| endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('e')); |
| expect(controller.selection.extentOffset, testValue.indexOf('e')); |
| |
| // Paste right before the 'e'. |
| await tester.tap(find.text('Paste')); |
| await tester.pump(); |
| expect(controller.text, 'abc d${testValue}ef ghi'); |
| }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. |
| |
| // Show the selection menu at the given index into the text by tapping to |
| // place the cursor and then tapping on the handle. |
| Future<void> showSelectionMenuAt(WidgetTester tester, TextEditingController controller, int index) async { |
| await tester.tapAt(tester.getCenter(find.byType(EditableText))); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| expect(find.text('Select all'), findsNothing); |
| |
| // Tap the selection handle to bring up the "paste / select all" menu for |
| // the last line of text. |
| await tester.tapAt(textOffsetToPosition(tester, index)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| // Tapping on the part of the handle's GestureDetector where it overlaps |
| // with the text itself does not show the menu, so add a small vertical |
| // offset to tap below the text. |
| await tester.tapAt(endpoints[0].point + const Offset(1.0, 13.0)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| } |
| |
| testWidgets( |
| 'Check the toolbar appears below the TextField when there is not enough space above the TextField to show it', |
| (WidgetTester tester) async { |
| // This is a regression test for |
| // https://github.com/flutter/flutter/issues/29808 |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: Padding( |
| padding: const EdgeInsets.all(30.0), |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| await showSelectionMenuAt(tester, controller, testValue.indexOf('e')); |
| |
| // Verify the selection toolbar position is below the text. |
| Offset toolbarTopLeft = tester.getTopLeft(find.text('Select all')); |
| Offset textFieldTopLeft = tester.getTopLeft(find.byType(TextField)); |
| expect(textFieldTopLeft.dy, lessThan(toolbarTopLeft.dy)); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: Padding( |
| padding: const EdgeInsets.all(150.0), |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| await showSelectionMenuAt(tester, controller, testValue.indexOf('e')); |
| |
| // Verify the selection toolbar position |
| toolbarTopLeft = tester.getTopLeft(find.text('Select all')); |
| textFieldTopLeft = tester.getTopLeft(find.byType(TextField)); |
| expect(toolbarTopLeft.dy, lessThan(textFieldTopLeft.dy)); |
| }, |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. |
| ); |
| |
| testWidgets( |
| 'the toolbar adjusts its position above/below when bottom inset changes', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Center( |
| child: Padding( |
| padding: const EdgeInsets.symmetric( |
| horizontal: 48.0, |
| ), |
| child: Column( |
| mainAxisSize: MainAxisSize.min, |
| children: <Widget>[ |
| IntrinsicHeight( |
| child: TextField( |
| controller: controller, |
| expands: true, |
| maxLines: null, |
| ), |
| ), |
| const SizedBox(height: 325.0), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| await showSelectionMenuAt(tester, controller, testValue.indexOf('e')); |
| |
| // Verify the selection toolbar position is above the text. |
| expect(find.text('Select all'), findsOneWidget); |
| Offset toolbarTopLeft = tester.getTopLeft(find.text('Select all')); |
| Offset textFieldTopLeft = tester.getTopLeft(find.byType(TextField)); |
| expect(toolbarTopLeft.dy, lessThan(textFieldTopLeft.dy)); |
| |
| // Add a viewInset tall enough to push the field to the top, where there |
| // is no room to display the toolbar above. This is similar to when the |
| // keyboard is shown. |
| tester.view.viewInsets = const FakeViewPadding(bottom: 500.0); |
| addTearDown(tester.view.reset); |
| await tester.pumpAndSettle(); |
| |
| // Verify the selection toolbar position is below the text. |
| toolbarTopLeft = tester.getTopLeft(find.text('Select all')); |
| textFieldTopLeft = tester.getTopLeft(find.byType(TextField)); |
| expect(toolbarTopLeft.dy, greaterThan(textFieldTopLeft.dy)); |
| |
| // Remove the viewInset, as if the keyboard were hidden. |
| tester.view.resetViewInsets(); |
| await tester.pumpAndSettle(); |
| |
| // Verify the selection toolbar position is below the text. |
| toolbarTopLeft = tester.getTopLeft(find.text('Select all')); |
| textFieldTopLeft = tester.getTopLeft(find.byType(TextField)); |
| expect(toolbarTopLeft.dy, lessThan(textFieldTopLeft.dy)); |
| }, |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. |
| ); |
| |
| testWidgets( |
| 'Toolbar appears in the right places in multiline inputs', |
| (WidgetTester tester) async { |
| // This is a regression test for |
| // https://github.com/flutter/flutter/issues/36749 |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Scaffold( |
| body: Padding( |
| padding: const EdgeInsets.all(30.0), |
| child: TextField( |
| controller: controller, |
| minLines: 6, |
| maxLines: 6, |
| ), |
| ), |
| ), |
| )); |
| |
| expect(find.text('Select all'), findsNothing); |
| const String testValue = 'abc\ndef\nghi\njkl\nmno\npqr'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| // Show the selection menu on the first line and verify the selection |
| // toolbar position is below the first line. |
| await showSelectionMenuAt(tester, controller, testValue.indexOf('c')); |
| expect(find.text('Select all'), findsOneWidget); |
| final Offset firstLineToolbarTopLeft = tester.getTopLeft(find.text('Select all')); |
| final Offset firstLineTopLeft = textOffsetToPosition(tester, testValue.indexOf('a')); |
| expect(firstLineTopLeft.dy, lessThan(firstLineToolbarTopLeft.dy)); |
| |
| // Show the selection menu on the second to last line and verify the |
| // selection toolbar position is above that line and above the first |
| // line's toolbar. |
| await showSelectionMenuAt(tester, controller, testValue.indexOf('o')); |
| expect(find.text('Select all'), findsOneWidget); |
| final Offset penultimateLineToolbarTopLeft = tester.getTopLeft(find.text('Select all')); |
| final Offset penultimateLineTopLeft = textOffsetToPosition(tester, testValue.indexOf('p')); |
| expect(penultimateLineToolbarTopLeft.dy, lessThan(penultimateLineTopLeft.dy)); |
| expect(penultimateLineToolbarTopLeft.dy, lessThan(firstLineToolbarTopLeft.dy)); |
| |
| // Show the selection menu on the last line and verify the selection |
| // toolbar position is above that line and below the position of the |
| // second to last line's toolbar. |
| await showSelectionMenuAt(tester, controller, testValue.indexOf('r')); |
| expect(find.text('Select all'), findsOneWidget); |
| final Offset lastLineToolbarTopLeft = tester.getTopLeft(find.text('Select all')); |
| final Offset lastLineTopLeft = textOffsetToPosition(tester, testValue.indexOf('p')); |
| expect(lastLineToolbarTopLeft.dy, lessThan(lastLineTopLeft.dy)); |
| expect(lastLineToolbarTopLeft.dy, greaterThan(penultimateLineToolbarTopLeft.dy)); |
| }, |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. |
| ); |
| |
| testWidgets('Selection toolbar fades in', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| // Tap the selection handle to bring up the "paste / select all" menu. |
| await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); |
| await tester.pump(); |
| // Allow time for the handle to appear and for a double tap to time out. |
| await tester.pump(const Duration(milliseconds: 600)); |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); |
| // Pump an extra frame to allow the selection menu to read the clipboard. |
| await tester.pump(); |
| await tester.pump(); |
| |
| // Toolbar should fade in. Starting at 0% opacity. |
| expect(find.text('Select all'), findsOneWidget); |
| final Element target = tester.element(find.text('Select all')); |
| final FadeTransition opacity = target.findAncestorWidgetOfExactType<FadeTransition>()!; |
| expect(opacity.opacity.value, equals(0.0)); |
| |
| // Still fading in. |
| await tester.pump(const Duration(milliseconds: 50)); |
| final FadeTransition opacity2 = target.findAncestorWidgetOfExactType<FadeTransition>()!; |
| expect(opacity, same(opacity2)); |
| expect(opacity.opacity.value, greaterThan(0.0)); |
| expect(opacity.opacity.value, lessThan(1.0)); |
| |
| // End the test here to ensure the animation is properly disposed of. |
| }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. |
| |
| testWidgets('An obscured TextField is selectable by default', (WidgetTester tester) async { |
| // This is a regression test for |
| // https://github.com/flutter/flutter/issues/32845 |
| |
| final TextEditingController controller = TextEditingController(); |
| Widget buildFrame(bool obscureText) { |
| return overlay( |
| child: TextField( |
| controller: controller, |
| obscureText: obscureText, |
| ), |
| ); |
| } |
| |
| // Obscure text and don't enable or disable selection. |
| await tester.pumpWidget(buildFrame(true)); |
| await tester.enterText(find.byType(TextField), 'abcdefghi'); |
| await skipPastScrollingAnimation(tester); |
| expect(controller.selection.isCollapsed, true); |
| |
| // Long press does select text. |
| final Offset ePos = textOffsetToPosition(tester, 1); |
| await tester.longPressAt(ePos, pointer: 7); |
| await tester.pump(); |
| expect(controller.selection.isCollapsed, false); |
| }); |
| |
| testWidgets('An obscured TextField is not selectable when disabled', (WidgetTester tester) async { |
| // This is a regression test for |
| // https://github.com/flutter/flutter/issues/32845 |
| |
| final TextEditingController controller = TextEditingController(); |
| Widget buildFrame(bool obscureText, bool enableInteractiveSelection) { |
| return overlay( |
| child: TextField( |
| controller: controller, |
| obscureText: obscureText, |
| enableInteractiveSelection: enableInteractiveSelection, |
| ), |
| ); |
| } |
| |
| // Explicitly disabled selection on obscured text. |
| await tester.pumpWidget(buildFrame(true, false)); |
| await tester.enterText(find.byType(TextField), 'abcdefghi'); |
| await skipPastScrollingAnimation(tester); |
| expect(controller.selection.isCollapsed, true); |
| |
| // Long press doesn't select text. |
| final Offset ePos2 = textOffsetToPosition(tester, 1); |
| await tester.longPressAt(ePos2, pointer: 7); |
| await tester.pump(); |
| expect(controller.selection.isCollapsed, true); |
| }); |
| |
| testWidgets('An obscured TextField is not selectable when read-only', (WidgetTester tester) async { |
| // This is a regression test for |
| // https://github.com/flutter/flutter/issues/32845 |
| |
| final TextEditingController controller = TextEditingController(); |
| Widget buildFrame(bool obscureText, bool readOnly) { |
| return overlay( |
| child: TextField( |
| controller: controller, |
| obscureText: obscureText, |
| readOnly: readOnly, |
| ), |
| ); |
| } |
| |
| // Explicitly disabled selection on obscured text that is read-only. |
| await tester.pumpWidget(buildFrame(true, true)); |
| await tester.enterText(find.byType(TextField), 'abcdefghi'); |
| await skipPastScrollingAnimation(tester); |
| expect(controller.selection.isCollapsed, true); |
| |
| // Long press doesn't select text. |
| final Offset ePos2 = textOffsetToPosition(tester, 1); |
| await tester.longPressAt(ePos2, pointer: 7); |
| await tester.pump(); |
| expect(controller.selection.isCollapsed, true); |
| }); |
| |
| testWidgets('An obscured TextField is selected as one word', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget(overlay( |
| child: TextField( |
| controller: controller, |
| obscureText: true, |
| ), |
| )); |
| await tester.enterText(find.byType(TextField), 'abcde fghi'); |
| await skipPastScrollingAnimation(tester); |
| |
| // Long press does select text. |
| final Offset bPos = textOffsetToPosition(tester, 1); |
| await tester.longPressAt(bPos, pointer: 7); |
| await tester.pump(); |
| final TextSelection selection = controller.selection; |
| expect(selection.isCollapsed, false); |
| expect(selection.baseOffset, 0); |
| expect(selection.extentOffset, 10); |
| }); |
| |
| testWidgets('An obscured TextField has correct default context menu', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget(overlay( |
| child: TextField( |
| controller: controller, |
| obscureText: true, |
| ), |
| )); |
| await tester.enterText(find.byType(TextField), 'abcde fghi'); |
| await skipPastScrollingAnimation(tester); |
| |
| // Long press to select text. |
| final Offset bPos = textOffsetToPosition(tester, 1); |
| await tester.longPressAt(bPos, pointer: 7); |
| await tester.pumpAndSettle(); |
| |
| // Should only have paste option when whole obscure text is selected. |
| expect(find.text('Paste'), findsOneWidget); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Select all'), findsNothing); |
| |
| // Long press at the end |
| final Offset iPos = textOffsetToPosition(tester, 10); |
| final Offset slightRight = iPos + const Offset(30.0, 0.0); |
| await tester.longPressAt(slightRight, pointer: 7); |
| await tester.pumpAndSettle(); |
| |
| // Should have paste and select all options when collapse. |
| expect(find.text('Paste'), findsOneWidget); |
| expect(find.text('Select all'), findsOneWidget); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Cut'), findsNothing); |
| }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. |
| |
| testWidgets('create selection overlay if none exists when toggleToolbar is called', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/111660 |
| final Widget testWidget = MaterialApp( |
| home: Scaffold( |
| appBar: AppBar( |
| title: const Text('Test'), |
| actions: <Widget>[ |
| PopupMenuButton<String>( |
| itemBuilder: (BuildContext context) { |
| return <String>{'About'}.map((String value) { |
| return PopupMenuItem<String>( |
| value: value, |
| child: Text(value), |
| ); |
| }).toList(); |
| }, |
| ), |
| ], |
| ), |
| body: const TextField(), |
| ), |
| ); |
| |
| await tester.pumpWidget(testWidget); |
| |
| // Tap on TextField. |
| final Offset textFieldStart = tester.getTopLeft(find.byType(TextField)); |
| final TestGesture gesture = await tester.startGesture(textFieldStart); |
| await tester.pump(const Duration(milliseconds: 300)); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // Tap on 3 dot menu. |
| await tester.tap(find.byType(PopupMenuButton<String>)); |
| await tester.pumpAndSettle(); |
| |
| // Tap on TextField. |
| await gesture.down(textFieldStart); |
| await tester.pump(const Duration(milliseconds: 300)); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // Tap on TextField again. |
| await tester.tapAt(textFieldStart); |
| await tester.pumpAndSettle(); |
| |
| expect(tester.takeException(), isNull); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS})); |
| |
| testWidgets('TextField height with minLines unset', (WidgetTester tester) async { |
| await tester.pumpWidget(textFieldBuilder()); |
| |
| RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); |
| |
| final RenderBox inputBox = findInputBox(); |
| final Size emptyInputSize = inputBox.size; |
| |
| await tester.enterText(find.byType(TextField), 'No wrapping here.'); |
| await tester.pumpWidget(textFieldBuilder()); |
| expect(findInputBox(), equals(inputBox)); |
| expect(inputBox.size, equals(emptyInputSize)); |
| |
| // Even when entering multiline text, TextField doesn't grow. It's a single |
| // line input. |
| await tester.enterText(find.byType(TextField), kThreeLines); |
| await tester.pumpWidget(textFieldBuilder()); |
| expect(findInputBox(), equals(inputBox)); |
| expect(inputBox.size, equals(emptyInputSize)); |
| |
| // maxLines: 3 makes the TextField 3 lines tall |
| await tester.enterText(find.byType(TextField), ''); |
| await tester.pumpWidget(textFieldBuilder(maxLines: 3)); |
| expect(findInputBox(), equals(inputBox)); |
| expect(inputBox.size.height, greaterThan(emptyInputSize.height)); |
| expect(inputBox.size.width, emptyInputSize.width); |
| |
| final Size threeLineInputSize = inputBox.size; |
| |
| // Filling with 3 lines of text stays the same size |
| await tester.enterText(find.byType(TextField), kThreeLines); |
| await tester.pumpWidget(textFieldBuilder(maxLines: 3)); |
| expect(findInputBox(), equals(inputBox)); |
| expect(inputBox.size, threeLineInputSize); |
| |
| // An extra line won't increase the size because we max at 3. |
| await tester.enterText(find.byType(TextField), kMoreThanFourLines); |
| await tester.pumpWidget(textFieldBuilder(maxLines: 3)); |
| expect(findInputBox(), equals(inputBox)); |
| expect(inputBox.size, threeLineInputSize); |
| |
| // But now it will... but it will max at four |
| await tester.enterText(find.byType(TextField), kMoreThanFourLines); |
| await tester.pumpWidget(textFieldBuilder(maxLines: 4)); |
| expect(findInputBox(), equals(inputBox)); |
| expect(inputBox.size.height, greaterThan(threeLineInputSize.height)); |
| expect(inputBox.size.width, threeLineInputSize.width); |
| |
| final Size fourLineInputSize = inputBox.size; |
| |
| // Now it won't max out until the end |
| await tester.enterText(find.byType(TextField), ''); |
| await tester.pumpWidget(textFieldBuilder(maxLines: null)); |
| expect(findInputBox(), equals(inputBox)); |
| expect(inputBox.size, equals(emptyInputSize)); |
| await tester.enterText(find.byType(TextField), kThreeLines); |
| await tester.pump(); |
| expect(inputBox.size, equals(threeLineInputSize)); |
| await tester.enterText(find.byType(TextField), kMoreThanFourLines); |
| await tester.pump(); |
| expect(inputBox.size.height, greaterThan(fourLineInputSize.height)); |
| expect(inputBox.size.width, fourLineInputSize.width); |
| }); |
| |
| testWidgets('TextField height with minLines and maxLines', (WidgetTester tester) async { |
| await tester.pumpWidget(textFieldBuilder()); |
| |
| RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); |
| |
| final RenderBox inputBox = findInputBox(); |
| final Size emptyInputSize = inputBox.size; |
| |
| await tester.enterText(find.byType(TextField), 'No wrapping here.'); |
| await tester.pumpWidget(textFieldBuilder()); |
| expect(findInputBox(), equals(inputBox)); |
| expect(inputBox.size, equals(emptyInputSize)); |
| |
| // min and max set to same value locks height to value. |
| await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 3)); |
| expect(findInputBox(), equals(inputBox)); |
| expect(inputBox.size.height, greaterThan(emptyInputSize.height)); |
| expect(inputBox.size.width, emptyInputSize.width); |
| |
| final Size threeLineInputSize = inputBox.size; |
| |
| // maxLines: null with minLines set grows beyond minLines |
| await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: null)); |
| expect(findInputBox(), equals(inputBox)); |
| expect(inputBox.size, threeLineInputSize); |
| await tester.enterText(find.byType(TextField), kMoreThanFourLines); |
| await tester.pump(); |
| expect(inputBox.size.height, greaterThan(threeLineInputSize.height)); |
| expect(inputBox.size.width, threeLineInputSize.width); |
| |
| // With minLines and maxLines set, input will expand through the range |
| await tester.enterText(find.byType(TextField), ''); |
| await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 4)); |
| expect(findInputBox(), equals(inputBox)); |
| expect(inputBox.size, equals(threeLineInputSize)); |
| await tester.enterText(find.byType(TextField), kMoreThanFourLines); |
| await tester.pump(); |
| expect(inputBox.size.height, greaterThan(threeLineInputSize.height)); |
| expect(inputBox.size.width, threeLineInputSize.width); |
| |
| // minLines can't be greater than maxLines. |
| expect(() async { |
| await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 2)); |
| }, throwsAssertionError); |
| |
| // maxLines defaults to 1 and can't be less than minLines |
| expect(() async { |
| await tester.pumpWidget(textFieldBuilder(minLines: 3)); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('Multiline text when wrapped in Expanded', (WidgetTester tester) async { |
| Widget expandedTextFieldBuilder({ |
| int? maxLines = 1, |
| int? minLines, |
| bool expands = false, |
| }) { |
| return boilerplate( |
| child: Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: <Widget>[ |
| Expanded( |
| child: TextField( |
| key: textFieldKey, |
| style: const TextStyle(color: Colors.black, fontSize: 34.0), |
| maxLines: maxLines, |
| minLines: minLines, |
| expands: expands, |
| decoration: const InputDecoration( |
| hintText: 'Placeholder', |
| ), |
| ), |
| ), |
| ], |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(expandedTextFieldBuilder()); |
| |
| RenderBox findBorder() { |
| return tester.renderObject(find.descendant( |
| of: find.byType(InputDecorator), |
| matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_BorderContainer'), |
| )); |
| } |
| final RenderBox border = findBorder(); |
| |
| // Without expanded: true and maxLines: null, the TextField does not expand |
| // to fill its parent when wrapped in an Expanded widget. |
| final Size unexpandedInputSize = border.size; |
| |
| // It does expand to fill its parent when expands: true, maxLines: null, and |
| // it's wrapped in an Expanded widget. |
| await tester.pumpWidget(expandedTextFieldBuilder(expands: true, maxLines: null)); |
| expect(border.size.height, greaterThan(unexpandedInputSize.height)); |
| expect(border.size.width, unexpandedInputSize.width); |
| |
| // min/maxLines that is not null and expands: true contradict each other. |
| expect(() async { |
| await tester.pumpWidget(expandedTextFieldBuilder(expands: true, maxLines: 4)); |
| }, throwsAssertionError); |
| expect(() async { |
| await tester.pumpWidget(expandedTextFieldBuilder(expands: true, minLines: 1, maxLines: null)); |
| }, throwsAssertionError); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/pull/29093 |
| testWidgets('Multiline text when wrapped in IntrinsicHeight', (WidgetTester tester) async { |
| final Key intrinsicHeightKey = UniqueKey(); |
| Widget intrinsicTextFieldBuilder(bool wrapInIntrinsic) { |
| final TextFormField textField = TextFormField( |
| key: textFieldKey, |
| style: const TextStyle(color: Colors.black, fontSize: 34.0), |
| maxLines: null, |
| decoration: const InputDecoration( |
| counterText: 'I am counter', |
| ), |
| ); |
| final Widget widget = wrapInIntrinsic |
| ? IntrinsicHeight(key: intrinsicHeightKey, child: textField) |
| : textField; |
| return boilerplate( |
| child: Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: <Widget>[widget], |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(intrinsicTextFieldBuilder(false)); |
| expect(find.byKey(intrinsicHeightKey), findsNothing); |
| |
| RenderBox findEditableText() => tester.renderObject(find.byType(EditableText)); |
| RenderBox editableText = findEditableText(); |
| final Size unwrappedEditableTextSize = editableText.size; |
| |
| // Wrapping in IntrinsicHeight should not affect the height of the input |
| await tester.pumpWidget(intrinsicTextFieldBuilder(true)); |
| editableText = findEditableText(); |
| expect(editableText.size.height, unwrappedEditableTextSize.height); |
| expect(editableText.size.width, unwrappedEditableTextSize.width); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/pull/29093 |
| testWidgets('errorText empty string', (WidgetTester tester) async { |
| Widget textFormFieldBuilder(String? errorText) { |
| return boilerplate( |
| child: Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: <Widget>[ |
| TextFormField( |
| key: textFieldKey, |
| maxLength: 3, |
| maxLengthEnforcement: MaxLengthEnforcement.none, |
| decoration: InputDecoration( |
| counterText: '', |
| errorText: errorText, |
| ), |
| ), |
| ], |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(textFormFieldBuilder(null)); |
| |
| RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); |
| final RenderBox inputBox = findInputBox(); |
| final Size errorNullInputSize = inputBox.size; |
| |
| // Setting errorText causes the input's height to increase to accommodate it |
| await tester.pumpWidget(textFormFieldBuilder('im errorText')); |
| expect(inputBox, findInputBox()); |
| expect(inputBox.size.height, greaterThan(errorNullInputSize.height)); |
| expect(inputBox.size.width, errorNullInputSize.width); |
| final Size errorInputSize = inputBox.size; |
| |
| // Setting errorText to an empty string causes the input's height to |
| // increase to accommodate it, even though it's not displayed. |
| // This may or may not be ideal behavior, but it is legacy behavior and |
| // there are visual tests that rely on it (see Github issue referenced at |
| // the top of this test). A counterText of empty string does not affect |
| // input height, however. |
| await tester.pumpWidget(textFormFieldBuilder('')); |
| expect(inputBox, findInputBox()); |
| expect(inputBox.size.height, errorInputSize.height); |
| expect(inputBox.size.width, errorNullInputSize.width); |
| }); |
| |
| testWidgets('Growable TextField when content height exceeds parent', (WidgetTester tester) async { |
| const double height = 200.0; |
| const double padding = 24.0; |
| |
| Widget containedTextFieldBuilder({ |
| Widget? counter, |
| String? helperText, |
| String? labelText, |
| Widget? prefix, |
| }) { |
| return boilerplate( |
| theme: ThemeData(useMaterial3: false), |
| child: SizedBox( |
| height: height, |
| child: TextField( |
| key: textFieldKey, |
| maxLines: null, |
| decoration: InputDecoration( |
| counter: counter, |
| helperText: helperText, |
| labelText: labelText, |
| prefix: prefix, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(containedTextFieldBuilder()); |
| RenderBox findEditableText() => tester.renderObject(find.byType(EditableText)); |
| |
| final RenderBox inputBox = findEditableText(); |
| |
| // With no decoration and when overflowing with content, the EditableText |
| // takes up the full height minus the padding, so the input fits perfectly |
| // inside the parent. |
| await tester.enterText(find.byType(TextField), 'a\n' * 11); |
| await tester.pump(); |
| expect(findEditableText(), equals(inputBox)); |
| expect(inputBox.size.height, height - padding); |
| |
| // Adding a counter causes the EditableText to shrink to fit the counter |
| // inside the parent as well. |
| const double counterHeight = 40.0; |
| const double subtextGap = 8.0; |
| const double counterSpace = counterHeight + subtextGap; |
| await tester.pumpWidget(containedTextFieldBuilder( |
| counter: Container(height: counterHeight), |
| )); |
| expect(findEditableText(), equals(inputBox)); |
| expect(inputBox.size.height, height - padding - counterSpace); |
| |
| // Including helperText causes the EditableText to shrink to fit the text |
| // inside the parent as well. |
| await tester.pumpWidget(containedTextFieldBuilder( |
| helperText: 'I am helperText', |
| )); |
| expect(findEditableText(), equals(inputBox)); |
| const double helperTextSpace = 12.0; |
| expect(inputBox.size.height, height - padding - helperTextSpace - subtextGap); |
| |
| // When both helperText and counter are present, EditableText shrinks by the |
| // height of the taller of the two in order to fit both within the parent. |
| await tester.pumpWidget(containedTextFieldBuilder( |
| counter: Container(height: counterHeight), |
| helperText: 'I am helperText', |
| )); |
| expect(findEditableText(), equals(inputBox)); |
| expect(inputBox.size.height, height - padding - counterSpace); |
| |
| // When a label is present, EditableText shrinks to fit it at the top so |
| // that the bottom of the input still lines up perfectly with the parent. |
| await tester.pumpWidget(containedTextFieldBuilder( |
| labelText: 'I am labelText', |
| )); |
| const double labelSpace = 16.0; |
| expect(findEditableText(), equals(inputBox)); |
| expect(inputBox.size.height, height - padding - labelSpace); |
| |
| // When decoration is present on the top and bottom, EditableText shrinks to |
| // fit both inside the parent independently. |
| await tester.pumpWidget(containedTextFieldBuilder( |
| counter: Container(height: counterHeight), |
| labelText: 'I am labelText', |
| )); |
| expect(findEditableText(), equals(inputBox)); |
| expect(inputBox.size.height, height - padding - counterSpace - labelSpace); |
| |
| // When a prefix or suffix is present in an input that's full of content, |
| // it is ignored and allowed to expand beyond the top of the input. Other |
| // top and bottom decoration is still respected. |
| await tester.pumpWidget(containedTextFieldBuilder( |
| counter: Container(height: counterHeight), |
| labelText: 'I am labelText', |
| prefix: const SizedBox( |
| width: 10, |
| height: 60, |
| ), |
| )); |
| expect(findEditableText(), equals(inputBox)); |
| expect( |
| inputBox.size.height, |
| height |
| - padding |
| - labelSpace |
| - counterSpace, |
| ); |
| }); |
| |
| testWidgets('Multiline hint text will wrap up to maxLines', (WidgetTester tester) async { |
| final Key textFieldKey = UniqueKey(); |
| |
| Widget builder(int? maxLines, final String hintMsg) { |
| return boilerplate( |
| child: TextField( |
| key: textFieldKey, |
| style: const TextStyle(color: Colors.black, fontSize: 34.0), |
| maxLines: maxLines, |
| decoration: InputDecoration( |
| hintText: hintMsg, |
| ), |
| ), |
| ); |
| } |
| |
| const String hintPlaceholder = 'Placeholder'; |
| const String multipleLineText = "Here's a text, which is more than one line, to demonstrate the multiple line hint text"; |
| await tester.pumpWidget(builder(null, hintPlaceholder)); |
| |
| RenderBox findHintText(String hint) => tester.renderObject(find.text(hint)); |
| |
| final RenderBox hintTextBox = findHintText(hintPlaceholder); |
| final Size oneLineHintSize = hintTextBox.size; |
| |
| await tester.pumpWidget(builder(null, hintPlaceholder)); |
| expect(findHintText(hintPlaceholder), equals(hintTextBox)); |
| expect(hintTextBox.size, equals(oneLineHintSize)); |
| |
| const int maxLines = 3; |
| await tester.pumpWidget(builder(maxLines, multipleLineText)); |
| final Text hintTextWidget = tester.widget(find.text(multipleLineText)); |
| expect(hintTextWidget.maxLines, equals(maxLines)); |
| expect(findHintText(multipleLineText).size.width, greaterThanOrEqualTo(oneLineHintSize.width)); |
| expect(findHintText(multipleLineText).size.height, greaterThanOrEqualTo(oneLineHintSize.height)); |
| }); |
| |
| testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| style: const TextStyle(color: Colors.black, fontSize: 34.0), |
| maxLines: 3, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = kThreeLines; |
| const String cutValue = 'First line of stuff'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| // Check that the text spans multiple lines. |
| final Offset firstPos = textOffsetToPosition(tester, testValue.indexOf('First')); |
| final Offset secondPos = textOffsetToPosition(tester, testValue.indexOf('Second')); |
| final Offset thirdPos = textOffsetToPosition(tester, testValue.indexOf('Third')); |
| final Offset middleStringPos = textOffsetToPosition(tester, testValue.indexOf('irst')); |
| expect(firstPos.dx, lessThan(middleStringPos.dx)); |
| expect(firstPos.dx, secondPos.dx); |
| expect(firstPos.dx, thirdPos.dx); |
| expect(firstPos.dy, lessThan(secondPos.dy)); |
| expect(secondPos.dy, lessThan(thirdPos.dy)); |
| |
| // Long press the 'n' in 'until' to select the word. |
| final Offset untilPos = textOffsetToPosition(tester, testValue.indexOf('until')+1); |
| TestGesture gesture = await tester.startGesture(untilPos, pointer: 7); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 39, |
| extentOffset: 44, |
| ), |
| ); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 2); |
| |
| // Drag the right handle to the third line, just after 'Third'. |
| Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0); |
| // The distance below the y value returned by textOffsetToPosition required |
| // to register a full vertical line drag. |
| const Offset downLineOffset = Offset(0.0, 3.0); |
| Offset newHandlePos = |
| textOffsetToPosition(tester, testValue.indexOf('Third') + 5) + downLineOffset; |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect( |
| controller.selection, |
| const TextSelection( |
| baseOffset: 39, |
| extentOffset: 50, |
| ), |
| ); |
| |
| // Drag the left handle to the first line, just after 'First'. |
| handlePos = endpoints[0].point + const Offset(-1.0, 1.0); |
| newHandlePos = textOffsetToPosition(tester, testValue.indexOf('First') + 5); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 5); |
| expect(controller.selection.extentOffset, 50); |
| |
| if (!isContextMenuProvidedByPlatform) { |
| await tester.tap(find.text('Cut')); |
| await tester.pump(); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.text, cutValue); |
| } |
| }); |
| |
| testWidgets('Can scroll multiline input', (WidgetTester tester) async { |
| final Key textFieldKey = UniqueKey(); |
| final TextEditingController controller = TextEditingController( |
| text: kMoreThanFourLines, |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| key: textFieldKey, |
| controller: controller, |
| style: const TextStyle(color: Colors.black, fontSize: 34.0), |
| maxLines: 2, |
| ), |
| ), |
| ); |
| |
| RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); |
| final RenderBox inputBox = findInputBox(); |
| |
| // Check that the last line of text is not displayed. |
| final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); |
| final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth')); |
| expect(firstPos.dx, fourthPos.dx); |
| expect(firstPos.dy, lessThan(fourthPos.dy)); |
| expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue); |
| expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(fourthPos)), isFalse); |
| |
| TestGesture gesture = await tester.startGesture(firstPos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveBy(const Offset(0.0, -1000.0)); |
| await tester.pump(const Duration(seconds: 1)); |
| // Wait and drag again to trigger https://github.com/flutter/flutter/issues/6329 |
| // (No idea why this is necessary, but the bug wouldn't repro without it.) |
| await gesture.moveBy(const Offset(0.0, -1000.0)); |
| await tester.pump(const Duration(seconds: 1)); |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| // Now the first line is scrolled up, and the fourth line is visible. |
| Offset newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); |
| Offset newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth')); |
| |
| expect(newFirstPos.dy, lessThan(firstPos.dy)); |
| expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isFalse); |
| expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isTrue); |
| |
| // Now try scrolling by dragging the selection handle. |
| // Long press the middle of the word "won't" in the fourth line. |
| final Offset selectedWordPos = textOffsetToPosition( |
| tester, |
| kMoreThanFourLines.indexOf('Fourth line') + 14, |
| ); |
| |
| gesture = await tester.startGesture(selectedWordPos, pointer: 7); |
| await tester.pump(const Duration(seconds: 1)); |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| expect(controller.selection.base.offset, 77); |
| expect(controller.selection.extent.offset, 82); |
| // Sanity check for the word selected is the intended one. |
| expect( |
| controller.text.substring(controller.selection.baseOffset, controller.selection.extentOffset), |
| "won't", |
| ); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 2); |
| |
| // Drag the left handle to the first line, just after 'First'. |
| final Offset handlePos = endpoints[0].point + const Offset(-1, 1); |
| final Offset newHandlePos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First') + 5); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(const Duration(seconds: 1)); |
| await gesture.moveTo(newHandlePos + const Offset(0.0, -10.0)); |
| await tester.pump(const Duration(seconds: 1)); |
| await gesture.up(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| // The text should have scrolled up with the handle to keep the active |
| // cursor visible, back to its original position. |
| newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); |
| newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth')); |
| expect(newFirstPos.dy, firstPos.dy); |
| expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue); |
| expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse); |
| }); |
| |
| testWidgets('TextField smoke test', (WidgetTester tester) async { |
| late String textFieldValue; |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| decoration: null, |
| onChanged: (String value) { |
| textFieldValue = value; |
| }, |
| ), |
| ), |
| ); |
| |
| Future<void> checkText(String testValue) { |
| return TestAsyncUtils.guard(() async { |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| // Check that the onChanged event handler fired. |
| expect(textFieldValue, equals(testValue)); |
| |
| await tester.pump(); |
| }); |
| } |
| |
| await checkText('Hello World'); |
| }); |
| |
| testWidgets('TextField with global key', (WidgetTester tester) async { |
| final GlobalKey textFieldKey = GlobalKey(debugLabel: 'textFieldKey'); |
| late String textFieldValue; |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: textFieldKey, |
| decoration: const InputDecoration( |
| hintText: 'Placeholder', |
| ), |
| onChanged: (String value) { textFieldValue = value; }, |
| ), |
| ), |
| ); |
| |
| Future<void> checkText(String testValue) async { |
| return TestAsyncUtils.guard(() async { |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| // Check that the onChanged event handler fired. |
| expect(textFieldValue, equals(testValue)); |
| |
| await tester.pump(); |
| }); |
| } |
| |
| await checkText('Hello World'); |
| }); |
| |
| testWidgets('TextField errorText trumps helperText', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: const TextField( |
| decoration: InputDecoration( |
| errorText: 'error text', |
| helperText: 'helper text', |
| ), |
| ), |
| ), |
| ), |
| ); |
| expect(find.text('helper text'), findsNothing); |
| expect(find.text('error text'), findsOneWidget); |
| }); |
| |
| testWidgets('TextField with default helperStyle', (WidgetTester tester) async { |
| final ThemeData themeData = ThemeData(hintColor: Colors.blue[500], useMaterial3: false); |
| await tester.pumpWidget( |
| overlay( |
| child: Theme( |
| data: themeData, |
| child: const TextField( |
| decoration: InputDecoration( |
| helperText: 'helper text', |
| ), |
| ), |
| ), |
| ), |
| ); |
| final Text helperText = tester.widget(find.text('helper text')); |
| expect(helperText.style!.color, themeData.hintColor); |
| expect(helperText.style!.fontSize, Typography.englishLike2014.bodySmall!.fontSize); |
| }); |
| |
| testWidgets('TextField with specified helperStyle', (WidgetTester tester) async { |
| final TextStyle style = TextStyle( |
| inherit: false, |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| decoration: InputDecoration( |
| helperText: 'helper text', |
| helperStyle: style, |
| ), |
| ), |
| ), |
| ); |
| final Text helperText = tester.widget(find.text('helper text')); |
| expect(helperText.style, style); |
| }); |
| |
| testWidgets('TextField with default hintStyle', (WidgetTester tester) async { |
| final TextStyle style = TextStyle( |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| final ThemeData themeData = ThemeData( |
| hintColor: Colors.blue[500], |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Theme( |
| data: themeData, |
| child: TextField( |
| decoration: const InputDecoration( |
| hintText: 'Placeholder', |
| ), |
| style: style, |
| ), |
| ), |
| ), |
| ); |
| |
| final Text hintText = tester.widget(find.text('Placeholder')); |
| expect(hintText.style!.color, themeData.hintColor); |
| expect(hintText.style!.fontSize, style.fontSize); |
| }); |
| |
| testWidgets('TextField with specified hintStyle', (WidgetTester tester) async { |
| final TextStyle hintStyle = TextStyle( |
| inherit: false, |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| decoration: InputDecoration( |
| hintText: 'Placeholder', |
| hintStyle: hintStyle, |
| ), |
| ), |
| ), |
| ); |
| |
| final Text hintText = tester.widget(find.text('Placeholder')); |
| expect(hintText.style, hintStyle); |
| }); |
| |
| testWidgets('TextField with specified prefixStyle', (WidgetTester tester) async { |
| final TextStyle prefixStyle = TextStyle( |
| inherit: false, |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| decoration: InputDecoration( |
| prefixText: 'Prefix:', |
| prefixStyle: prefixStyle, |
| ), |
| ), |
| ), |
| ); |
| |
| final Text prefixText = tester.widget(find.text('Prefix:')); |
| expect(prefixText.style, prefixStyle); |
| }); |
| |
| testWidgets('TextField prefix and suffix create a sibling node', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: TextEditingController(text: 'some text'), |
| decoration: const InputDecoration( |
| prefixText: 'Prefix', |
| suffixText: 'Suffix', |
| ), |
| ), |
| ), |
| ); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| id: 2, |
| textDirection: TextDirection.ltr, |
| label: 'Prefix', |
| ), |
| TestSemantics.rootChild( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| value: 'some text', |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| ], |
| ), |
| TestSemantics.rootChild( |
| id: 3, |
| textDirection: TextDirection.ltr, |
| label: 'Suffix', |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async { |
| final TextStyle suffixStyle = TextStyle( |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| decoration: InputDecoration( |
| suffixText: '.com', |
| suffixStyle: suffixStyle, |
| ), |
| ), |
| ), |
| ); |
| |
| final Text suffixText = tester.widget(find.text('.com')); |
| expect(suffixText.style, suffixStyle); |
| }); |
| |
| testWidgets('TextField prefix and suffix appear correctly with no hint or label', (WidgetTester tester) async { |
| final Key secondKey = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Column( |
| children: <Widget>[ |
| const TextField( |
| decoration: InputDecoration( |
| labelText: 'First', |
| ), |
| ), |
| TextField( |
| key: secondKey, |
| decoration: const InputDecoration( |
| prefixText: 'Prefix', |
| suffixText: 'Suffix', |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| expect(find.text('Prefix'), findsOneWidget); |
| expect(find.text('Suffix'), findsOneWidget); |
| |
| // Focus the Input. The prefix should still display. |
| await tester.tap(find.byKey(secondKey)); |
| await tester.pump(); |
| |
| expect(find.text('Prefix'), findsOneWidget); |
| expect(find.text('Suffix'), findsOneWidget); |
| |
| // Enter some text, and the prefix should still display. |
| await tester.enterText(find.byKey(secondKey), 'Hi'); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| expect(find.text('Prefix'), findsOneWidget); |
| expect(find.text('Suffix'), findsOneWidget); |
| }); |
| |
| testWidgets('TextField prefix and suffix appear correctly with hint text', (WidgetTester tester) async { |
| final TextStyle hintStyle = TextStyle( |
| inherit: false, |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| final Key secondKey = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Column( |
| children: <Widget>[ |
| const TextField( |
| decoration: InputDecoration( |
| labelText: 'First', |
| ), |
| ), |
| TextField( |
| key: secondKey, |
| decoration: InputDecoration( |
| hintText: 'Hint', |
| hintStyle: hintStyle, |
| prefixText: 'Prefix', |
| suffixText: 'Suffix', |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| // Neither the prefix or the suffix should initially be visible, only the hint. |
| expect(getOpacity(tester, find.text('Prefix')), 0.0); |
| expect(getOpacity(tester, find.text('Suffix')), 0.0); |
| expect(getOpacity(tester, find.text('Hint')), 1.0); |
| |
| await tester.tap(find.byKey(secondKey)); |
| await tester.pumpAndSettle(); |
| |
| // Focus the Input. The hint, prefix, and suffix should appear |
| expect(getOpacity(tester, find.text('Prefix')), 1.0); |
| expect(getOpacity(tester, find.text('Suffix')), 1.0); |
| expect(getOpacity(tester, find.text('Hint')), 1.0); |
| |
| // Enter some text, and the hint should disappear and the prefix and suffix |
| // should continue to be visible |
| await tester.enterText(find.byKey(secondKey), 'Hi'); |
| await tester.pumpAndSettle(); |
| |
| expect(getOpacity(tester, find.text('Prefix')), 1.0); |
| expect(getOpacity(tester, find.text('Suffix')), 1.0); |
| expect(getOpacity(tester, find.text('Hint')), 0.0); |
| |
| // Check and make sure that the right styles were applied. |
| final Text prefixText = tester.widget(find.text('Prefix')); |
| expect(prefixText.style, hintStyle); |
| final Text suffixText = tester.widget(find.text('Suffix')); |
| expect(suffixText.style, hintStyle); |
| }); |
| |
| testWidgets('TextField prefix and suffix appear correctly with label text', (WidgetTester tester) async { |
| final TextStyle prefixStyle = TextStyle( |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| final TextStyle suffixStyle = TextStyle( |
| color: Colors.green[500], |
| fontSize: 12.0, |
| ); |
| final Key secondKey = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Column( |
| children: <Widget>[ |
| const TextField( |
| decoration: InputDecoration( |
| labelText: 'First', |
| ), |
| ), |
| TextField( |
| key: secondKey, |
| decoration: InputDecoration( |
| labelText: 'Label', |
| prefixText: 'Prefix', |
| prefixStyle: prefixStyle, |
| suffixText: 'Suffix', |
| suffixStyle: suffixStyle, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| // Not focused. The prefix and suffix should not appear, but the label should. |
| expect(getOpacity(tester, find.text('Prefix')), 0.0); |
| expect(getOpacity(tester, find.text('Suffix')), 0.0); |
| expect(find.text('Label'), findsOneWidget); |
| |
| // Focus the input. The label, prefix, and suffix should appear. |
| await tester.tap(find.byKey(secondKey)); |
| await tester.pumpAndSettle(); |
| |
| expect(getOpacity(tester, find.text('Prefix')), 1.0); |
| expect(getOpacity(tester, find.text('Suffix')), 1.0); |
| expect(find.text('Label'), findsOneWidget); |
| |
| // Enter some text. The label, prefix, and suffix should remain visible. |
| await tester.enterText(find.byKey(secondKey), 'Hi'); |
| await tester.pumpAndSettle(); |
| |
| expect(getOpacity(tester, find.text('Prefix')), 1.0); |
| expect(getOpacity(tester, find.text('Suffix')), 1.0); |
| expect(find.text('Label'), findsOneWidget); |
| |
| // Check and make sure that the right styles were applied. |
| final Text prefixText = tester.widget(find.text('Prefix')); |
| expect(prefixText.style, prefixStyle); |
| final Text suffixText = tester.widget(find.text('Suffix')); |
| expect(suffixText.style, suffixStyle); |
| }); |
| |
| testWidgets('TextField label text animates', (WidgetTester tester) async { |
| final Key secondKey = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Column( |
| children: <Widget>[ |
| const TextField( |
| decoration: InputDecoration( |
| labelText: 'First', |
| ), |
| ), |
| TextField( |
| key: secondKey, |
| decoration: const InputDecoration( |
| labelText: 'Second', |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| Offset pos = tester.getTopLeft(find.text('Second')); |
| |
| // Focus the Input. The label should start animating upwards. |
| await tester.tap(find.byKey(secondKey)); |
| await tester.idle(); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| Offset newPos = tester.getTopLeft(find.text('Second')); |
| expect(newPos.dy, lessThan(pos.dy)); |
| |
| // Label should still be sliding upward. |
| await tester.pump(const Duration(milliseconds: 50)); |
| pos = newPos; |
| newPos = tester.getTopLeft(find.text('Second')); |
| expect(newPos.dy, lessThan(pos.dy)); |
| }); |
| |
| testWidgets('Icon is separated from input/label by 16+12', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: const TextField( |
| decoration: InputDecoration( |
| icon: Icon(Icons.phone), |
| labelText: 'label', |
| filled: true, |
| ), |
| ), |
| ), |
| ); |
| final double iconRight = tester.getTopRight(find.byType(Icon)).dx; |
| // Per https://material.io/go/design-text-fields#text-fields-layout |
| // There's a 16 dps gap between the right edge of the icon and the text field's |
| // container, and the 12dps more padding between the left edge of the container |
| // and the left edge of the input and label. |
| expect(iconRight + 28.0, equals(tester.getTopLeft(find.text('label')).dx)); |
| expect(iconRight + 28.0, equals(tester.getTopLeft(find.byType(EditableText)).dx)); |
| }); |
| |
| testWidgets('Collapsed hint text placement', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: const TextField( |
| decoration: InputDecoration.collapsed( |
| hintText: 'hint', |
| ), |
| strutStyle: StrutStyle.disabled, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.getTopLeft(find.text('hint')), equals(tester.getTopLeft(find.byType(EditableText)))); |
| }); |
| |
| testWidgets('Can align to center', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: const SizedBox( |
| width: 300.0, |
| child: TextField( |
| textAlign: TextAlign.center, |
| decoration: null, |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderEditable editable = findRenderEditable(tester); |
| assert(editable.size.width == 300); |
| Offset topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft, |
| ); |
| |
| // The overlay() function centers its child within a 800x600 view. |
| // Default cursorWidth is 2.0, test viewWidth is 800 |
| // Centered cursor topLeft.dx: 399 == viewWidth/2 - cursorWidth/2 |
| expect(topLeft.dx, equals(399.0)); |
| |
| await tester.enterText(find.byType(TextField), 'abcd'); |
| await tester.pump(); |
| |
| topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft, |
| ); |
| |
| // TextPosition(offset: 2) - center of 'abcd' |
| expect(topLeft.dx, equals(399.0)); |
| }); |
| |
| testWidgets('Can align to center within center', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: const SizedBox( |
| width: 300.0, |
| child: Center( |
| child: TextField( |
| textAlign: TextAlign.center, |
| decoration: null, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderEditable editable = findRenderEditable(tester); |
| Offset topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft, |
| ); |
| |
| // The overlay() function centers its child within a 800x600 view. |
| // Default cursorWidth is 2.0, test viewWidth is 800 |
| // Centered cursor topLeft.dx: 399 == viewWidth/2 - cursorWidth/2 |
| expect(topLeft.dx, equals(399.0)); |
| |
| await tester.enterText(find.byType(TextField), 'abcd'); |
| await tester.pump(); |
| |
| topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft, |
| ); |
| |
| // TextPosition(offset: 2) - center of 'abcd' |
| expect(topLeft.dx, equals(399.0)); |
| }); |
| |
| testWidgets('Controller can update server', (WidgetTester tester) async { |
| final TextEditingController controller1 = TextEditingController( |
| text: 'Initial Text', |
| ); |
| final TextEditingController controller2 = TextEditingController( |
| text: 'More Text', |
| ); |
| |
| TextEditingController? currentController; |
| late StateSetter setState; |
| |
| await tester.pumpWidget( |
| overlay( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setter) { |
| setState = setter; |
| return TextField(controller: currentController); |
| }, |
| ), |
| ), |
| ); |
| expect(tester.testTextInput.editingState, isNull); |
| |
| // Initial state with null controller. |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| expect(tester.testTextInput.editingState!['text'], isEmpty); |
| |
| // Update the controller from null to controller1. |
| setState(() { |
| currentController = controller1; |
| }); |
| await tester.pump(); |
| expect(tester.testTextInput.editingState!['text'], equals('Initial Text')); |
| |
| // Verify that updates to controller1 are handled. |
| controller1.text = 'Updated Text'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('Updated Text')); |
| |
| // Verify that switching from controller1 to controller2 is handled. |
| setState(() { |
| currentController = controller2; |
| }); |
| await tester.pump(); |
| expect(tester.testTextInput.editingState!['text'], equals('More Text')); |
| |
| // Verify that updates to controller1 are ignored. |
| controller1.text = 'Ignored Text'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('More Text')); |
| |
| // Verify that updates to controller text are handled. |
| controller2.text = 'Additional Text'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('Additional Text')); |
| |
| // Verify that updates to controller selection are handled. |
| controller2.selection = const TextSelection(baseOffset: 0, extentOffset: 5); |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['selectionBase'], equals(0)); |
| expect(tester.testTextInput.editingState!['selectionExtent'], equals(5)); |
| |
| // Verify that calling clear() clears the text. |
| controller2.clear(); |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('')); |
| |
| // Verify that switching from controller2 to null preserves current text. |
| controller2.text = 'The Final Cut'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('The Final Cut')); |
| setState(() { |
| currentController = null; |
| }); |
| await tester.pump(); |
| expect(tester.testTextInput.editingState!['text'], equals('The Final Cut')); |
| |
| // Verify that changes to controller2 are ignored. |
| controller2.text = 'Goodbye Cruel World'; |
| expect(tester.testTextInput.editingState!['text'], equals('The Final Cut')); |
| }); |
| |
| testWidgets('Cannot enter new lines onto single line TextField', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField(controller: textController, decoration: null), |
| )); |
| |
| await tester.enterText(find.byType(TextField), 'abc\ndef'); |
| |
| expect(textController.text, 'abcdef'); |
| }); |
| |
| testWidgets('Injected formatters are chained', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| decoration: null, |
| inputFormatters: <TextInputFormatter> [ |
| FilteringTextInputFormatter.deny( |
| RegExp(r'[a-z]'), |
| replacementString: '#', |
| ), |
| ], |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六'); |
| // The default single line formatter replaces \n with empty string. |
| expect(textController.text, '#一#二#三#四#五#六'); |
| }); |
| |
| testWidgets('Injected formatters are chained (deprecated names)', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| decoration: null, |
| inputFormatters: <TextInputFormatter> [ |
| FilteringTextInputFormatter.deny( |
| RegExp(r'[a-z]'), |
| replacementString: '#', |
| ), |
| ], |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六'); |
| // The default single line formatter replaces \n with empty string. |
| expect(textController.text, '#一#二#三#四#五#六'); |
| }); |
| |
| testWidgets('Chained formatters are in sequence', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| decoration: null, |
| maxLines: 2, |
| inputFormatters: <TextInputFormatter> [ |
| FilteringTextInputFormatter.deny( |
| RegExp(r'[a-z]'), |
| replacementString: '12\n', |
| ), |
| FilteringTextInputFormatter.allow(RegExp(r'\n[0-9]')), |
| ], |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), 'a1b2c3'); |
| // The first formatter turns it into |
| // 12\n112\n212\n3 |
| // The second formatter turns it into |
| // \n1\n2\n3 |
| // Multiline is allowed since maxLine != 1. |
| expect(textController.text, '\n1\n2\n3'); |
| }); |
| |
| testWidgets('Chained formatters are in sequence (deprecated names)', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| decoration: null, |
| maxLines: 2, |
| inputFormatters: <TextInputFormatter> [ |
| FilteringTextInputFormatter.deny( |
| RegExp(r'[a-z]'), |
| replacementString: '12\n', |
| ), |
| FilteringTextInputFormatter.allow(RegExp(r'\n[0-9]')), |
| ], |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), 'a1b2c3'); |
| // The first formatter turns it into |
| // 12\n112\n212\n3 |
| // The second formatter turns it into |
| // \n1\n2\n3 |
| // Multiline is allowed since maxLine != 1. |
| expect(textController.text, '\n1\n2\n3'); |
| }); |
| |
| testWidgets('Pasted values are formatted', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: textController, |
| decoration: null, |
| inputFormatters: <TextInputFormatter> [ |
| FilteringTextInputFormatter.digitsOnly, |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.enterText(find.byType(TextField), 'a1b\n2c3'); |
| expect(textController.text, '123'); |
| await skipPastScrollingAnimation(tester); |
| |
| await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2'))); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(textController.selection), |
| renderEditable, |
| ); |
| await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| |
| Clipboard.setData(const ClipboardData(text: '一4二\n5三6')); |
| await tester.tap(find.text('Paste')); |
| await tester.pump(); |
| // Puts 456 before the 2 in 123. |
| expect(textController.text, '145623'); |
| }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. |
| |
| testWidgets('Pasted values are formatted (deprecated names)', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: textController, |
| decoration: null, |
| inputFormatters: <TextInputFormatter> [ |
| FilteringTextInputFormatter.digitsOnly, |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.enterText(find.byType(TextField), 'a1b\n2c3'); |
| expect(textController.text, '123'); |
| await skipPastScrollingAnimation(tester); |
| |
| await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2'))); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(textController.selection), |
| renderEditable, |
| ); |
| await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| |
| Clipboard.setData(const ClipboardData(text: '一4二\n5三6')); |
| await tester.tap(find.text('Paste')); |
| await tester.pump(); |
| // Puts 456 before the 2 in 123. |
| expect(textController.text, '145623'); |
| }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. |
| |
| testWidgets('Do not add LengthLimiting formatter to the user supplied list', (WidgetTester tester) async { |
| final List<TextInputFormatter> formatters = <TextInputFormatter>[]; |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| decoration: null, |
| maxLength: 5, |
| inputFormatters: formatters, |
| ), |
| ), |
| ); |
| |
| expect(formatters.isEmpty, isTrue); |
| }); |
| |
| testWidgets('Text field scrolls the caret into view', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: SizedBox( |
| width: 100.0, |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final String longText = 'a' * 20; |
| await tester.enterText(find.byType(TextField), longText); |
| await skipPastScrollingAnimation(tester); |
| |
| ScrollableState scrollableState = tester.firstState(find.byType(Scrollable)); |
| expect(scrollableState.position.pixels, equals(0.0)); |
| |
| // Move the caret to the end of the text and check that the text field |
| // scrolls to make the caret visible. |
| scrollableState = tester.firstState(find.byType(Scrollable)); |
| final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); |
| editableTextState.userUpdateTextEditingValue( |
| editableTextState.textEditingValue.copyWith( |
| selection: TextSelection.collapsed(offset: longText.length), |
| ), |
| null, |
| ); |
| |
| await tester.pump(); // TODO(ianh): Figure out why this extra pump is needed. |
| await skipPastScrollingAnimation(tester); |
| |
| scrollableState = tester.firstState(find.byType(Scrollable)); |
| // For a horizontal input, scrolls to the exact position of the caret. |
| expect(scrollableState.position.pixels, equals(222.0)); |
| }); |
| |
| testWidgets('Multiline text field scrolls the caret into view', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| maxLines: 6, |
| ), |
| ), |
| ); |
| |
| const String tallText = 'a\nb\nc\nd\ne\nf\ng'; // One line over max |
| await tester.enterText(find.byType(TextField), tallText); |
| await skipPastScrollingAnimation(tester); |
| |
| ScrollableState scrollableState = tester.firstState(find.byType(Scrollable)); |
| expect(scrollableState.position.pixels, equals(0.0)); |
| |
| // Move the caret to the end of the text and check that the text field |
| // scrolls to make the caret visible. |
| final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); |
| editableTextState.userUpdateTextEditingValue( |
| editableTextState.textEditingValue.copyWith( |
| selection: const TextSelection.collapsed(offset: tallText.length), |
| ), |
| null, |
| ); |
| await tester.pump(); |
| await skipPastScrollingAnimation(tester); |
| |
| // Should have scrolled down exactly one line height (7 lines of text in 6 |
| // line text field). |
| final double lineHeight = findRenderEditable(tester).preferredLineHeight; |
| scrollableState = tester.firstState(find.byType(Scrollable)); |
| expect(scrollableState.position.pixels, moreOrLessEquals(lineHeight, epsilon: 0.1)); |
| }); |
| |
| testWidgets('haptic feedback', (WidgetTester tester) async { |
| final FeedbackTester feedback = FeedbackTester(); |
| addTearDown(feedback.dispose); |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: SizedBox( |
| width: 100.0, |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(const Duration(seconds: 1)); |
| expect(feedback.clickSoundCount, 0); |
| expect(feedback.hapticCount, 0); |
| |
| await tester.longPress(find.byType(TextField)); |
| await tester.pumpAndSettle(const Duration(seconds: 1)); |
| expect(feedback.clickSoundCount, 0); |
| expect(feedback.hapticCount, 1); |
| }); |
| |
| testWidgets('Text field drops selection color when losing focus', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/103341. |
| final Key key1 = UniqueKey(); |
| final Key key2 = UniqueKey(); |
| final TextEditingController controller1 = TextEditingController(); |
| const Color selectionColor = Colors.orange; |
| const Color cursorColor = Colors.red; |
| |
| await tester.pumpWidget( |
| overlay( |
| child: DefaultSelectionStyle( |
| selectionColor: selectionColor, |
| cursorColor: cursorColor, |
| child: Column( |
| children: <Widget>[ |
| TextField( |
| key: key1, |
| controller: controller1, |
| ), |
| TextField(key: key2), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| const TextSelection selection = TextSelection(baseOffset: 0, extentOffset: 4); |
| final EditableTextState state1 = tester.state<EditableTextState>(find.byType(EditableText).first); |
| final EditableTextState state2 = tester.state<EditableTextState>(find.byType(EditableText).last); |
| |
| await tester.tap(find.byKey(key1)); |
| await tester.enterText(find.byKey(key1), 'abcd'); |
| await tester.pump(); |
| |
| await tester.tap(find.byKey(key2)); |
| await tester.enterText(find.byKey(key2), 'dcba'); |
| await tester.pump(); |
| |
| // Focus and selection is active on first TextField, so the second TextFields |
| // selectionColor should be dropped. |
| await tester.tap(find.byKey(key1)); |
| controller1.selection = const TextSelection(baseOffset: 0, extentOffset: 4); |
| await tester.pump(); |
| expect(controller1.selection, selection); |
| expect(state1.widget.selectionColor, selectionColor); |
| expect(state2.widget.selectionColor, null); |
| |
| // Focus and selection is active on second TextField, so the first TextFields |
| // selectionColor should be dropped. |
| await tester.tap(find.byKey(key2)); |
| await tester.pump(); |
| expect(state1.widget.selectionColor, null); |
| expect(state2.widget.selectionColor, selectionColor); |
| }); |
| |
| testWidgets('Selection is consistent with text length', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| controller.text = 'abcde'; |
| controller.selection = const TextSelection.collapsed(offset: 5); |
| |
| controller.text = ''; |
| expect(controller.selection.start, lessThanOrEqualTo(0)); |
| expect(controller.selection.end, lessThanOrEqualTo(0)); |
| |
| late FlutterError error; |
| try { |
| controller.selection = const TextSelection.collapsed(offset: 10); |
| } on FlutterError catch (e) { |
| error = e; |
| } finally { |
| expect(error.diagnostics.length, 1); |
| expect( |
| error.toStringDeep(), |
| equalsIgnoringHashCodes( |
| 'FlutterError\n' |
| ' invalid text selection: TextSelection.collapsed(offset: 10,\n' |
| ' affinity: TextAffinity.downstream, isDirectional: false)\n', |
| ), |
| ); |
| } |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/35848 |
| testWidgets('Clearing text field with suffixIcon does not cause text selection exception', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Prefilled text.', |
| ); |
| |
| await tester.pumpWidget( |
| boilerplate( |
| child: TextField( |
| controller: controller, |
| decoration: InputDecoration( |
| suffixIcon: IconButton( |
| icon: const Icon(Icons.close), |
| onPressed: controller.clear, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(IconButton)); |
| expect(controller.text, ''); |
| }); |
| |
| testWidgets('maxLength limits input.', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), '0123456789101112'); |
| expect(textController.text, '0123456789'); |
| }); |
| |
| testWidgets('maxLength limits input with surrogate pairs.', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| ), |
| )); |
| |
| const String surrogatePair = '😆'; |
| await tester.enterText(find.byType(TextField), '${surrogatePair}0123456789101112'); |
| expect(textController.text, '${surrogatePair}012345678'); |
| }); |
| |
| testWidgets('maxLength limits input with grapheme clusters.', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| ), |
| )); |
| |
| const String graphemeCluster = '👨👩👦'; |
| await tester.enterText(find.byType(TextField), '${graphemeCluster}0123456789101112'); |
| expect(textController.text, '${graphemeCluster}012345678'); |
| }); |
| |
| testWidgets('maxLength limits input in the center of a maxed-out field.', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/37420. |
| final TextEditingController textController = TextEditingController(); |
| const String testValue = '0123456789'; |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| ), |
| )); |
| |
| // Max out the character limit in the field. |
| await tester.enterText(find.byType(TextField), testValue); |
| expect(textController.text, testValue); |
| |
| // Entering more characters at the end does nothing. |
| await tester.enterText(find.byType(TextField), '${testValue}9999999'); |
| expect(textController.text, testValue); |
| |
| // Entering text in the middle of the field also does nothing. |
| await tester.enterText(find.byType(TextField), '0123455555555556789'); |
| expect(textController.text, testValue); |
| }); |
| |
| testWidgets( |
| 'maxLength limits input in the center of a maxed-out field, with collapsed selection', |
| (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| const String testValue = '0123456789'; |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| ), |
| )); |
| |
| // Max out the character limit in the field. |
| await tester.showKeyboard(find.byType(TextField)); |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: testValue, |
| selection: TextSelection.collapsed(offset: 10), |
| )); |
| await tester.pump(); |
| expect(textController.text, testValue); |
| |
| // Entering more characters at the end does nothing. |
| await tester.showKeyboard(find.byType(TextField)); |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: '${testValue}9999999', |
| selection: TextSelection.collapsed(offset: 10 + 7), |
| )); |
| await tester.pump(); |
| |
| expect(textController.text, testValue); |
| |
| // Entering text in the middle of the field also does nothing. |
| // Entering more characters at the end does nothing. |
| await tester.showKeyboard(find.byType(TextField)); |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: '0123455555555556789', |
| selection: TextSelection.collapsed(offset: 19), |
| )); |
| await tester.pump(); |
| |
| expect(textController.text, testValue); |
| }, |
| ); |
| |
| testWidgets( |
| 'maxLength limits input in the center of a maxed-out field, with non-collapsed selection', |
| (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| const String testValue = '0123456789'; |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| maxLengthEnforcement: MaxLengthEnforcement.enforced, |
| ), |
| )); |
| |
| // Max out the character limit in the field. |
| await tester.showKeyboard(find.byType(TextField)); |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: testValue, |
| selection: TextSelection(baseOffset: 8, extentOffset: 10), |
| )); |
| await tester.pump(); |
| expect(textController.text, testValue); |
| |
| // Entering more characters at the end does nothing. |
| await tester.showKeyboard(find.byType(TextField)); |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: '01234569999999', |
| selection: TextSelection.collapsed(offset: 14), |
| )); |
| await tester.pump(); |
| |
| expect(textController.text, '0123456999'); |
| }, |
| ); |
| |
| testWidgets('maxLength limits input length even if decoration is null.', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| decoration: null, |
| maxLength: 10, |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), '0123456789101112'); |
| expect(textController.text, '0123456789'); |
| }); |
| |
| testWidgets('maxLength still works with other formatters', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| inputFormatters: <TextInputFormatter> [ |
| FilteringTextInputFormatter.deny( |
| RegExp(r'[a-z]'), |
| replacementString: '#', |
| ), |
| ], |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六'); |
| // The default single line formatter replaces \n with empty string. |
| expect(textController.text, '#一#二#三#四#五'); |
| }); |
| |
| testWidgets('maxLength still works with other formatters (deprecated names)', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| inputFormatters: <TextInputFormatter> [ |
| FilteringTextInputFormatter.deny( |
| RegExp(r'[a-z]'), |
| replacementString: '#', |
| ), |
| ], |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六'); |
| // The default single line formatter replaces \n with empty string. |
| expect(textController.text, '#一#二#三#四#五'); |
| }); |
| |
| testWidgets("maxLength isn't enforced when maxLengthEnforcement.none.", (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| maxLengthEnforcement: MaxLengthEnforcement.none, |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), '0123456789101112'); |
| expect(textController.text, '0123456789101112'); |
| }); |
| |
| testWidgets('maxLength shows warning when maxLengthEnforcement.none.', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| const TextStyle testStyle = TextStyle(color: Colors.deepPurpleAccent); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| decoration: const InputDecoration(errorStyle: testStyle), |
| controller: textController, |
| maxLength: 10, |
| maxLengthEnforcement: MaxLengthEnforcement.none, |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), '0123456789101112'); |
| await tester.pump(); |
| |
| expect(textController.text, '0123456789101112'); |
| expect(find.text('16/10'), findsOneWidget); |
| Text counterTextWidget = tester.widget(find.text('16/10')); |
| expect(counterTextWidget.style!.color, equals(Colors.deepPurpleAccent)); |
| |
| await tester.enterText(find.byType(TextField), '0123456789'); |
| await tester.pump(); |
| |
| expect(textController.text, '0123456789'); |
| expect(find.text('10/10'), findsOneWidget); |
| counterTextWidget = tester.widget(find.text('10/10')); |
| expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent))); |
| }); |
| |
| testWidgets('maxLength shows warning in Material 3', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| final ThemeData theme = ThemeData.from( |
| colorScheme: const ColorScheme.light().copyWith(error: Colors.deepPurpleAccent), |
| useMaterial3: true, |
| ); |
| await tester.pumpWidget(boilerplate( |
| theme: theme, |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| maxLengthEnforcement: MaxLengthEnforcement.none, |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), '0123456789101112'); |
| await tester.pump(); |
| |
| expect(textController.text, '0123456789101112'); |
| expect(find.text('16/10'), findsOneWidget); |
| Text counterTextWidget = tester.widget(find.text('16/10')); |
| expect(counterTextWidget.style!.color, equals(Colors.deepPurpleAccent)); |
| |
| await tester.enterText(find.byType(TextField), '0123456789'); |
| await tester.pump(); |
| |
| expect(textController.text, '0123456789'); |
| expect(find.text('10/10'), findsOneWidget); |
| counterTextWidget = tester.widget(find.text('10/10')); |
| expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent))); |
| }); |
| |
| testWidgets('maxLength shows warning when maxLengthEnforcement.none with surrogate pairs.', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| const TextStyle testStyle = TextStyle(color: Colors.deepPurpleAccent); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| decoration: const InputDecoration(errorStyle: testStyle), |
| controller: textController, |
| maxLength: 10, |
| maxLengthEnforcement: MaxLengthEnforcement.none, |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), '😆012345678910111'); |
| await tester.pump(); |
| |
| expect(textController.text, '😆012345678910111'); |
| expect(find.text('16/10'), findsOneWidget); |
| Text counterTextWidget = tester.widget(find.text('16/10')); |
| expect(counterTextWidget.style!.color, equals(Colors.deepPurpleAccent)); |
| |
| await tester.enterText(find.byType(TextField), '😆012345678'); |
| await tester.pump(); |
| |
| expect(textController.text, '😆012345678'); |
| expect(find.text('10/10'), findsOneWidget); |
| counterTextWidget = tester.widget(find.text('10/10')); |
| expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent))); |
| }); |
| |
| testWidgets('maxLength shows warning when maxLengthEnforcement.none with grapheme clusters.', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| const TextStyle testStyle = TextStyle(color: Colors.deepPurpleAccent); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| decoration: const InputDecoration(errorStyle: testStyle), |
| controller: textController, |
| maxLength: 10, |
| maxLengthEnforcement: MaxLengthEnforcement.none, |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), '👨👩👦012345678910111'); |
| await tester.pump(); |
| |
| expect(textController.text, '👨👩👦012345678910111'); |
| expect(find.text('16/10'), findsOneWidget); |
| Text counterTextWidget = tester.widget(find.text('16/10')); |
| expect(counterTextWidget.style!.color, equals(Colors.deepPurpleAccent)); |
| |
| await tester.enterText(find.byType(TextField), '👨👩👦012345678'); |
| await tester.pump(); |
| |
| expect(textController.text, '👨👩👦012345678'); |
| expect(find.text('10/10'), findsOneWidget); |
| counterTextWidget = tester.widget(find.text('10/10')); |
| expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent))); |
| }); |
| |
| testWidgets('maxLength limits input with surrogate pairs.', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| ), |
| )); |
| |
| const String surrogatePair = '😆'; |
| await tester.enterText(find.byType(TextField), '${surrogatePair}0123456789101112'); |
| expect(textController.text, '${surrogatePair}012345678'); |
| }); |
| |
| testWidgets('maxLength limits input with grapheme clusters.', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| ), |
| )); |
| |
| const String graphemeCluster = '👨👩👦'; |
| await tester.enterText(find.byType(TextField), '${graphemeCluster}0123456789101112'); |
| expect(textController.text, '${graphemeCluster}012345678'); |
| }); |
| |
| testWidgets('setting maxLength shows counter', (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLength: 10, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('0/10'), findsOneWidget); |
| |
| await tester.enterText(find.byType(TextField), '01234'); |
| await tester.pump(); |
| |
| expect(find.text('5/10'), findsOneWidget); |
| }); |
| |
| testWidgets('maxLength counter measures surrogate pairs as one character', (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLength: 10, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('0/10'), findsOneWidget); |
| |
| const String surrogatePair = '😆'; |
| await tester.enterText(find.byType(TextField), surrogatePair); |
| await tester.pump(); |
| |
| expect(find.text('1/10'), findsOneWidget); |
| }); |
| |
| testWidgets('maxLength counter measures grapheme clusters as one character', (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLength: 10, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('0/10'), findsOneWidget); |
| |
| const String familyEmoji = '👨👩👦'; |
| await tester.enterText(find.byType(TextField), familyEmoji); |
| await tester.pump(); |
| |
| expect(find.text('1/10'), findsOneWidget); |
| }); |
| |
| testWidgets('setting maxLength to TextField.noMaxLength shows only entered length', (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLength: TextField.noMaxLength, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('0'), findsOneWidget); |
| |
| await tester.enterText(find.byType(TextField), '01234'); |
| await tester.pump(); |
| |
| expect(find.text('5'), findsOneWidget); |
| }); |
| |
| testWidgets('passing a buildCounter shows returned widget', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| buildCounter: (BuildContext context, { required int currentLength, int? maxLength, required bool isFocused }) { |
| return Text('$currentLength of $maxLength'); |
| }, |
| maxLength: 10, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('0 of 10'), findsOneWidget); |
| |
| await tester.enterText(find.byType(TextField), '01234'); |
| await tester.pump(); |
| |
| expect(find.text('5 of 10'), findsOneWidget); |
| }); |
| |
| testWidgets('TextField identifies as text field in semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLength: 10, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField])); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Disabled text field does not have tap action', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLength: 10, |
| enabled: false, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(semantics, isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.tap]))); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Disabled text field semantics node still contains value', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: TextEditingController(text: 'text'), |
| maxLength: 10, |
| enabled: false, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(semantics, includesNodeWith(actions: <SemanticsAction>[], value: 'text')); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Readonly text field does not have tap action', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLength: 10, |
| readOnly: true, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(semantics, isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.tap]))); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Disabled text field hides helper and counter', (WidgetTester tester) async { |
| const String helperText = 'helper text'; |
| const String counterText = 'counter text'; |
| const String errorText = 'error text'; |
| Widget buildFrame(bool enabled, bool hasError) { |
| return MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: Center( |
| child: TextField( |
| decoration: InputDecoration( |
| labelText: 'label text', |
| helperText: helperText, |
| counterText: counterText, |
| errorText: hasError ? errorText : null, |
| enabled: enabled, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame(true, false)); |
| Text helperWidget = tester.widget(find.text(helperText)); |
| Text counterWidget = tester.widget(find.text(counterText)); |
| expect(helperWidget.style!.color, isNot(equals(Colors.transparent))); |
| expect(counterWidget.style!.color, isNot(equals(Colors.transparent))); |
| await tester.pumpWidget(buildFrame(true, true)); |
| counterWidget = tester.widget(find.text(counterText)); |
| Text errorWidget = tester.widget(find.text(errorText)); |
| expect(helperWidget.style!.color, isNot(equals(Colors.transparent))); |
| expect(errorWidget.style!.color, isNot(equals(Colors.transparent))); |
| |
| // When enabled is false, the helper/error and counter are not visible. |
| await tester.pumpWidget(buildFrame(false, false)); |
| helperWidget = tester.widget(find.text(helperText)); |
| counterWidget = tester.widget(find.text(counterText)); |
| expect(helperWidget.style!.color, equals(Colors.transparent)); |
| expect(counterWidget.style!.color, equals(Colors.transparent)); |
| await tester.pumpWidget(buildFrame(false, true)); |
| errorWidget = tester.widget(find.text(errorText)); |
| counterWidget = tester.widget(find.text(counterText)); |
| expect(counterWidget.style!.color, equals(Colors.transparent)); |
| expect(errorWidget.style!.color, equals(Colors.transparent)); |
| }); |
| |
| testWidgets('Disabled text field has default M2 disabled text style for the input text', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| enabled: false, |
| ), |
| ), |
| ), |
| ), |
| ); |
| final EditableText editableText = tester.widget(find.byType(EditableText)); |
| expect(editableText.style.color, Colors.black38); // Colors.black38 is the default disabled color for ThemeData.light(). |
| }); |
| |
| testWidgets('Disabled text field has default M3 disabled text style for the input text', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| |
| final ThemeData theme = ThemeData.light(useMaterial3: true); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: theme, |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| enabled: false, |
| ), |
| ), |
| ), |
| ), |
| ); |
| final EditableText editableText = tester.widget(find.byType(EditableText)); |
| expect(editableText.style.color, theme.textTheme.bodyLarge!.color!.withOpacity(0.38)); |
| }); |
| |
| testWidgets('currentValueLength/maxValueLength are in the tree', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| maxLength: 10, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(semantics, includesNodeWith( |
| flags: <SemanticsFlag>[SemanticsFlag.isTextField], |
| maxValueLength: 10, |
| currentValueLength: 0, |
| )); |
| |
| await tester.showKeyboard(find.byType(TextField)); |
| const String testValue = '123'; |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: testValue, |
| selection: TextSelection.collapsed(offset: 3), |
| composing: TextRange(start: 0, end: testValue.length), |
| )); |
| await tester.pump(); |
| |
| expect(semantics, includesNodeWith( |
| flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isFocused], |
| maxValueLength: 10, |
| currentValueLength: 3, |
| )); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Read only TextField identifies as read only text field in semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLength: 10, |
| readOnly: true, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| semantics, |
| includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isReadOnly]), |
| ); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets("Disabled TextField can't be traversed to.", (WidgetTester tester) async { |
| final FocusNode focusNode1 = FocusNode(debugLabel: 'TextField 1'); |
| final FocusNode focusNode2 = FocusNode(debugLabel: 'TextField 2'); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: FocusScope( |
| child: Center( |
| child: Column( |
| children: <Widget>[ |
| TextField( |
| focusNode: focusNode1, |
| autofocus: true, |
| maxLength: 10, |
| enabled: true, |
| ), |
| TextField( |
| focusNode: focusNode2, |
| maxLength: 10, |
| enabled: false, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| expect(focusNode1.hasPrimaryFocus, isTrue); |
| expect(focusNode2.hasPrimaryFocus, isFalse); |
| |
| expect(focusNode1.nextFocus(), isTrue); |
| await tester.pump(); |
| |
| expect(focusNode1.hasPrimaryFocus, isTrue); |
| expect(focusNode2.hasPrimaryFocus, isFalse); |
| }); |
| |
| group('Keyboard Tests', () { |
| late TextEditingController controller; |
| |
| setUp( () { |
| controller = TextEditingController(); |
| }); |
| |
| Future<void> setupWidget(WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: RawKeyboardListener( |
| focusNode: focusNode, |
| child: TextField( |
| controller: controller, |
| maxLines: 3, |
| ), |
| ), |
| ), |
| ), |
| ); |
| await tester.pump(); |
| } |
| |
| testWidgets('Shift test 1', (WidgetTester tester) async { |
| await setupWidget(tester); |
| const String testValue = 'a big house'; |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| await tester.idle(); |
| // Need to wait for selection to catch up. |
| await tester.pump(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, -1); |
| }, variant: KeySimulatorTransitModeVariant.all()); |
| |
| testWidgets('Shift test 2', (WidgetTester tester) async { |
| await setupWidget(tester); |
| |
| const String testValue = 'abcdefghi'; |
| await tester.showKeyboard(find.byType(TextField)); |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: testValue, |
| selection: TextSelection.collapsed(offset: 3), |
| composing: TextRange(start: 0, end: testValue.length), |
| )); |
| await tester.pump(); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 1); |
| }, variant: KeySimulatorTransitModeVariant.all()); |
| |
| testWidgets('Control Shift test', (WidgetTester tester) async { |
| await setupWidget(tester); |
| const String testValue = 'their big house'; |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| await tester.pumpAndSettle(); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.control); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 5); |
| }, variant: KeySimulatorTransitModeVariant.all()); |
| |
| testWidgets('Down and up test', (WidgetTester tester) async { |
| await setupWidget(tester); |
| const String testValue = 'a big house'; |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| await tester.idle(); |
| // Need to wait for selection to catch up. |
| await tester.pump(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowUp); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, -11); |
| |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowUp); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| await tester.pumpAndSettle(); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 0); |
| }, variant: KeySimulatorTransitModeVariant.all()); |
| |
| testWidgets('Down and up test 2', (WidgetTester tester) async { |
| await setupWidget(tester); |
| const String testValue = 'a big house\njumped over a mouse\nOne more line yay'; // 11 \n 19 |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| for (int i = 0; i < 5; i += 1) { |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| } |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pumpAndSettle(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 12); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pumpAndSettle(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 32); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| await tester.pumpAndSettle(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 12); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| await tester.pumpAndSettle(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 0); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| await tester.pumpAndSettle(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, -5); |
| }, variant: KeySimulatorTransitModeVariant.all()); |
| |
| testWidgets('Read only keyboard selection test', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(text: 'readonly'); |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| readOnly: true, |
| ), |
| ), |
| ); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft); |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, -1); |
| }, variant: KeySimulatorTransitModeVariant.all()); |
| }, skip: areKeyEventsHandledByPlatform); // [intended] only applies to platforms where we handle key events. |
| |
| testWidgets('Copy paste test', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| final TextEditingController controller = TextEditingController(); |
| final TextField textField = |
| TextField( |
| controller: controller, |
| maxLines: 3, |
| ); |
| |
| String clipboardContent = ''; |
| tester.binding.defaultBinaryMessenger |
| .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { |
| if (methodCall.method == 'Clipboard.setData') { |
| // ignore: avoid_dynamic_calls |
| clipboardContent = methodCall.arguments['text'] as String; |
| } else if (methodCall.method == 'Clipboard.getData') { |
| return <String, dynamic>{'text': clipboardContent}; |
| } |
| return null; |
| }); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: RawKeyboardListener( |
| focusNode: focusNode, |
| child: textField, |
| ), |
| ), |
| ), |
| ); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| |
| const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| // Select the first 5 characters |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| for (int i = 0; i < 5; i += 1) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| } |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| |
| // Copy them |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight); |
| await tester.sendKeyEvent(LogicalKeyboardKey.keyC); |
| await tester.pumpAndSettle(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); |
| await tester.pumpAndSettle(); |
| |
| expect(clipboardContent, 'a big'); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| |
| // Paste them |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV); |
| await tester.pumpAndSettle(); |
| await tester.pump(const Duration(milliseconds: 200)); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); |
| await tester.pumpAndSettle(); |
| |
| const String expected = 'a biga big house\njumped over a mouse'; |
| expect(find.text(expected), findsOneWidget, reason: 'Because text contains ${controller.text}'); |
| }, |
| skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events. |
| variant: KeySimulatorTransitModeVariant.all() |
| ); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/78219 |
| testWidgets('Paste does not crash after calling TextController.text setter', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| final TextEditingController controller = TextEditingController(); |
| final TextField textField = TextField( |
| controller: controller, |
| obscureText: true, |
| ); |
| |
| const String clipboardContent = 'I love Flutter!'; |
| tester.binding.defaultBinaryMessenger |
| .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { |
| if (methodCall.method == 'Clipboard.getData') { |
| return <String, dynamic>{'text': clipboardContent}; |
| } |
| return null; |
| }); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: RawKeyboardListener( |
| focusNode: focusNode, |
| child: textField, |
| ), |
| ), |
| ), |
| ); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| // Clear the text. |
| controller.text = ''; |
| |
| // Paste clipboardContent to the text field. |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV); |
| await tester.pumpAndSettle(); |
| await tester.pump(const Duration(milliseconds: 200)); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); |
| await tester.pumpAndSettle(); |
| |
| // Clipboard content is correctly pasted. |
| expect(find.text(clipboardContent), findsOneWidget); |
| }, |
| skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events. |
| variant: KeySimulatorTransitModeVariant.all(), |
| ); |
| |
| testWidgets('Cut test', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| final TextEditingController controller = TextEditingController(); |
| final TextField textField = |
| TextField( |
| controller: controller, |
| maxLines: 3, |
| ); |
| String clipboardContent = ''; |
| tester.binding.defaultBinaryMessenger |
| .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { |
| if (methodCall.method == 'Clipboard.setData') { |
| // ignore: avoid_dynamic_calls |
| clipboardContent = methodCall.arguments['text'] as String; |
| } else if (methodCall.method == 'Clipboard.getData') { |
| return <String, dynamic>{'text': clipboardContent}; |
| } |
| return null; |
| }); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: RawKeyboardListener( |
| focusNode: focusNode, |
| child: textField, |
| ), |
| ), |
| ), |
| ); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| |
| const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| // Select the first 5 characters |
| for (int i = 0; i < 5; i += 1) { |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| await tester.pumpAndSettle(); |
| } |
| |
| // Cut them |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight); |
| await tester.sendKeyEvent(LogicalKeyboardKey.keyX); |
| await tester.pumpAndSettle(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); |
| await tester.pumpAndSettle(); |
| |
| expect(clipboardContent, 'a big'); |
| |
| for (int i = 0; i < 5; i += 1) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| } |
| |
| // Paste them |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV); |
| await tester.pumpAndSettle(); |
| await tester.pump(const Duration(milliseconds: 200)); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); |
| await tester.pumpAndSettle(); |
| |
| const String expected = ' housa bige\njumped over a mouse'; |
| expect(find.text(expected), findsOneWidget); |
| }, |
| skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events. |
| variant: KeySimulatorTransitModeVariant.all() |
| ); |
| |
| testWidgets('Select all test', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| final TextEditingController controller = TextEditingController(); |
| final TextField textField = |
| TextField( |
| controller: controller, |
| maxLines: 3, |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: RawKeyboardListener( |
| focusNode: focusNode, |
| child: textField, |
| ), |
| ), |
| ), |
| ); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| |
| const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| // Select All |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.control); |
| await tester.sendKeyEvent(LogicalKeyboardKey.keyA); |
| await tester.pumpAndSettle(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.control); |
| await tester.pumpAndSettle(); |
| |
| // Delete them |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.delete); |
| await tester.pumpAndSettle(); |
| await tester.pump(const Duration(milliseconds: 200)); |
| |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.delete); |
| await tester.pumpAndSettle(); |
| |
| const String expected = ''; |
| expect(find.text(expected), findsOneWidget); |
| }, |
| skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events. |
| variant: KeySimulatorTransitModeVariant.all() |
| ); |
| |
| testWidgets('Delete test', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| final TextEditingController controller = TextEditingController(); |
| final TextField textField = |
| TextField( |
| controller: controller, |
| maxLines: 3, |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: RawKeyboardListener( |
| focusNode: focusNode, |
| child: textField, |
| ), |
| ), |
| ), |
| ); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| |
| const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| // Delete |
| for (int i = 0; i < 6; i += 1) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.delete); |
| await tester.pumpAndSettle(); |
| } |
| |
| const String expected = 'house\njumped over a mouse'; |
| expect(find.text(expected), findsOneWidget); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.control); |
| await tester.sendKeyEvent(LogicalKeyboardKey.keyA); |
| await tester.pumpAndSettle(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.control); |
| await tester.pumpAndSettle(); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.delete); |
| await tester.pumpAndSettle(); |
| |
| const String expected2 = ''; |
| expect(find.text(expected2), findsOneWidget); |
| }, |
| skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events. |
| variant: KeySimulatorTransitModeVariant.all(), |
| ); |
| |
| testWidgets('Changing positions of text fields', (WidgetTester tester) async { |
| |
| final FocusNode focusNode = FocusNode(); |
| final List<RawKeyEvent> events = <RawKeyEvent>[]; |
| |
| final TextEditingController c1 = TextEditingController(); |
| final TextEditingController c2 = TextEditingController(); |
| final Key key1 = UniqueKey(); |
| final Key key2 = UniqueKey(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: |
| Material( |
| child: RawKeyboardListener( |
| focusNode: focusNode, |
| onKey: events.add, |
| child: Column( |
| crossAxisAlignment: CrossAxisAlignment.stretch, |
| children: <Widget>[ |
| TextField( |
| key: key1, |
| controller: c1, |
| maxLines: 3, |
| ), |
| TextField( |
| key: key2, |
| controller: c2, |
| maxLines: 3, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'a big house'; |
| await tester.enterText(find.byType(TextField).first, testValue); |
| |
| await tester.idle(); |
| // Need to wait for selection to catch up. |
| await tester.pump(); |
| await tester.tap(find.byType(TextField).first); |
| await tester.pumpAndSettle(); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| for (int i = 0; i < 5; i += 1) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pumpAndSettle(); |
| } |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| |
| expect(c1.selection.extentOffset - c1.selection.baseOffset, -5); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: |
| Material( |
| child: RawKeyboardListener( |
| focusNode: focusNode, |
| onKey: events.add, |
| child: Column( |
| crossAxisAlignment: CrossAxisAlignment.stretch, |
| children: <Widget>[ |
| TextField( |
| key: key2, |
| controller: c2, |
| maxLines: 3, |
| ), |
| TextField( |
| key: key1, |
| controller: c1, |
| maxLines: 3, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| for (int i = 0; i < 5; i += 1) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pumpAndSettle(); |
| } |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| |
| expect(c1.selection.extentOffset - c1.selection.baseOffset, -10); |
| }, |
| skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events. |
| variant: KeySimulatorTransitModeVariant.all() |
| ); |
| |
| testWidgets('Changing focus test', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| final List<RawKeyEvent> events = <RawKeyEvent>[]; |
| |
| final TextEditingController c1 = TextEditingController(); |
| final TextEditingController c2 = TextEditingController(); |
| final Key key1 = UniqueKey(); |
| final Key key2 = UniqueKey(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: |
| Material( |
| child: RawKeyboardListener( |
| focusNode: focusNode, |
| onKey: events.add, |
| child: Column( |
| crossAxisAlignment: CrossAxisAlignment.stretch, |
| children: <Widget>[ |
| TextField( |
| key: key1, |
| controller: c1, |
| maxLines: 3, |
| ), |
| TextField( |
| key: key2, |
| controller: c2, |
| maxLines: 3, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'a big house'; |
| await tester.enterText(find.byType(TextField).first, testValue); |
| await tester.idle(); |
| await tester.pump(); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField).first); |
| await tester.pumpAndSettle(); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| for (int i = 0; i < 5; i += 1) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pumpAndSettle(); |
| } |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| |
| expect(c1.selection.extentOffset - c1.selection.baseOffset, -5); |
| expect(c2.selection.extentOffset - c2.selection.baseOffset, 0); |
| |
| await tester.enterText(find.byType(TextField).last, testValue); |
| await tester.idle(); |
| await tester.pump(); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField).last); |
| await tester.pumpAndSettle(); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| for (int i = 0; i < 5; i += 1) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pumpAndSettle(); |
| } |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| |
| expect(c1.selection.extentOffset - c1.selection.baseOffset, -5); |
| expect(c2.selection.extentOffset - c2.selection.baseOffset, -5); |
| }, |
| skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events. |
| variant: KeySimulatorTransitModeVariant.all() |
| ); |
| |
| testWidgets('Caret works when maxLines is null', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ); |
| |
| const String testValue = 'x'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.length); |
| |
| // Tap the selection handle to bring up the "paste / select all" menu. |
| await tester.tapAt(textOffsetToPosition(tester, 0)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is |
| |
| // Confirm that the selection was updated. |
| expect(controller.selection.baseOffset, 0); |
| }); |
| |
| testWidgets('TextField baseline alignment no-strut', (WidgetTester tester) async { |
| final TextEditingController controllerA = TextEditingController(text: 'A'); |
| final TextEditingController controllerB = TextEditingController(text: 'B'); |
| final Key keyA = UniqueKey(); |
| final Key keyB = UniqueKey(); |
| |
| await tester.pumpWidget( |
| Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: Row( |
| crossAxisAlignment: CrossAxisAlignment.baseline, |
| textBaseline: TextBaseline.alphabetic, |
| children: <Widget>[ |
| Expanded( |
| child: TextField( |
| key: keyA, |
| decoration: null, |
| controller: controllerA, |
| // The point size of the font must be a multiple of 4 until |
| // https://github.com/flutter/flutter/issues/122066 is resolved. |
| style: const TextStyle(fontFamily: 'FlutterTest', fontSize: 12.0), |
| strutStyle: StrutStyle.disabled, |
| ), |
| ), |
| const Text( |
| 'abc', |
| // The point size of the font must be a multiple of 4 until |
| // https://github.com/flutter/flutter/issues/122066 is resolved. |
| style: TextStyle(fontFamily: 'FlutterTest', fontSize: 24.0), |
| ), |
| Expanded( |
| child: TextField( |
| key: keyB, |
| decoration: null, |
| controller: controllerB, |
| // The point size of the font must be a multiple of 4 until |
| // https://github.com/flutter/flutter/issues/122066 is resolved. |
| style: const TextStyle(fontFamily: 'FlutterTest', fontSize: 36.0), |
| strutStyle: StrutStyle.disabled, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| // The test font extends 0.25 * fontSize below the baseline. |
| // So the three row elements line up like this: |
| // |
| // A abc B |
| // --------- baseline |
| // 3 6 9 space below the baseline = 0.25 * fontSize |
| // --------- rowBottomY |
| |
| final double rowBottomY = tester.getBottomLeft(find.byType(Row)).dy; |
| expect(tester.getBottomLeft(find.byKey(keyA)).dy, rowBottomY - 6.0); |
| expect(tester.getBottomLeft(find.text('abc')).dy, rowBottomY - 3.0); |
| expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY); |
| }); |
| |
| testWidgets('TextField baseline alignment', (WidgetTester tester) async { |
| final TextEditingController controllerA = TextEditingController(text: 'A'); |
| final TextEditingController controllerB = TextEditingController(text: 'B'); |
| final Key keyA = UniqueKey(); |
| final Key keyB = UniqueKey(); |
| |
| await tester.pumpWidget( |
| Theme( |
| data: ThemeData(useMaterial3: false), |
| child: overlay( |
| child: Row( |
| crossAxisAlignment: CrossAxisAlignment.baseline, |
| textBaseline: TextBaseline.alphabetic, |
| children: <Widget>[ |
| Expanded( |
| child: TextField( |
| key: keyA, |
| decoration: null, |
| controller: controllerA, |
| // The point size of the font must be a multiple of 4 until |
| // https://github.com/flutter/flutter/issues/122066 is resolved. |
| style: const TextStyle(fontFamily: 'FlutterTest', fontSize: 12.0), |
| ), |
| ), |
| const Text( |
| 'abc', |
| // The point size of the font must be a multiple of 4 until |
| // https://github.com/flutter/flutter/issues/122066 is resolved. |
| style: TextStyle(fontFamily: 'FlutterTest', fontSize: 24.0), |
| ), |
| Expanded( |
| child: TextField( |
| key: keyB, |
| decoration: null, |
| controller: controllerB, |
| // The point size of the font must be a multiple of 4 until |
| // https://github.com/flutter/flutter/issues/122066 is resolved. |
| style: const TextStyle(fontFamily: 'FlutterTest', fontSize: 36.0), |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| // The test font extends 0.25 * fontSize below the baseline. |
| // So the three row elements line up like this: |
| // |
| // A abc B |
| // --------- baseline |
| // 3 6 9 space below the baseline = 0.25 * fontSize |
| // --------- rowBottomY |
| |
| final double rowBottomY = tester.getBottomLeft(find.byType(Row)).dy; |
| // The values here should match the version with strut disabled ('TextField baseline alignment no-strut') |
| expect(tester.getBottomLeft(find.byKey(keyA)).dy, rowBottomY - 6.0); |
| expect(tester.getBottomLeft(find.text('abc')).dy, rowBottomY - 3.0); |
| expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY); |
| }); |
| |
| testWidgets('TextField semantics include label when unfocused and label/hint when focused if input is empty', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final TextEditingController controller = TextEditingController(text: ''); |
| final Key key = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: key, |
| controller: controller, |
| decoration: const InputDecoration( |
| hintText: 'hint', |
| labelText: 'label', |
| ), |
| ), |
| ), |
| ); |
| |
| final SemanticsNode node = tester.getSemantics(find.byKey(key)); |
| |
| expect(node.label, 'label'); |
| expect(node.value, ''); |
| |
| // Focus text field. |
| await tester.tap(find.byKey(key)); |
| await tester.pump(); |
| |
| expect(node.label, 'label'); |
| expect(node.value, ''); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('TextField semantics alway include label and not hint when input value is not empty', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final TextEditingController controller = TextEditingController(text: 'value'); |
| final Key key = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: key, |
| controller: controller, |
| decoration: const InputDecoration( |
| hintText: 'hint', |
| labelText: 'label', |
| ), |
| ), |
| ), |
| ); |
| |
| final SemanticsNode node = tester.getSemantics(find.byKey(key)); |
| |
| expect(node.label, 'label'); |
| expect(node.value, 'value'); |
| |
| // Focus text field. |
| await tester.tap(find.byKey(key)); |
| await tester.pump(); |
| |
| expect(node.label, 'label'); |
| expect(node.value, 'value'); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('TextField semantics always include label when no hint is given', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final TextEditingController controller = TextEditingController(text: 'value'); |
| final Key key = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: key, |
| controller: controller, |
| decoration: const InputDecoration( |
| labelText: 'label', |
| ), |
| ), |
| ), |
| ); |
| |
| final SemanticsNode node = tester.getSemantics(find.byKey(key)); |
| |
| expect(node.label, 'label'); |
| expect(node.value, 'value'); |
| |
| // Focus text field. |
| await tester.tap(find.byKey(key)); |
| await tester.pump(); |
| |
| expect(node.label, 'label'); |
| expect(node.value, 'value'); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('TextField semantics only include hint when it is visible', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final TextEditingController controller = TextEditingController(text: 'value'); |
| final Key key = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: key, |
| controller: controller, |
| decoration: const InputDecoration( |
| hintText: 'hint', |
| ), |
| ), |
| ), |
| ); |
| |
| final SemanticsNode node = tester.getSemantics(find.byKey(key)); |
| |
| expect(node.label, ''); |
| expect(node.value, 'value'); |
| |
| // Focus text field. |
| await tester.tap(find.byKey(key)); |
| await tester.pump(); |
| |
| expect(node.label, ''); |
| expect(node.value, 'value'); |
| |
| // Clear the Text. |
| await tester.enterText(find.byType(TextField), ''); |
| await tester.pumpAndSettle(); |
| |
| expect(node.value, ''); |
| expect(node.label, 'hint'); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('TextField semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final TextEditingController controller = TextEditingController(); |
| final Key key = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: key, |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| |
| controller.text = 'Guten Tag'; |
| await tester.pump(); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| value: 'Guten Tag', |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| |
| await tester.tap(find.byKey(key)); |
| await tester.pump(); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| value: 'Guten Tag', |
| textSelection: const TextSelection.collapsed(offset: 9), |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| SemanticsAction.moveCursorBackwardByCharacter, |
| SemanticsAction.moveCursorBackwardByWord, |
| SemanticsAction.setSelection, |
| SemanticsAction.setText, |
| SemanticsAction.paste, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| SemanticsFlag.isFocused, |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| |
| controller.selection = const TextSelection.collapsed(offset: 4); |
| await tester.pump(); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| textSelection: const TextSelection.collapsed(offset: 4), |
| value: 'Guten Tag', |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| SemanticsAction.moveCursorBackwardByCharacter, |
| SemanticsAction.moveCursorForwardByCharacter, |
| SemanticsAction.moveCursorBackwardByWord, |
| SemanticsAction.moveCursorForwardByWord, |
| SemanticsAction.setSelection, |
| SemanticsAction.setText, |
| SemanticsAction.paste, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| SemanticsFlag.isFocused, |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| |
| controller.text = 'Schönen Feierabend'; |
| controller.selection = const TextSelection.collapsed(offset: 0); |
| await tester.pump(); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| textSelection: const TextSelection.collapsed(offset: 0), |
| value: 'Schönen Feierabend', |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| SemanticsAction.moveCursorForwardByCharacter, |
| SemanticsAction.moveCursorForwardByWord, |
| SemanticsAction.setSelection, |
| SemanticsAction.setText, |
| SemanticsAction.paste, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| SemanticsFlag.isFocused, |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| |
| semantics.dispose(); |
| }); |
| |
| // Regressing test for https://github.com/flutter/flutter/issues/99763 |
| testWidgets('Update textField semantics when obscureText changes', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final TextEditingController controller = TextEditingController(); |
| await tester.pumpWidget(_ObscureTextTestWidget(controller: controller)); |
| |
| controller.text = 'Hello'; |
| await tester.pump(); |
| |
| expect( |
| semantics, |
| includesNodeWith( |
| actions: <SemanticsAction>[SemanticsAction.tap], |
| textDirection: TextDirection.ltr, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| ], |
| value: 'Hello', |
| ) |
| ); |
| |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pump(); |
| |
| expect( |
| semantics, |
| includesNodeWith( |
| actions: <SemanticsAction>[SemanticsAction.tap], |
| textDirection: TextDirection.ltr, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| SemanticsFlag.isObscured, |
| ], |
| ) |
| ); |
| |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pump(); |
| |
| expect( |
| semantics, |
| includesNodeWith( |
| actions: <SemanticsAction>[SemanticsAction.tap], |
| textDirection: TextDirection.ltr, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| ], |
| value: 'Hello', |
| ) |
| ); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('TextField semantics, enableInteractiveSelection = false', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final TextEditingController controller = TextEditingController(); |
| final Key key = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: key, |
| controller: controller, |
| enableInteractiveSelection: false, |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byKey(key)); |
| await tester.pump(); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| SemanticsAction.setText, |
| // Absent the following because enableInteractiveSelection: false |
| // SemanticsAction.moveCursorBackwardByCharacter, |
| // SemanticsAction.moveCursorBackwardByWord, |
| // SemanticsAction.setSelection, |
| // SemanticsAction.paste, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| SemanticsFlag.isFocused, |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('TextField semantics for selections', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final TextEditingController controller = TextEditingController() |
| ..text = 'Hello'; |
| final Key key = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: key, |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| id: 1, |
| value: 'Hello', |
| textDirection: TextDirection.ltr, |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| |
| // Focus the text field |
| await tester.tap(find.byKey(key)); |
| await tester.pump(); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| id: 1, |
| value: 'Hello', |
| textSelection: const TextSelection.collapsed(offset: 5), |
| textDirection: TextDirection.ltr, |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| SemanticsAction.moveCursorBackwardByCharacter, |
| SemanticsAction.moveCursorBackwardByWord, |
| SemanticsAction.setSelection, |
| SemanticsAction.setText, |
| SemanticsAction.paste, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| SemanticsFlag.isFocused, |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| |
| controller.selection = const TextSelection(baseOffset: 5, extentOffset: 3); |
| await tester.pump(); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| id: 1, |
| value: 'Hello', |
| textSelection: const TextSelection(baseOffset: 5, extentOffset: 3), |
| textDirection: TextDirection.ltr, |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| SemanticsAction.moveCursorBackwardByCharacter, |
| SemanticsAction.moveCursorForwardByCharacter, |
| SemanticsAction.moveCursorBackwardByWord, |
| SemanticsAction.moveCursorForwardByWord, |
| SemanticsAction.setSelection, |
| SemanticsAction.setText, |
| SemanticsAction.paste, |
| SemanticsAction.cut, |
| SemanticsAction.copy, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| SemanticsFlag.isFocused, |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('TextField change selection with semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; |
| final TextEditingController controller = TextEditingController() |
| ..text = 'Hello'; |
| final Key key = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: key, |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| // Focus the text field |
| await tester.tap(find.byKey(key)); |
| await tester.pump(); |
| |
| const int inputFieldId = 1; |
| |
| expect(controller.selection, const TextSelection.collapsed(offset: 5, affinity: TextAffinity.upstream)); |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| id: inputFieldId, |
| value: 'Hello', |
| textSelection: const TextSelection.collapsed(offset: 5), |
| textDirection: TextDirection.ltr, |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| SemanticsAction.moveCursorBackwardByCharacter, |
| SemanticsAction.moveCursorBackwardByWord, |
| SemanticsAction.setSelection, |
| SemanticsAction.setText, |
| SemanticsAction.paste, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| SemanticsFlag.isFocused, |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| |
| // move cursor back once |
| semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <dynamic, dynamic>{ |
| 'base': 4, |
| 'extent': 4, |
| }); |
| await tester.pump(); |
| expect(controller.selection, const TextSelection.collapsed(offset: 4)); |
| |
| // move cursor to front |
| semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <dynamic, dynamic>{ |
| 'base': 0, |
| 'extent': 0, |
| }); |
| await tester.pump(); |
| expect(controller.selection, const TextSelection.collapsed(offset: 0)); |
| |
| // select all |
| semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <dynamic, dynamic>{ |
| 'base': 0, |
| 'extent': 5, |
| }); |
| await tester.pump(); |
| expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| id: inputFieldId, |
| value: 'Hello', |
| textSelection: const TextSelection(baseOffset: 0, extentOffset: 5), |
| textDirection: TextDirection.ltr, |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| SemanticsAction.moveCursorBackwardByCharacter, |
| SemanticsAction.moveCursorBackwardByWord, |
| SemanticsAction.setSelection, |
| SemanticsAction.setText, |
| SemanticsAction.paste, |
| SemanticsAction.cut, |
| SemanticsAction.copy, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| SemanticsFlag.isFocused, |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Can activate TextField with explicit controller via semantics ', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/17801 |
| |
| const String textInTextField = 'Hello'; |
| |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; |
| final TextEditingController controller = TextEditingController() |
| ..text = textInTextField; |
| final Key key = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: key, |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const int inputFieldId = 1; |
| |
| expect(semantics, hasSemantics( |
| TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: inputFieldId, |
| flags: <SemanticsFlag>[SemanticsFlag.isTextField], |
| actions: <SemanticsAction>[SemanticsAction.tap], |
| value: textInTextField, |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ignoreRect: true, ignoreTransform: true, |
| )); |
| |
| semanticsOwner.performAction(inputFieldId, SemanticsAction.tap); |
| await tester.pump(); |
| |
| expect(semantics, hasSemantics( |
| TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: inputFieldId, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| SemanticsFlag.isFocused, |
| ], |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| SemanticsAction.moveCursorBackwardByCharacter, |
| SemanticsAction.moveCursorBackwardByWord, |
| SemanticsAction.setSelection, |
| SemanticsAction.setText, |
| SemanticsAction.paste, |
| ], |
| value: textInTextField, |
| textDirection: TextDirection.ltr, |
| textSelection: const TextSelection( |
| baseOffset: textInTextField.length, |
| extentOffset: textInTextField.length, |
| ), |
| ), |
| ], |
| ), |
| ignoreRect: true, ignoreTransform: true, |
| )); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('When clipboard empty, no semantics paste option', (WidgetTester tester) async { |
| const String textInTextField = 'Hello'; |
| |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; |
| final TextEditingController controller = TextEditingController() |
| ..text = textInTextField; |
| final Key key = UniqueKey(); |
| |
| // Clear the clipboard. |
| await Clipboard.setData(const ClipboardData(text: '')); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: key, |
| controller: controller, |
| ), |
| ), |
| ); |
| |
| const int inputFieldId = 1; |
| |
| expect(semantics, hasSemantics( |
| TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: inputFieldId, |
| flags: <SemanticsFlag>[SemanticsFlag.isTextField], |
| actions: <SemanticsAction>[SemanticsAction.tap], |
| value: textInTextField, |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ignoreRect: true, ignoreTransform: true, |
| )); |
| |
| semanticsOwner.performAction(inputFieldId, SemanticsAction.tap); |
| await tester.pump(); |
| |
| expect(semantics, hasSemantics( |
| TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: inputFieldId, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| SemanticsFlag.isFocused, |
| ], |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| SemanticsAction.moveCursorBackwardByCharacter, |
| SemanticsAction.moveCursorBackwardByWord, |
| SemanticsAction.setSelection, |
| SemanticsAction.setText, |
| // No paste option. |
| ], |
| value: textInTextField, |
| textDirection: TextDirection.ltr, |
| textSelection: const TextSelection( |
| baseOffset: textInTextField.length, |
| extentOffset: textInTextField.length, |
| ), |
| ), |
| ], |
| ), |
| ignoreRect: true, ignoreTransform: true, |
| )); |
| |
| semantics.dispose(); |
| |
| // On web, we don't check for pasteability because that triggers a |
| // permission dialog in the browser. |
| // https://github.com/flutter/flutter/pull/57139#issuecomment-629048058 |
| }, skip: isBrowser); // [intended] see above. |
| |
| testWidgets('TextField throws when not descended from a Material widget', (WidgetTester tester) async { |
| const Widget textField = TextField(); |
| await tester.pumpWidget(textField); |
| final dynamic exception = tester.takeException(); |
| expect(exception, isFlutterError); |
| expect(exception.toString(), startsWith('No Material widget found.')); |
| }); |
| |
| testWidgets('TextField loses focus when disabled', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(debugLabel: 'TextField Focus Node'); |
| |
| await tester.pumpWidget( |
| boilerplate( |
| child: TextField( |
| focusNode: focusNode, |
| autofocus: true, |
| enabled: true, |
| ), |
| ), |
| ); |
| expect(focusNode.hasFocus, isTrue); |
| |
| await tester.pumpWidget( |
| boilerplate( |
| child: TextField( |
| focusNode: focusNode, |
| autofocus: true, |
| enabled: false, |
| ), |
| ), |
| ); |
| expect(focusNode.hasFocus, isFalse); |
| |
| await tester.pumpWidget( |
| boilerplate( |
| child: Builder(builder: (BuildContext context) { |
| return MediaQuery( |
| data: MediaQuery.of(context).copyWith( |
| navigationMode: NavigationMode.directional, |
| ), |
| child: TextField( |
| focusNode: focusNode, |
| autofocus: true, |
| enabled: true, |
| ), |
| ); |
| }), |
| ), |
| ); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| |
| expect(focusNode.hasFocus, isTrue); |
| |
| await tester.pumpWidget( |
| boilerplate( |
| child: Builder(builder: (BuildContext context) { |
| return MediaQuery( |
| data: MediaQuery.of(context).copyWith( |
| navigationMode: NavigationMode.directional, |
| ), |
| child: TextField( |
| focusNode: focusNode, |
| autofocus: true, |
| enabled: false, |
| ), |
| ); |
| }), |
| ), |
| ); |
| await tester.pump(); |
| |
| expect(focusNode.hasFocus, isTrue); |
| }); |
| |
| testWidgets('TextField displays text with text direction', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: const Material( |
| child: TextField( |
| textDirection: TextDirection.rtl, |
| ), |
| ), |
| ), |
| ); |
| |
| RenderEditable editable = findRenderEditable(tester); |
| |
| await tester.enterText(find.byType(TextField), '0123456789101112'); |
| await tester.pumpAndSettle(); |
| Offset topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 10)).topLeft, |
| ); |
| |
| expect(topLeft.dx, equals(701)); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: const Material( |
| child: TextField( |
| textDirection: TextDirection.ltr, |
| ), |
| ), |
| ), |
| ); |
| |
| editable = findRenderEditable(tester); |
| |
| await tester.enterText(find.byType(TextField), '0123456789101112'); |
| await tester.pumpAndSettle(); |
| topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 10)).topLeft, |
| ); |
| |
| expect(topLeft.dx, equals(160.0)); |
| }); |
| |
| testWidgets('TextField semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final TextEditingController controller = TextEditingController(); |
| final Key key = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: key, |
| controller: controller, |
| maxLength: 10, |
| decoration: const InputDecoration( |
| labelText: 'label', |
| hintText: 'hint', |
| helperText: 'helper', |
| ), |
| ), |
| ), |
| ); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| label: 'label', |
| id: 1, |
| textDirection: TextDirection.ltr, |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| ], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| label: 'helper', |
| textDirection: TextDirection.ltr, |
| ), |
| TestSemantics( |
| id: 3, |
| label: '10 characters remaining', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| label: 'label', |
| id: 1, |
| textDirection: TextDirection.ltr, |
| textSelection: const TextSelection(baseOffset: 0, extentOffset: 0), |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| SemanticsAction.setSelection, |
| SemanticsAction.setText, |
| SemanticsAction.paste, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| SemanticsFlag.isFocused, |
| ], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| label: 'helper', |
| textDirection: TextDirection.ltr, |
| ), |
| TestSemantics( |
| id: 3, |
| label: '10 characters remaining', |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isLiveRegion, |
| ], |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true)); |
| |
| controller.text = 'hello'; |
| await tester.pump(); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('InputDecoration counterText can have a semanticCounterText', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final TextEditingController controller = TextEditingController(); |
| final Key key = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: key, |
| controller: controller, |
| decoration: const InputDecoration( |
| labelText: 'label', |
| hintText: 'hint', |
| helperText: 'helper', |
| counterText: '0/10', |
| semanticCounterText: '0 out of 10', |
| ), |
| ), |
| ), |
| ); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| label: 'label', |
| textDirection: TextDirection.ltr, |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| ], |
| children: <TestSemantics>[ |
| TestSemantics( |
| label: 'helper', |
| textDirection: TextDirection.ltr, |
| ), |
| TestSemantics( |
| label: '0 out of 10', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true, ignoreId: true)); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('InputDecoration errorText semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final TextEditingController controller = TextEditingController(); |
| final Key key = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: key, |
| controller: controller, |
| decoration: const InputDecoration( |
| labelText: 'label', |
| hintText: 'hint', |
| errorText: 'oh no!', |
| ), |
| ), |
| ), |
| ); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| label: 'label', |
| textDirection: TextDirection.ltr, |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| ], |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isTextField, |
| ], |
| children: <TestSemantics>[ |
| TestSemantics( |
| label: 'oh no!', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true, ignoreId: true)); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('floating label does not overlap with value at large textScaleFactors', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(text: 'Just some text'); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Scaffold( |
| body: MediaQuery( |
| data: const MediaQueryData(textScaleFactor: 4.0), |
| child: Center( |
| child: TextField( |
| decoration: const InputDecoration(labelText: 'Label', border: UnderlineInputBorder()), |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(TextField)); |
| final Rect labelRect = tester.getRect(find.text('Label')); |
| final Rect fieldRect = tester.getRect(find.text('Just some text')); |
| expect(labelRect.bottom, lessThanOrEqualTo(fieldRect.top)); |
| }); |
| |
| testWidgets('TextField scrolls into view but does not bounce (SingleChildScrollView)', (WidgetTester tester) async { |
| // This is a regression test for https://github.com/flutter/flutter/issues/20485 |
| |
| final Key textField1 = UniqueKey(); |
| final Key textField2 = UniqueKey(); |
| final ScrollController scrollController = ScrollController(); |
| |
| double? minOffset; |
| double? maxOffset; |
| |
| scrollController.addListener(() { |
| final double offset = scrollController.offset; |
| minOffset = math.min(minOffset ?? offset, offset); |
| maxOffset = math.max(maxOffset ?? offset, offset); |
| }); |
| |
| Widget buildFrame(Axis scrollDirection) { |
| return MaterialApp( |
| home: Scaffold( |
| body: SafeArea( |
| child: SingleChildScrollView( |
| physics: const BouncingScrollPhysics(), |
| controller: scrollController, |
| child: Column( |
| children: <Widget>[ |
| SizedBox( // visible when scrollOffset is 0.0 |
| height: 100.0, |
| width: 100.0, |
| child: TextField(key: textField1, scrollPadding: const EdgeInsets.all(200.0)), |
| ), |
| const SizedBox( |
| height: 600.0, // Same size as the frame. Initially |
| width: 800.0, // textField2 is not visible |
| ), |
| SizedBox( // visible when scrollOffset is 200.0 |
| height: 100.0, |
| width: 100.0, |
| child: TextField(key: textField2, scrollPadding: const EdgeInsets.all(200.0)), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame(Axis.vertical)); |
| await tester.enterText(find.byKey(textField1), '1'); |
| await tester.pumpAndSettle(); |
| await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view |
| await tester.pumpAndSettle(); |
| await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view |
| await tester.pumpAndSettle(); |
| |
| expect(minOffset, 0.0); |
| expect(maxOffset, 200.0); |
| |
| minOffset = null; |
| maxOffset = null; |
| |
| await tester.pumpWidget(buildFrame(Axis.horizontal)); |
| await tester.enterText(find.byKey(textField1), '1'); |
| await tester.pumpAndSettle(); |
| await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view |
| await tester.pumpAndSettle(); |
| await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view |
| await tester.pumpAndSettle(); |
| |
| expect(minOffset, 0.0); |
| expect(maxOffset, 200.0); |
| }); |
| |
| testWidgets('TextField scrolls into view but does not bounce (ListView)', (WidgetTester tester) async { |
| // This is a regression test for https://github.com/flutter/flutter/issues/20485 |
| |
| final Key textField1 = UniqueKey(); |
| final Key textField2 = UniqueKey(); |
| final ScrollController scrollController = ScrollController(); |
| |
| double? minOffset; |
| double? maxOffset; |
| |
| scrollController.addListener(() { |
| final double offset = scrollController.offset; |
| minOffset = math.min(minOffset ?? offset, offset); |
| maxOffset = math.max(maxOffset ?? offset, offset); |
| }); |
| |
| Widget buildFrame(Axis scrollDirection) { |
| return MaterialApp( |
| home: Scaffold( |
| body: SafeArea( |
| child: ListView( |
| physics: const BouncingScrollPhysics(), |
| controller: scrollController, |
| children: <Widget>[ |
| SizedBox( // visible when scrollOffset is 0.0 |
| height: 100.0, |
| width: 100.0, |
| child: TextField(key: textField1, scrollPadding: const EdgeInsets.all(200.0)), |
| ), |
| const SizedBox( |
| height: 450.0, // 50.0 smaller than the overall frame so that both |
| width: 650.0, // textfields are always partially visible. |
| ), |
| SizedBox( // visible when scrollOffset = 50.0 |
| height: 100.0, |
| width: 100.0, |
| child: TextField(key: textField2, scrollPadding: const EdgeInsets.all(200.0)), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame(Axis.vertical)); |
| await tester.enterText(find.byKey(textField1), '1'); // textfield1 is visible |
| await tester.pumpAndSettle(); |
| await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view |
| await tester.pumpAndSettle(); |
| await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view |
| await tester.pumpAndSettle(); |
| |
| expect(minOffset, 0.0); |
| expect(maxOffset, 50.0); |
| |
| minOffset = null; |
| maxOffset = null; |
| |
| await tester.pumpWidget(buildFrame(Axis.horizontal)); |
| await tester.enterText(find.byKey(textField1), '1'); // textfield1 is visible |
| await tester.pumpAndSettle(); |
| await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view |
| await tester.pumpAndSettle(); |
| await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view |
| await tester.pumpAndSettle(); |
| |
| expect(minOffset, 0.0); |
| expect(maxOffset, 50.0); |
| }); |
| |
| testWidgets('onTap is called upon tap', (WidgetTester tester) async { |
| int tapCount = 0; |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| onTap: () { |
| tapCount += 1; |
| }, |
| ), |
| ), |
| ); |
| |
| expect(tapCount, 0); |
| await tester.tap(find.byType(TextField)); |
| // Wait a bit so they're all single taps and not double taps. |
| await tester.pump(const Duration(milliseconds: 300)); |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(const Duration(milliseconds: 300)); |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(const Duration(milliseconds: 300)); |
| expect(tapCount, 3); |
| }); |
| |
| testWidgets('onTap is not called, field is disabled', (WidgetTester tester) async { |
| int tapCount = 0; |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| enabled: false, |
| onTap: () { |
| tapCount += 1; |
| }, |
| ), |
| ), |
| ); |
| |
| expect(tapCount, 0); |
| await tester.tap(find.byType(TextField)); |
| await tester.tap(find.byType(TextField)); |
| await tester.tap(find.byType(TextField)); |
| expect(tapCount, 0); |
| }); |
| |
| testWidgets('Includes cursor for TextField', (WidgetTester tester) async { |
| // This is a regression test for https://github.com/flutter/flutter/issues/24612 |
| |
| Widget buildFrame({ |
| double? stepWidth, |
| required double cursorWidth, |
| required TextAlign textAlign, |
| }) { |
| return MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Scaffold( |
| body: Center( |
| child: Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: <Widget>[ |
| IntrinsicWidth( |
| stepWidth: stepWidth, |
| child: TextField( |
| textAlign: textAlign, |
| cursorWidth: cursorWidth, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| // A cursor of default size doesn't cause the TextField to increase its |
| // width. |
| const String text = '1234'; |
| double? stepWidth = 80.0; |
| await tester.pumpWidget(buildFrame( |
| stepWidth: 80.0, |
| cursorWidth: 2.0, |
| textAlign: TextAlign.left, |
| )); |
| await tester.enterText(find.byType(TextField), text); |
| await tester.pumpAndSettle(); |
| expect(tester.getSize(find.byType(TextField)).width, stepWidth); |
| |
| // A wide cursor is counted in the width of the text and causes the |
| // TextField to increase to twice the stepWidth. |
| await tester.pumpWidget(buildFrame( |
| stepWidth: stepWidth, |
| cursorWidth: 18.0, |
| textAlign: TextAlign.left, |
| )); |
| await tester.enterText(find.byType(TextField), text); |
| await tester.pumpAndSettle(); |
| expect(tester.getSize(find.byType(TextField)).width, 2 * stepWidth); |
| |
| // A null stepWidth causes the TextField to perfectly wrap the text plus |
| // the cursor regardless of alignment. |
| stepWidth = null; |
| const double WIDTH_OF_CHAR = 16.0; |
| const double CARET_GAP = 1.0; |
| await tester.pumpWidget(buildFrame( |
| stepWidth: stepWidth, |
| cursorWidth: 18.0, |
| textAlign: TextAlign.left, |
| )); |
| await tester.enterText(find.byType(TextField), text); |
| await tester.pumpAndSettle(); |
| expect(tester.getSize(find.byType(TextField)).width, WIDTH_OF_CHAR * text.length + 18.0 + CARET_GAP); |
| await tester.pumpWidget(buildFrame( |
| stepWidth: stepWidth, |
| cursorWidth: 18.0, |
| textAlign: TextAlign.right, |
| )); |
| await tester.enterText(find.byType(TextField), text); |
| await tester.pumpAndSettle(); |
| expect(tester.getSize(find.byType(TextField)).width, WIDTH_OF_CHAR * text.length + 18.0 + CARET_GAP); |
| }); |
| |
| testWidgets('TextField style is merged with theme', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/23994 |
| |
| final ThemeData themeData = ThemeData( |
| useMaterial3: false, |
| textTheme: TextTheme( |
| titleMedium: TextStyle( |
| color: Colors.blue[500], |
| ), |
| ), |
| ); |
| |
| Widget buildFrame(TextStyle style) { |
| return MaterialApp( |
| theme: themeData, |
| home: Material( |
| child: Center( |
| child: TextField( |
| style: style, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| // Empty TextStyle is overridden by theme |
| await tester.pumpWidget(buildFrame(const TextStyle())); |
| EditableText editableText = tester.widget(find.byType(EditableText)); |
| expect(editableText.style.color, themeData.textTheme.titleMedium!.color); |
| expect(editableText.style.background, themeData.textTheme.titleMedium!.background); |
| expect(editableText.style.shadows, themeData.textTheme.titleMedium!.shadows); |
| expect(editableText.style.decoration, themeData.textTheme.titleMedium!.decoration); |
| expect(editableText.style.locale, themeData.textTheme.titleMedium!.locale); |
| expect(editableText.style.wordSpacing, themeData.textTheme.titleMedium!.wordSpacing); |
| |
| // Properties set on TextStyle override theme |
| const Color setColor = Colors.red; |
| await tester.pumpWidget(buildFrame(const TextStyle(color: setColor))); |
| editableText = tester.widget(find.byType(EditableText)); |
| expect(editableText.style.color, setColor); |
| |
| // inherit: false causes nothing to be merged in from theme |
| await tester.pumpWidget(buildFrame(const TextStyle( |
| fontSize: 24.0, |
| textBaseline: TextBaseline.alphabetic, |
| inherit: false, |
| ))); |
| editableText = tester.widget(find.byType(EditableText)); |
| expect(editableText.style.color, isNull); |
| }); |
| |
| testWidgets('TextField style is merged with theme in Material 3', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/23994 |
| |
| final ThemeData themeData = ThemeData( |
| useMaterial3: true, |
| textTheme: TextTheme( |
| bodyLarge: TextStyle( |
| color: Colors.blue[500], |
| ), |
| ), |
| ); |
| |
| Widget buildFrame(TextStyle style) { |
| return MaterialApp( |
| theme: themeData, |
| home: Material( |
| child: Center( |
| child: TextField( |
| style: style, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| // Empty TextStyle is overridden by theme |
| await tester.pumpWidget(buildFrame(const TextStyle())); |
| EditableText editableText = tester.widget(find.byType(EditableText)); |
| |
| // According to material 3 spec, the input text should be the color of onSurface. |
| // https://github.com/flutter/flutter/issues/107686 is tracking this issue. |
| expect(editableText.style.color, themeData.textTheme.bodyLarge!.color); |
| |
| expect(editableText.style.background, themeData.textTheme.bodyLarge!.background); |
| expect(editableText.style.shadows, themeData.textTheme.bodyLarge!.shadows); |
| expect(editableText.style.decoration, themeData.textTheme.bodyLarge!.decoration); |
| expect(editableText.style.locale, themeData.textTheme.bodyLarge!.locale); |
| expect(editableText.style.wordSpacing, themeData.textTheme.bodyLarge!.wordSpacing); |
| |
| // Properties set on TextStyle override theme |
| const Color setColor = Colors.red; |
| await tester.pumpWidget(buildFrame(const TextStyle(color: setColor))); |
| editableText = tester.widget(find.byType(EditableText)); |
| expect(editableText.style.color, setColor); |
| |
| // inherit: false causes nothing to be merged in from theme |
| await tester.pumpWidget(buildFrame(const TextStyle( |
| fontSize: 24.0, |
| textBaseline: TextBaseline.alphabetic, |
| inherit: false, |
| ))); |
| editableText = tester.widget(find.byType(EditableText)); |
| expect(editableText.style.color, isNull); |
| }); |
| |
| testWidgets('style enforces required fields', (WidgetTester tester) async { |
| Widget buildFrame(TextStyle style) { |
| return MaterialApp( |
| home: Material( |
| child: TextField( |
| style: style, |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame(const TextStyle( |
| inherit: false, |
| fontSize: 12.0, |
| textBaseline: TextBaseline.alphabetic, |
| ))); |
| expect(tester.takeException(), isNull); |
| |
| // With inherit not set to false, will pickup required fields from theme |
| await tester.pumpWidget(buildFrame(const TextStyle( |
| fontSize: 12.0, |
| ))); |
| expect(tester.takeException(), isNull); |
| |
| await tester.pumpWidget(buildFrame(const TextStyle( |
| inherit: false, |
| fontSize: 12.0, |
| ))); |
| expect(tester.takeException(), isNotNull); |
| }); |
| |
| testWidgets( |
| 'tap moves cursor to the edge of the word it tapped', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pump(); |
| |
| // We moved the cursor. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), |
| ); |
| |
| // But don't trigger the toolbar. |
| expect(find.byType(CupertinoButton), findsNothing); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets( |
| 'tap with a mouse does not move cursor to the edge of the word', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| final TestGesture gesture = await tester.startGesture( |
| textfieldStart + const Offset(50.0, 9.0), |
| pointer: 1, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await gesture.up(); |
| |
| // Cursor at tap position, not at word edge. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 3), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets('tap moves cursor to the position tapped', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pump(); |
| |
| // We moved the cursor. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 3), |
| ); |
| |
| // But don't trigger the toolbar. |
| expect(find.byType(TextButton), findsNothing); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows })); |
| |
| testWidgets( |
| 'two slow taps do not trigger a word selection', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. |
| // On macOS, we select the precise position of the tap. |
| final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset pos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'. |
| |
| await tester.tapAt(pos); |
| await tester.pump(const Duration(milliseconds: 500)); |
| await tester.tapAt(pos); |
| await tester.pump(); |
| |
| // Plain collapsed selection. |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6); |
| |
| // Toolbar shows on iOS. |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformMobile ? findsNWidgets(2) : findsNothing); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'Tapping on a collapsed selection toggles the toolbar', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neigse Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', |
| ); |
| // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| maxLines: 2, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final double lineHeight = findRenderEditable(tester).preferredLineHeight; |
| final Offset begPos = textOffsetToPosition(tester, 0); |
| final Offset endPos = textOffsetToPosition(tester, 35) + const Offset(200.0, 0.0); // Index of 'Bonaventure|' + Offset(200.0,0), which is at the end of the first line. |
| final Offset vPos = textOffsetToPosition(tester, 29); // Index of 'Bonav|enture'. |
| final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'. |
| |
| // This tap just puts the cursor somewhere different than where the double |
| // tap will occur to test that the double tap moves the existing cursor first. |
| await tester.tapAt(wPos); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| await tester.tapAt(vPos); |
| await tester.pump(const Duration(milliseconds: 500)); |
| // First tap moved the cursor. Here we tap the position where 'v' is located. |
| // On iOS this will select the closest word edge, in this case the cursor is placed |
| // at the end of the word 'Bonaventure|'. |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 35); |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| await tester.tapAt(vPos); |
| await tester.pumpAndSettle(const Duration(milliseconds: 500)); |
| // Second tap toggles the toolbar. Here we tap on 'v' again, and select the word edge. Since |
| // the selection has not changed we toggle the toolbar. |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 35); |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2)); |
| |
| // Tap the 'v' position again to hide the toolbar. |
| await tester.tapAt(vPos); |
| await tester.pumpAndSettle(); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 35); |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| // Long press at the end of the first line to move the cursor to the end of the first line |
| // where the word wrap is. Since there is a word wrap here, and the direction of the text is LTR, |
| // the TextAffinity will be upstream and against the natural direction. The toolbar is also |
| // shown after a long press. |
| await tester.longPressAt(endPos); |
| await tester.pumpAndSettle(const Duration(milliseconds: 500)); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 46); |
| expect(controller.selection.affinity, TextAffinity.upstream); |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2)); |
| |
| // Tap at the same position to toggle the toolbar. |
| await tester.tapAt(endPos); |
| await tester.pumpAndSettle(); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 46); |
| expect(controller.selection.affinity, TextAffinity.upstream); |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| // Tap at the beginning of the second line to move the cursor to the front of the first word on the |
| // second line, where the word wrap is. Since there is a word wrap here, and the direction of the text is LTR, |
| // the TextAffinity will be downstream and following the natural direction. The toolbar will be hidden after this tap. |
| await tester.tapAt(begPos + Offset(0.0, lineHeight)); |
| await tester.pumpAndSettle(); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 46); |
| expect(controller.selection.affinity, TextAffinity.downstream); |
| expect(find.byType(CupertinoButton), findsNothing); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets( |
| 'Tapping on a non-collapsed selection toggles the toolbar and retains the selection', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset vPos = textOffsetToPosition(tester, 29); // Index of 'Bonav|enture'. |
| final Offset ePos = textOffsetToPosition(tester, 35) + const Offset(7.0, 0.0); // Index of 'Bonaventure|' + Offset(7.0,0), which taps slightly to the right of the end of the text. |
| final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'. |
| |
| // This tap just puts the cursor somewhere different than where the double |
| // tap will occur to test that the double tap moves the existing cursor first. |
| await tester.tapAt(wPos); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| await tester.tapAt(vPos); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap moved the cursor. |
| expect(controller.selection.isCollapsed, true); |
| expect( |
| controller.selection.baseOffset, |
| 35, |
| ); |
| await tester.tapAt(vPos); |
| await tester.pumpAndSettle(const Duration(milliseconds: 500)); |
| |
| // Second tap selects the word around the cursor. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 24, extentOffset: 35), |
| ); |
| |
| // Selected text shows 3 toolbar buttons. |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| |
| // Tap the selected word to hide the toolbar and retain the selection. |
| await tester.tapAt(vPos); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 24, extentOffset: 35), |
| ); |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| // Tap the selected word to show the toolbar and retain the selection. |
| await tester.tapAt(vPos); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 24, extentOffset: 35), |
| ); |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| |
| // Tap past the selected word to move the cursor and hide the toolbar. |
| await tester.tapAt(ePos); |
| await tester.pumpAndSettle(); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 35); |
| expect(find.byType(CupertinoButton), findsNothing); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets( |
| 'double tap selects word and first tap of double tap moves cursor (iOS)', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'. |
| final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'. |
| |
| // This tap just puts the cursor somewhere different than where the double |
| // tap will occur to test that the double tap moves the existing cursor first. |
| await tester.tapAt(wPos); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| await tester.tapAt(pPos); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap moved the cursor. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream), |
| ); |
| await tester.tapAt(pPos); |
| await tester.pumpAndSettle(); |
| |
| // Second tap selects the word around the cursor. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 8, extentOffset: 12), |
| ); |
| |
| // Selected text shows 3 toolbar buttons. |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets('iOS selectWordEdge works correctly', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'blah1 blah2', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| // Initially, the menu is not shown and there is no selection. |
| expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1)); |
| final Offset pos1 = textOffsetToPosition(tester, 1); |
| TestGesture gesture = await tester.startGesture(pos1); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(controller.selection, const TextSelection.collapsed(offset: 5, affinity: TextAffinity.upstream)); |
| |
| final Offset pos0 = textOffsetToPosition(tester, 0); |
| gesture = await tester.startGesture(pos0); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(controller.selection, const TextSelection.collapsed(offset: 0)); |
| }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); |
| |
| testWidgets( |
| 'double tap does not select word on read-only obscured field', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| obscureText: true, |
| readOnly: true, |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| // This tap just puts the cursor somewhere different than where the double |
| // tap will occur to test that the double tap moves the existing cursor first. |
| await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap moved the cursor. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 35), |
| ); |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pumpAndSettle(); |
| |
| // Second tap doesn't select anything. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 35), |
| ); |
| |
| // Selected text shows nothing. |
| expect(find.byType(CupertinoButton), findsNothing); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'double tap selects word and first tap of double tap moves cursor and shows toolbar', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| // This tap just puts the cursor somewhere different than where the double |
| // tap will occur to test that the double tap moves the existing cursor first. |
| await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap moved the cursor. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 9), |
| ); |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pumpAndSettle(); |
| |
| // Second tap selects the word around the cursor. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 8, extentOffset: 12), |
| ); |
| |
| // Selected text shows 4 toolbar buttons: cut, copy, paste, select all |
| expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4)); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }), |
| ); |
| |
| testWidgets( |
| 'Custom toolbar test - Android text selection controls', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pumpAndSettle(); |
| |
| // Selected text shows 4 toolbar buttons: cut, copy, paste, select all |
| expect(find.byType(TextButton), findsNWidgets(4)); |
| }, |
| variant: TargetPlatformVariant.all(), |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu., |
| ); |
| |
| testWidgets( |
| 'Custom toolbar test - Cupertino text selection controls', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| selectionControls: cupertinoTextSelectionControls, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pumpAndSettle(); |
| |
| // Selected text shows 3 toolbar buttons: cut, copy, paste |
| expect(find.byType(CupertinoButton), findsNWidgets(3)); |
| }, |
| variant: TargetPlatformVariant.all(), |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu., |
| ); |
| |
| testWidgets('selectionControls is passed to EditableText', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Scaffold( |
| body: TextField( |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final EditableText widget = tester.widget(find.byType(EditableText)); |
| expect(widget.selectionControls, equals(materialTextSelectionControls)); |
| }); |
| |
| testWidgets( |
| 'Can double click + drag with a mouse to select word by word', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| final Offset hPos = textOffsetToPosition(tester, testValue.indexOf('h')); |
| |
| // Tap on text field to gain focus, and set selection to '|e'. |
| final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('e')); |
| |
| // Here we tap on '|e' again, to register a double tap. This will select |
| // the word at the tapped position. |
| await gesture.down(ePos); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, 7); |
| |
| // Drag, right after the double tap, to select word by word. |
| // Moving to the position of 'h', will extend the selection to 'ghi'. |
| await gesture.moveTo(hPos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, testValue.indexOf('d')); |
| expect(controller.selection.extentOffset, testValue.indexOf('i') + 1); |
| }, |
| ); |
| |
| testWidgets( |
| 'Can double tap + drag to select word by word', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| final Offset hPos = textOffsetToPosition(tester, testValue.indexOf('h')); |
| |
| // Tap on text field to gain focus, and set selection to '|e'. |
| final TestGesture gesture = await tester.startGesture(ePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, testValue.indexOf('e')); |
| |
| // Here we tap on '|e' again, to register a double tap. This will select |
| // the word at the tapped position. |
| await gesture.down(ePos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, 7); |
| |
| // Drag, right after the double tap, to select word by word. |
| // Moving to the position of 'h', will extend the selection to 'ghi'. |
| await gesture.moveTo(hPos); |
| await tester.pumpAndSettle(); |
| |
| // Toolbar should be hidden during a drag. |
| expect(find.byType(TextButton), findsNothing); |
| expect(controller.selection.baseOffset, testValue.indexOf('d')); |
| expect(controller.selection.extentOffset, testValue.indexOf('i') + 1); |
| |
| // Toolbar should re-appear after a drag. |
| await gesture.up(); |
| await tester.pump(); |
| expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4)); |
| }, |
| ); |
| |
| group('Triple tap/click', () { |
| const String testValueA = 'Now is the time for\n' // 20 |
| 'all good people\n' // 20 + 16 => 36 |
| 'to come to the aid\n' // 36 + 19 => 55 |
| 'of their country.'; // 55 + 17 => 72 |
| const String testValueB = 'Today is the time for\n' // 22 |
| 'all good people\n' // 22 + 16 => 38 |
| 'to come to the aid\n' // 38 + 19 => 57 |
| 'of their country.'; // 57 + 17 => 74 |
| testWidgets( |
| 'Can triple tap to select a paragraph on mobile platforms when tapping at a word edge', |
| (WidgetTester tester) async { |
| // TODO(Renzo-Olivares): Enable for iOS, currently broken because selection overlay blocks the TextSelectionGestureDetector https://github.com/flutter/flutter/issues/123415. |
| final TextEditingController controller = TextEditingController(); |
| final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.enterText(find.byType(TextField), testValueA); |
| await skipPastScrollingAnimation(tester); |
| expect(controller.value.text, testValueA); |
| |
| final Offset firstLinePos = textOffsetToPosition(tester, 6); |
| |
| // Tap on text field to gain focus, and set selection to 'is|' on the first line. |
| final TestGesture gesture = await tester.startGesture(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 6); |
| |
| // Here we tap on same position again, to register a double tap. This will select |
| // the word at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 6); |
| expect(controller.selection.extentOffset, isTargetPlatformApple ? 6 : 7); |
| |
| // Here we tap on same position again, to register a triple tap. This will select |
| // the paragraph at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 20); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }), |
| ); |
| |
| testWidgets( |
| 'Can triple tap to select a paragraph on mobile platforms', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.enterText(find.byType(TextField), testValueB); |
| await skipPastScrollingAnimation(tester); |
| expect(controller.value.text, testValueB); |
| |
| final Offset firstLinePos = tester.getTopLeft(find.byType(TextField)) + const Offset(50.0, 9.0); |
| |
| // Tap on text field to gain focus, and move the selection. |
| final TestGesture gesture = await tester.startGesture(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3); |
| |
| // Here we tap on same position again, to register a double tap. This will select |
| // the word at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 5); |
| |
| // Here we tap on same position again, to register a triple tap. This will select |
| // the paragraph at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 22); |
| }, |
| variant: TargetPlatformVariant.mobile(), |
| ); |
| |
| testWidgets( |
| 'triple tap chains work on Non-Apple mobile platforms', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 3); |
| await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7), |
| ); |
| expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4)); |
| |
| await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 35), |
| ); |
| // Triple tap selecting the same paragraph somewhere else is fine. |
| await tester.tapAt(textfieldStart + const Offset(100.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap hides the toolbar and moves the selection. |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 6); |
| expect(find.byType(TextButton), findsNothing); |
| // Second tap shows the toolbar and selects the word. |
| await tester.tapAt(textfieldStart + const Offset(100.0, 9.0)); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7), |
| ); |
| expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4)); |
| |
| // Third tap shows the toolbar and selects the paragraph. |
| await tester.tapAt(textfieldStart + const Offset(100.0, 9.0)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 35), |
| ); |
| expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap moved the cursor and hid the toolbar. |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 9); |
| expect(find.byType(TextButton), findsNothing); |
| // Second tap selects the word. |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 8, extentOffset: 12), |
| ); |
| expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4)); |
| |
| // Third tap selects the paragraph and shows the toolbar. |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 35), |
| ); |
| expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }), |
| ); |
| |
| testWidgets( |
| 'triple tap chains work on Apple platforms', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure\nThe fox jumped over the fence.', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 7); |
| await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7), |
| ); |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| |
| await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 36), |
| ); |
| // Triple tap selecting the same paragraph somewhere else is fine. |
| await tester.tapAt(textfieldStart + const Offset(100.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap hides the toolbar and retains the selection. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 36), |
| ); |
| expect(find.byType(CupertinoButton), findsNothing); |
| // Second tap shows the toolbar and selects the word. |
| await tester.tapAt(textfieldStart + const Offset(100.0, 9.0)); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7), |
| ); |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| |
| // Third tap shows the toolbar and selects the paragraph. |
| await tester.tapAt(textfieldStart + const Offset(100.0, 9.0)); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 36), |
| ); |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| |
| await tester.tapAt(textfieldStart + const Offset(150.0, 50.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap moved the cursor and hid the toolbar. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 50, affinity: TextAffinity.upstream), |
| ); |
| expect(find.byType(CupertinoButton), findsNothing); |
| // Second tap selects the word. |
| await tester.tapAt(textfieldStart + const Offset(150.0, 50.0)); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 44, extentOffset: 50), |
| ); |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| |
| // Third tap selects the paragraph and shows the toolbar. |
| await tester.tapAt(textfieldStart + const Offset(150.0, 50.0)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 36, extentOffset: 66), |
| ); |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets( |
| 'triple click chains work', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: testValueA, |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textFieldStart = tester.getTopLeft(find.byType(TextField)); |
| final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux; |
| |
| // First click moves the cursor to the point of the click, not the edge of |
| // the clicked word. |
| final TestGesture gesture = await tester.startGesture( |
| textFieldStart + const Offset(210.0, 9.0), |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 13); |
| |
| // Second click selects the word. |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 11, extentOffset: 15), |
| ); |
| |
| // Triple click selects the paragraph. |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| // Wait for the consecutive tap timer to timeout so the next |
| // tap is not detected as a triple tap. |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| expect( |
| controller.selection, |
| TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20), |
| ); |
| |
| // Triple click selecting the same paragraph somewhere else is fine. |
| await gesture.down(textFieldStart + const Offset(100.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First click moved the cursor. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 6), |
| ); |
| await gesture.down(textFieldStart + const Offset(100.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| // Second click selected the word. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 6, extentOffset: 7), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(100.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| // Wait for the consecutive tap timer to timeout so the tap count |
| // is reset. |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| // Third click selected the paragraph. |
| expect( |
| controller.selection, |
| TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First click moved the cursor. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 9), |
| ); |
| await gesture.down(textFieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| // Second click selected the word. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 7, extentOffset: 10), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| // Third click selects the paragraph. |
| expect( |
| controller.selection, |
| TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20), |
| ); |
| }, |
| variant: TargetPlatformVariant.desktop(), |
| ); |
| |
| testWidgets( |
| 'triple click after a click on desktop platforms', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: testValueA, |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textFieldStart = tester.getTopLeft(find.byType(TextField)); |
| final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux; |
| |
| final TestGesture gesture = await tester.startGesture( |
| textFieldStart + const Offset(50.0, 9.0), |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 3), |
| ); |
| // First click moves the selection. |
| await gesture.down(textFieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 9), |
| ); |
| |
| // Double click selection to select a word. |
| await gesture.down(textFieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 7, extentOffset: 10), |
| ); |
| |
| // Triple click selection to select a paragraph. |
| await gesture.down(textFieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20), |
| ); |
| |
| }, |
| variant: TargetPlatformVariant.desktop(), |
| ); |
| |
| testWidgets( |
| 'Can triple tap to select all on a single-line textfield on mobile platforms', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: testValueB, |
| ); |
| final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset firstLinePos = tester.getTopLeft(find.byType(TextField)) + const Offset(50.0, 9.0); |
| |
| // Tap on text field to gain focus, and set selection somewhere on the first word. |
| final TestGesture gesture = await tester.startGesture( |
| firstLinePos, |
| pointer: 7, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3); |
| |
| // Here we tap on same position again, to register a double tap. This will select |
| // the word at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 5); |
| |
| // Here we tap on same position again, to register a triple tap. This will select |
| // the entire text field if it is a single-line field. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 74); |
| }, |
| variant: TargetPlatformVariant.mobile(), |
| ); |
| |
| testWidgets( |
| 'Can triple click to select all on a single-line textfield on desktop platforms', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: testValueA, |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset firstLinePos = textOffsetToPosition(tester, 5); |
| |
| // Tap on text field to gain focus, and set selection to 'i|s' on the first line. |
| final TestGesture gesture = await tester.startGesture( |
| firstLinePos, |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 5); |
| |
| // Here we tap on same position again, to register a double tap. This will select |
| // the word at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, 6); |
| |
| // Here we tap on same position again, to register a triple tap. This will select |
| // the entire text field if it is a single-line field. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 72); |
| }, |
| variant: TargetPlatformVariant.desktop(), |
| ); |
| |
| testWidgets( |
| 'Can triple click to select a line on Linux', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.enterText(find.byType(TextField), testValueA); |
| await skipPastScrollingAnimation(tester); |
| expect(controller.value.text, testValueA); |
| |
| final Offset firstLinePos = textOffsetToPosition(tester, 5); |
| |
| // Tap on text field to gain focus, and set selection to 'i|s' on the first line. |
| final TestGesture gesture = await tester.startGesture( |
| firstLinePos, |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 5); |
| |
| // Here we tap on same position again, to register a double tap. This will select |
| // the word at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, 6); |
| |
| // Here we tap on same position again, to register a triple tap. This will select |
| // the paragraph at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 19); |
| }, |
| variant: TargetPlatformVariant.only(TargetPlatform.linux), |
| ); |
| |
| testWidgets( |
| 'Can triple click to select a paragraph', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.enterText(find.byType(TextField), testValueA); |
| await skipPastScrollingAnimation(tester); |
| expect(controller.value.text, testValueA); |
| |
| final Offset firstLinePos = textOffsetToPosition(tester, 5); |
| |
| // Tap on text field to gain focus, and set selection to 'i|s' on the first line. |
| final TestGesture gesture = await tester.startGesture( |
| firstLinePos, |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 5); |
| |
| // Here we tap on same position again, to register a double tap. This will select |
| // the word at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, 6); |
| |
| // Here we tap on same position again, to register a triple tap. This will select |
| // the paragraph at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 20); |
| }, |
| variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }), |
| ); |
| |
| testWidgets( |
| 'Can triple click + drag to select line by line on Linux', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.enterText(find.byType(TextField), testValueA); |
| await skipPastScrollingAnimation(tester); |
| expect(controller.value.text, testValueA); |
| |
| final Offset firstLinePos = textOffsetToPosition(tester, 5); |
| |
| // Tap on text field to gain focus, and set selection to 'i|s' on the first line. |
| final TestGesture gesture = await tester.startGesture( |
| firstLinePos, |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 5); |
| |
| // Here we tap on same position again, to register a double tap. This will select |
| // the word at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, 6); |
| |
| // Here we tap on the same position again, to register a triple tap. This will select |
| // the line at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 19); |
| |
| // Drag, down after the triple tap, to select line by line. |
| // Moving down will extend the selection to the second line. |
| await gesture.moveTo(firstLinePos + const Offset(0, 10.0)); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 35); |
| |
| // Moving down will extend the selection to the third line. |
| await gesture.moveTo(firstLinePos + const Offset(0, 20.0)); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 54); |
| |
| // Moving down will extend the selection to the last line. |
| await gesture.moveTo(firstLinePos + const Offset(0, 40.0)); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 72); |
| |
| // Moving up will extend the selection to the third line. |
| await gesture.moveTo(firstLinePos + const Offset(0, 20.0)); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 54); |
| |
| // Moving up will extend the selection to the second line. |
| await gesture.moveTo(firstLinePos + const Offset(0, 10.0)); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 35); |
| |
| // Moving up will extend the selection to the first line. |
| await gesture.moveTo(firstLinePos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 19); |
| }, |
| variant: TargetPlatformVariant.only(TargetPlatform.linux), |
| ); |
| |
| testWidgets( |
| 'Can triple click + drag to select paragraph by paragraph', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.enterText(find.byType(TextField), testValueA); |
| await skipPastScrollingAnimation(tester); |
| expect(controller.value.text, testValueA); |
| |
| final Offset firstLinePos = textOffsetToPosition(tester, 5); |
| |
| // Tap on text field to gain focus, and set selection to 'i|s' on the first line. |
| final TestGesture gesture = await tester.startGesture( |
| firstLinePos, |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 5); |
| |
| // Here we tap on same position again, to register a double tap. This will select |
| // the word at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, 6); |
| |
| // Here we tap on the same position again, to register a triple tap. This will select |
| // the paragraph at the tapped position. |
| await gesture.down(firstLinePos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 20); |
| |
| // Drag, down after the triple tap, to select paragraph by paragraph. |
| // Moving down will extend the selection to the second line. |
| await gesture.moveTo(firstLinePos + const Offset(0, 10.0)); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 36); |
| |
| // Moving down will extend the selection to the third line. |
| await gesture.moveTo(firstLinePos + const Offset(0, 20.0)); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 55); |
| |
| // Moving down will extend the selection to the last line. |
| await gesture.moveTo(firstLinePos + const Offset(0, 40.0)); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 72); |
| |
| // Moving up will extend the selection to the third line. |
| await gesture.moveTo(firstLinePos + const Offset(0, 20.0)); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 55); |
| |
| // Moving up will extend the selection to the second line. |
| await gesture.moveTo(firstLinePos + const Offset(0, 10.0)); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 36); |
| |
| // Moving up will extend the selection to the first line. |
| await gesture.moveTo(firstLinePos); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, 0); |
| expect(controller.selection.extentOffset, 20); |
| }, |
| variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }), |
| ); |
| |
| testWidgets( |
| 'Going past triple click retains the selection on Apple platforms', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: testValueA, |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textFieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| // First click moves the cursor to the point of the click, not the edge of |
| // the clicked word. |
| final TestGesture gesture = await tester.startGesture( |
| textFieldStart + const Offset(210.0, 9.0), |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 13); |
| |
| // Second click selects the word. |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 11, extentOffset: 15), |
| ); |
| |
| // Triple click selects the paragraph. |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 20), |
| ); |
| |
| // Clicking again retains the selection. |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 20), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| // Clicking again retains the selection. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 20), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| // Clicking again retains the selection. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 20), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'Tap count resets when going past a triple tap on Android, Fuchsia, and Linux', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: testValueA, |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textFieldStart = tester.getTopLeft(find.byType(TextField)); |
| final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux; |
| |
| // First click moves the cursor to the point of the click, not the edge of |
| // the clicked word. |
| final TestGesture gesture = await tester.startGesture( |
| textFieldStart + const Offset(210.0, 9.0), |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 13); |
| |
| // Second click selects the word. |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 11, extentOffset: 15), |
| ); |
| |
| // Triple click selects the paragraph. |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20), |
| ); |
| |
| // Clicking again moves the caret to the tapped positio. |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 13); |
| |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| // Clicking again selects the word. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 11, extentOffset: 15), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| // Clicking again selects the paragraph. |
| expect( |
| controller.selection, |
| TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // Clicking again moves the caret to the tapped position. |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 13); |
| |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| // Clicking again selects the word. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 11, extentOffset: 15), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| // Clicking again selects the paragraph. |
| expect( |
| controller.selection, |
| TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux }), |
| ); |
| |
| testWidgets( |
| 'Double click and triple click alternate on Windows', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: testValueA, |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| maxLines: null, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textFieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| // First click moves the cursor to the point of the click, not the edge of |
| // the clicked word. |
| final TestGesture gesture = await tester.startGesture( |
| textFieldStart + const Offset(210.0, 9.0), |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, 13); |
| |
| // Second click selects the word. |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 11, extentOffset: 15), |
| ); |
| |
| // Triple click selects the paragraph. |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 20), |
| ); |
| |
| // Clicking again selects the word. |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 11, extentOffset: 15), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| // Clicking again selects the paragraph. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 20), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| // Clicking again selects the word. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 11, extentOffset: 15), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // Clicking again selects the paragraph. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 20), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| // Clicking again selects the word. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 11, extentOffset: 15), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(210.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| // Clicking again selects the paragraph. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 20), |
| ); |
| }, |
| variant: TargetPlatformVariant.only(TargetPlatform.windows), |
| ); |
| }); |
| |
| testWidgets( |
| 'double tap on top of cursor also selects word', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Tap to put the cursor after the "w". |
| const int index = 3; |
| await tester.tapAt(textOffsetToPosition(tester, index)); |
| await tester.pump(const Duration(milliseconds: 500)); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: index), |
| ); |
| |
| // Double tap on the same location. |
| await tester.tapAt(textOffsetToPosition(tester, index)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| // First tap doesn't change the selection |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: index), |
| ); |
| |
| // Second tap selects the word around the cursor. |
| await tester.tapAt(textOffsetToPosition(tester, index)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7), |
| ); |
| |
| // Selected text shows 4 toolbar buttons: cut, copy, paste, select all |
| expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4)); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }), |
| ); |
| |
| testWidgets( |
| 'double double tap just shows the selection menu', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: '', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Double tap on the same location shows the selection menu. |
| await tester.tapAt(textOffsetToPosition(tester, 0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.tapAt(textOffsetToPosition(tester, 0)); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), findsOneWidget); |
| |
| // Double tap again keeps the selection menu visible. |
| await tester.tapAt(textOffsetToPosition(tester, 0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.tapAt(textOffsetToPosition(tester, 0)); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), findsOneWidget); |
| }, |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu., |
| ); |
| |
| testWidgets( |
| 'double long press just shows the selection menu', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: '', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Long press shows the selection menu. |
| await tester.longPressAt(textOffsetToPosition(tester, 0)); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), findsOneWidget); |
| |
| // Long press again keeps the selection menu visible. |
| await tester.longPressAt(textOffsetToPosition(tester, 0)); |
| await tester.pump(); |
| expect(find.text('Paste'), findsOneWidget); |
| }, |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu., |
| ); |
| |
| testWidgets( |
| 'A single tap hides the selection menu', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: '', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Long press shows the selection menu. |
| await tester.longPress(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), findsOneWidget); |
| |
| // Tap hides the selection menu. |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| expect(find.text('Paste'), findsNothing); |
| }, |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu., |
| ); |
| |
| testWidgets('Drag selection hides the selection menu', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'blah1 blah2', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| // Initially, the menu is not shown and there is no selection. |
| expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1)); |
| final Offset midBlah1 = textOffsetToPosition(tester, 2); |
| final Offset midBlah2 = textOffsetToPosition(tester, 8); |
| |
| // Right click the second word. |
| final TestGesture gesture = await tester.startGesture( |
| midBlah2, |
| kind: PointerDeviceKind.mouse, |
| buttons: kSecondaryMouseButton, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The toolbar is shown. |
| expect(find.text('Paste'), findsOneWidget); |
| |
| // Drag the mouse to the first word. |
| final TestGesture gesture2 = await tester.startGesture( |
| midBlah1, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture2.moveTo(midBlah2); |
| await tester.pump(); |
| await gesture2.up(); |
| await tester.pumpAndSettle(); |
| |
| // The toolbar is hidden. |
| expect(find.text('Paste'), findsNothing); |
| }, |
| variant: TargetPlatformVariant.desktop(), |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. |
| ); |
| |
| testWidgets( |
| 'Long press on an autofocused field shows the selection menu', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: '', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| autofocus: true, |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| // This extra pump allows the selection set by autofocus to propagate to |
| // the RenderEditable. |
| await tester.pump(); |
| |
| // Long press shows the selection menu. |
| expect(find.text('Paste'), findsNothing); |
| await tester.longPress(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), findsOneWidget); |
| }, |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu., |
| ); |
| |
| testWidgets( |
| 'double tap hold selects word', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| final TestGesture gesture = |
| await tester.startGesture(textfieldStart + const Offset(150.0, 9.0)); |
| // Hold the press. |
| await tester.pumpAndSettle(); |
| |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 8, extentOffset: 12), |
| ); |
| |
| // Selected text shows 3 toolbar buttons. |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| |
| await gesture.up(); |
| await tester.pump(); |
| |
| // Still selected. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 8, extentOffset: 12), |
| ); |
| // The toolbar is still showing. |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'tap after a double tap select is not affected', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. |
| // On macOS, we select the precise position of the tap. |
| final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'. |
| final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r' |
| |
| await tester.tapAt(pPos); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap moved the cursor. |
| expect( |
| controller.selection, |
| isTargetPlatformMobile ? const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream) : const TextSelection.collapsed(offset: 9), |
| ); |
| await tester.tapAt(pPos); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| await tester.tapAt(ePos); |
| await tester.pump(); |
| |
| // Plain collapsed selection at the edge of first word on iOS. In iOS 12, |
| // the first tap after a double tap ends up putting the cursor at where |
| // you tapped instead of the edge like every other single tap. This is |
| // likely a bug in iOS 12 and not present in other versions. |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6); |
| |
| // No toolbar. |
| expect(find.byType(CupertinoButton), findsNothing); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'long press moves cursor to the exact long press position and shows toolbar when the field is focused', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| autofocus: true, |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // This extra pump allows the selection set by autofocus to propagate to |
| // the RenderEditable. |
| await tester.pump(); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pumpAndSettle(); |
| |
| // Collapsed cursor for iOS long press. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 3), |
| ); |
| |
| // Collapsed toolbar shows 2 buttons. |
| final int buttons = defaultTargetPlatform == TargetPlatform.iOS ? 2 : 1; |
| expect( |
| find.byType(CupertinoButton), |
| isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(buttons), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'long press that starts on an unfocused TextField selects the word at the exact long press position and shows toolbar', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pumpAndSettle(); |
| |
| // Collapsed cursor for iOS long press. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7), |
| ); |
| |
| // Collapsed toolbar shows 3 buttons. |
| expect( |
| find.byType(CupertinoButton), |
| isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'long press selects word and shows toolbar', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pumpAndSettle(); |
| |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7), |
| ); |
| |
| // Collapsed toolbar shows 4 buttons: cut, copy, paste, select all |
| expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4)); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }), |
| ); |
| |
| testWidgets( |
| 'long press tap cannot initiate a double tap', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| autofocus: true, |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // This extra pump is so autofocus can propogate to renderEditable. |
| await tester.pump(); |
| |
| final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r' |
| |
| await tester.longPressAt(ePos); |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| // Tap slightly behind the previous tap to avoid tapping the context menu |
| // on desktop. |
| final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; |
| final Offset secondTapPos = isTargetPlatformMobile |
| ? ePos |
| : ePos + const Offset(-1.0, 0.0); |
| await tester.tapAt(secondTapPos); |
| await tester.pump(); |
| |
| // The cursor does not move and the toolbar is toggled. |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, 6); |
| |
| // The toolbar from the long press is now dismissed by the second tap. |
| expect(find.byType(CupertinoButton), findsNothing); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'long press drag extends the selection to the word under the drag and shows toolbar on lift on non-Apple platforms', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = |
| await tester.startGesture(textOffsetToPosition(tester, 18)); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| // Long press selects the word at the long presses position. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 13, extentOffset: 23), |
| ); |
| // Cursor move doesn't trigger a toolbar initially. |
| expect(find.byType(TextButton), findsNothing); |
| |
| await gesture.moveBy(const Offset(100, 0)); |
| await tester.pump(); |
| |
| // The selection is now moved with the drag. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 13, extentOffset: 35), |
| ); |
| // Still no toolbar. |
| expect(find.byType(TextButton), findsNothing); |
| |
| // The selection is moved on a backwards drag. |
| await gesture.moveBy(const Offset(-200, 0)); |
| await tester.pump(); |
| |
| // The selection is now moved with the drag. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 23, extentOffset: 8), |
| ); |
| // Still no toolbar. |
| expect(find.byType(TextButton), findsNothing); |
| |
| await gesture.moveBy(const Offset(-100, 0)); |
| await tester.pump(); |
| |
| // The selection is now moved with the drag. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 23, extentOffset: 0), |
| ); |
| // Still no toolbar. |
| expect(find.byType(TextButton), findsNothing); |
| |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The selection isn't affected by the gesture lift. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 23, extentOffset: 0), |
| ); |
| // The toolbar now shows up. |
| expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4)); |
| }, |
| variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'long press drag on a focused TextField moves the cursor under the drag and shows toolbar on lift', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| autofocus: true, |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // This extra pump is so autofocus can propogate to renderEditable. |
| await tester.pump(); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| final TestGesture gesture = |
| await tester.startGesture(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| // Long press on iOS shows collapsed selection cursor. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 3), |
| ); |
| // Cursor move doesn't trigger a toolbar initially. |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| await gesture.moveBy(const Offset(50, 0)); |
| await tester.pump(); |
| |
| // The selection position is now moved with the drag. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 6), |
| ); |
| // Still no toolbar. |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| await gesture.moveBy(const Offset(50, 0)); |
| await tester.pump(); |
| |
| // The selection position is now moved with the drag. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 9), |
| ); |
| // Still no toolbar. |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The selection isn't affected by the gesture lift. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 9), |
| ); |
| // The toolbar now shows up. |
| final int buttons = defaultTargetPlatform == TargetPlatform.iOS ? 2 : 1; |
| expect( |
| find.byType(CupertinoButton), |
| isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(buttons), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'long press drag on an unfocused TextField selects word-by-word and shows toolbar on lift', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| final TestGesture gesture = |
| await tester.startGesture(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| // Long press on iOS shows collapsed selection cursor. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7), |
| ); |
| // Cursor move doesn't trigger a toolbar initially. |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| await gesture.moveBy(const Offset(100, 0)); |
| await tester.pump(); |
| |
| // The selection position is now moved with the drag. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 12), |
| ); |
| // Still no toolbar. |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| await gesture.moveBy(const Offset(100, 0)); |
| await tester.pump(); |
| |
| // The selection position is now moved with the drag. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 23), |
| ); |
| // Still no toolbar. |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The selection isn't affected by the gesture lift. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 23), |
| ); |
| // The toolbar now shows up. |
| expect( |
| find.byType(CupertinoButton), |
| isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets('long press drag can edge scroll on non-Apple platforms', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| |
| List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection( |
| const TextSelection.collapsed(offset: 66), // Last character's position. |
| ); |
| |
| expect(lastCharEndpoint.length, 1); |
| // Just testing the test and making sure that the last character is off |
| // the right side of the screen. |
| expect(lastCharEndpoint[0].point.dx, 1056); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| final TestGesture gesture = |
| await tester.startGesture(textfieldStart); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.upstream), |
| ); |
| expect(find.byType(TextButton), findsNothing); |
| |
| await gesture.moveBy(const Offset(900, 5)); |
| // To the edge of the screen basically. |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 59), |
| ); |
| // Keep moving out. |
| await gesture.moveBy(const Offset(1, 0)); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 66), |
| ); |
| await gesture.moveBy(const Offset(1, 0)); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream), |
| ); // We're at the edge now. |
| expect(find.byType(TextButton), findsNothing); |
| |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The selection isn't affected by the gesture lift. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream), |
| ); |
| // The toolbar now shows up. |
| expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| |
| lastCharEndpoint = renderEditable.getEndpointsForSelection( |
| const TextSelection.collapsed(offset: 66), // Last character's position. |
| ); |
| |
| expect(lastCharEndpoint.length, 1); |
| // The last character is now on screen near the right edge. |
| expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(798, epsilon: 1)); |
| |
| final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection( |
| const TextSelection.collapsed(offset: 0), // First character's position. |
| ); |
| expect(firstCharEndpoint.length, 1); |
| // The first character is now offscreen to the left. |
| expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257.0, epsilon: 1)); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows })); |
| |
| testWidgets('long press drag can edge scroll on Apple platforms - unfocused TextField', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| |
| List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection( |
| const TextSelection.collapsed(offset: 66), // Last character's position. |
| ); |
| |
| expect(lastCharEndpoint.length, 1); |
| // Just testing the test and making sure that the last character is off |
| // the right side of the screen. |
| expect(lastCharEndpoint[0].point.dx, 1056); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| final TestGesture gesture = |
| await tester.startGesture(textfieldStart); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.upstream), |
| ); |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| await gesture.moveBy(const Offset(900, 5)); |
| // To the edge of the screen basically. |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 59), |
| ); |
| // Keep moving out. |
| await gesture.moveBy(const Offset(1, 0)); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 66), |
| ); |
| await gesture.moveBy(const Offset(1, 0)); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream), |
| ); // We're at the edge now. |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The selection isn't affected by the gesture lift. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream), |
| ); |
| // The toolbar now shows up. |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| |
| lastCharEndpoint = renderEditable.getEndpointsForSelection( |
| const TextSelection.collapsed(offset: 66), // Last character's position. |
| ); |
| |
| expect(lastCharEndpoint.length, 1); |
| // The last character is now on screen near the right edge. |
| expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(798, epsilon: 1)); |
| |
| final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection( |
| const TextSelection.collapsed(offset: 0), // First character's position. |
| ); |
| expect(firstCharEndpoint.length, 1); |
| // The first character is now offscreen to the left. |
| expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257.0, epsilon: 1)); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('long press drag can edge scroll on Apple platforms - focused TextField', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: Center( |
| child: TextField( |
| autofocus: true, |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // This extra pump is so autofocus can propogate to renderEditable. |
| await tester.pump(); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| |
| List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection( |
| const TextSelection.collapsed(offset: 66), // Last character's position. |
| ); |
| |
| expect(lastCharEndpoint.length, 1); |
| // Just testing the test and making sure that the last character is off |
| // the right side of the screen. |
| expect(lastCharEndpoint[0].point.dx, 1056); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| final TestGesture gesture = |
| await tester.startGesture(textfieldStart + const Offset(300, 5)); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 19, affinity: TextAffinity.upstream), |
| ); |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| await gesture.moveBy(const Offset(600, 0)); |
| // To the edge of the screen basically. |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 56), |
| ); |
| // Keep moving out. |
| await gesture.moveBy(const Offset(1, 0)); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 62), |
| ); |
| await gesture.moveBy(const Offset(1, 0)); |
| await tester.pump(); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream), |
| ); // We're at the edge now. |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The selection isn't affected by the gesture lift. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream), |
| ); |
| // The toolbar now shows up. |
| final int buttons = defaultTargetPlatform == TargetPlatform.iOS ? 2 : 1; |
| expect( |
| find.byType(CupertinoButton), |
| isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(buttons), |
| ); |
| |
| lastCharEndpoint = renderEditable.getEndpointsForSelection( |
| const TextSelection.collapsed(offset: 66), // Last character's position. |
| ); |
| |
| expect(lastCharEndpoint.length, 1); |
| // The last character is now on screen near the right edge. |
| expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(798, epsilon: 1)); |
| |
| final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection( |
| const TextSelection.collapsed(offset: 0), // First character's position. |
| ); |
| expect(firstCharEndpoint.length, 1); |
| // The first character is now offscreen to the left. |
| expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257.0, epsilon: 1)); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('mouse click and drag can edge scroll', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| final Size screenSize = MediaQuery.of(tester.element(find.byType(TextField))).size; |
| // Just testing the test and making sure that the last character is off |
| // the right side of the screen. |
| expect(textOffsetToPosition(tester, 66).dx, greaterThan(screenSize.width)); |
| |
| final TestGesture gesture = |
| await tester.startGesture( |
| textOffsetToPosition(tester, 19), |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| |
| await gesture.moveTo(textOffsetToPosition(tester, 56)); |
| // To the edge of the screen basically. |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 19, extentOffset: 56), |
| ); |
| |
| // Keep moving out. |
| await gesture.moveTo(textOffsetToPosition(tester, 62)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 19, extentOffset: 62), |
| ); |
| await gesture.moveTo(textOffsetToPosition(tester, 66)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 19, extentOffset: 66), |
| ); // We're at the edge now. |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The selection isn't affected by the gesture lift. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 19, extentOffset: 66), |
| ); |
| |
| // The last character is now on screen near the right edge. |
| expect( |
| textOffsetToPosition(tester, 66).dx, |
| moreOrLessEquals(TestSemantics.fullScreen.width, epsilon: 2.0), |
| ); |
| |
| // The first character is now offscreen to the left. |
| expect(textOffsetToPosition(tester, 0).dx, lessThan(-100.0)); |
| }, variant: TargetPlatformVariant.all()); |
| |
| testWidgets('keyboard selection change scrolls the field', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Just testing the test and making sure that the last character is off |
| // the right side of the screen. |
| expect(textOffsetToPosition(tester, 66).dx, 1056); |
| |
| await tester.tapAt(textOffsetToPosition(tester, 13)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 13), |
| ); |
| |
| // Move to position 56 with the right arrow (near the edge of the screen). |
| for (int i = 0; i < (56 - 13); i += 1) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| } |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| // arrowRight always sets the affinity to downstream. |
| const TextSelection.collapsed(offset: 56), |
| ); |
| |
| // Keep moving out. |
| for (int i = 0; i < (62 - 56); i += 1) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| } |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 62), |
| ); |
| for (int i = 0; i < (66 - 62); i += 1) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| } |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 66), |
| ); // We're at the edge now. |
| |
| await tester.pumpAndSettle(); |
| |
| // The last character is now on screen near the right edge. |
| expect( |
| textOffsetToPosition(tester, 66).dx, |
| moreOrLessEquals(TestSemantics.fullScreen.width, epsilon: 2.0), |
| ); |
| |
| // The first character is now offscreen to the left. |
| expect(textOffsetToPosition(tester, 0).dx, moreOrLessEquals(-257.0, epsilon: 1)); |
| }, variant: TargetPlatformVariant.all(), |
| skip: isBrowser, // [intended] Browser handles arrow keys differently. |
| ); |
| |
| testWidgets('long press drag can edge scroll vertically', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neigse Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| autofocus: true, |
| maxLines: 2, |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // This extra pump is so autofocus can propogate to renderEditable. |
| await tester.pump(); |
| |
| // Just testing the test and making sure that the last character is outside |
| // the bottom of the field. |
| final int textLength = controller.text.length; |
| final double lineHeight = findRenderEditable(tester).preferredLineHeight; |
| final double firstCharY = textOffsetToPosition(tester, 0).dy; |
| expect( |
| textOffsetToPosition(tester, textLength).dy, |
| moreOrLessEquals(firstCharY + lineHeight * 2, epsilon: 1), |
| ); |
| |
| // Start long pressing on the first line. |
| final TestGesture gesture = |
| await tester.startGesture(textOffsetToPosition(tester, 19)); |
| await tester.pump(const Duration(milliseconds: 500)); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 19), |
| ); |
| await tester.pumpAndSettle(); |
| |
| // Move down to the second line. |
| await gesture.moveBy(Offset(0.0, lineHeight)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 65), |
| ); |
| |
| // Still hasn't scrolled. |
| expect( |
| textOffsetToPosition(tester, 65).dy, |
| moreOrLessEquals(firstCharY + lineHeight, epsilon: 1), |
| ); |
| |
| // Keep selecting down to the third and final line. |
| await gesture.moveBy(Offset(0.0, lineHeight)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 110), |
| ); |
| |
| // The last character is no longer three line heights down from the top of |
| // the field, it's now only two line heights down, because it has scrolled |
| // down by one line. |
| expect( |
| textOffsetToPosition(tester, 110).dy, |
| moreOrLessEquals(firstCharY + lineHeight, epsilon: 1), |
| ); |
| |
| // Likewise, the first character is now scrolled out of the top of the field |
| // by one line. |
| expect( |
| textOffsetToPosition(tester, 0).dy, |
| moreOrLessEquals(firstCharY - lineHeight, epsilon: 1), |
| ); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('keyboard selection change scrolls the field vertically', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLines: 2, |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Just testing the test and making sure that the last character is outside |
| // the bottom of the field. |
| final int textLength = controller.text.length; |
| final double lineHeight = findRenderEditable(tester).preferredLineHeight; |
| final double firstCharY = textOffsetToPosition(tester, 0).dy; |
| expect( |
| textOffsetToPosition(tester, textLength).dy, |
| moreOrLessEquals(firstCharY + lineHeight * 2, epsilon: 1), |
| ); |
| |
| await tester.tapAt(textOffsetToPosition(tester, 13)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 13), |
| ); |
| |
| // Move down to the second line. |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 59), |
| ); |
| |
| // Still hasn't scrolled. |
| expect( |
| textOffsetToPosition(tester, 66).dy, |
| moreOrLessEquals(firstCharY + lineHeight, epsilon: 1), |
| ); |
| |
| // Move down to the third and final line. |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 104), |
| ); |
| |
| // The last character is no longer three line heights down from the top of |
| // the field, it's now only two line heights down, because it has scrolled |
| // down by one line. |
| expect( |
| textOffsetToPosition(tester, textLength).dy, |
| moreOrLessEquals(firstCharY + lineHeight, epsilon: 1), |
| ); |
| |
| // Likewise, the first character is now scrolled out of the top of the field |
| // by one line. |
| expect( |
| textOffsetToPosition(tester, 0).dy, |
| moreOrLessEquals(firstCharY - lineHeight, epsilon: 1), |
| ); |
| }, variant: TargetPlatformVariant.all(), |
| skip: isBrowser, // [intended] Browser handles arrow keys differently. |
| ); |
| |
| testWidgets('mouse click and drag can edge scroll vertically', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLines: 2, |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Just testing the test and making sure that the last character is outside |
| // the bottom of the field. |
| final int textLength = controller.text.length; |
| final double lineHeight = findRenderEditable(tester).preferredLineHeight; |
| final double firstCharY = textOffsetToPosition(tester, 0).dy; |
| expect( |
| textOffsetToPosition(tester, textLength).dy, |
| moreOrLessEquals(firstCharY + lineHeight * 2, epsilon: 1), |
| ); |
| |
| // Start selecting on the first line. |
| final TestGesture gesture = |
| await tester.startGesture( |
| textOffsetToPosition(tester, 19), |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| |
| // Still hasn't scrolled. |
| expect( |
| textOffsetToPosition(tester, 60).dy, |
| moreOrLessEquals(firstCharY + lineHeight, epsilon: 1), |
| ); |
| |
| // Select down to the second line. |
| await gesture.moveBy(Offset(0.0, lineHeight)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 19, extentOffset: 65), |
| ); |
| |
| // Still hasn't scrolled. |
| expect( |
| textOffsetToPosition(tester, 60).dy, |
| moreOrLessEquals(firstCharY + lineHeight, epsilon: 1), |
| ); |
| |
| // Keep selecting down to the third and final line. |
| await gesture.moveBy(Offset(0.0, lineHeight)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 19, extentOffset: 110), |
| ); |
| |
| // The last character is no longer three line heights down from the top of |
| // the field, it's now only two line heights down, because it has scrolled |
| // down by one line. |
| expect( |
| textOffsetToPosition(tester, textLength).dy, |
| moreOrLessEquals(firstCharY + lineHeight, epsilon: 1), |
| ); |
| |
| // Likewise, the first character is now scrolled out of the top of the field |
| // by one line. |
| expect( |
| textOffsetToPosition(tester, 0).dy, |
| moreOrLessEquals(firstCharY - lineHeight, epsilon: 1), |
| ); |
| }, variant: TargetPlatformVariant.all()); |
| |
| testWidgets( |
| 'long tap after a double tap select is not affected', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. |
| // On macOS, we select the precise position of the tap. |
| final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel' |
| final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r' |
| |
| await tester.tapAt(pPos); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap moved the cursor to the beginning of the second word. |
| expect( |
| controller.selection, |
| isTargetPlatformMobile ? const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream) : const TextSelection.collapsed(offset: 9), |
| ); |
| await tester.tapAt(pPos); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| await tester.longPressAt(ePos); |
| await tester.pumpAndSettle(); |
| |
| // Plain collapsed selection at the exact tap position. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 6), |
| ); |
| |
| // Long press toolbar. |
| final int buttons = defaultTargetPlatform == TargetPlatform.iOS ? 2 : 1; |
| expect( |
| find.byType(CupertinoButton), |
| isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(buttons), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'double tap after a long tap is not affected', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. |
| // On macOS, we select the precise position of the tap. |
| final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: Center( |
| child: TextField( |
| autofocus: true, |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // This extra pump is so autofocus can propogate to renderEditable. |
| await tester.pump(); |
| |
| // The second tap is slightly higher to avoid tapping the context menu on |
| // desktop. |
| final Offset pPos = textOffsetToPosition(tester, 9) + const Offset(0.0, -20.0); // Index of 'P|eel' |
| final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater' |
| |
| await tester.longPressAt(wPos); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 3), |
| ); |
| |
| await tester.tapAt(pPos); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap moved the cursor. |
| expect( |
| controller.selection, |
| isTargetPlatformMobile ? const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream) : const TextSelection.collapsed(offset: 9), |
| ); |
| await tester.tapAt(pPos); |
| await tester.pumpAndSettle(); |
| |
| // Double tap selection. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 8, extentOffset: 12), |
| ); |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'double click after a click on desktop platforms', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textFieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| final TestGesture gesture = await tester.startGesture( |
| textFieldStart + const Offset(50.0, 9.0), |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 3), |
| ); |
| |
| await gesture.down(textFieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First click moved the cursor to the precise location, not the start of |
| // the word. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 9), |
| ); |
| |
| // Double click selection. |
| await gesture.down(textFieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 8, extentOffset: 12), |
| ); |
| // The text selection toolbar isn't shown on Mac without a right click. |
| expect(find.byType(CupertinoButton), findsNothing); |
| }, |
| variant: TargetPlatformVariant.desktop(), |
| ); |
| |
| testWidgets( |
| 'double tap chains work', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), |
| ); |
| await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7), |
| ); |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| |
| // Double tap selecting the same word somewhere else is fine. |
| await tester.tapAt(textfieldStart + const Offset(100.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap hides the toolbar and retains the selection. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7), |
| ); |
| expect(find.byType(CupertinoButton), findsNothing); |
| // Second tap shows the toolbar and retains the selection. |
| await tester.tapAt(textfieldStart + const Offset(100.0, 9.0)); |
| // Wait for the consecutive tap timer to timeout so the next |
| // tap is not detected as a triple tap. |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7), |
| ); |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap moved the cursor and hid the toolbar. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream) |
| ); |
| expect(find.byType(CupertinoButton), findsNothing); |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 8, extentOffset: 12), |
| ); |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets( |
| 'double click chains work', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textFieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| // First click moves the cursor to the point of the click, not the edge of |
| // the clicked word. |
| final TestGesture gesture = await tester.startGesture( |
| textFieldStart + const Offset(50.0, 9.0), |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 3), |
| ); |
| |
| // Second click selects. |
| await gesture.down(textFieldStart + const Offset(50.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| // Wait for the consecutive tap timer to timeout so the next |
| // tap is not detected as a triple tap. |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7), |
| ); |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| // Double tap selecting the same word somewhere else is fine. |
| await gesture.down(textFieldStart + const Offset(100.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap moved the cursor. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 6), |
| ); |
| await gesture.down(textFieldStart + const Offset(100.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| // Wait for the consecutive tap timer to timeout so the next |
| // tap is not detected as a triple tap. |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 0, extentOffset: 7), |
| ); |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| await gesture.down(textFieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| // First tap moved the cursor. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 9), |
| ); |
| await gesture.down(textFieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 8, extentOffset: 12), |
| ); |
| expect(find.byType(CupertinoButton), findsNothing); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }), |
| ); |
| |
| testWidgets('double tapping a space selects the previous word on iOS', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: ' blah blah \n blah', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLines: null, |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, -1); |
| expect(controller.value.selection.extentOffset, -1); |
| |
| // Put the cursor at the end of the field. |
| await tester.tapAt(textOffsetToPosition(tester, 19)); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, 19); |
| expect(controller.value.selection.extentOffset, 19); |
| |
| // Double tapping does the same thing. |
| await tester.pump(const Duration(milliseconds: 500)); |
| await tester.tapAt(textOffsetToPosition(tester, 5)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.tapAt(textOffsetToPosition(tester, 5)); |
| await tester.pumpAndSettle(); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.extentOffset, 5); |
| expect(controller.value.selection.baseOffset, 1); |
| |
| // Put the cursor at the end of the field. |
| await tester.tapAt(textOffsetToPosition(tester, 19)); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, 19); |
| expect(controller.value.selection.extentOffset, 19); |
| |
| // Double tapping does the same thing for the first space. |
| await tester.pump(const Duration(milliseconds: 500)); |
| await tester.tapAt(textOffsetToPosition(tester, 0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.tapAt(textOffsetToPosition(tester, 0)); |
| await tester.pumpAndSettle(); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, 0); |
| expect(controller.value.selection.extentOffset, 1); |
| |
| // Put the cursor at the end of the field. |
| await tester.tapAt(textOffsetToPosition(tester, 19)); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, 19); |
| expect(controller.value.selection.extentOffset, 19); |
| |
| // Double tapping the last space selects all previous contiguous spaces on |
| // both lines and the previous word. |
| await tester.pump(const Duration(milliseconds: 500)); |
| await tester.tapAt(textOffsetToPosition(tester, 14)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.tapAt(textOffsetToPosition(tester, 14)); |
| await tester.pumpAndSettle(); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, 6); |
| expect(controller.value.selection.extentOffset, 14); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); |
| |
| testWidgets('selecting a space selects the space on non-iOS platforms', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: ' blah blah', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, -1); |
| expect(controller.value.selection.extentOffset, -1); |
| |
| // Put the cursor at the end of the field. |
| await tester.tapAt(textOffsetToPosition(tester, 10)); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, 10); |
| expect(controller.value.selection.extentOffset, 10); |
| |
| // Double tapping the second space selects it. |
| await tester.pump(const Duration(milliseconds: 500)); |
| await tester.tapAt(textOffsetToPosition(tester, 5)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.tapAt(textOffsetToPosition(tester, 5)); |
| await tester.pumpAndSettle(); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, 5); |
| expect(controller.value.selection.extentOffset, 6); |
| |
| // Tap at the end of the text to move the selection to the end. On some |
| // platforms, the context menu "Cut" button blocks this tap, so move it out |
| // of the way by an Offset. |
| await tester.tapAt(textOffsetToPosition(tester, 10) + const Offset(200.0, 0.0)); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, 10); |
| expect(controller.value.selection.extentOffset, 10); |
| |
| // Double tapping the second space selects it. |
| await tester.pump(const Duration(milliseconds: 500)); |
| await tester.tapAt(textOffsetToPosition(tester, 0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.tapAt(textOffsetToPosition(tester, 0)); |
| await tester.pumpAndSettle(); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, 0); |
| expect(controller.value.selection.extentOffset, 1); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia, TargetPlatform.android })); |
| |
| testWidgets('selecting a space selects the space on Desktop platforms', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: ' blah blah', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, -1); |
| expect(controller.value.selection.extentOffset, -1); |
| |
| // Put the cursor at the end of the field. |
| final TestGesture gesture = await tester.startGesture( |
| textOffsetToPosition(tester, 10), |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, 10); |
| expect(controller.value.selection.extentOffset, 10); |
| |
| // Double clicking the second space selects it. |
| await tester.pump(const Duration(milliseconds: 500)); |
| await gesture.down(textOffsetToPosition(tester, 5)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await gesture.down(textOffsetToPosition(tester, 5)); |
| await tester.pump(); |
| await gesture.up(); |
| // Wait for the consecutive tap timer to timeout so our next tap is not |
| // detected as a triple tap. |
| await tester.pumpAndSettle(kDoubleTapTimeout); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, 5); |
| expect(controller.value.selection.extentOffset, 6); |
| |
| // Put the cursor at the end of the field. |
| await gesture.down(textOffsetToPosition(tester, 10)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, 10); |
| expect(controller.value.selection.extentOffset, 10); |
| |
| // Double tapping the second space selects it. |
| await tester.pump(const Duration(milliseconds: 500)); |
| await gesture.down(textOffsetToPosition(tester, 0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await gesture.down(textOffsetToPosition(tester, 0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(controller.value.selection, isNotNull); |
| expect(controller.value.selection.baseOffset, 0); |
| expect(controller.value.selection.extentOffset, 1); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux })); |
| |
| testWidgets('Force press does not set selection on Android or Fuchsia touch devices', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset offset = tester.getTopLeft(find.byType(TextField)) + const Offset(150.0, 9.0); |
| |
| final int pointerValue = tester.nextPointer; |
| final TestGesture gesture = await tester.createGesture(); |
| await gesture.downWithCustomEvent( |
| offset, |
| PointerDownEvent( |
| pointer: pointerValue, |
| position: offset, |
| pressure: 0.0, |
| pressureMax: 6.0, |
| pressureMin: 0.0, |
| ), |
| ); |
| await gesture.updateWithCustomEvent(PointerMoveEvent( |
| pointer: pointerValue, |
| position: offset + const Offset(150.0, 9.0), |
| pressure: 0.5, |
| pressureMin: 0, |
| )); |
| |
| await gesture.up(); |
| await tester.pump(); |
| |
| // We don't want this gesture to select any word on Android. |
| expect(controller.selection, const TextSelection.collapsed(offset: -1)); |
| expect(find.byType(TextButton), findsNothing); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia })); |
| |
| testWidgets('Force press sets selection on desktop platforms that do not support it', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset offset = tester.getTopLeft(find.byType(TextField)) + const Offset(150.0, 9.0); |
| |
| final int pointerValue = tester.nextPointer; |
| final TestGesture gesture = await tester.createGesture(); |
| await gesture.downWithCustomEvent( |
| offset, |
| PointerDownEvent( |
| pointer: pointerValue, |
| position: offset, |
| pressure: 0.0, |
| pressureMax: 6.0, |
| pressureMin: 0.0, |
| ), |
| ); |
| await gesture.updateWithCustomEvent(PointerMoveEvent( |
| pointer: pointerValue, |
| position: offset + const Offset(150.0, 9.0), |
| pressure: 0.5, |
| pressureMin: 0, |
| )); |
| |
| await gesture.up(); |
| await tester.pump(); |
| |
| // We don't want this gesture to select any word on Android. |
| expect(controller.selection, const TextSelection.collapsed(offset: 9)); |
| expect(find.byType(TextButton), findsNothing); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.windows })); |
| |
| testWidgets('force press selects word', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| final int pointerValue = tester.nextPointer; |
| final Offset offset = textfieldStart + const Offset(150.0, 9.0); |
| final TestGesture gesture = await tester.createGesture(); |
| await gesture.downWithCustomEvent( |
| offset, |
| PointerDownEvent( |
| pointer: pointerValue, |
| position: offset, |
| pressure: 0.0, |
| pressureMax: 6.0, |
| pressureMin: 0.0, |
| ), |
| ); |
| |
| await gesture.updateWithCustomEvent( |
| PointerMoveEvent( |
| pointer: pointerValue, |
| position: textfieldStart + const Offset(150.0, 9.0), |
| pressure: 0.5, |
| pressureMin: 0, |
| ), |
| ); |
| // We expect the force press to select a word at the given location. |
| expect( |
| controller.selection, |
| const TextSelection(baseOffset: 8, extentOffset: 12), |
| ); |
| |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); |
| |
| testWidgets('tap on non-force-press-supported devices work', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget(Container(key: GlobalKey())); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| final int pointerValue = tester.nextPointer; |
| final Offset offset = textfieldStart + const Offset(150.0, 9.0); |
| final TestGesture gesture = await tester.createGesture(); |
| await gesture.downWithCustomEvent( |
| offset, |
| PointerDownEvent( |
| pointer: pointerValue, |
| position: offset, |
| // iPhone 6 and below report 0 across the board. |
| pressure: 0, |
| pressureMax: 0, |
| pressureMin: 0, |
| ), |
| ); |
| |
| await gesture.updateWithCustomEvent( |
| PointerMoveEvent( |
| pointer: pointerValue, |
| position: textfieldStart + const Offset(150.0, 9.0), |
| pressure: 0.5, |
| pressureMin: 0, |
| ), |
| ); |
| await gesture.up(); |
| // The event should fallback to a normal tap and move the cursor. |
| // Single taps selects the edge of the word. |
| expect( |
| controller.selection, |
| const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream), |
| ); |
| |
| await tester.pump(); |
| // Single taps shouldn't trigger the toolbar. |
| expect(find.byType(CupertinoButton), findsNothing); |
| |
| // TODO(gspencergoog): Add in TargetPlatform.macOS in the line below when we figure out what global state is leaking. |
| // https://github.com/flutter/flutter/issues/43445 |
| }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); |
| |
| testWidgets('default TextField debugFillProperties', (WidgetTester tester) async { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| |
| const TextField().debugFillProperties(builder); |
| |
| final List<String> description = builder.properties |
| .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) |
| .map((DiagnosticsNode node) => node.toString()).toList(); |
| |
| expect(description, <String>[]); |
| }); |
| |
| testWidgets('TextField implements debugFillProperties', (WidgetTester tester) async { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| |
| // Not checking controller, inputFormatters, focusNode |
| const TextField( |
| decoration: InputDecoration(labelText: 'foo'), |
| keyboardType: TextInputType.text, |
| textInputAction: TextInputAction.done, |
| style: TextStyle(color: Color(0xff00ff00)), |
| textAlign: TextAlign.end, |
| textDirection: TextDirection.ltr, |
| autofocus: true, |
| autocorrect: false, |
| maxLines: 10, |
| maxLength: 100, |
| maxLengthEnforcement: MaxLengthEnforcement.none, |
| smartDashesType: SmartDashesType.disabled, |
| smartQuotesType: SmartQuotesType.disabled, |
| enabled: false, |
| cursorWidth: 1.0, |
| cursorHeight: 1.0, |
| cursorRadius: Radius.zero, |
| cursorColor: Color(0xff00ff00), |
| keyboardAppearance: Brightness.dark, |
| scrollPadding: EdgeInsets.zero, |
| scrollPhysics: ClampingScrollPhysics(), |
| enableInteractiveSelection: false, |
| ).debugFillProperties(builder); |
| |
| final List<String> description = builder.properties |
| .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) |
| .map((DiagnosticsNode node) => node.toString()).toList(); |
| |
| expect(description, <String>[ |
| 'enabled: false', |
| 'decoration: InputDecoration(labelText: "foo")', |
| 'style: TextStyle(inherit: true, color: Color(0xff00ff00))', |
| 'autofocus: true', |
| 'autocorrect: false', |
| 'smartDashesType: disabled', |
| 'smartQuotesType: disabled', |
| 'maxLines: 10', |
| 'maxLength: 100', |
| 'maxLengthEnforcement: none', |
| 'textInputAction: done', |
| 'textAlign: end', |
| 'textDirection: ltr', |
| 'cursorWidth: 1.0', |
| 'cursorHeight: 1.0', |
| 'cursorRadius: Radius.circular(0.0)', |
| 'cursorColor: Color(0xff00ff00)', |
| 'keyboardAppearance: Brightness.dark', |
| 'scrollPadding: EdgeInsets.zero', |
| 'selection disabled', |
| 'scrollPhysics: ClampingScrollPhysics', |
| ]); |
| }); |
| |
| testWidgets( |
| 'strut basic single line', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(platform: TargetPlatform.android), |
| home: const Material( |
| child: Center( |
| child: TextField(), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.getSize(find.byType(TextField)), |
| // The TextField will be as tall as the decoration (24) plus the metrics |
| // from the default TextStyle of the theme (16), or 40 altogether. |
| // Because this is less than the kMinInteractiveDimension, it will be |
| // increased to that value (48). |
| const Size(800, kMinInteractiveDimension), |
| ); |
| }, |
| ); |
| |
| testWidgets( |
| 'strut TextStyle increases height', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(platform: TargetPlatform.android, useMaterial3: false), |
| home: const Material( |
| child: Center( |
| child: TextField( |
| style: TextStyle(fontSize: 20), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.getSize(find.byType(TextField)), |
| // Strut should inherit the TextStyle.fontSize by default and produce the |
| // same height as if it were disabled. |
| const Size(800, kMinInteractiveDimension), // Because 44 < 48. |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(platform: TargetPlatform.android), |
| home: const Material( |
| child: Center( |
| child: TextField( |
| style: TextStyle(fontSize: 20), |
| strutStyle: StrutStyle.disabled, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.getSize(find.byType(TextField)), |
| // The height here should match the previous version with strut enabled. |
| const Size(800, kMinInteractiveDimension), // Because 44 < 48. |
| ); |
| }, |
| ); |
| |
| testWidgets( |
| 'strut basic multi line', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(platform: TargetPlatform.android, useMaterial3: false), |
| home: const Material( |
| child: Center( |
| child: TextField( |
| maxLines: 6, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.getSize(find.byType(TextField)), |
| // The height should be the input decoration (24) plus 6x the strut height (16). |
| const Size(800, 120), |
| ); |
| }, |
| ); |
| |
| testWidgets( |
| 'strut no force small strut', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(platform: TargetPlatform.android, useMaterial3: false), |
| home: const Material( |
| child: Center( |
| child: TextField( |
| maxLines: 6, |
| strutStyle: StrutStyle( |
| // The small strut is overtaken by the larger |
| // TextStyle fontSize. |
| fontSize: 5, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.getSize(find.byType(TextField)), |
| // When the strut's height is smaller than TextStyle's and forceStrutHeight |
| // is disabled, then the TextStyle takes precedence. Should be the same height |
| // as 'strut basic multi line'. |
| const Size(800, 120), |
| ); |
| }, |
| ); |
| |
| testWidgets( |
| 'strut no force large strut', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(platform: TargetPlatform.android, useMaterial3: false), |
| home: const Material( |
| child: Center( |
| child: TextField( |
| maxLines: 6, |
| strutStyle: StrutStyle( |
| fontSize: 25, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.getSize(find.byType(TextField)), |
| // When the strut's height is larger than TextStyle's and forceStrutHeight |
| // is disabled, then the StrutStyle takes precedence. |
| const Size(800, 174), |
| ); |
| }, |
| skip: isBrowser, // TODO(mdebbar): https://github.com/flutter/flutter/issues/32243 |
| ); |
| |
| testWidgets( |
| 'strut height override', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(platform: TargetPlatform.android, useMaterial3: false), |
| home: const Material( |
| child: Center( |
| child: TextField( |
| maxLines: 3, |
| strutStyle: StrutStyle( |
| fontSize: 8, |
| forceStrutHeight: true, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.getSize(find.byType(TextField)), |
| // The smaller font size of strut make the field shorter than normal. |
| const Size(800, 48), |
| ); |
| }, |
| skip: isBrowser, // TODO(mdebbar): https://github.com/flutter/flutter/issues/32243 |
| ); |
| |
| testWidgets( |
| 'strut forces field taller', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(platform: TargetPlatform.android, useMaterial3: false), |
| home: const Material( |
| child: Center( |
| child: TextField( |
| maxLines: 3, |
| style: TextStyle(fontSize: 10), |
| strutStyle: StrutStyle( |
| fontSize: 18, |
| forceStrutHeight: true, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.getSize(find.byType(TextField)), |
| // When the strut fontSize is larger than a provided TextStyle, the |
| // strut's height takes precedence. |
| const Size(800, 78), |
| ); |
| }, |
| skip: isBrowser, // TODO(mdebbar): https://github.com/flutter/flutter/issues/32243 |
| ); |
| |
| testWidgets('Caret center position', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: Theme( |
| data: ThemeData(useMaterial3: false), |
| child: const SizedBox( |
| width: 300.0, |
| child: TextField( |
| textAlign: TextAlign.center, |
| decoration: null, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderEditable editable = findRenderEditable(tester); |
| |
| await tester.enterText(find.byType(TextField), 'abcd'); |
| await tester.pump(); |
| |
| |
| Offset topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 4)).topLeft, |
| ); |
| expect(topLeft.dx, equals(431)); |
| |
| topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 3)).topLeft, |
| ); |
| expect(topLeft.dx, equals(415)); |
| |
| topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft, |
| ); |
| expect(topLeft.dx, equals(399)); |
| |
| topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 1)).topLeft, |
| ); |
| expect(topLeft.dx, equals(383)); |
| }); |
| |
| testWidgets('Caret indexes into trailing whitespace center align', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: Theme( |
| data: ThemeData(useMaterial3: false), |
| child: const SizedBox( |
| width: 300.0, |
| child: TextField( |
| textAlign: TextAlign.center, |
| decoration: null, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderEditable editable = findRenderEditable(tester); |
| |
| await tester.enterText(find.byType(TextField), 'abcd '); |
| await tester.pump(); |
| |
| Offset topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 7)).topLeft, |
| ); |
| expect(topLeft.dx, equals(479)); |
| |
| topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 8)).topLeft, |
| ); |
| expect(topLeft.dx, equals(495)); |
| |
| topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 4)).topLeft, |
| ); |
| expect(topLeft.dx, equals(431)); |
| |
| topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 3)).topLeft, |
| ); |
| expect(topLeft.dx, equals(415)); // Should be same as equivalent in 'Caret center position' |
| |
| topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft, |
| ); |
| expect(topLeft.dx, equals(399)); // Should be same as equivalent in 'Caret center position' |
| |
| topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 1)).topLeft, |
| ); |
| expect(topLeft.dx, equals(383)); // Should be same as equivalent in 'Caret center position' |
| }); |
| |
| testWidgets('selection handles are rendered and not faded away', (WidgetTester tester) async { |
| const String testText = 'lorem ipsum'; |
| final TextEditingController controller = TextEditingController(text: testText); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| final RenderEditable renderEditable = state.renderEditable; |
| |
| await tester.tapAt(const Offset(20, 10)); |
| renderEditable.selectWord(cause: SelectionChangedCause.longPress); |
| await tester.pumpAndSettle(); |
| |
| final List<FadeTransition> transitions = find.descendant( |
| of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'), |
| matching: find.byType(FadeTransition), |
| ).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList(); |
| expect(transitions.length, 2); |
| final FadeTransition left = transitions[0]; |
| final FadeTransition right = transitions[1]; |
| expect(left.opacity.value, equals(1.0)); |
| expect(right.opacity.value, equals(1.0)); |
| }); |
| |
| testWidgets('iOS selection handles are rendered and not faded away', (WidgetTester tester) async { |
| const String testText = 'lorem ipsum'; |
| final TextEditingController controller = TextEditingController(text: testText); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderEditable renderEditable = |
| tester.state<EditableTextState>(find.byType(EditableText)).renderEditable; |
| |
| await tester.tapAt(const Offset(20, 10)); |
| renderEditable.selectWord(cause: SelectionChangedCause.longPress); |
| await tester.pumpAndSettle(); |
| |
| final List<FadeTransition> transitions = |
| find.byType(FadeTransition).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList(); |
| expect(transitions.length, 2); |
| final FadeTransition left = transitions[0]; |
| final FadeTransition right = transitions[1]; |
| |
| expect(left.opacity.value, equals(1.0)); |
| expect(right.opacity.value, equals(1.0)); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('iPad Scribble selection change shows selection handles', (WidgetTester tester) async { |
| const String testText = 'lorem ipsum'; |
| final TextEditingController controller = TextEditingController(text: testText); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.testTextInput.startScribbleInteraction(); |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: testText, |
| selection: TextSelection(baseOffset: 2, extentOffset: 7), |
| )); |
| await tester.pumpAndSettle(); |
| |
| final List<FadeTransition> transitions = |
| find.byType(FadeTransition).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList(); |
| expect(transitions.length, 2); |
| final FadeTransition left = transitions[0]; |
| final FadeTransition right = transitions[1]; |
| |
| expect(left.opacity.value, equals(1.0)); |
| expect(right.opacity.value, equals(1.0)); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); |
| |
| testWidgets('Tap shows handles but not toolbar', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'abc def ghi', |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ); |
| |
| // Tap to trigger the text field. |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| expect(editableText.selectionOverlay!.handlesAreVisible, isTrue); |
| expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse); |
| }); |
| |
| testWidgets( |
| 'Tap in empty text field does not show handles nor toolbar', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ); |
| |
| // Tap to trigger the text field. |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| expect(editableText.selectionOverlay!.handlesAreVisible, isFalse); |
| expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse); |
| }, |
| ); |
| |
| testWidgets('Long press shows handles and toolbar', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'abc def ghi', |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ); |
| |
| // Long press to trigger the text field. |
| await tester.longPress(find.byType(TextField)); |
| await tester.pump(); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| expect(editableText.selectionOverlay!.handlesAreVisible, isTrue); |
| expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue); |
| }); |
| |
| testWidgets( |
| 'Long press in empty text field shows handles and toolbar', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ); |
| |
| // Tap to trigger the text field. |
| await tester.longPress(find.byType(TextField)); |
| await tester.pump(); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| expect(editableText.selectionOverlay!.handlesAreVisible, isTrue); |
| expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue); |
| }, |
| ); |
| |
| testWidgets('Double tap shows handles and toolbar', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'abc def ghi', |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ); |
| |
| // Double tap to trigger the text field. |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| expect(editableText.selectionOverlay!.handlesAreVisible, isTrue); |
| expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue); |
| }); |
| |
| testWidgets( |
| 'Double tap in empty text field shows toolbar but not handles', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ); |
| |
| // Double tap to trigger the text field. |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| expect(editableText.selectionOverlay!.handlesAreVisible, isFalse); |
| expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue); |
| }, |
| ); |
| |
| testWidgets( |
| 'Mouse tap does not show handles nor toolbar', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'abc def ghi', |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ); |
| |
| // Long press to trigger the text field. |
| final Offset textFieldPos = tester.getCenter(find.byType(TextField)); |
| final TestGesture gesture = await tester.startGesture( |
| textFieldPos, |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse); |
| expect(editableText.selectionOverlay!.handlesAreVisible, isFalse); |
| }, |
| ); |
| |
| testWidgets( |
| 'Mouse long press does not show handles nor toolbar', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'abc def ghi', |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ); |
| |
| // Long press to trigger the text field. |
| final Offset textFieldPos = tester.getCenter(find.byType(TextField)); |
| final TestGesture gesture = await tester.startGesture( |
| textFieldPos, |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pump(); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse); |
| expect(editableText.selectionOverlay!.handlesAreVisible, isFalse); |
| }, |
| ); |
| |
| testWidgets( |
| 'Mouse double tap does not show handles nor toolbar', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'abc def ghi', |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ); |
| |
| // Double tap to trigger the text field. |
| final Offset textFieldPos = tester.getCenter(find.byType(TextField)); |
| final TestGesture gesture = await tester.startGesture( |
| textFieldPos, |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await gesture.up(); |
| await tester.pump(); |
| await gesture.down(textFieldPos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse); |
| expect(editableText.selectionOverlay!.handlesAreVisible, isFalse); |
| }, |
| ); |
| |
| testWidgets('Does not show handles when updated from the web engine', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'abc def ghi', |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ); |
| |
| // Interact with the text field to establish the input connection. |
| final Offset topLeft = tester.getTopLeft(find.byType(EditableText)); |
| final TestGesture gesture = await tester.startGesture( |
| topLeft + const Offset(0.0, 5.0), |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| final EditableTextState state = tester.state(find.byType(EditableText)); |
| expect(state.selectionOverlay!.handlesAreVisible, isFalse); |
| expect(controller.selection, const TextSelection.collapsed(offset: 0)); |
| |
| if (kIsWeb) { |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: 'abc def ghi', |
| selection: TextSelection(baseOffset: 2, extentOffset: 7), |
| )); |
| // Wait for all the `setState` calls to be flushed. |
| await tester.pumpAndSettle(); |
| expect( |
| state.currentTextEditingValue.selection, |
| const TextSelection(baseOffset: 2, extentOffset: 7), |
| ); |
| expect(state.selectionOverlay!.handlesAreVisible, isFalse); |
| } |
| }); |
| |
| testWidgets('Tapping selection handles toggles the toolbar', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'abc def ghi', |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ); |
| |
| // Tap to position the cursor and show the selection handles. |
| final Offset ePos = textOffsetToPosition(tester, 5); // Index of 'e'. |
| await tester.tapAt(ePos, pointer: 7); |
| await tester.pumpAndSettle(); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse); |
| expect(editableText.selectionOverlay!.handlesAreVisible, isTrue); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 1); |
| |
| // Tap the handle to show the toolbar. |
| final Offset handlePos = endpoints[0].point + const Offset(0.0, 1.0); |
| await tester.tapAt(handlePos, pointer: 7); |
| await tester.pump(); |
| expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue); |
| |
| // Tap the handle again to hide the toolbar. |
| await tester.tapAt(handlePos, pointer: 7); |
| await tester.pump(); |
| expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse); |
| }); |
| |
| testWidgets('when TextField would be blocked by keyboard, it is shown with enough space for the selection handle', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Scaffold( |
| body: Center( |
| child: ListView( |
| controller: scrollController, |
| children: <Widget>[ |
| Container(height: 579), // Push field almost off screen. |
| const TextField(), |
| Container(height: 1000), |
| ], |
| ), |
| ), |
| ), |
| )); |
| |
| // Tap the TextField to put the cursor into it and bring it into view. |
| expect(scrollController.offset, 0.0); |
| await tester.tapAt(tester.getTopLeft(find.byType(TextField))); |
| await tester.pumpAndSettle(); |
| |
| // The ListView has scrolled to keep the TextField and cursor handle |
| // visible. |
| expect(scrollController.offset, 50.0); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/74566 |
| testWidgets('TextField and last input character are visible on the screen when the cursor is not shown', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| final ScrollController textFieldScrollController = ScrollController(); |
| |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Scaffold( |
| body: Center( |
| child: ListView( |
| controller: scrollController, |
| children: <Widget>[ |
| Container(height: 579), // Push field almost off screen. |
| TextField( |
| scrollController: textFieldScrollController, |
| showCursor: false, |
| ), |
| Container(height: 1000), |
| ], |
| ), |
| ), |
| ), |
| )); |
| |
| // Tap the TextField to bring it into view. |
| expect(scrollController.offset, 0.0); |
| await tester.tapAt(tester.getTopLeft(find.byType(TextField))); |
| await tester.pumpAndSettle(); |
| |
| // The ListView has scrolled to keep the TextField visible. |
| expect(scrollController.offset, 50.0); |
| expect(textFieldScrollController.offset, 0.0); |
| |
| // After entering some long text, the last input character remains on the screen. |
| final String testValue = 'I love Flutter!' * 10; |
| tester.testTextInput.updateEditingValue(TextEditingValue( |
| text: testValue, |
| selection: TextSelection.collapsed(offset: testValue.length), |
| )); |
| await tester.pump(); |
| await tester.pumpAndSettle(); // Text scroll animation. |
| |
| expect(textFieldScrollController.offset, 1602.0); |
| }); |
| |
| group('height', () { |
| testWidgets('By default, TextField is at least kMinInteractiveDimension high', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData(), |
| home: const Scaffold( |
| body: Center( |
| child: TextField(), |
| ), |
| ), |
| )); |
| |
| final RenderBox renderBox = tester.renderObject(find.byType(TextField)); |
| expect(renderBox.size.height, greaterThanOrEqualTo(kMinInteractiveDimension)); |
| }); |
| |
| testWidgets("When text is very small, TextField still doesn't go below kMinInteractiveDimension height", (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData(), |
| home: const Scaffold( |
| body: Center( |
| child: TextField( |
| style: TextStyle(fontSize: 2.0), |
| ), |
| ), |
| ), |
| )); |
| |
| final RenderBox renderBox = tester.renderObject(find.byType(TextField)); |
| expect(renderBox.size.height, kMinInteractiveDimension); |
| }); |
| |
| testWidgets('When isDense, TextField can go below kMinInteractiveDimension height', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData(), |
| home: const Scaffold( |
| body: Center( |
| child: TextField( |
| decoration: InputDecoration( |
| isDense: true, |
| ), |
| ), |
| ), |
| ), |
| )); |
| |
| final RenderBox renderBox = tester.renderObject(find.byType(TextField)); |
| expect(renderBox.size.height, lessThan(kMinInteractiveDimension)); |
| }); |
| |
| group('intrinsics', () { |
| Widget buildTest({ required bool isDense }) { |
| return MaterialApp( |
| home: Scaffold( |
| body: CustomScrollView( |
| slivers: <Widget>[ |
| SliverFillRemaining( |
| hasScrollBody: false, |
| child: Column( |
| children: <Widget>[ |
| TextField( |
| decoration: InputDecoration( |
| isDense: isDense, |
| ), |
| ), |
| Container( |
| height: 1000, |
| ), |
| ], |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| } |
| |
| testWidgets('By default, intrinsic height is at least kMinInteractiveDimension high', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/54729 |
| // If the intrinsic height does not match that of the height after |
| // performLayout, this will fail. |
| await tester.pumpWidget(buildTest(isDense: false)); |
| }); |
| |
| testWidgets('When isDense, intrinsic height can go below kMinInteractiveDimension height', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/54729 |
| // If the intrinsic height does not match that of the height after |
| // performLayout, this will fail. |
| await tester.pumpWidget(buildTest(isDense: true)); |
| }); |
| }); |
| }); |
| testWidgets("Arrow keys don't move input focus", (WidgetTester tester) async { |
| final TextEditingController controller1 = TextEditingController(); |
| final TextEditingController controller2 = TextEditingController(); |
| final TextEditingController controller3 = TextEditingController(); |
| final TextEditingController controller4 = TextEditingController(); |
| final TextEditingController controller5 = TextEditingController(); |
| final FocusNode focusNode1 = FocusNode(debugLabel: 'Field 1'); |
| final FocusNode focusNode2 = FocusNode(debugLabel: 'Field 2'); |
| final FocusNode focusNode3 = FocusNode(debugLabel: 'Field 3'); |
| final FocusNode focusNode4 = FocusNode(debugLabel: 'Field 4'); |
| final FocusNode focusNode5 = FocusNode(debugLabel: 'Field 5'); |
| |
| // Lay out text fields in a "+" formation, and focus the center one. |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData(), |
| home: Scaffold( |
| body: Center( |
| child: Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| mainAxisSize: MainAxisSize.min, |
| children: <Widget>[ |
| SizedBox( |
| width: 100.0, |
| child: TextField( |
| controller: controller1, |
| focusNode: focusNode1, |
| ), |
| ), |
| Row( |
| mainAxisAlignment: MainAxisAlignment.center, |
| mainAxisSize: MainAxisSize.min, |
| children: <Widget>[ |
| SizedBox( |
| width: 100.0, |
| child: TextField( |
| controller: controller2, |
| focusNode: focusNode2, |
| ), |
| ), |
| SizedBox( |
| width: 100.0, |
| child: TextField( |
| controller: controller3, |
| focusNode: focusNode3, |
| ), |
| ), |
| SizedBox( |
| width: 100.0, |
| child: TextField( |
| controller: controller4, |
| focusNode: focusNode4, |
| ), |
| ), |
| ], |
| ), |
| SizedBox( |
| width: 100.0, |
| child: TextField( |
| controller: controller5, |
| focusNode: focusNode5, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| )); |
| |
| focusNode3.requestFocus(); |
| await tester.pump(); |
| expect(focusNode3.hasPrimaryFocus, isTrue); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| await tester.pump(); |
| expect(focusNode3.hasPrimaryFocus, isTrue); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pump(); |
| expect(focusNode3.hasPrimaryFocus, isTrue); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pump(); |
| expect(focusNode3.hasPrimaryFocus, isTrue); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pump(); |
| expect(focusNode3.hasPrimaryFocus, isTrue); |
| }); |
| |
| testWidgets('Scrolling shortcuts are disabled in text fields', (WidgetTester tester) async { |
| bool scrollInvoked = false; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Actions( |
| actions: <Type, Action<Intent>>{ |
| ScrollIntent: CallbackAction<ScrollIntent>(onInvoke: (Intent intent) { |
| scrollInvoked = true; |
| return null; |
| }), |
| }, |
| child: Material( |
| child: ListView( |
| children: const <Widget>[ |
| Padding(padding: EdgeInsets.symmetric(vertical: 200)), |
| TextField(), |
| Padding(padding: EdgeInsets.symmetric(vertical: 800)), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| await tester.pump(); |
| expect(scrollInvoked, isFalse); |
| |
| // Set focus on the text field. |
| await tester.tapAt(tester.getTopLeft(find.byType(TextField))); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.space); |
| expect(scrollInvoked, isFalse); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| expect(scrollInvoked, isFalse); |
| }); |
| |
| testWidgets("A buildCounter that returns null doesn't affect the size of the TextField", (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/44909 |
| |
| final GlobalKey textField1Key = GlobalKey(); |
| final GlobalKey textField2Key = GlobalKey(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Column( |
| children: <Widget>[ |
| TextField(key: textField1Key), |
| TextField( |
| key: textField2Key, |
| maxLength: 1, |
| buildCounter: (BuildContext context, {required int currentLength, required bool isFocused, int? maxLength}) => null, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pumpAndSettle(); |
| final Size textFieldSize1 = tester.getSize(find.byKey(textField1Key)); |
| final Size textFieldSize2 = tester.getSize(find.byKey(textField2Key)); |
| |
| expect(textFieldSize1, equals(textFieldSize2)); |
| }); |
| |
| testWidgets( |
| 'The selection menu displays in an Overlay without error', |
| (WidgetTester tester) async { |
| // This is a regression test for |
| // https://github.com/flutter/flutter/issues/43787 |
| final TextEditingController controller = TextEditingController( |
| text: 'This is a test that shows some odd behavior with Text Selection!', |
| ); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: ColoredBox( |
| color: Colors.grey, |
| child: Center( |
| child: Container( |
| color: Colors.red, |
| width: 300, |
| height: 600, |
| child: Overlay( |
| initialEntries: <OverlayEntry>[ |
| OverlayEntry( |
| builder: (BuildContext context) => Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ), |
| )); |
| |
| await showSelectionMenuAt(tester, controller, controller.text.indexOf('test')); |
| await tester.pumpAndSettle(); |
| expect(tester.takeException(), isNull); |
| }, |
| ); |
| |
| testWidgets('clipboard status is checked via hasStrings without getting the full clipboard contents', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| bool calledGetData = false; |
| bool calledHasStrings = false; |
| tester.binding.defaultBinaryMessenger |
| .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { |
| switch (methodCall.method) { |
| case 'Clipboard.getData': |
| calledGetData = true; |
| case 'Clipboard.hasStrings': |
| calledHasStrings = true; |
| default: |
| break; |
| } |
| return null; |
| }); |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); |
| |
| // Double tap like when showing the text selection menu on Android/iOS. |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| await tester.pump(); |
| |
| // getData is not called unless something is pasted. hasStrings is used to |
| // check the status of the clipboard. |
| expect(calledGetData, false); |
| // hasStrings is checked in order to decide if the content can be pasted. |
| expect(calledHasStrings, true); |
| }); |
| |
| testWidgets('TextField changes mouse cursor when hovered', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: MouseRegion( |
| cursor: SystemMouseCursors.forbidden, |
| child: TextField( |
| mouseCursor: SystemMouseCursors.grab, |
| decoration: InputDecoration( |
| // Add an icon so that the left edge is not the text area |
| icon: Icon(Icons.person), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Center, which is within the text area |
| final Offset center = tester.getCenter(find.byType(TextField)); |
| // Top left, which is not the text area |
| final Offset edge = tester.getTopLeft(find.byType(TextField)) + const Offset(1, 1); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); |
| await gesture.addPointer(location: center); |
| |
| await tester.pump(); |
| |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab); |
| |
| // Test default cursor |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: MouseRegion( |
| cursor: SystemMouseCursors.forbidden, |
| child: TextField( |
| decoration: InputDecoration( |
| icon: Icon(Icons.person), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); |
| await gesture.moveTo(edge); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); |
| await gesture.moveTo(center); |
| |
| // Test default cursor when disabled |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: MouseRegion( |
| cursor: SystemMouseCursors.forbidden, |
| child: TextField( |
| enabled: false, |
| decoration: InputDecoration( |
| icon: Icon(Icons.person), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); |
| await gesture.moveTo(edge); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); |
| await gesture.moveTo(center); |
| }); |
| |
| testWidgets('TextField icons change mouse cursor when hovered', (WidgetTester tester) async { |
| // Test default cursor in icons area. |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: MouseRegion( |
| cursor: SystemMouseCursors.forbidden, |
| child: TextField( |
| decoration: InputDecoration( |
| icon: Icon(Icons.label), |
| prefixIcon: Icon(Icons.cabin), |
| suffixIcon: Icon(Icons.person), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Center, which is within the text area |
| final Offset center = tester.getCenter(find.byType(TextField)); |
| // The Icon area |
| final Offset iconArea = tester.getCenter(find.byIcon(Icons.label)); |
| // The prefix Icon area |
| final Offset prefixIconArea = tester.getCenter(find.byIcon(Icons.cabin)); |
| // The suffix Icon area |
| final Offset suffixIconArea = tester.getCenter(find.byIcon(Icons.person)); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); |
| await gesture.addPointer(location: center); |
| |
| await tester.pump(); |
| |
| await gesture.moveTo(center); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); |
| |
| await gesture.moveTo(iconArea); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); |
| |
| await gesture.moveTo(prefixIconArea); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); |
| |
| await gesture.moveTo(suffixIconArea); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); |
| await gesture.moveTo(center); |
| |
| // Test click cursor in icons area for buttons. |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: MouseRegion( |
| cursor: SystemMouseCursors.forbidden, |
| child: TextField( |
| decoration: InputDecoration( |
| icon: IconButton( |
| icon: const Icon(Icons.label), |
| onPressed: () {}, |
| ), |
| prefixIcon: IconButton( |
| icon: const Icon(Icons.cabin), |
| onPressed: () {}, |
| ), |
| suffixIcon: IconButton( |
| icon: const Icon(Icons.person), |
| onPressed: () {}, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| |
| await gesture.moveTo(center); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); |
| |
| await gesture.moveTo(iconArea); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); |
| |
| await gesture.moveTo(prefixIconArea); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); |
| |
| await gesture.moveTo(suffixIconArea); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); |
| }); |
| |
| testWidgets('Text selection menu does not change mouse cursor when hovered', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: MouseRegion( |
| cursor: SystemMouseCursors.forbidden, |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Copy'), findsNothing); |
| |
| final TestGesture gesture = await tester.startGesture( |
| textOffsetToPosition(tester, 3), |
| kind: PointerDeviceKind.mouse, |
| buttons: kSecondaryMouseButton, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); |
| expect(find.text('Paste'), findsOneWidget); |
| |
| await gesture.moveTo(tester.getCenter(find.text('Paste'))); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); |
| }, |
| variant: TargetPlatformVariant.desktop(), |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. |
| ); |
| |
| testWidgets('Caret rtl with changing width', (WidgetTester tester) async { |
| late StateSetter setState; |
| bool isWide = false; |
| const double wideWidth = 300.0; |
| const double narrowWidth = 200.0; |
| final TextEditingController controller = TextEditingController(); |
| await tester.pumpWidget( |
| boilerplate( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setter) { |
| setState = setter; |
| return SizedBox( |
| width: isWide ? wideWidth : narrowWidth, |
| child: TextField( |
| key: textFieldKey, |
| controller: controller, |
| textDirection: TextDirection.rtl, |
| ), |
| ); |
| }, |
| ), |
| ), |
| ); |
| |
| // The cursor is on the right of the input because it's RTL. |
| RenderEditable editable = findRenderEditable(tester); |
| double cursorRight = editable.getLocalRectForCaret( |
| TextPosition(offset: controller.value.text.length), |
| ).topRight.dx; |
| double inputWidth = editable.size.width; |
| expect(inputWidth, narrowWidth); |
| expect(cursorRight, inputWidth - kCaretGap); |
| |
| // After entering some text, the cursor remains on the right of the input. |
| await tester.enterText(find.byType(TextField), '12345'); |
| await tester.pump(); |
| editable = findRenderEditable(tester); |
| cursorRight = editable.getLocalRectForCaret( |
| TextPosition(offset: controller.value.text.length), |
| ).topRight.dx; |
| inputWidth = editable.size.width; |
| expect(cursorRight, inputWidth - kCaretGap); |
| |
| // Since increasing the width of the input moves its right edge further to |
| // the right, the cursor has followed this change and still appears on the |
| // right of the input. |
| setState(() { |
| isWide = true; |
| }); |
| await tester.pump(); |
| editable = findRenderEditable(tester); |
| cursorRight = editable.getLocalRectForCaret( |
| TextPosition(offset: controller.value.text.length), |
| ).topRight.dx; |
| inputWidth = editable.size.width; |
| expect(inputWidth, wideWidth); |
| expect(cursorRight, inputWidth - kCaretGap); |
| }); |
| |
| testWidgets('Text selection menu hides after select all on desktop', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| final String selectAll = defaultTargetPlatform == TargetPlatform.macOS |
| ? 'Select All' |
| : 'Select all'; |
| |
| expect(find.text(selectAll), findsNothing); |
| expect(find.text('Copy'), findsNothing); |
| |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(10.0, 0.0) + textOffsetToPosition(tester, controller.text.length), |
| kind: PointerDeviceKind.mouse, |
| buttons: kSecondaryMouseButton, |
| ); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect( |
| controller.value.selection, |
| TextSelection.collapsed( |
| offset: controller.text.length, |
| affinity: TextAffinity.upstream, |
| ), |
| ); |
| expect(find.text(selectAll), findsOneWidget); |
| |
| await tester.tapAt(tester.getCenter(find.text(selectAll))); |
| |
| await tester.pump(); |
| expect(find.text(selectAll), findsNothing); |
| expect(find.text('Copy'), findsNothing); |
| }, |
| // All desktop platforms except MacOS, which has no select all button. |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.windows }), |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. |
| ); |
| |
| // Regressing test for https://github.com/flutter/flutter/issues/70625 |
| testWidgets('TextFields can inherit [FloatingLabelBehaviour] from InputDecorationTheme.', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| Widget textFieldBuilder({ FloatingLabelBehavior behavior = FloatingLabelBehavior.auto }) { |
| return MaterialApp( |
| theme: ThemeData( |
| useMaterial3: false, |
| inputDecorationTheme: InputDecorationTheme( |
| floatingLabelBehavior: behavior, |
| ), |
| ), |
| home: Scaffold( |
| body: TextField( |
| focusNode: focusNode, |
| decoration: const InputDecoration( |
| labelText: 'Label', |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(textFieldBuilder()); |
| // The label will be positioned within the content when unfocused. |
| expect(tester.getTopLeft(find.text('Label')).dy, 20.0); |
| |
| focusNode.requestFocus(); |
| await tester.pumpAndSettle(); // label animation. |
| // The label will float above the content when focused. |
| expect(tester.getTopLeft(find.text('Label')).dy, 12.0); |
| |
| focusNode.unfocus(); |
| await tester.pumpAndSettle(); // label animation. |
| |
| await tester.pumpWidget(textFieldBuilder(behavior: FloatingLabelBehavior.never)); |
| await tester.pumpAndSettle(); // theme animation. |
| // The label will be positioned within the content. |
| expect(tester.getTopLeft(find.text('Label')).dy, 20.0); |
| |
| focusNode.requestFocus(); |
| await tester.pumpAndSettle(); // label animation. |
| // The label will always be positioned within the content. |
| expect(tester.getTopLeft(find.text('Label')).dy, 20.0); |
| |
| await tester.pumpWidget(textFieldBuilder(behavior: FloatingLabelBehavior.always)); |
| await tester.pumpAndSettle(); // theme animation. |
| // The label will always float above the content. |
| expect(tester.getTopLeft(find.text('Label')).dy, 12.0); |
| |
| focusNode.unfocus(); |
| await tester.pumpAndSettle(); // label animation. |
| // The label will always float above the content. |
| expect(tester.getTopLeft(find.text('Label')).dy, 12.0); |
| }); |
| |
| group('MaxLengthEnforcement', () { |
| const int maxLength = 5; |
| |
| Future<void> setupWidget( |
| WidgetTester tester, |
| MaxLengthEnforcement? enforcement, |
| ) async { |
| final Widget widget = MaterialApp( |
| home: Material( |
| child: TextField( |
| maxLength: maxLength, |
| maxLengthEnforcement: enforcement, |
| ), |
| ), |
| ); |
| |
| await tester.pumpWidget(widget); |
| await tester.pumpAndSettle(); |
| } |
| |
| testWidgets('using none enforcement.', (WidgetTester tester) async { |
| const MaxLengthEnforcement enforcement = MaxLengthEnforcement.none; |
| |
| await setupWidget(tester, enforcement); |
| |
| final EditableTextState state = tester.state(find.byType(EditableText)); |
| |
| state.updateEditingValue(const TextEditingValue(text: 'abc')); |
| expect(state.currentTextEditingValue.text, 'abc'); |
| expect(state.currentTextEditingValue.composing, TextRange.empty); |
| |
| state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6))); |
| expect(state.currentTextEditingValue.text, 'abcdef'); |
| expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6)); |
| |
| state.updateEditingValue(const TextEditingValue(text: 'abcdef')); |
| expect(state.currentTextEditingValue.text, 'abcdef'); |
| expect(state.currentTextEditingValue.composing, TextRange.empty); |
| }); |
| |
| testWidgets('using enforced.', (WidgetTester tester) async { |
| const MaxLengthEnforcement enforcement = MaxLengthEnforcement.enforced; |
| |
| await setupWidget(tester, enforcement); |
| |
| final EditableTextState state = tester.state(find.byType(EditableText)); |
| |
| state.updateEditingValue(const TextEditingValue(text: 'abc')); |
| expect(state.currentTextEditingValue.text, 'abc'); |
| expect(state.currentTextEditingValue.composing, TextRange.empty); |
| |
| state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5))); |
| expect(state.currentTextEditingValue.text, 'abcde'); |
| expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); |
| |
| state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6))); |
| expect(state.currentTextEditingValue.text, 'abcde'); |
| expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); |
| |
| state.updateEditingValue(const TextEditingValue(text: 'abcdef')); |
| expect(state.currentTextEditingValue.text, 'abcde'); |
| expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); |
| }); |
| |
| testWidgets('using truncateAfterCompositionEnds.', (WidgetTester tester) async { |
| const MaxLengthEnforcement enforcement = MaxLengthEnforcement.truncateAfterCompositionEnds; |
| |
| await setupWidget(tester, enforcement); |
| |
| final EditableTextState state = tester.state(find.byType(EditableText)); |
| |
| state.updateEditingValue(const TextEditingValue(text: 'abc')); |
| expect(state.currentTextEditingValue.text, 'abc'); |
| expect(state.currentTextEditingValue.composing, TextRange.empty); |
| |
| state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5))); |
| expect(state.currentTextEditingValue.text, 'abcde'); |
| expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); |
| |
| state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6))); |
| expect(state.currentTextEditingValue.text, 'abcdef'); |
| expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6)); |
| |
| state.updateEditingValue(const TextEditingValue(text: 'abcdef')); |
| expect(state.currentTextEditingValue.text, 'abcde'); |
| expect(state.currentTextEditingValue.composing, TextRange.empty); |
| }); |
| |
| testWidgets('using default behavior for different platforms.', (WidgetTester tester) async { |
| await setupWidget(tester, null); |
| |
| final EditableTextState state = tester.state(find.byType(EditableText)); |
| |
| state.updateEditingValue(const TextEditingValue(text: '侬好啊')); |
| expect(state.currentTextEditingValue.text, '侬好啊'); |
| expect(state.currentTextEditingValue.composing, TextRange.empty); |
| |
| state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友', composing: TextRange(start: 3, end: 5))); |
| expect(state.currentTextEditingValue.text, '侬好啊旁友'); |
| expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); |
| |
| state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友们', composing: TextRange(start: 3, end: 6))); |
| if (kIsWeb || |
| defaultTargetPlatform == TargetPlatform.iOS || |
| defaultTargetPlatform == TargetPlatform.macOS || |
| defaultTargetPlatform == TargetPlatform.linux || |
| defaultTargetPlatform == TargetPlatform.fuchsia |
| ) { |
| expect(state.currentTextEditingValue.text, '侬好啊旁友们'); |
| expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6)); |
| } else { |
| expect(state.currentTextEditingValue.text, '侬好啊旁友'); |
| expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); |
| } |
| |
| state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友')); |
| expect(state.currentTextEditingValue.text, '侬好啊旁友'); |
| expect(state.currentTextEditingValue.composing, TextRange.empty); |
| }); |
| }); |
| |
| testWidgets('TextField does not leak touch events when deadline has exceeded', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/118340. |
| int textFieldTapCount = 0; |
| int prefixTapCount = 0; |
| int suffixTapCount = 0; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: TextField( |
| onTap: () { textFieldTapCount += 1; }, |
| decoration: InputDecoration( |
| labelText: 'Label', |
| prefix: ElevatedButton( |
| onPressed: () { prefixTapCount += 1; }, |
| child: const Text('prefix'), |
| ), |
| suffix: ElevatedButton( |
| onPressed: () { suffixTapCount += 1; }, |
| child: const Text('suffix'), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| TestGesture gesture = |
| await tester.startGesture( |
| tester.getRect(find.text('prefix')).center, |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| expect(textFieldTapCount, 0); |
| expect(prefixTapCount, 1); |
| expect(suffixTapCount, 0); |
| |
| gesture = await tester.startGesture( |
| tester.getRect(find.text('suffix')).center, |
| pointer: 7, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| expect(textFieldTapCount, 0); |
| expect(prefixTapCount, 1); |
| expect(suffixTapCount, 1); |
| }); |
| |
| testWidgets('prefix/suffix buttons do not leak touch events', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/39376. |
| |
| int textFieldTapCount = 0; |
| int prefixTapCount = 0; |
| int suffixTapCount = 0; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: TextField( |
| onTap: () { textFieldTapCount += 1; }, |
| decoration: InputDecoration( |
| labelText: 'Label', |
| prefix: ElevatedButton( |
| onPressed: () { prefixTapCount += 1; }, |
| child: const Text('prefix'), |
| ), |
| suffix: ElevatedButton( |
| onPressed: () { suffixTapCount += 1; }, |
| child: const Text('suffix'), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.text('prefix')); |
| expect(textFieldTapCount, 0); |
| expect(prefixTapCount, 1); |
| expect(suffixTapCount, 0); |
| |
| await tester.tap(find.text('suffix')); |
| expect(textFieldTapCount, 0); |
| expect(prefixTapCount, 1); |
| expect(suffixTapCount, 1); |
| }); |
| |
| testWidgets('autofill info has hint text', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| decoration: InputDecoration( |
| hintText: 'placeholder text' |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(TextField)); |
| |
| expect( |
| tester.testTextInput.setClientArgs?['autofill'], |
| containsPair('hintText', 'placeholder text'), |
| ); |
| }); |
| |
| testWidgets('TextField at rest does not push any layers with alwaysNeedsAddToScene', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField(), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse); |
| }); |
| |
| testWidgets('Focused TextField does not push any layers with alwaysNeedsAddToScene', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField(focusNode: focusNode), |
| ), |
| ), |
| ), |
| ); |
| await tester.showKeyboard(find.byType(TextField)); |
| |
| expect(focusNode.hasFocus, isTrue); |
| expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse); |
| }); |
| |
| testWidgets('TextField does not push any layers with alwaysNeedsAddToScene after toolbar is dismissed', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField(focusNode: focusNode), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.showKeyboard(find.byType(TextField)); |
| |
| // Bring up the toolbar. |
| const String testValue = 'A B C'; |
| tester.testTextInput.updateEditingValue( |
| const TextEditingValue( |
| text: testValue, |
| ), |
| ); |
| await tester.pump(); |
| final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); |
| state.renderEditable.selectWordsInRange(from: Offset.zero, cause: SelectionChangedCause.tap); |
| expect(state.showToolbar(), true); |
| await tester.pumpAndSettle(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('Copy'), findsOneWidget); // Toolbar is visible |
| |
| // Hide the toolbar |
| focusNode.unfocus(); |
| await tester.pumpAndSettle(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('Copy'), findsNothing); // Toolbar is not visible |
| |
| expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse); |
| }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. |
| |
| testWidgets('cursor blinking respects TickerMode', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| Widget builder({required bool tickerMode}) { |
| return MaterialApp( |
| home: Material( |
| child: Center( |
| child: TickerMode(enabled: tickerMode, child: TextField(focusNode: focusNode)), |
| ), |
| ), |
| ); |
| } |
| |
| // TickerMode is on, cursor is blinking. |
| await tester.pumpWidget(builder(tickerMode: true)); |
| await tester.showKeyboard(find.byType(TextField)); |
| final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); |
| final RenderEditable editable = state.renderEditable; |
| expect(editable.showCursor.value, isTrue); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isTrue); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| |
| // TickerMode is off, cursor does not blink. |
| await tester.pumpWidget(builder(tickerMode: false)); |
| expect(editable.showCursor.value, isFalse); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| |
| // TickerMode is on, cursor blinks again. |
| await tester.pumpWidget(builder(tickerMode: true)); |
| expect(editable.showCursor.value, isTrue); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isTrue); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| |
| // Dismissing focus while tickerMode is off does not start cursor blinking |
| // when tickerMode is turned on again. |
| await tester.pumpWidget(builder(tickerMode: false)); |
| focusNode.unfocus(); |
| await tester.pump(); |
| expect(editable.showCursor.value, isFalse); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| await tester.pumpWidget(builder(tickerMode: true)); |
| expect(editable.showCursor.value, isFalse); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| |
| // Focusing while tickerMode is off does not start cursor blinking... |
| await tester.pumpWidget(builder(tickerMode: false)); |
| await tester.showKeyboard(find.byType(TextField)); |
| expect(editable.showCursor.value, isFalse); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| |
| // ... but it does start when tickerMode is switched on again. |
| await tester.pumpWidget(builder(tickerMode: true)); |
| expect(editable.showCursor.value, isTrue); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isFalse); |
| await tester.pump(state.cursorBlinkInterval); |
| expect(editable.showCursor.value, isTrue); |
| }); |
| |
| testWidgets('can shift + tap to select with a keyboard (Apple platforms)', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Atwater Peel Sherbrooke Bonaventure', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField(controller: controller), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tapAt(textOffsetToPosition(tester, 13)); |
| await tester.pumpAndSettle(); |
| expect(controller.selection.baseOffset, 13); |
| expect(controller.selection.extentOffset, 13); |
| |
| await tester.pump(kDoubleTapTimeout); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.tapAt(textOffsetToPosition(tester, 20)); |
| await tester.pumpAndSettle(); |
| expect(controller.selection.baseOffset, 13); |
| expect(controller.selection.extentOffset, 20); |
| |
|