| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:math' as math; |
| import 'dart:ui' as ui show window; |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart' show DragStartBehavior, PointerDeviceKind; |
| |
| import '../widgets/semantics_tester.dart'; |
| import 'feedback_tester.dart'; |
| |
| 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; |
| 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({ Widget child }) { |
| return 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: Overlay( |
| initialEntries: <OverlayEntry>[ |
| OverlayEntry( |
| builder: (BuildContext context) { |
| return Center( |
| child: Material( |
| child: child, |
| ), |
| ); |
| }, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| Widget boilerplate({ Widget child }) { |
| return 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; |
| } |
| |
| void main() { |
| final MockClipboard mockClipboard = MockClipboard(); |
| SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall); |
| |
| const String kThreeLines = |
| 'First line of text is\n' |
| 'Second line goes until\n' |
| 'Third line of stuff'; |
| const String kMoreThanFourLines = |
| kThreeLines + |
| '\nFourth line won\'t display and ends at'; |
| |
| // Returns the first RenderEditable. |
| RenderEditable findRenderEditable(WidgetTester tester) { |
| final RenderObject root = tester.renderObject(find.byType(EditableText)); |
| expect(root, isNotNull); |
| |
| RenderEditable renderEditable; |
| void recursiveFinder(RenderObject child) { |
| if (child is RenderEditable) { |
| renderEditable = child; |
| return; |
| } |
| child.visitChildren(recursiveFinder); |
| } |
| root.visitChildren(recursiveFinder); |
| expect(renderEditable, isNotNull); |
| return renderEditable; |
| } |
| |
| List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) { |
| return points.map<TextSelectionPoint>((TextSelectionPoint point) { |
| return TextSelectionPoint( |
| box.localToGlobal(point.point), |
| point.direction, |
| ); |
| }).toList(); |
| } |
| |
| Offset textOffsetToPosition(WidgetTester tester, int offset) { |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection( |
| TextSelection.collapsed(offset: offset), |
| ), |
| renderEditable, |
| ); |
| expect(endpoints.length, 1); |
| return endpoints[0].point + const Offset(0.0, -2.0); |
| } |
| |
| setUp(() { |
| debugResetSemanticsIdCounter(); |
| }); |
| |
| 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('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async { |
| final VoidCallback 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 { |
| await tester.enterText(find.byType(TextField), testValue); |
| // Check that the onChanged event handler fired. |
| expect(textFieldValue, equals(testValue)); |
| 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 checkCursorToggle(); |
| }); |
| |
| testWidgets('Cursor animates on iOS', (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| 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); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }); |
| |
| testWidgets('Cursor radius is 2.0 on iOS', (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| 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)); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }); |
| |
| 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.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)); |
| }); |
| |
| // TODO(hansmuller): restore these tests after the fix for #24876 has landed. |
| /* |
| testWidgets('cursor layout has correct width', (WidgetTester tester) async { |
| EditableText.debugDeterministicCursor = true; |
| await tester.pumpWidget( |
| overlay( |
| child: const RepaintBoundary( |
| child: TextField( |
| cursorWidth: 15.0, |
| ), |
| ), |
| ) |
| ); |
| await tester.enterText(find.byType(TextField), ' '); |
| await skipPastScrollingAnimation(tester); |
| |
| await expectLater( |
| find.byType(TextField), |
| matchesGoldenFile('text_field_test.0.0.png'), |
| ); |
| EditableText.debugDeterministicCursor = false; |
| }, skip: !Platform.isLinux); |
| |
| testWidgets('cursor layout has correct radius', (WidgetTester tester) async { |
| EditableText.debugDeterministicCursor = true; |
| await tester.pumpWidget( |
| overlay( |
| child: const RepaintBoundary( |
| child: TextField( |
| cursorWidth: 15.0, |
| cursorRadius: Radius.circular(3.0), |
| ), |
| ), |
| ) |
| ); |
| await tester.enterText(find.byType(TextField), ' '); |
| await skipPastScrollingAnimation(tester); |
| |
| await expectLater( |
| find.byType(TextField), |
| matchesGoldenFile('text_field_test.1.0.png'), |
| ); |
| EditableText.debugDeterministicCursor = false; |
| }, skip: !Platform.isLinux); |
| */ |
| |
| 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; |
| |
| // Gap between caret and edge of input, defined in editable.dart. |
| const int _kCaretGap = 1; |
| |
| expect(cursorOffsetSpaces.dx, inputWidth - _kCaretGap); |
| }); |
| |
| testWidgets('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.text; |
| expect(editText.substring(editText.length - 1), newChar); |
| |
| await tester.pump(const Duration(seconds: 2)); |
| |
| editText = findRenderEditable(tester).text.text; |
| expect(editText.substring(editText.length - 1), '\u2022'); |
| }); |
| |
| 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, -1); |
| expect(controller.selection.extentOffset, -1); |
| }); |
| |
| 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')); |
| final TestGesture gesture = await tester.startGesture(ePos, pointer: 7); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pump(); |
| |
| // 'def' is selected. |
| expect(controller.selection.baseOffset, testValue.indexOf('d')); |
| expect(controller.selection.extentOffset, testValue.indexOf('f')+1); |
| }); |
| |
| testWidgets('Mouse long press is just like a tap', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: controller, |
| ), |
| ) |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| expect(controller.value.text, testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| expect(controller.selection.isCollapsed, true); |
| |
| // Long press the 'e' using a mouse device. |
| final int eIndex = testValue.indexOf('e'); |
| final Offset ePos = textOffsetToPosition(tester, eIndex); |
| final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pump(); |
| |
| // The cursor is placed just like a regular tap. |
| expect(controller.selection.baseOffset, eIndex); |
| expect(controller.selection.extentOffset, eIndex); |
| }); |
| |
| testWidgets('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')); |
| final TestGesture gesture = await tester.startGesture(ePos, pointer: 7); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.selection.baseOffset, -1); |
| expect(controller.selection.extentOffset, -1); |
| }); |
| |
| testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); |
| |
| final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); |
| await tester.pump(); |
| await gesture.moveTo(gPos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, testValue.indexOf('e')); |
| expect(controller.selection.extentOffset, testValue.indexOf('g')); |
| }); |
| |
| testWidgets('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); |
| 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); |
| await tester.pump(); |
| await gesture.moveTo(ePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.baseOffset, testValue.indexOf('e')); |
| expect(controller.selection.extentOffset, testValue.indexOf('g')); |
| }); |
| |
| testWidgets('Slow mouse dragging also selects text', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'abc def ghi'; |
| await tester.enterText(find.byType(TextField), testValue); |
| await skipPastScrollingAnimation(tester); |
| |
| final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); |
| final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); |
| |
| final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.moveTo(gPos); |
| await tester.pump(); |
| await gesture.up(); |
| |
| expect(controller.selection.baseOffset, testValue.indexOf('e')); |
| expect(controller.selection.extentOffset, testValue.indexOf('g')); |
| }); |
| |
| testWidgets('Can drag handles to change selection', (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, 9); // Position of 'h'. |
| 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, 9); |
| |
| // Drag the left handle 2 letters to the left. |
| handlePos = endpoints[0].point + const Offset(-1.0, 1.0); |
| newHandlePos = textOffsetToPosition(tester, 2); // Position of 'c'. |
| 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, 2); |
| expect(controller.selection.extentOffset, 9); |
| }); |
| |
| 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 of '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(1.0, 1.0); |
| Offset newHandlePos = textOffsetToPosition(tester, 5); // Position of '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 of '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('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, |
| ); |
| 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 |
| |
| // 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(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| 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 |
| |
| // PASTE right before the 'e'. |
| await tester.tap(find.text('PASTE')); |
| await tester.pump(); |
| expect(controller.text, 'abc d${testValue}ef ghi'); |
| }); |
| |
| 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(); |
| 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, |
| ); |
| await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); |
| await tester.pump(); |
| |
| // Toolbar should fade in. Starting at 0% opacity. |
| final Element target = tester.element(find.text('SELECT ALL')); |
| final FadeTransition opacity = target.ancestorWidgetOfExactType(FadeTransition); |
| expect(opacity, isNotNull); |
| expect(opacity.opacity.value, equals(0.0)); |
| |
| // Still fading in. |
| await tester.pump(const Duration(milliseconds: 50)); |
| final FadeTransition opacity2 = target.ancestorWidgetOfExactType(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. |
| }); |
| |
| testWidgets('An obscured TextField is not selectable by default', (WidgetTester tester) async { |
| // This is a regression test for |
| // https://github.com/flutter/flutter/issues/24100 |
| |
| final TextEditingController controller = TextEditingController(); |
| Widget buildFrame(bool obscureText, bool enableInteractiveSelection) { |
| return overlay( |
| child: TextField( |
| controller: controller, |
| obscureText: obscureText, |
| enableInteractiveSelection: enableInteractiveSelection, |
| ), |
| ); |
| } |
| |
| // Obscure text and don't enable or disable selection |
| await tester.pumpWidget(buildFrame(true, null)); |
| await tester.enterText(find.byType(TextField), 'abcdefghi'); |
| await skipPastScrollingAnimation(tester); |
| expect(controller.selection.isCollapsed, true); |
| |
| // Long press doesn't select anything |
| final Offset ePos = textOffsetToPosition(tester, 1); |
| final TestGesture gesture = await tester.startGesture(ePos, pointer: 7); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pump(); |
| expect(controller.selection.isCollapsed, true); |
| }); |
| |
| testWidgets('An obscured TextField is selectable when enabled', (WidgetTester tester) async { |
| // This is a regression test for |
| // https://github.com/flutter/flutter/issues/24100 |
| |
| final TextEditingController controller = TextEditingController(); |
| Widget buildFrame(bool obscureText, bool enableInteractiveSelection) { |
| return overlay( |
| child: TextField( |
| controller: controller, |
| obscureText: obscureText, |
| enableInteractiveSelection: enableInteractiveSelection, |
| ), |
| ); |
| } |
| |
| // Explicitly allow selection on obscured text |
| await tester.pumpWidget(buildFrame(true, true)); |
| await tester.enterText(find.byType(TextField), 'abcdefghi'); |
| await skipPastScrollingAnimation(tester); |
| expect(controller.selection.isCollapsed, true); |
| |
| // Long press does select text |
| final Offset ePos2 = textOffsetToPosition(tester, 1); |
| final TestGesture gesture2 = await tester.startGesture(ePos2, pointer: 7); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture2.up(); |
| await tester.pump(); |
| expect(controller.selection.isCollapsed, false); |
| }); |
| |
| 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); |
| expect(() async { |
| await tester.pumpWidget(textFieldBuilder(minLines: 3)); |
| }, 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, |
| maxLengthEnforced: false, |
| 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: Container( |
| 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: Container( |
| 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 demostrate 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, |
| strutStyle: StrutStyle.disabled, |
| ), |
| ), |
| ); |
| |
| 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, 39); |
| expect(controller.selection.extentOffset, 50); |
| |
| // Drag the left handle to the first line, just after 'First'. |
| handlePos = endpoints[0].point + const Offset(-1.0, 1.0); |
| newHandlePos = textOffsetToPosition(tester, testValue.indexOf('First') + 5); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveTo(newHandlePos); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pump(); |
| |
| expect(controller.selection.baseOffset, 5); |
| expect(controller.selection.extentOffset, 50); |
| |
| await tester.tap(find.text('CUT')); |
| await tester.pump(); |
| expect(controller.selection.isCollapsed, true); |
| expect(controller.text, cutValue); |
| }); |
| |
| testWidgets('Can scroll multiline input', (WidgetTester tester) async { |
| final Key textFieldKey = UniqueKey(); |
| final TextEditingController controller = TextEditingController( |
| text: kMoreThanFourLines, |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| dragStartBehavior: DragStartBehavior.down, |
| key: textFieldKey, |
| controller: controller, |
| style: const TextStyle(color: Colors.black, fontSize: 34.0), |
| maxLines: 2, |
| ), |
| ), |
| ); |
| |
| RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); |
| final RenderBox inputBox = findInputBox(); |
| |
| // Check that the last line of text is not displayed. |
| final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); |
| final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth')); |
| expect(firstPos.dx, 0); |
| expect(fourthPos.dx, 0); |
| expect(firstPos.dx, fourthPos.dx); |
| expect(firstPos.dy, lessThan(fourthPos.dy)); |
| expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue); |
| expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(fourthPos)), isFalse); |
| |
| TestGesture gesture = await tester.startGesture(firstPos, pointer: 7); |
| await tester.pump(); |
| await gesture.moveBy(const Offset(0.0, -1000.0)); |
| await tester.pump(const Duration(seconds: 1)); |
| // Wait and drag again to trigger https://github.com/flutter/flutter/issues/6329 |
| // (No idea why this is necessary, but the bug wouldn't repro without it.) |
| await gesture.moveBy(const Offset(0.0, -1000.0)); |
| await tester.pump(const Duration(seconds: 1)); |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| // Now the first line is scrolled up, and the fourth line is visible. |
| Offset newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); |
| Offset newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth')); |
| |
| expect(newFirstPos.dy, lessThan(firstPos.dy)); |
| expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isFalse); |
| expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isTrue); |
| |
| // Now try scrolling by dragging the selection handle. |
| // Long press the middle of the word "won't" in the fourth line. |
| final Offset selectedWordPos = textOffsetToPosition( |
| tester, |
| kMoreThanFourLines.indexOf('Fourth line') + 14, |
| ); |
| |
| gesture = await tester.startGesture(selectedWordPos, pointer: 7); |
| await tester.pump(const Duration(seconds: 1)); |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| expect(controller.selection.base.offset, 77); |
| expect(controller.selection.extent.offset, 82); |
| // Sanity check for the word selected is the intended one. |
| expect( |
| controller.text.substring(controller.selection.baseOffset, controller.selection.extentOffset), |
| "won't", |
| ); |
| |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(controller.selection), |
| renderEditable, |
| ); |
| expect(endpoints.length, 2); |
| |
| // Drag the left handle to the first line, just after 'First'. |
| final Offset handlePos = endpoints[0].point + const Offset(-1, 1); |
| final Offset newHandlePos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First') + 5); |
| gesture = await tester.startGesture(handlePos, pointer: 7); |
| await tester.pump(const Duration(seconds: 1)); |
| await gesture.moveTo(newHandlePos + const Offset(0.0, -10.0)); |
| await tester.pump(const Duration(seconds: 1)); |
| await gesture.up(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| // The text should have scrolled up with the handle to keep the active |
| // cursor visible, back to its original position. |
| newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); |
| newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth')); |
| expect(newFirstPos.dy, firstPos.dy); |
| expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue); |
| expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse); |
| }); |
| |
| testWidgets('TextField smoke test', (WidgetTester tester) async { |
| String textFieldValue; |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| decoration: null, |
| onChanged: (String value) { |
| textFieldValue = value; |
| }, |
| ), |
| ), |
| ); |
| |
| Future<void> checkText(String testValue) { |
| return TestAsyncUtils.guard(() async { |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| // Check that the onChanged event handler fired. |
| expect(textFieldValue, equals(testValue)); |
| |
| await tester.pump(); |
| }); |
| } |
| |
| await checkText('Hello World'); |
| }); |
| |
| testWidgets('TextField with global key', (WidgetTester tester) async { |
| final GlobalKey textFieldKey = GlobalKey(debugLabel: 'textFieldKey'); |
| String textFieldValue; |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| key: textFieldKey, |
| decoration: const InputDecoration( |
| hintText: 'Placeholder', |
| ), |
| onChanged: (String value) { textFieldValue = value; }, |
| ), |
| ), |
| ); |
| |
| Future<void> checkText(String testValue) async { |
| return TestAsyncUtils.guard(() async { |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| // Check that the onChanged event handler fired. |
| expect(textFieldValue, equals(testValue)); |
| |
| await tester.pump(); |
| }); |
| } |
| |
| await checkText('Hello World'); |
| }); |
| |
| testWidgets('TextField errorText trumps helperText', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: const TextField( |
| decoration: InputDecoration( |
| errorText: 'error text', |
| helperText: 'helper text', |
| ), |
| ), |
| ), |
| ); |
| expect(find.text('helper text'), findsNothing); |
| expect(find.text('error text'), findsOneWidget); |
| }); |
| |
| testWidgets('TextField with default helperStyle', (WidgetTester tester) async { |
| final ThemeData themeData = ThemeData(hintColor: Colors.blue[500]); |
| await tester.pumpWidget( |
| overlay( |
| child: Theme( |
| data: themeData, |
| child: const TextField( |
| decoration: InputDecoration( |
| helperText: 'helper text', |
| ), |
| ), |
| ), |
| ), |
| ); |
| final Text helperText = tester.widget(find.text('helper text')); |
| expect(helperText.style.color, themeData.hintColor); |
| expect(helperText.style.fontSize, Typography.englishLike2014.caption.fontSize); |
| }); |
| |
| testWidgets('TextField with specified helperStyle', (WidgetTester tester) async { |
| final TextStyle style = TextStyle( |
| inherit: false, |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| decoration: InputDecoration( |
| helperText: 'helper text', |
| helperStyle: style, |
| ), |
| ), |
| ), |
| ); |
| final Text helperText = tester.widget(find.text('helper text')); |
| expect(helperText.style, style); |
| }); |
| |
| testWidgets('TextField with default hintStyle', (WidgetTester tester) async { |
| final TextStyle style = TextStyle( |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| final ThemeData themeData = ThemeData( |
| hintColor: Colors.blue[500], |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Theme( |
| data: themeData, |
| child: TextField( |
| decoration: const InputDecoration( |
| hintText: 'Placeholder', |
| ), |
| style: style, |
| ), |
| ), |
| ), |
| ); |
| |
| final Text hintText = tester.widget(find.text('Placeholder')); |
| expect(hintText.style.color, themeData.hintColor); |
| expect(hintText.style.fontSize, style.fontSize); |
| }); |
| |
| testWidgets('TextField with specified hintStyle', (WidgetTester tester) async { |
| final TextStyle hintStyle = TextStyle( |
| inherit: false, |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| decoration: InputDecoration( |
| hintText: 'Placeholder', |
| hintStyle: hintStyle, |
| ), |
| ), |
| ), |
| ); |
| |
| final Text hintText = tester.widget(find.text('Placeholder')); |
| expect(hintText.style, hintStyle); |
| }); |
| |
| testWidgets('TextField with specified prefixStyle', (WidgetTester tester) async { |
| final TextStyle prefixStyle = TextStyle( |
| inherit: false, |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| decoration: InputDecoration( |
| prefixText: 'Prefix:', |
| prefixStyle: prefixStyle, |
| ), |
| ), |
| ), |
| ); |
| |
| final Text prefixText = tester.widget(find.text('Prefix:')); |
| expect(prefixText.style, prefixStyle); |
| }); |
| |
| testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async { |
| final TextStyle suffixStyle = TextStyle( |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| decoration: InputDecoration( |
| suffixText: '.com', |
| suffixStyle: suffixStyle, |
| ), |
| ), |
| ), |
| ); |
| |
| final Text suffixText = tester.widget(find.text('.com')); |
| expect(suffixText.style, suffixStyle); |
| }); |
| |
| testWidgets('TextField prefix and suffix appear correctly with no hint or label', (WidgetTester tester) async { |
| final Key secondKey = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Column( |
| children: <Widget>[ |
| const TextField( |
| decoration: InputDecoration( |
| labelText: 'First', |
| ), |
| ), |
| TextField( |
| key: secondKey, |
| decoration: const InputDecoration( |
| prefixText: 'Prefix', |
| suffixText: 'Suffix', |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| expect(find.text('Prefix'), findsOneWidget); |
| expect(find.text('Suffix'), findsOneWidget); |
| |
| // Focus the Input. The prefix should still display. |
| await tester.tap(find.byKey(secondKey)); |
| await tester.pump(); |
| |
| expect(find.text('Prefix'), findsOneWidget); |
| expect(find.text('Suffix'), findsOneWidget); |
| |
| // Enter some text, and the prefix should still display. |
| await tester.enterText(find.byKey(secondKey), 'Hi'); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| expect(find.text('Prefix'), findsOneWidget); |
| expect(find.text('Suffix'), findsOneWidget); |
| }); |
| |
| testWidgets('TextField prefix and suffix appear correctly with hint text', (WidgetTester tester) async { |
| final TextStyle hintStyle = TextStyle( |
| inherit: false, |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| final Key secondKey = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Column( |
| children: <Widget>[ |
| const TextField( |
| decoration: InputDecoration( |
| labelText: 'First', |
| ), |
| ), |
| TextField( |
| key: secondKey, |
| decoration: InputDecoration( |
| hintText: 'Hint', |
| hintStyle: hintStyle, |
| prefixText: 'Prefix', |
| suffixText: 'Suffix', |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| // Neither the prefix or the suffix should initially be visible, only the hint. |
| expect(getOpacity(tester, find.text('Prefix')), 0.0); |
| expect(getOpacity(tester, find.text('Suffix')), 0.0); |
| expect(getOpacity(tester, find.text('Hint')), 1.0); |
| |
| await tester.tap(find.byKey(secondKey)); |
| await tester.pumpAndSettle(); |
| |
| // Focus the Input. The hint, prefix, and suffix should appear |
| expect(getOpacity(tester, find.text('Prefix')), 1.0); |
| expect(getOpacity(tester, find.text('Suffix')), 1.0); |
| expect(getOpacity(tester, find.text('Hint')), 1.0); |
| |
| // Enter some text, and the hint should disappear and the prefix and suffix |
| // should continue to be visible |
| await tester.enterText(find.byKey(secondKey), 'Hi'); |
| await tester.pumpAndSettle(); |
| |
| expect(getOpacity(tester, find.text('Prefix')), 1.0); |
| expect(getOpacity(tester, find.text('Suffix')), 1.0); |
| expect(getOpacity(tester, find.text('Hint')), 0.0); |
| |
| // Check and make sure that the right styles were applied. |
| final Text prefixText = tester.widget(find.text('Prefix')); |
| expect(prefixText.style, hintStyle); |
| final Text suffixText = tester.widget(find.text('Suffix')); |
| expect(suffixText.style, hintStyle); |
| }); |
| |
| testWidgets('TextField prefix and suffix appear correctly with label text', (WidgetTester tester) async { |
| final TextStyle prefixStyle = TextStyle( |
| color: Colors.pink[500], |
| fontSize: 10.0, |
| ); |
| final TextStyle suffixStyle = TextStyle( |
| color: Colors.green[500], |
| fontSize: 12.0, |
| ); |
| final Key secondKey = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Column( |
| children: <Widget>[ |
| const TextField( |
| decoration: InputDecoration( |
| labelText: 'First', |
| ), |
| ), |
| TextField( |
| key: secondKey, |
| decoration: InputDecoration( |
| labelText: 'Label', |
| prefixText: 'Prefix', |
| prefixStyle: prefixStyle, |
| suffixText: 'Suffix', |
| suffixStyle: suffixStyle, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| // Not focused. The prefix and suffix should not appear, but the label should. |
| expect(getOpacity(tester, find.text('Prefix')), 0.0); |
| expect(getOpacity(tester, find.text('Suffix')), 0.0); |
| expect(find.text('Label'), findsOneWidget); |
| |
| // Focus the input. The label, prefix, and suffix should appear. |
| await tester.tap(find.byKey(secondKey)); |
| await tester.pumpAndSettle(); |
| |
| expect(getOpacity(tester, find.text('Prefix')), 1.0); |
| expect(getOpacity(tester, find.text('Suffix')), 1.0); |
| expect(find.text('Label'), findsOneWidget); |
| |
| // Enter some text. The label, prefix, and suffix should remain visible. |
| await tester.enterText(find.byKey(secondKey), 'Hi'); |
| await tester.pumpAndSettle(); |
| |
| expect(getOpacity(tester, find.text('Prefix')), 1.0); |
| expect(getOpacity(tester, find.text('Suffix')), 1.0); |
| expect(find.text('Label'), findsOneWidget); |
| |
| // Check and make sure that the right styles were applied. |
| final Text prefixText = tester.widget(find.text('Prefix')); |
| expect(prefixText.style, prefixStyle); |
| final Text suffixText = tester.widget(find.text('Suffix')); |
| expect(suffixText.style, suffixStyle); |
| }); |
| |
| testWidgets('TextField label text animates', (WidgetTester tester) async { |
| final Key secondKey = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Column( |
| children: <Widget>[ |
| const TextField( |
| decoration: InputDecoration( |
| labelText: 'First', |
| ), |
| ), |
| TextField( |
| key: secondKey, |
| decoration: const InputDecoration( |
| labelText: 'Second', |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| Offset pos = tester.getTopLeft(find.text('Second')); |
| |
| // Focus the Input. The label should start animating upwards. |
| await tester.tap(find.byKey(secondKey)); |
| await tester.idle(); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| Offset newPos = tester.getTopLeft(find.text('Second')); |
| expect(newPos.dy, lessThan(pos.dy)); |
| |
| // Label should still be sliding upward. |
| await tester.pump(const Duration(milliseconds: 50)); |
| pos = newPos; |
| newPos = tester.getTopLeft(find.text('Second')); |
| expect(newPos.dy, lessThan(pos.dy)); |
| }); |
| |
| testWidgets('Icon is separated from input/label by 16+12', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: const TextField( |
| decoration: InputDecoration( |
| icon: Icon(Icons.phone), |
| labelText: 'label', |
| filled: true, |
| ), |
| ), |
| ), |
| ); |
| final double iconRight = tester.getTopRight(find.byType(Icon)).dx; |
| // Per https://material.io/go/design-text-fields#text-fields-layout |
| // There's a 16 dps gap between the right edge of the icon and the text field's |
| // container, and the 12dps more padding between the left edge of the container |
| // and the left edge of the input and label. |
| expect(iconRight + 28.0, equals(tester.getTopLeft(find.text('label')).dx)); |
| expect(iconRight + 28.0, equals(tester.getTopLeft(find.byType(EditableText)).dx)); |
| }); |
| |
| testWidgets('Collapsed hint text placement', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: const TextField( |
| decoration: InputDecoration.collapsed( |
| hintText: 'hint', |
| ), |
| strutStyle: StrutStyle.disabled, |
| ), |
| ), |
| ); |
| |
| expect(tester.getTopLeft(find.text('hint')), equals(tester.getTopLeft(find.byType(TextField)))); |
| }); |
| |
| testWidgets('Can align to center', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: Container( |
| width: 300.0, |
| child: const TextField( |
| textAlign: TextAlign.center, |
| decoration: null, |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderEditable editable = findRenderEditable(tester); |
| Offset topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft, |
| ); |
| |
| // The overlay() function centers its child within a 800x600 window. |
| // Default cursorWidth is 2.0, test windowWidth is 800 |
| // Centered cursor topLeft.dx: 399 == windowWidth/2 - cursorWidth/2 |
| expect(topLeft.dx, equals(399.0)); |
| |
| await tester.enterText(find.byType(TextField), 'abcd'); |
| await tester.pump(); |
| |
| topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft, |
| ); |
| |
| // TextPosition(offset: 2) - center of 'abcd' |
| expect(topLeft.dx, equals(399.0)); |
| }); |
| |
| testWidgets('Can align to center within center', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| overlay( |
| child: Container( |
| width: 300.0, |
| child: const Center( |
| child: TextField( |
| textAlign: TextAlign.center, |
| decoration: null, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderEditable editable = findRenderEditable(tester); |
| Offset topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft, |
| ); |
| |
| // The overlay() function centers its child within a 800x600 window. |
| // Default cursorWidth is 2.0, test windowWidth is 800 |
| // Centered cursor topLeft.dx: 399 == windowWidth/2 - cursorWidth/2 |
| expect(topLeft.dx, equals(399.0)); |
| |
| await tester.enterText(find.byType(TextField), 'abcd'); |
| await tester.pump(); |
| |
| topLeft = editable.localToGlobal( |
| editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft, |
| ); |
| |
| // TextPosition(offset: 2) - center of 'abcd' |
| expect(topLeft.dx, equals(399.0)); |
| }); |
| |
| testWidgets('Controller can update server', (WidgetTester tester) async { |
| final TextEditingController controller1 = TextEditingController( |
| text: 'Initial Text', |
| ); |
| final TextEditingController controller2 = TextEditingController( |
| text: 'More Text', |
| ); |
| |
| TextEditingController currentController; |
| StateSetter setState; |
| |
| await tester.pumpWidget( |
| overlay( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setter) { |
| setState = setter; |
| return TextField(controller: currentController); |
| } |
| ), |
| ), |
| ); |
| expect(tester.testTextInput.editingState['text'], isEmpty); |
| |
| // Initial state with null controller. |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| expect(tester.testTextInput.editingState['text'], isEmpty); |
| |
| // Update the controller from null to controller1. |
| setState(() { |
| currentController = controller1; |
| }); |
| await tester.pump(); |
| expect(tester.testTextInput.editingState['text'], equals('Initial Text')); |
| |
| // Verify that updates to controller1 are handled. |
| controller1.text = 'Updated Text'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState['text'], equals('Updated Text')); |
| |
| // Verify that switching from controller1 to controller2 is handled. |
| setState(() { |
| currentController = controller2; |
| }); |
| await tester.pump(); |
| expect(tester.testTextInput.editingState['text'], equals('More Text')); |
| |
| // Verify that updates to controller1 are ignored. |
| controller1.text = 'Ignored Text'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState['text'], equals('More Text')); |
| |
| // Verify that updates to controller text are handled. |
| controller2.text = 'Additional Text'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState['text'], equals('Additional Text')); |
| |
| // Verify that updates to controller selection are handled. |
| controller2.selection = const TextSelection(baseOffset: 0, extentOffset: 5); |
| await tester.idle(); |
| expect(tester.testTextInput.editingState['selectionBase'], equals(0)); |
| expect(tester.testTextInput.editingState['selectionExtent'], equals(5)); |
| |
| // Verify that calling clear() clears the text. |
| controller2.clear(); |
| await tester.idle(); |
| expect(tester.testTextInput.editingState['text'], equals('')); |
| |
| // Verify that switching from controller2 to null preserves current text. |
| controller2.text = 'The Final Cut'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState['text'], equals('The Final Cut')); |
| setState(() { |
| currentController = null; |
| }); |
| await tester.pump(); |
| expect(tester.testTextInput.editingState['text'], equals('The Final Cut')); |
| |
| // Verify that changes to controller2 are ignored. |
| controller2.text = 'Goodbye Cruel World'; |
| expect(tester.testTextInput.editingState['text'], equals('The Final Cut')); |
| }); |
| |
| testWidgets('Cannot enter new lines onto single line TextField', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField(controller: textController, decoration: null), |
| )); |
| |
| await tester.enterText(find.byType(TextField), 'abc\ndef'); |
| |
| expect(textController.text, 'abcdef'); |
| }); |
| |
| testWidgets('Injected formatters are chained', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| decoration: null, |
| inputFormatters: <TextInputFormatter> [ |
| BlacklistingTextInputFormatter( |
| RegExp(r'[a-z]'), |
| replacementString: '#', |
| ), |
| ], |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), 'a一b二c三\ndå››e五få…'); |
| // The default single line formatter replaces \n with empty string. |
| expect(textController.text, '#一#二#三#å››#五#å…'); |
| }); |
| |
| testWidgets('Chained formatters are in sequence', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| decoration: null, |
| maxLines: 2, |
| inputFormatters: <TextInputFormatter> [ |
| BlacklistingTextInputFormatter( |
| RegExp(r'[a-z]'), |
| replacementString: '12\n', |
| ), |
| WhitelistingTextInputFormatter(RegExp(r'\n[0-9]')), |
| ], |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), 'a1b2c3'); |
| // The first formatter turns it into |
| // 12\n112\n212\n3 |
| // The second formatter turns it into |
| // \n1\n2\n3 |
| // Multiline is allowed since maxLine != 1. |
| expect(textController.text, '\n1\n2\n3'); |
| }); |
| |
| testWidgets('Pasted values are formatted', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: TextField( |
| controller: textController, |
| decoration: null, |
| inputFormatters: <TextInputFormatter> [ |
| WhitelistingTextInputFormatter.digitsOnly, |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.enterText(find.byType(TextField), 'a1b\n2c3'); |
| expect(textController.text, '123'); |
| await skipPastScrollingAnimation(tester); |
| |
| await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2'))); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| final RenderEditable renderEditable = findRenderEditable(tester); |
| final List<TextSelectionPoint> endpoints = globalize( |
| renderEditable.getEndpointsForSelection(textController.selection), |
| renderEditable, |
| ); |
| await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero |
| |
| Clipboard.setData(const ClipboardData(text: '一4二\n5三6')); |
| await tester.tap(find.text('PASTE')); |
| await tester.pump(); |
| // Puts 456 before the 2 in 123. |
| expect(textController.text, '145623'); |
| }); |
| |
| testWidgets('Text field scrolls the caret into view', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Container( |
| width: 100.0, |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| final String longText = 'a' * 20; |
| await tester.enterText(find.byType(TextField), longText); |
| await skipPastScrollingAnimation(tester); |
| |
| ScrollableState scrollableState = tester.firstState(find.byType(Scrollable)); |
| expect(scrollableState.position.pixels, equals(0.0)); |
| |
| // Move the caret to the end of the text and check that the text field |
| // scrolls to make the caret visible. |
| controller.selection = TextSelection.collapsed(offset: longText.length); |
| await tester.pump(); // TODO(ianh): Figure out why this extra pump is needed. |
| await skipPastScrollingAnimation(tester); |
| |
| scrollableState = tester.firstState(find.byType(Scrollable)); |
| // For a horizontal input, scrolls to the exact position of the caret. |
| expect(scrollableState.position.pixels, equals(222.0)); |
| }); |
| |
| testWidgets('Multiline text field scrolls the caret into view', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Container( |
| child: TextField( |
| controller: controller, |
| maxLines: 6, |
| ), |
| ), |
| ), |
| ); |
| |
| const String tallText = 'a\nb\nc\nd\ne\nf\ng'; // One line over max |
| await tester.enterText(find.byType(TextField), tallText); |
| await skipPastScrollingAnimation(tester); |
| |
| ScrollableState scrollableState = tester.firstState(find.byType(Scrollable)); |
| expect(scrollableState.position.pixels, equals(0.0)); |
| |
| // Move the caret to the end of the text and check that the text field |
| // scrolls to make the caret visible. |
| controller.selection = const TextSelection.collapsed(offset: tallText.length); |
| await tester.pump(); |
| await skipPastScrollingAnimation(tester); |
| |
| // Should have scrolled down exactly one line height (7 lines of text in 6 |
| // line text field). |
| final double lineHeight = findRenderEditable(tester).preferredLineHeight; |
| scrollableState = tester.firstState(find.byType(Scrollable)); |
| expect(scrollableState.position.pixels, closeTo(lineHeight, 0.1)); |
| }); |
| |
| testWidgets('haptic feedback', (WidgetTester tester) async { |
| final FeedbackTester feedback = FeedbackTester(); |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Container( |
| width: 100.0, |
| child: TextField( |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(const Duration(seconds: 1)); |
| expect(feedback.clickSoundCount, 0); |
| expect(feedback.hapticCount, 0); |
| |
| await tester.longPress(find.byType(TextField)); |
| await tester.pumpAndSettle(const Duration(seconds: 1)); |
| expect(feedback.clickSoundCount, 0); |
| expect(feedback.hapticCount, 1); |
| |
| feedback.dispose(); |
| }); |
| |
| testWidgets('Text field drops selection when losing focus', (WidgetTester tester) async { |
| final Key key1 = UniqueKey(); |
| final TextEditingController controller1 = TextEditingController(); |
| final Key key2 = UniqueKey(); |
| |
| await tester.pumpWidget( |
| overlay( |
| child: Column( |
| children: <Widget>[ |
| TextField( |
| key: key1, |
| controller: controller1, |
| ), |
| TextField(key: key2), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byKey(key1)); |
| await tester.enterText(find.byKey(key1), 'abcd'); |
| await tester.pump(); |
| controller1.selection = const TextSelection(baseOffset: 0, extentOffset: 3); |
| await tester.pump(); |
| expect(controller1.selection, isNot(equals(TextRange.empty))); |
| |
| await tester.tap(find.byKey(key2)); |
| await tester.pump(); |
| expect(controller1.selection, equals(TextRange.empty)); |
| }); |
| |
| testWidgets('Selection is consistent with text length', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| |
| controller.text = 'abcde'; |
| controller.selection = const TextSelection.collapsed(offset: 5); |
| |
| controller.text = ''; |
| expect(controller.selection.start, lessThanOrEqualTo(0)); |
| expect(controller.selection.end, lessThanOrEqualTo(0)); |
| |
| expect(() { |
| controller.selection = const TextSelection.collapsed(offset: 10); |
| }, throwsFlutterError); |
| }); |
| |
| testWidgets('maxLength limits input.', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), '0123456789101112'); |
| expect(textController.text, '0123456789'); |
| }); |
| |
| testWidgets('maxLength limits input length even if decoration is null.', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| decoration: null, |
| maxLength: 10, |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), '0123456789101112'); |
| expect(textController.text, '0123456789'); |
| }); |
| |
| testWidgets('maxLength still works with other formatters.', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| inputFormatters: <TextInputFormatter> [ |
| BlacklistingTextInputFormatter( |
| RegExp(r'[a-z]'), |
| replacementString: '#', |
| ), |
| ], |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), 'a一b二c三\ndå››e五få…'); |
| // The default single line formatter replaces \n with empty string. |
| expect(textController.text, '#一#二#三#四#五'); |
| }); |
| |
| testWidgets("maxLength isn't enforced when maxLengthEnforced is false.", (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| controller: textController, |
| maxLength: 10, |
| maxLengthEnforced: false, |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), '0123456789101112'); |
| expect(textController.text, '0123456789101112'); |
| }); |
| |
| testWidgets('maxLength shows warning when maxLengthEnforced is false.', (WidgetTester tester) async { |
| final TextEditingController textController = TextEditingController(); |
| const TextStyle testStyle = TextStyle(color: Colors.deepPurpleAccent); |
| |
| await tester.pumpWidget(boilerplate( |
| child: TextField( |
| decoration: const InputDecoration(errorStyle: testStyle), |
| controller: textController, |
| maxLength: 10, |
| maxLengthEnforced: false, |
| ), |
| )); |
| |
| await tester.enterText(find.byType(TextField), '0123456789101112'); |
| await tester.pump(); |
| |
| expect(textController.text, '0123456789101112'); |
| expect(find.text('16/10'), findsOneWidget); |
| Text counterTextWidget = tester.widget(find.text('16/10')); |
| expect(counterTextWidget.style.color, equals(Colors.deepPurpleAccent)); |
| |
| await tester.enterText(find.byType(TextField), '0123456789'); |
| await tester.pump(); |
| |
| expect(textController.text, '0123456789'); |
| expect(find.text('10/10'), findsOneWidget); |
| counterTextWidget = tester.widget(find.text('10/10')); |
| expect(counterTextWidget.style.color, isNot(equals(Colors.deepPurpleAccent))); |
| }); |
| |
| testWidgets('setting maxLength shows counter', (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLength: 10, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('0/10'), findsOneWidget); |
| |
| await tester.enterText(find.byType(TextField), '01234'); |
| await tester.pump(); |
| |
| expect(find.text('5/10'), findsOneWidget); |
| }); |
| |
| testWidgets('setting maxLength to TextField.noMaxLength shows only entered length', (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLength: TextField.noMaxLength, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('0'), findsOneWidget); |
| |
| await tester.enterText(find.byType(TextField), '01234'); |
| await tester.pump(); |
| |
| expect(find.text('5'), findsOneWidget); |
| }); |
| |
| testWidgets('passing a buildCounter shows returned widget', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| buildCounter: (BuildContext context, { int currentLength, int maxLength, bool isFocused }) { |
| return Text('${currentLength.toString()} of ${maxLength.toString()}'); |
| }, |
| maxLength: 10, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('0 of 10'), findsOneWidget); |
| |
| await tester.enterText(find.byType(TextField), '01234'); |
| await tester.pump(); |
| |
| expect(find.text('5 of 10'), findsOneWidget); |
| }); |
| |
| testWidgets('TextField identifies as text field in semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| maxLength: 10, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField])); |
| |
| semantics.dispose(); |
| }); |
| |
| void sendFakeKeyEvent(Map<String, dynamic> data) { |
| BinaryMessages.handlePlatformMessage( |
| SystemChannels.keyEvent.name, |
| SystemChannels.keyEvent.codec.encodeMessage(data), |
| (ByteData data) { }, |
| ); |
| } |
| |
| void sendKeyEventWithCode(int code, bool down, bool shiftDown, bool ctrlDown) { |
| |
| int metaState = shiftDown ? 1 : 0; |
| if (ctrlDown) |
| metaState |= 1 << 12; |
| |
| sendFakeKeyEvent(<String, dynamic>{ |
| 'type': down ? 'keydown' : 'keyup', |
| 'keymap': 'android', |
| 'keyCode': code, |
| 'hidUsage': 0x04, |
| 'codePoint': 0x64, |
| 'metaState': metaState, |
| }); |
| } |
| |
| group('Keyboard Tests', () { |
| TextEditingController controller; |
| |
| setUp( () { |
| controller = TextEditingController(); |
| }); |
| |
| MaterialApp setupWidget() { |
| |
| final FocusNode focusNode = FocusNode(); |
| controller = TextEditingController(); |
| |
| return MaterialApp( |
| home: Material( |
| child: RawKeyboardListener( |
| focusNode: focusNode, |
| onKey: null, |
| child: TextField( |
| controller: controller, |
| maxLines: 3, |
| strutStyle: StrutStyle.disabled, |
| ), |
| ) , |
| ), |
| ); |
| } |
| |
| testWidgets('Shift test 1', (WidgetTester tester) async { |
| |
| await tester.pumpWidget(setupWidget()); |
| const String testValue = 'a big house'; |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown, SHIFT_ON |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 1); |
| }); |
| |
| testWidgets('Control Shift test', (WidgetTester tester) async { |
| await tester.pumpWidget(setupWidget()); |
| const String testValue = 'their big house'; |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| await tester.pumpAndSettle(); |
| sendKeyEventWithCode(22, true, true, true); // RIGHT_ARROW keydown SHIFT_ON, CONTROL_ON |
| |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 5); |
| }); |
| |
| testWidgets('Down and up test', (WidgetTester tester) async { |
| await tester.pumpWidget(setupWidget()); |
| const String testValue = 'a big house'; |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| sendKeyEventWithCode(20, true, true, false); // DOWN_ARROW keydown |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 11); |
| |
| sendKeyEventWithCode(20, false, true, false); // DOWN_ARROW keyup |
| await tester.pumpAndSettle(); |
| sendKeyEventWithCode(19, true, true, false); // UP_ARROW keydown |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 0); |
| }); |
| |
| testWidgets('Down and up test 2', (WidgetTester tester) async { |
| await tester.pumpWidget(setupWidget()); |
| const String testValue = 'a big house\njumped over a mouse\nOne more line yay'; // 11 \n 19 |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| for (int i = 0; i < 5; i += 1) { |
| sendKeyEventWithCode(22, true, false, false); // RIGHT_ARROW keydown |
| await tester.pumpAndSettle(); |
| sendKeyEventWithCode(22, false, false, false); // RIGHT_ARROW keyup |
| await tester.pumpAndSettle(); |
| } |
| sendKeyEventWithCode(20, true, true, false); // DOWN_ARROW keydown |
| await tester.pumpAndSettle(); |
| sendKeyEventWithCode(20, false, true, false); // DOWN_ARROW keyup |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 12); |
| |
| sendKeyEventWithCode(20, true, true, false); // DOWN_ARROW keydown |
| await tester.pumpAndSettle(); |
| sendKeyEventWithCode(20, false, true, false); // DOWN_ARROW keyup |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 32); |
| |
| sendKeyEventWithCode(19, true, true, false); // UP_ARROW keydown |
| await tester.pumpAndSettle(); |
| sendKeyEventWithCode(19, false, true, false); // UP_ARROW keyup |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 12); |
| |
| sendKeyEventWithCode(19, true, true, false); // UP_ARROW keydown |
| await tester.pumpAndSettle(); |
| sendKeyEventWithCode(19, false, true, false); // UP_ARROW keyup |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 0); |
| |
| sendKeyEventWithCode(19, true, true, false); // UP_ARROW keydown |
| await tester.pumpAndSettle(); |
| sendKeyEventWithCode(19, false, true, false); // UP_ARROW keyup |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.extentOffset - controller.selection.baseOffset, 5); |
| }); |
| }); |
| |
| const int _kXKeyCode = 52; |
| const int _kCKeyCode = 31; |
| const int _kVKeyCode = 50; |
| const int _kAKeyCode = 29; |
| const int _kDelKeyCode = 112; |
| |
| testWidgets('Copy paste test', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| final TextEditingController controller = TextEditingController(); |
| final TextField textField = |
| TextField( |
| controller: controller, |
| maxLines: 3, |
| ); |
| |
| String clipboardContent = ''; |
| SystemChannels.platform |
| .setMockMethodCallHandler((MethodCall methodCall) async { |
| if (methodCall.method == 'Clipboard.setData') |
| clipboardContent = methodCall.arguments['text']; |
| else if (methodCall.method == 'Clipboard.getData') |
| return <String, dynamic>{'text': clipboardContent}; |
| return null; |
| }); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: RawKeyboardListener( |
| focusNode: focusNode, |
| onKey: null, |
| child: textField, |
| ), |
| ), |
| ), |
| ); |
| |
| const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 |
| await tester.enterText(find.byType(TextField), testValue); |
| |
| await tester.idle(); |
| await tester.tap(find.byType(TextField)); |
| await tester.pumpAndSettle(); |
| |
| // Select the first 5 characters |
| for (int i = 0; i < 5; i += 1) { |
| sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown shift |
| await tester.pumpAndSettle(); |
| sendKeyEventWithCode(22, false, false, false); // RIGHT_ARROW keyup |
| await tester.pumpAndSettle(); |
| } |
| |
| // Copy them |
| sendKeyEventWithCode(_kCKeyCode, true, false, true); // keydown control |
| await tester.pumpAndSettle(); |
| sendKeyEventWithCode(_kCKeyCode, false, false, false); // keyup control |
| await tester.pumpAndSettle(); |
| |
| expect(clipboardContent, 'a big'); |
| |
| sendKeyEventWithCode(22, true, false, false); // RIGHT_ARROW keydown |
| await tester.pumpAndSettle(); |
| sendKeyEventWithCode(22, false, false, false); // RIGHT_ARROW keyup |
| await tester.pumpAndSettle(); |
| |
| // Paste them |
| sendKeyEventWithCode(_kVKeyCode, true, false, true); // Control V keydown |
| await tester.pumpAndSettle(); |
|