| // 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. |
| |
| // TODO(gspencergoog): Remove this tag once this test's state leaks/test |
| // dependencies have been fixed. |
| // https://github.com/flutter/flutter/issues/85160 |
| // Fails with "flutter test --test-randomize-ordering-seed=3890307731" |
| @Tags(<String>['no-shuffle']) |
| |
| import 'dart:math' as math; |
| import 'dart:ui' as ui show window, BoxHeightStyle, BoxWidthStyle, WindowPadding; |
| |
| 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/editable_text_utils.dart' show findRenderEditable, globalize, textOffsetToPosition; |
| import '../widgets/semantics_tester.dart'; |
| import 'feedback_tester.dart'; |
| |
| typedef FormatEditUpdateCallback = void Function(TextEditingValue, TextEditingValue); |
| |
| // On web, the context menu (aka toolbar) is provided by the browser. |
| final bool isContextMenuProvidedByPlatform = isBrowser; |
| |
| // On web, key events in text fields are handled by the browser. |
| final bool areKeyEventsHandledByPlatform = isBrowser; |
| |
| class MockClipboard { |
| Object _clipboardData = <String, dynamic>{ |
| 'text': null, |
| }; |
| |
| Future<dynamic> handleMethodCall(MethodCall methodCall) async { |
| switch (methodCall.method) { |
| case 'Clipboard.getData': |
| return _clipboardData; |
| case 'Clipboard.setData': |
| _clipboardData = methodCall.arguments as Object; |
| break; |
| } |
| } |
| } |
| |
| 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(), |
| ], |
| child: DefaultTextEditingShortcuts( |
| child: DefaultTextEditingActions( |
| 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 }) { |
| return MaterialApp( |
| 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; |
| } |
| } |
| |
| // Used to set window.viewInsets since the real ui.WindowPadding has only a |
| // private constructor. |
| class _TestWindowPadding implements ui.WindowPadding { |
| const _TestWindowPadding({ |
| required this.bottom, |
| }); |
| |
| @override |
| final double bottom; |
| |
| @override |
| double get top => 0.0; |
| |
| @override |
| double get left => 0.0; |
| |
| @override |
| double get right => 0.0; |
| } |
| |
| 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('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, |
| ); |
| addTearDown(gesture.removePointer); |
| 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: 5, 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(baseOffset: 11, extentOffset: 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(baseOffset: 16, extentOffset: 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: TargetPlatformVariant.desktop(), |
| skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. |
| ); |
| |
| testWidgets('Activates the text field when receives semantics focus on Mac', (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 })); |
| |
| 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(); |
| }); |
| |
| testWidgets('Cursor animates', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: TextField(), |
| ), |
| ), |
| ); |
| |
| final Finder textFinder = find.byType(TextField); |
| await tester.tap(textFinder); |
| await tester.pump(); |
| |
| final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); |
| final RenderEditable renderEditable = editableTextState.renderEditable; |
| |
| expect(renderEditable.cursorColor!.alpha, 255); |
| |
| await tester.pump(const Duration(milliseconds: 100)); |
| await tester.pump(const Duration(milliseconds: 400)); |
| |
| expect(renderEditable.cursorColor!.alpha, 255); |
| |
| await tester.pump(const Duration(milliseconds: 200)); |
| await tester.pump(const Duration(milliseconds: 100)); |
| |
| expect(renderEditable.cursorColor!.alpha, 110); |
| |
| await tester.pump(const Duration(milliseconds: 100)); |
| |
| expect(renderEditable.cursorColor!.alpha, 16); |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| expect(renderEditable.cursorColor!.alpha, 0); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| // 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, affinity: TextAffinity.downstream)); |
| }); |
| |
| 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('Material cursor android golden', (WidgetTester tester) async { |
| final Widget widget = 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 = 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_${describeEnum(debugDefaultTargetPlatformOverride!).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( |
| const MaterialApp( |
| home: 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', |
| ); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| controller: controller, |
| toolbarOptions: const ToolbarOptions(copy: true), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| 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: 8, affinity: TextAffinity.downstream), |
| ); |
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); |
| 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( |
| 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( |
| 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, |
| selectionWidthStyle: ui.BoxWidthStyle.tight, |
| 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); |
| |
| // Use toolbar to select all text. |
| if (isContextMenuProvidedByPlatform) { |
| 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'), |
| ); |
| }); |
| |
| 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 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.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( |
| 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( |
| 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( |
| 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( |
| 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('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.iOS, 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(TextSelectionOverlay.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); |
| expect(fadeFinder, findsNothing); |
| |
| // 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)); |
| final FadeTransition 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); |
| expect(fadeFinder, findsNothing); |
| }); |
| |
| 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); |
| addTearDown(gesture.removePointer); |
| 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(FadeTransition), |
| matching: find.byType(SizedBox), |
| ).first); |
| expect(container.size, Size.zero); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows })); |
| |
| testWidgets('Sawping 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('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); |
| addTearDown(gesture.removePointer); |
| 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('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(fontFamily: 'Ahem', 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); |
| addTearDown(gesture.removePointer); |
| 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(4.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); |
| addTearDown(gesture.removePointer); |
| 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); |
| addTearDown(gesture.removePointer); |
| 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', (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); |
| final 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, 0); |
| 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, 11); |
| }); |
| |
| 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 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, |
| minLines: null, |
| 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.binding.window.viewInsetsTestValue = const _TestWindowPadding( |
| bottom: 500.0, |
| ); |
| addTearDown(tester.binding.window.clearViewInsetsTestValue); |
| 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.binding.window.clearViewInsetsTestValue(); |
| 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( |
| 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. |
| 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 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('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( |
| 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( |
| 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, 0); |
| expect(secondPos.dx, 0); |
| expect(thirdPos.dx, 0); |
| expect(middleStringPos.dx, 34); |
| 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.baseOffset, 39); |
| expect(controller.selection.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); |
| Offset newHandlePos = textOffsetToPosition(tester, testValue.indexOf('Third') + 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, |