blob: e3afd170f5b5abc61cf9dbcda20823c264906980 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// no-shuffle:
// //TODO(gspencergoog): Remove this tag once this test's state leaks/test
// dependencies have been fixed.
// https://github.com/flutter/flutter/issues/85160
// Fails with "flutter test --test-randomize-ordering-seed=456"
// reduced-test-set:
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set', 'no-shuffle'])
import 'dart:math' as math;
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, WindowPadding;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/clipboard_utils.dart';
import '../widgets/editable_text_utils.dart' show OverflowWidgetTextEditingController, findRenderEditable, globalize, textOffsetToPosition;
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';
typedef FormatEditUpdateCallback = void Function(TextEditingValue, TextEditingValue);
// On web, the context menu (aka toolbar) is provided by the browser.
const bool isContextMenuProvidedByPlatform = isBrowser;
// On web, key events in text fields are handled by the browser.
const bool areKeyEventsHandledByPlatform = isBrowser;
class MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
@override
bool isSupported(Locale locale) => true;
@override
Future<MaterialLocalizations> load(Locale locale) => DefaultMaterialLocalizations.load(locale);
@override
bool shouldReload(MaterialLocalizationsDelegate old) => false;
}
class WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
@override
bool isSupported(Locale locale) => true;
@override
Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale);
@override
bool shouldReload(WidgetsLocalizationsDelegate old) => false;
}
Widget overlay({ required Widget child }) {
final OverlayEntry entry = OverlayEntry(
builder: (BuildContext context) {
return Center(
child: Material(
child: child,
),
);
},
);
return overlayWithEntry(entry);
}
Widget overlayWithEntry(OverlayEntry entry) {
return Localizations(
locale: const Locale('en', 'US'),
delegates: <LocalizationsDelegate<dynamic>>[
WidgetsLocalizationsDelegate(),
MaterialLocalizationsDelegate(),
],
child: DefaultTextEditingShortcuts(
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(size: Size(800.0, 600.0)),
child: Overlay(
initialEntries: <OverlayEntry>[
entry,
],
),
),
),
),
);
}
Widget boilerplate({ required Widget child, ThemeData? theme }) {
return MaterialApp(
theme: theme,
home: Localizations(
locale: const Locale('en', 'US'),
delegates: <LocalizationsDelegate<dynamic>>[
WidgetsLocalizationsDelegate(),
MaterialLocalizationsDelegate(),
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(size: Size(800.0, 600.0)),
child: Center(
child: Material(
child: child,
),
),
),
),
),
);
}
Future<void> skipPastScrollingAnimation(WidgetTester tester) async {
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
}
double getOpacity(WidgetTester tester, Finder finder) {
return tester.widget<FadeTransition>(
find.ancestor(
of: finder,
matching: find.byType(FadeTransition),
),
).opacity.value;
}
class TestFormatter extends TextInputFormatter {
TestFormatter(this.onFormatEditUpdate);
FormatEditUpdateCallback onFormatEditUpdate;
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
onFormatEditUpdate(oldValue, newValue);
return newValue;
}
}
// Used to set window.viewInsets since the real ui.WindowPadding has only a
// private constructor.
class _TestWindowPadding implements ui.WindowPadding {
const _TestWindowPadding({
required this.bottom,
});
@override
final double bottom;
@override
double get top => 0.0;
@override
double get left => 0.0;
@override
double get right => 0.0;
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final MockClipboard mockClipboard = MockClipboard();
const String kThreeLines =
'First line of text is\n'
'Second line goes until\n'
'Third line of stuff';
const String kMoreThanFourLines =
'$kThreeLines\n'
"Fourth line won't display and ends at";
// Gap between caret and edge of input, defined in editable.dart.
const int kCaretGap = 1;
setUp(() async {
debugResetSemanticsIdCounter();
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
mockClipboard.handleMethodCall,
);
// Fill the clipboard so that the Paste option is available in the text
// selection menu.
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
null,
);
});
final Key textFieldKey = UniqueKey();
Widget textFieldBuilder({
int? maxLines = 1,
int? minLines,
}) {
return boilerplate(
child: TextField(
key: textFieldKey,
style: const TextStyle(color: Colors.black, fontSize: 34.0),
maxLines: maxLines,
minLines: minLines,
decoration: const InputDecoration(
hintText: 'Placeholder',
),
),
);
}
testWidgets('text field selection toolbar should hide when the user starts typing', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 100,
height: 100,
child: TextField(
decoration: InputDecoration(hintText: 'Placeholder'),
),
),
),
),
),
);
await tester.showKeyboard(find.byType(TextField));
const String testValue = 'A B C';
tester.testTextInput.updateEditingValue(
const TextEditingValue(
text: testValue,
),
);
await tester.pump();
// The selectWordsInRange with SelectionChangedCause.tap seems to be needed to show the toolbar.
// (This is true even if we provide selection parameter to the TextEditingValue above.)
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
state.renderEditable.selectWordsInRange(from: Offset.zero, cause: SelectionChangedCause.tap);
expect(state.showToolbar(), true);
// This is needed for the AnimatedOpacity to turn from 0 to 1 so the toolbar is visible.
await tester.pumpAndSettle();
// Sanity check that the toolbar widget exists.
expect(find.text('Paste'), findsOneWidget);
const String newValue = 'A B C D';
tester.testTextInput.updateEditingValue(
const TextEditingValue(
text: newValue,
),
);
await tester.pump();
expect(state.selectionOverlay!.toolbarIsVisible, isFalse);
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
testWidgets('Composing change does not hide selection handle caret', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/108673
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
),
),
);
const String testValue = 'I Love Flutter!';
await tester.enterText(find.byType(TextField), testValue);
expect(controller.value.text, testValue);
await skipPastScrollingAnimation(tester);
// Handle not shown.
expect(controller.selection.isCollapsed, true);
final Finder fadeFinder = find.byType(FadeTransition);
FadeTransition handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(0.0));
// Tap on the text field to show the handle.
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
expect(fadeFinder, findsNWidgets(1));
handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(1.0));
final RenderObject handleRenderObjectBegin = tester.renderObject(fadeFinder.at(0));
expect(
controller.value,
const TextEditingValue(
text: 'I Love Flutter!',
selection: TextSelection.collapsed(offset: 15, affinity: TextAffinity.upstream),
),
);
// Simulate text composing change.
tester.testTextInput.updateEditingValue(
controller.value.copyWith(
composing: const TextRange(start: 7, end: 15),
),
);
await skipPastScrollingAnimation(tester);
expect(
controller.value,
const TextEditingValue(
text: 'I Love Flutter!',
selection: TextSelection.collapsed(offset: 15, affinity: TextAffinity.upstream),
composing: TextRange(start: 7, end: 15),
),
);
// Handle still shown.
expect(controller.selection.isCollapsed, true);
handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(1.0));
// Simulate text composing and affinity change.
tester.testTextInput.updateEditingValue(
controller.value.copyWith(
selection: controller.value.selection.copyWith(affinity: TextAffinity.downstream),
composing: const TextRange(start: 8, end: 15),
),
);
await skipPastScrollingAnimation(tester);
expect(
controller.value,
const TextEditingValue(
text: 'I Love Flutter!',
selection: TextSelection.collapsed(offset: 15, affinity: TextAffinity.upstream),
composing: TextRange(start: 8, end: 15),
),
);
// Handle still shown.
expect(controller.selection.isCollapsed, true);
handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(1.0));
final RenderObject handleRenderObjectEnd = tester.renderObject(fadeFinder.at(0));
// The RenderObject sub-tree should not be unmounted.
expect(identical(handleRenderObjectBegin, handleRenderObjectEnd), true);
});
testWidgets('can use the desktop cut/copy/paste buttons on Mac', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'blah1 blah2',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
// Initially, the menu is not shown and there is no selection.
expect(find.byType(CupertinoButton), findsNothing);
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
final Offset midBlah1 = textOffsetToPosition(tester, 2);
// Right clicking shows the menu.
final TestGesture gesture = await tester.startGesture(
midBlah1,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.text('Cut'), findsOneWidget);
expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsOneWidget);
// Copy the first word.
await tester.tap(find.text('Copy'));
await tester.pumpAndSettle();
expect(controller.text, 'blah1 blah2');
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.byType(CupertinoButton), findsNothing);
// Paste it at the end.
await gesture.down(textOffsetToPosition(tester, controller.text.length));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream));
expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsOneWidget);
await tester.tap(find.text('Paste'));
await tester.pumpAndSettle();
expect(controller.text, 'blah1 blah2blah1');
expect(controller.selection, const TextSelection.collapsed(offset: 16));
// Cut the first word.
await gesture.down(midBlah1);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(find.text('Cut'), findsOneWidget);
expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsOneWidget);
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
await tester.tap(find.text('Cut'));
await tester.pumpAndSettle();
expect(controller.text, ' blah2blah1');
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0));
expect(find.byType(CupertinoButton), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets('can use the desktop cut/copy/paste buttons on Windows and Linux', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'blah1 blah2',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
// Initially, the menu is not shown and there is no selection.
expect(find.byType(CupertinoButton), findsNothing);
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
final Offset midBlah1 = textOffsetToPosition(tester, 2);
// Right clicking shows the menu.
TestGesture gesture = await tester.startGesture(
midBlah1,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await tester.pump();
await gesture.up();
await gesture.removePointer();
await tester.pumpAndSettle();
expect(controller.selection, const TextSelection.collapsed(offset: 2));
expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsOneWidget);
expect(find.text('Select all'), findsOneWidget);
// Double tap to select the first word, then right click to show the menu.
final Offset startBlah1 = textOffsetToPosition(tester, 0);
gesture = await tester.startGesture(
startBlah1,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 100));
await gesture.down(startBlah1);
await tester.pump();
await gesture.up();
await gesture.removePointer();
await tester.pumpAndSettle();
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsNothing);
expect(find.text('Select all'), findsNothing);
gesture = await tester.startGesture(
midBlah1,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await tester.pump();
await gesture.up();
await gesture.removePointer();
await tester.pumpAndSettle();
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.text('Cut'), findsOneWidget);
expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsOneWidget);
// Copy the first word.
await tester.tap(find.text('Copy'));
await tester.pumpAndSettle();
expect(controller.text, 'blah1 blah2');
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.byType(CupertinoButton), findsNothing);
// Paste it at the end.
gesture = await tester.startGesture(
textOffsetToPosition(tester, controller.text.length),
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await gesture.removePointer();
expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream));
gesture = await tester.startGesture(
textOffsetToPosition(tester, controller.text.length),
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await tester.pump();
await gesture.up();
await gesture.removePointer();
await tester.pumpAndSettle();
expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream));
expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsOneWidget);
await tester.tap(find.text('Paste'));
await tester.pumpAndSettle();
expect(controller.text, 'blah1 blah2blah1');
expect(controller.selection, const TextSelection.collapsed(offset: 16));
// Cut the first word.
gesture = await tester.startGesture(
midBlah1,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 100));
await gesture.down(startBlah1);
await tester.pump();
await gesture.up();
await gesture.removePointer();
await tester.pumpAndSettle();
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsNothing);
expect(find.text('Select all'), findsNothing);
gesture = await tester.startGesture(
textOffsetToPosition(tester, controller.text.length),
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await tester.pump();
await gesture.up();
await gesture.removePointer();
await tester.pumpAndSettle();
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.text('Cut'), findsOneWidget);
expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsOneWidget);
await tester.tap(find.text('Cut'));
await tester.pumpAndSettle();
expect(controller.text, ' blah2blah1');
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0));
expect(find.byType(CupertinoButton), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.windows }),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets('uses DefaultSelectionStyle for selection and cursor colors if provided', (WidgetTester tester) async {
const Color selectionColor = Colors.orange;
const Color cursorColor = Colors.red;
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: DefaultSelectionStyle(
selectionColor: selectionColor,
cursorColor: cursorColor,
child: TextField(autofocus: true),
),
),
),
);
await tester.pump();
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
expect(state.widget.selectionColor, selectionColor);
expect(state.widget.cursorColor, cursorColor);
});
testWidgets('Activates the text field when receives semantics focus on Mac, Windows', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(focusNode: focusNode),
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.didGainAccessibilityFocus,
],
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
));
expect(focusNode.hasFocus, isFalse);
semanticsOwner.performAction(4, SemanticsAction.didGainAccessibilityFocus);
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isTrue);
semantics.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows }));
testWidgets('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async {
void onEditingComplete() { }
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
onEditingComplete: onEditingComplete,
),
),
),
);
final Finder editableTextFinder = find.byType(EditableText);
expect(editableTextFinder, findsOneWidget);
final EditableText editableTextWidget = tester.widget(editableTextFinder);
expect(editableTextWidget.onEditingComplete, onEditingComplete);
});
testWidgets('TextField has consistent size', (WidgetTester tester) async {
final Key textFieldKey = UniqueKey();
String? textFieldValue;
await tester.pumpWidget(
overlay(
child: TextField(
key: textFieldKey,
decoration: const InputDecoration(
hintText: 'Placeholder',
),
onChanged: (String value) {
textFieldValue = value;
},
),
),
);
RenderBox findTextFieldBox() => tester.renderObject(find.byKey(textFieldKey));
final RenderBox inputBox = findTextFieldBox();
final Size emptyInputSize = inputBox.size;
Future<void> checkText(String testValue) async {
return TestAsyncUtils.guard(() async {
expect(textFieldValue, isNull);
await tester.enterText(find.byType(TextField), testValue);
// Check that the onChanged event handler fired.
expect(textFieldValue, equals(testValue));
textFieldValue = null;
await skipPastScrollingAnimation(tester);
});
}
await checkText(' ');
expect(findTextFieldBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize));
await checkText('Test');
expect(findTextFieldBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize));
});
testWidgets('Cursor blinks', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
child: const TextField(
decoration: InputDecoration(
hintText: 'Placeholder',
),
),
),
);
await tester.showKeyboard(find.byType(TextField));
final EditableTextState editableText = tester.state(find.byType(EditableText));
// Check that the cursor visibility toggles after each blink interval.
Future<void> checkCursorToggle() async {
final bool initialShowCursor = editableText.cursorCurrentlyVisible;
await tester.pump(editableText.cursorBlinkInterval);
expect(editableText.cursorCurrentlyVisible, equals(!initialShowCursor));
await tester.pump(editableText.cursorBlinkInterval);
expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
await tester.pump(editableText.cursorBlinkInterval ~/ 10);
expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
await tester.pump(editableText.cursorBlinkInterval);
expect(editableText.cursorCurrentlyVisible, equals(!initialShowCursor));
await tester.pump(editableText.cursorBlinkInterval);
expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
}
await checkCursorToggle();
await tester.showKeyboard(find.byType(TextField));
// Try the test again with a nonempty EditableText.
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: 'X',
selection: TextSelection.collapsed(offset: 1),
));
await tester.idle();
expect(tester.state(find.byType(EditableText)), editableText);
await checkCursorToggle();
});
// Regression test for https://github.com/flutter/flutter/issues/78918.
testWidgets('RenderEditable sets correct text editing value', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'how are you');
final UniqueKey icon = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
decoration: InputDecoration(
suffixIcon: IconButton(
key: icon,
icon: const Icon(Icons.cancel),
onPressed: () => controller.clear(),
),
),
),
),
),
);
await tester.tap(find.byKey(icon));
await tester.pump();
expect(controller.text, '');
expect(controller.selection, const TextSelection.collapsed(offset: 0));
});
testWidgets('Cursor radius is 2.0', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: TextField(),
),
),
);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorRadius, const Radius.circular(2.0));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('cursor has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
child: const TextField(
),
),
);
final TextField textField = tester.firstWidget(find.byType(TextField));
expect(textField.cursorWidth, 2.0);
expect(textField.cursorHeight, null);
expect(textField.cursorRadius, null);
});
testWidgets('cursor has expected radius value', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
child: const TextField(
cursorRadius: Radius.circular(3.0),
),
),
);
final TextField textField = tester.firstWidget(find.byType(TextField));
expect(textField.cursorWidth, 2.0);
expect(textField.cursorRadius, const Radius.circular(3.0));
});
testWidgets('clipBehavior has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
child: const TextField(
),
),
);
final TextField textField = tester.firstWidget(find.byType(TextField));
expect(textField.clipBehavior, Clip.hardEdge);
});
testWidgets('Overflow clipBehavior none golden', (WidgetTester tester) async {
final Widget widget = overlay(
child: RepaintBoundary(
key: const ValueKey<int>(1),
child: SizedBox(
height: 200,
width: 200,
child: Center(
child: TextField(
controller: OverflowWidgetTextEditingController(),
clipBehavior: Clip.none,
),
),
),
),
);
await tester.pumpWidget(widget);
final TextField textField = tester.firstWidget(find.byType(TextField));
expect(textField.clipBehavior, Clip.none);
final EditableText editableText = tester.firstWidget(find.byType(EditableText));
expect(editableText.clipBehavior, Clip.none);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('overflow_clipbehavior_none.material.0.png'),
);
});
testWidgets('Material cursor android golden', (WidgetTester tester) async {
final Widget widget = overlay(
child: const RepaintBoundary(
key: ValueKey<int>(1),
child: TextField(
cursorColor: Colors.blue,
cursorWidth: 15,
cursorRadius: Radius.circular(3.0),
),
),
);
await tester.pumpWidget(widget);
const String testValue = 'A short phrase';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
await tester.pump();
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_cursor_test.material.0.png'),
);
});
testWidgets('Material cursor golden', (WidgetTester tester) async {
final Widget widget = overlay(
child: const RepaintBoundary(
key: ValueKey<int>(1),
child: TextField(
cursorColor: Colors.blue,
cursorWidth: 15,
cursorRadius: Radius.circular(3.0),
),
),
);
await tester.pumpWidget(widget);
const String testValue = 'A short phrase';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
await tester.pump();
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile(
'text_field_cursor_test_${debugDefaultTargetPlatformOverride!.name.toLowerCase()}.material.1.png',
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('TextInputFormatter gets correct selection value', (WidgetTester tester) async {
late TextEditingValue actualOldValue;
late TextEditingValue actualNewValue;
void callBack(TextEditingValue oldValue, TextEditingValue newValue) {
actualOldValue = oldValue;
actualNewValue = newValue;
}
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController(text: '123');
await tester.pumpWidget(
boilerplate(
child: TextField(
controller: controller,
focusNode: focusNode,
inputFormatters: <TextInputFormatter>[TestFormatter(callBack)],
),
),
);
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
await tester.pumpAndSettle();
expect(
actualOldValue,
const TextEditingValue(
text: '123',
selection: TextSelection.collapsed(offset: 3, affinity: TextAffinity.upstream),
),
);
expect(
actualNewValue,
const TextEditingValue(
text: '12',
selection: TextSelection.collapsed(offset: 2),
),
);
}, skip: areKeyEventsHandledByPlatform); // [intended] only applies to platforms where we handle key events.
testWidgets('text field selection toolbar renders correctly inside opacity', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 100,
height: 100,
child: Opacity(
opacity: 0.5,
child: TextField(
decoration: InputDecoration(hintText: 'Placeholder'),
),
),
),
),
),
),
);
await tester.showKeyboard(find.byType(TextField));
const String testValue = 'A B C';
tester.testTextInput.updateEditingValue(
const TextEditingValue(
text: testValue,
),
);
await tester.pump();
// The selectWordsInRange with SelectionChangedCause.tap seems to be needed to show the toolbar.
// (This is true even if we provide selection parameter to the TextEditingValue above.)
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
state.renderEditable.selectWordsInRange(from: Offset.zero, cause: SelectionChangedCause.tap);
expect(state.showToolbar(), true);
// This is needed for the AnimatedOpacity to turn from 0 to 1 so the toolbar is visible.
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1));
// Sanity check that the toolbar widget exists.
expect(find.text('Paste'), findsOneWidget);
await expectLater(
// The toolbar exists in the Overlay above the MaterialApp.
find.byType(Overlay),
matchesGoldenFile('text_field_opacity_test.0.png'),
);
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
testWidgets('text field toolbar options correctly changes options',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
toolbarOptions: const ToolbarOptions(copy: true),
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
// This tap just puts the cursor somewhere different than where the double
// tap will occur to test that the double tap moves the existing cursor first.
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
);
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump();
// Second tap selects the word around the cursor.
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
// Selected text shows 'Copy', and not 'Paste', 'Cut', 'Select All'.
expect(find.text('Paste'), findsNothing);
expect(find.text('Copy'), findsOneWidget);
expect(find.text('Cut'), findsNothing);
expect(find.text('Select All'), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets('text selection style 1', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwasssup!',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: RepaintBoundary(
child: Container(
width: 650.0,
height: 600.0,
decoration: const BoxDecoration(
color: Color(0xff00ff00),
),
child: Column(
children: <Widget>[
TextField(
key: const Key('field0'),
controller: controller,
style: const TextStyle(height: 4, color: Colors.black45),
toolbarOptions: const ToolbarOptions(copy: true, selectAll: true),
selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingTop,
selectionWidthStyle: ui.BoxWidthStyle.max,
maxLines: 3,
),
],
),
),
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
await tester.longPressAt(textfieldStart + const Offset(50.0, 2.0));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textfieldStart + const Offset(100.0, 107.0));
await tester.pump(const Duration(milliseconds: 300));
await expectLater(
find.byType(MaterialApp),
matchesGoldenFile('text_field_golden.TextSelectionStyle.1.png'),
);
});
testWidgets('text selection style 2', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwasssup!',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: RepaintBoundary(
child: Container(
width: 650.0,
height: 600.0,
decoration: const BoxDecoration(
color: Color(0xff00ff00),
),
child: Column(
children: <Widget>[
TextField(
key: const Key('field0'),
controller: controller,
style: const TextStyle(height: 4, color: Colors.black45),
toolbarOptions: const ToolbarOptions(copy: true, selectAll: true),
selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingBottom,
maxLines: 3,
),
],
),
),
),
),
),
),
);
final EditableTextState editableTextState = tester.state(find.byType(EditableText));
// Double tap to select the first word.
const int index = 4;
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
expect(editableTextState.selectionOverlay!.handlesAreVisible, isTrue);
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 7);
// Use toolbar to select all text.
if (isContextMenuProvidedByPlatform) {
controller.selection = TextSelection(baseOffset: 0, extentOffset: controller.text.length);
expect(controller.selection.extentOffset, controller.text.length);
} else {
await tester.tap(find.text('Select all'));
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, controller.text.length);
}
await expectLater(
find.byType(MaterialApp),
matchesGoldenFile('text_field_golden.TextSelectionStyle.2.png'),
);
// Text selection styles are not fully supported on web.
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/93723
testWidgets(
'text field toolbar options correctly changes options',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
toolbarOptions: const ToolbarOptions(copy: true),
),
),
),
),
);
final Offset pos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
await tester.tapAt(pos);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(pos);
await tester.pump();
// Selected text shows 'Copy', and not 'Paste', 'Cut', 'Select all'.
expect(find.text('Paste'), findsNothing);
expect(find.text('Copy'), findsOneWidget);
expect(find.text('Cut'), findsNothing);
expect(find.text('Select all'), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.android,
TargetPlatform.fuchsia,
TargetPlatform.linux,
TargetPlatform.windows,
}),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets('cursor layout has correct width', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController.fromValue(
const TextEditingValue(selection: TextSelection.collapsed(offset: 0)),
);
final FocusNode focusNode = FocusNode();
EditableText.debugDeterministicCursor = true;
await tester.pumpWidget(
overlay(
child: RepaintBoundary(
child: TextField(
cursorWidth: 15.0,
controller: controller,
focusNode: focusNode,
),
),
),
);
focusNode.requestFocus();
await tester.pump();
await expectLater(
find.byType(TextField),
matchesGoldenFile('text_field_cursor_width_test.0.png'),
);
EditableText.debugDeterministicCursor = false;
});
testWidgets('cursor layout has correct radius', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController.fromValue(
const TextEditingValue(selection: TextSelection.collapsed(offset: 0)),
);
final FocusNode focusNode = FocusNode();
EditableText.debugDeterministicCursor = true;
await tester.pumpWidget(
overlay(
child: RepaintBoundary(
child: TextField(
cursorWidth: 15.0,
cursorRadius: const Radius.circular(3.0),
controller: controller,
focusNode: focusNode,
),
),
),
);
focusNode.requestFocus();
await tester.pump();
await expectLater(
find.byType(TextField),
matchesGoldenFile('text_field_cursor_width_test.1.png'),
);
EditableText.debugDeterministicCursor = false;
});
testWidgets('cursor layout has correct height', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController.fromValue(
const TextEditingValue(selection: TextSelection.collapsed(offset: 0)),
);
final FocusNode focusNode = FocusNode();
EditableText.debugDeterministicCursor = true;
await tester.pumpWidget(
overlay(
child: RepaintBoundary(
child: TextField(
cursorWidth: 15.0,
cursorHeight: 30.0,
controller: controller,
focusNode: focusNode,
),
),
),
);
focusNode.requestFocus();
await tester.pump();
await expectLater(
find.byType(TextField),
matchesGoldenFile('text_field_cursor_width_test.2.png'),
);
EditableText.debugDeterministicCursor = false;
});
testWidgets('Overflowing a line with spaces stops the cursor at the end', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
key: textFieldKey,
controller: controller,
maxLines: null,
),
),
);
expect(controller.selection.baseOffset, -1);
expect(controller.selection.extentOffset, -1);
const String testValueOneLine = 'enough text to be exactly at the end of the line.';
await tester.enterText(find.byType(TextField), testValueOneLine);
await skipPastScrollingAnimation(tester);
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
RenderBox inputBox = findInputBox();
final Size oneLineInputSize = inputBox.size;
await tester.tapAt(textOffsetToPosition(tester, testValueOneLine.length));
await tester.pump();
const String testValueTwoLines = 'enough text to overflow the first line and go to the second';
await tester.enterText(find.byType(TextField), testValueTwoLines);
await skipPastScrollingAnimation(tester);
expect(inputBox, findInputBox());
inputBox = findInputBox();
expect(inputBox.size.height, greaterThan(oneLineInputSize.height));
final Size twoLineInputSize = inputBox.size;
// Enter a string with the same number of characters as testValueTwoLines,
// but where the overflowing part is all spaces. Assert that it only renders
// on one line.
const String testValueSpaces = '$testValueOneLine ';
expect(testValueSpaces.length, testValueTwoLines.length);
await tester.enterText(find.byType(TextField), testValueSpaces);
await skipPastScrollingAnimation(tester);
expect(inputBox, findInputBox());
inputBox = findInputBox();
expect(inputBox.size.height, oneLineInputSize.height);
// Swapping the final space for a letter causes it to wrap to 2 lines.
const String testValueSpacesOverflow = '$testValueOneLine a';
expect(testValueSpacesOverflow.length, testValueTwoLines.length);
await tester.enterText(find.byType(TextField), testValueSpacesOverflow);
await skipPastScrollingAnimation(tester);
expect(inputBox, findInputBox());
inputBox = findInputBox();
expect(inputBox.size.height, twoLineInputSize.height);
// Positioning the cursor at the end of a line overflowing with spaces puts
// it inside the input still.
await tester.enterText(find.byType(TextField), testValueSpaces);
await skipPastScrollingAnimation(tester);
await tester.tapAt(textOffsetToPosition(tester, testValueSpaces.length));
await tester.pump();
final double inputWidth = findRenderEditable(tester).size.width;
final Offset cursorOffsetSpaces = findRenderEditable(tester).getLocalRectForCaret(
const TextPosition(offset: testValueSpaces.length),
).bottomRight;
expect(cursorOffsetSpaces.dx, inputWidth - kCaretGap);
});
testWidgets('Overflowing a line with spaces stops the cursor at the end (rtl direction)', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
child: const TextField(
textDirection: TextDirection.rtl,
maxLines: null,
),
),
);
const String testValueOneLine = 'enough text to be exactly at the end of the line.';
const String testValueSpaces = '$testValueOneLine ';
// Positioning the cursor at the end of a line overflowing with spaces puts
// it inside the input still.
await tester.enterText(find.byType(TextField), testValueSpaces);
await skipPastScrollingAnimation(tester);
await tester.tapAt(textOffsetToPosition(tester, testValueSpaces.length));
await tester.pump();
final Offset cursorOffsetSpaces = findRenderEditable(tester).getLocalRectForCaret(
const TextPosition(offset: testValueSpaces.length),
).topLeft;
expect(cursorOffsetSpaces.dx >= 0, isTrue);
});
testWidgets('mobile obscureText control test', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
child: const TextField(
obscureText: true,
decoration: InputDecoration(
hintText: 'Placeholder',
),
),
),
);
await tester.showKeyboard(find.byType(TextField));
const String testValue = 'ABC';
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: testValue,
selection: TextSelection.collapsed(offset: testValue.length),
));
await tester.pump();
// Enter a character into the obscured field and verify that the character
// is temporarily shown to the user and then changed to a bullet.
const String newChar = 'X';
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: testValue + newChar,
selection: TextSelection.collapsed(offset: testValue.length + 1),
));
await tester.pump();
String editText = (findRenderEditable(tester).text! as TextSpan).text!;
expect(editText.substring(editText.length - 1), newChar);
await tester.pump(const Duration(seconds: 2));
editText = (findRenderEditable(tester).text! as TextSpan).text!;
expect(editText.substring(editText.length - 1), '\u2022');
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }));
testWidgets('desktop obscureText control test', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
child: const TextField(
obscureText: true,
decoration: InputDecoration(
hintText: 'Placeholder',
),
),
),
);
await tester.showKeyboard(find.byType(TextField));
const String testValue = 'ABC';
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: testValue,
selection: TextSelection.collapsed(offset: testValue.length),
));
await tester.pump();
// Enter a character into the obscured field and verify that the character
// isn't shown to the user.
const String newChar = 'X';
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: testValue + newChar,
selection: TextSelection.collapsed(offset: testValue.length + 1),
));
await tester.pump();
final String editText = (findRenderEditable(tester).text! as TextSpan).text!;
expect(editText.substring(editText.length - 1), '\u2022');
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.macOS,
TargetPlatform.linux,
TargetPlatform.windows,
}));
testWidgets('Caret position is updated on tap', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
),
),
);
expect(controller.selection.baseOffset, -1);
expect(controller.selection.extentOffset, -1);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
// Tap to reposition the caret.
final int tapIndex = testValue.indexOf('e');
final Offset ePos = textOffsetToPosition(tester, tapIndex);
await tester.tapAt(ePos);
await tester.pump();
expect(controller.selection.baseOffset, tapIndex);
expect(controller.selection.extentOffset, tapIndex);
});
testWidgets('enableInteractiveSelection = false, tap', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
enableInteractiveSelection: false,
),
),
);
expect(controller.selection.baseOffset, -1);
expect(controller.selection.extentOffset, -1);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
// Tap would ordinarily reposition the caret.
final int tapIndex = testValue.indexOf('e');
final Offset ePos = textOffsetToPosition(tester, tapIndex);
await tester.tapAt(ePos);
await tester.pump();
expect(controller.selection.baseOffset, testValue.length);
expect(controller.selection.isCollapsed, isTrue);
});
testWidgets('Can long press to select', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
expect(controller.value.text, testValue);
await skipPastScrollingAnimation(tester);
expect(controller.selection.isCollapsed, true);
// Long press the 'e' to select 'def'.
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
await tester.longPressAt(ePos, pointer: 7);
await tester.pump();
// 'def' is selected.
expect(controller.selection.baseOffset, testValue.indexOf('d'));
expect(controller.selection.extentOffset, testValue.indexOf('f')+1);
// Tapping elsewhere immediately collapses and moves the cursor.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('h')));
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('h'));
});
testWidgets("Slight movements in longpress don't hide/show handles", (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
expect(controller.value.text, testValue);
await skipPastScrollingAnimation(tester);
expect(controller.selection.isCollapsed, true);
// Long press the 'e' to select 'def', but don't release the gesture.
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
await tester.pump(const Duration(seconds: 2));
await tester.pumpAndSettle();
// Handles are shown
final Finder fadeFinder = find.byType(FadeTransition);
expect(fadeFinder, findsNWidgets(2)); // 2 handles, 1 toolbar
FadeTransition handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(1.0));
// Move the gesture very slightly
await gesture.moveBy(const Offset(1.0, 1.0));
await tester.pump(SelectionOverlay.fadeDuration * 0.5);
handle = tester.widget(fadeFinder.at(0));
// The handle should still be fully opaque.
expect(handle.opacity.value, equals(1.0));
});
testWidgets('Long pressing a field with selection 0,0 shows the selection menu', (WidgetTester tester) async {
await tester.pumpWidget(overlay(
child: TextField(
controller: TextEditingController.fromValue(
const TextEditingValue(
selection: TextSelection(baseOffset: 0, extentOffset: 0),
),
),
),
));
expect(find.text('Paste'), findsNothing);
final Offset emptyPos = textOffsetToPosition(tester, 0);
await tester.longPressAt(emptyPos, pointer: 7);
await tester.pumpAndSettle();
expect(find.text('Paste'), findsOneWidget);
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
testWidgets('Entering text hides selection handle caret', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
),
),
);
const String testValue = 'abcdefghi';
await tester.enterText(find.byType(TextField), testValue);
expect(controller.value.text, testValue);
await skipPastScrollingAnimation(tester);
// Handle not shown.
expect(controller.selection.isCollapsed, true);
final Finder fadeFinder = find.byType(FadeTransition);
FadeTransition handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(0.0));
// Tap on the text field to show the handle.
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(fadeFinder, findsNWidgets(1));
handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(1.0));
// Enter more text.
const String testValueAddition = 'jklmni';
await tester.enterText(find.byType(TextField), testValueAddition);
expect(controller.value.text, testValueAddition);
await skipPastScrollingAnimation(tester);
// Handle not shown.
expect(controller.selection.isCollapsed, true);
handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(0.0));
});
testWidgets('selection handles are excluded from the semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
),
),
);
const String testValue = 'abcdefghi';
await tester.enterText(find.byType(TextField), testValue);
expect(controller.value.text, testValue);
await skipPastScrollingAnimation(tester);
// Tap on the text field to show the handle.
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
// The semantics should only have the text field.
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isFocused],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.paste,
SemanticsAction.setText,
SemanticsAction.moveCursorBackwardByWord,
],
value: 'abcdefghi',
textDirection: TextDirection.ltr,
textSelection: const TextSelection.collapsed(offset: 9),
),
],
),
ignoreRect: true,
ignoreTransform: true,
));
semantics.dispose();
});
testWidgets('Mouse long press is just like a tap', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
expect(controller.value.text, testValue);
await skipPastScrollingAnimation(tester);
expect(controller.selection.isCollapsed, true);
// Long press the 'e' using a mouse device.
final int eIndex = testValue.indexOf('e');
final Offset ePos = textOffsetToPosition(tester, eIndex);
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pump();
// The cursor is placed just like a regular tap.
expect(controller.selection.baseOffset, eIndex);
expect(controller.selection.extentOffset, eIndex);
});
testWidgets('Read only text field basic', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'readonly');
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
readOnly: true,
),
),
);
// Read only text field cannot open keyboard.
await tester.showKeyboard(find.byType(TextField));
// On web, we always create a client connection to the engine.
expect(tester.testTextInput.hasAnyClients, isBrowser ? isTrue : isFalse);
await skipPastScrollingAnimation(tester);
expect(controller.selection.isCollapsed, true);
await tester.tap(find.byType(TextField));
await tester.pump();
// On web, we always create a client connection to the engine.
expect(tester.testTextInput.hasAnyClients, isBrowser ? isTrue : isFalse);
final EditableTextState editableText = tester.state(find.byType(EditableText));
// Collapse selection should not paint.
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
// Long press on the 'd' character of text 'readOnly' to show context menu.
const int dIndex = 3;
final Offset dPos = textOffsetToPosition(tester, dIndex);
await tester.longPressAt(dPos);
await tester.pumpAndSettle();
// Context menu should not have paste and cut.
expect(find.text('Copy'), isContextMenuProvidedByPlatform ? findsNothing : findsOneWidget);
expect(find.text('Paste'), findsNothing);
expect(find.text('Cut'), findsNothing);
});
testWidgets('does not paint toolbar when no options available', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: TextField(
readOnly: true,
),
),
),
);
await tester.tap(find.byType(TextField));
await tester.pump(const Duration(milliseconds: 50));
await tester.tap(find.byType(TextField));
// Wait for context menu to be built.
await tester.pumpAndSettle();
expect(find.byType(CupertinoTextSelectionToolbar), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('text field build empty toolbar when no options available', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: TextField(
readOnly: true,
),
),
),
);
await tester.tap(find.byType(TextField));
await tester.pump(const Duration(milliseconds: 50));
await tester.tap(find.byType(TextField));
// Wait for context menu to be built.
await tester.pumpAndSettle();
final RenderBox container = tester.renderObject(find.descendant(
of: find.byType(SnapshotWidget),
matching: find.byType(SizedBox),
).first);
expect(container.size, Size.zero);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));
testWidgets('Swapping controllers should update selection', (WidgetTester tester) async {
TextEditingController controller = TextEditingController(text: 'readonly');
final OverlayEntry entry = OverlayEntry(
builder: (BuildContext context) {
return Center(
child: Material(
child: TextField(
controller: controller,
readOnly: true,
),
),
);
},
);
await tester.pumpWidget(overlayWithEntry(entry));
const int dIndex = 3;
final Offset dPos = textOffsetToPosition(tester, dIndex);
await tester.longPressAt(dPos);
await tester.pumpAndSettle();
final EditableTextState state = tester.state(find.byType(EditableText));
TextSelection currentOverlaySelection =
state.selectionOverlay!.value.selection;
expect(currentOverlaySelection.baseOffset, 0);
expect(currentOverlaySelection.extentOffset, 8);
// Update selection from [0 to 8] to [1 to 7].
controller = TextEditingController.fromValue(
controller.value.copyWith(selection: const TextSelection(
baseOffset: 1,
extentOffset: 7,
)),
);
// Mark entry to be dirty in order to trigger overlay update.
entry.markNeedsBuild();
await tester.pump();
currentOverlaySelection = state.selectionOverlay!.value.selection;
expect(currentOverlaySelection.baseOffset, 1);
expect(currentOverlaySelection.extentOffset, 7);
});
testWidgets('Read only text should not compose', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController.fromValue(
const TextEditingValue(
text: 'readonly',
composing: TextRange(start: 0, end: 8), // Simulate text composing.
),
);
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
readOnly: true,
),
),
);
final RenderEditable renderEditable = findRenderEditable(tester);
// There should be no composing.
expect(renderEditable.text, TextSpan(text:'readonly', style: renderEditable.text!.style));
});
testWidgets('Dynamically switching between read only and not read only should hide or show collapse cursor', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'readonly');
bool readOnly = true;
final OverlayEntry entry = OverlayEntry(
builder: (BuildContext context) {
return Center(
child: Material(
child: TextField(
controller: controller,
readOnly: readOnly,
),
),
);
},
);
await tester.pumpWidget(overlayWithEntry(entry));
await tester.tap(find.byType(TextField));
await tester.pump();
final EditableTextState editableText = tester.state(find.byType(EditableText));
// Collapse selection should not paint.
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
readOnly = false;
// Mark entry to be dirty in order to trigger overlay update.
entry.markNeedsBuild();
await tester.pumpAndSettle();
expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
readOnly = true;
entry.markNeedsBuild();
await tester.pumpAndSettle();
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
});
testWidgets('Dynamically switching to read only should close input connection', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'readonly');
bool readOnly = false;
final OverlayEntry entry = OverlayEntry(
builder: (BuildContext context) {
return Center(
child: Material(
child: TextField(
controller: controller,
readOnly: readOnly,
),
),
);
},
);
await tester.pumpWidget(overlayWithEntry(entry));
await tester.tap(find.byType(TextField));
await tester.pump();
expect(tester.testTextInput.hasAnyClients, true);
readOnly = true;
// Mark entry to be dirty in order to trigger overlay update.
entry.markNeedsBuild();
await tester.pump();
// On web, we always have a client connection to the engine.
expect(tester.testTextInput.hasAnyClients, isBrowser ? isTrue : isFalse);
});
testWidgets('Dynamically switching to non read only should open input connection', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'readonly');
bool readOnly = true;
final OverlayEntry entry = OverlayEntry(
builder: (BuildContext context) {
return Center(
child: Material(
child: TextField(
controller: controller,
readOnly: readOnly,
),
),
);
},
);
await tester.pumpWidget(overlayWithEntry(entry));
await tester.tap(find.byType(TextField));
await tester.pump();
// On web, we always have a client connection to the engine.
expect(tester.testTextInput.hasAnyClients, isBrowser ? isTrue : isFalse);
readOnly = false;
// Mark entry to be dirty in order to trigger overlay update.
entry.markNeedsBuild();
await tester.pump();
expect(tester.testTextInput.hasAnyClients, true);
});
testWidgets('enableInteractiveSelection = false, long-press', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
enableInteractiveSelection: false,
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
expect(controller.value.text, testValue);
await skipPastScrollingAnimation(tester);
expect(controller.selection.isCollapsed, true);
// Long press the 'e' to select 'def'.
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
await tester.longPressAt(ePos, pointer: 7);
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.length);
});
testWidgets('Selection updates on tap down (Desktop platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset ePos = textOffsetToPosition(tester, 5);
final Offset gPos = textOffsetToPosition(tester, 8);
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
await gesture.down(gPos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
// This should do nothing. The selection is set on tap down on desktop platforms.
await gesture.up();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
},
variant: TargetPlatformVariant.desktop(),
);
testWidgets('Selection updates on tap up (Mobile platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset ePos = textOffsetToPosition(tester, 5);
final Offset gPos = textOffsetToPosition(tester, 8);
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
await gesture.down(gPos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
final TestGesture touchGesture = await tester.startGesture(ePos);
await touchGesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
// On iOS a tap to select, selects the word edge instead of the exact tap position.
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5);
expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5);
// Selection should stay the same since it is set on tap up for mobile platforms.
await touchGesture.down(gPos);
await tester.pump();
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5);
expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5);
await touchGesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
},
variant: TargetPlatformVariant.mobile(),
);
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(2.0, 0.0));
await tester.pumpAndSettle();
expect(selectionChangedCount, 0);
// Now a text selection change will occur after a significant movement.
await gesture.moveTo(hPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(selectionChangedCount, 1);
expect(controller.selection.baseOffset, 2);
expect(controller.selection.extentOffset, 9);
});
testWidgets('Dragging in opposite direction also works', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
final TestGesture gesture = await tester.startGesture(gPos, kind: PointerDeviceKind.mouse);
await tester.pump();
await gesture.moveTo(ePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, testValue.indexOf('g'));
expect(controller.selection.extentOffset, testValue.indexOf('e'));
});
testWidgets('Slow mouse dragging also selects text', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await tester.pump(const Duration(seconds: 2));
await gesture.moveTo(gPos);
await tester.pump();
await gesture.up();
expect(controller.selection.baseOffset, testValue.indexOf('e'));
expect(controller.selection.extentOffset, testValue.indexOf('g'));
});
testWidgets('Can drag handles to change selection', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
// Long press the 'e' to select 'def'.
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
final TextSelection selection = controller.selection;
expect(selection.baseOffset, 4);
expect(selection.extentOffset, 7);
final RenderEditable renderEditable = findRenderEditable(tester);
List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 2);
// Drag the right handle 2 letters to the right.
// We use a small offset because the endpoint is on the very corner
// of the handle.
Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
Offset newHandlePos = textOffsetToPosition(tester, testValue.length);
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 11);
// Drag the left handle 2 letters to the left.
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
newHandlePos = textOffsetToPosition(tester, 2);
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
switch (defaultTargetPlatform) {
// On Apple platforms, dragging the base handle makes it the extent.
case TargetPlatform.iOS:
case TargetPlatform.macOS:
expect(controller.selection.baseOffset, 11);
expect(controller.selection.extentOffset, 2);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(controller.selection.baseOffset, 2);
expect(controller.selection.extentOffset, 11);
break;
}
// Drag the left handle 2 letters to the left again.
endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
newHandlePos = textOffsetToPosition(tester, 0);
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
// The left handle was already the extent, and it remains so.
expect(controller.selection.baseOffset, 11);
expect(controller.selection.extentOffset, 0);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 11);
break;
}
},
variant: TargetPlatformVariant.all(),
);
testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
// Long press the 'e' to select 'def'.
final Offset ePos = textOffsetToPosition(tester, 5); // Position before 'e'.
TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
final TextSelection selection = controller.selection;
expect(selection.baseOffset, 4);
expect(selection.extentOffset, 7);
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 2);
// Drag the right handle until there's only 1 char selected.
// We use a small offset because the endpoint is on the very corner
// of the handle.
final Offset handlePos = endpoints[1].point + const Offset(4.0, 0.0);
Offset newHandlePos = textOffsetToPosition(tester, 5); // Position before 'e'.
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 5);
newHandlePos = textOffsetToPosition(tester, 2); // Position before 'c'.
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
// The selection doesn't move beyond the left handle. There's always at
// least 1 char selected.
expect(controller.selection.extentOffset, 5);
});
testWidgets("dragging caret within a word doesn't affect composing region", (WidgetTester tester) async {
const String testValue = 'abc def ghi';
final TextEditingController controller = TextEditingController.fromValue(
const TextEditingValue(
text: testValue,
selection: TextSelection(
baseOffset: 4,
extentOffset: 4,
affinity: TextAffinity.upstream,
),
composing: TextRange(
start: 4,
end: 7,
),
),
);
await tester.pumpWidget(
overlay(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
);
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 4);
expect(controller.value.composing.start, 4);
expect(controller.value.composing.end, 7);
// Tap the caret to show the handle.
final Offset ePos = textOffsetToPosition(tester, 4);
await tester.tapAt(ePos);
await tester.pumpAndSettle();
final TextSelection selection = controller.selection;
expect(controller.selection.isCollapsed, true);
expect(selection.baseOffset, 4);
expect(controller.value.composing.start, 4);
expect(controller.value.composing.end, 7);
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 1);
// Drag the right handle 2 letters to the right.
// We use a small offset because the endpoint is on the very corner
// of the handle.
final Offset handlePos = endpoints[0].point + const Offset(1.0, 1.0);
final Offset newHandlePos = textOffsetToPosition(tester, 7);
final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 7);
expect(controller.value.composing.start, 4);
expect(controller.value.composing.end, 7);
},
skip: kIsWeb, // [intended] text selection is handled by the browser
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })
);
testWidgets('Can use selection toolbar', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
// Tap the selection handle to bring up the "paste / select all" menu.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
RenderEditable renderEditable = findRenderEditable(tester);
List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
// Tapping on the part of the handle's GestureDetector where it overlaps
// with the text itself does not show the menu, so add a small vertical
// offset to tap below the text.
await tester.tapAt(endpoints[0].point + const Offset(1.0, 13.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
// Select all should select all the text.
await tester.tap(find.text('Select all'));
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, testValue.length);
// Copy should reset the selection.
await tester.tap(find.text('Copy'));
await skipPastScrollingAnimation(tester);
expect(controller.selection.isCollapsed, true);
// Tap again to bring back the menu.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pump();
// Allow time for handle to appear and double tap to time out.
await tester.pump(const Duration(milliseconds: 300));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('e'));
expect(controller.selection.extentOffset, testValue.indexOf('e'));
renderEditable = findRenderEditable(tester);
endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('e'));
expect(controller.selection.extentOffset, testValue.indexOf('e'));
// Paste right before the 'e'.
await tester.tap(find.text('Paste'));
await tester.pump();
expect(controller.text, 'abc d${testValue}ef ghi');
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
// Show the selection menu at the given index into the text by tapping to
// place the cursor and then tapping on the handle.
Future<void> showSelectionMenuAt(WidgetTester tester, TextEditingController controller, int index) async {
await tester.tapAt(tester.getCenter(find.byType(EditableText)));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
expect(find.text('Select all'), findsNothing);
// Tap the selection handle to bring up the "paste / select all" menu for
// the last line of text.
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
// Tapping on the part of the handle's GestureDetector where it overlaps
// with the text itself does not show the menu, so add a small vertical
// offset to tap below the text.
await tester.tapAt(endpoints[0].point + const Offset(1.0, 13.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
}
testWidgets(
'Check the toolbar appears below the TextField when there is not enough space above the TextField to show it',
(WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/29808
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(30.0),
child: TextField(
controller: controller,
),
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
await showSelectionMenuAt(tester, controller, testValue.indexOf('e'));
// Verify the selection toolbar position is below the text.
Offset toolbarTopLeft = tester.getTopLeft(find.text('Select all'));
Offset textFieldTopLeft = tester.getTopLeft(find.byType(TextField));
expect(textFieldTopLeft.dy, lessThan(toolbarTopLeft.dy));
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(150.0),
child: TextField(
controller: controller,
),
),
),
));
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
await showSelectionMenuAt(tester, controller, testValue.indexOf('e'));
// Verify the selection toolbar position
toolbarTopLeft = tester.getTopLeft(find.text('Select all'));
textFieldTopLeft = tester.getTopLeft(find.byType(TextField));
expect(toolbarTopLeft.dy, lessThan(textFieldTopLeft.dy));
},
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets(
'the toolbar adjusts its position above/below when bottom inset changes',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 48.0,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IntrinsicHeight(
child: TextField(
controller: controller,
expands: true,
maxLines: null,
),
),
const SizedBox(height: 325.0),
],
),
),
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
await showSelectionMenuAt(tester, controller, testValue.indexOf('e'));
// Verify the selection toolbar position is above the text.
expect(find.text('Select all'), findsOneWidget);
Offset toolbarTopLeft = tester.getTopLeft(find.text('Select all'));
Offset textFieldTopLeft = tester.getTopLeft(find.byType(TextField));
expect(toolbarTopLeft.dy, lessThan(textFieldTopLeft.dy));
// Add a viewInset tall enough to push the field to the top, where there
// is no room to display the toolbar above. This is similar to when the
// keyboard is shown.
tester.binding.window.viewInsetsTestValue = const _TestWindowPadding(
bottom: 500.0,
);
addTearDown(tester.binding.window.clearViewInsetsTestValue);
await tester.pumpAndSettle();
// Verify the selection toolbar position is below the text.
toolbarTopLeft = tester.getTopLeft(find.text('Select all'));
textFieldTopLeft = tester.getTopLeft(find.byType(TextField));
expect(toolbarTopLeft.dy, greaterThan(textFieldTopLeft.dy));
// Remove the viewInset, as if the keyboard were hidden.
tester.binding.window.clearViewInsetsTestValue();
await tester.pumpAndSettle();
// Verify the selection toolbar position is below the text.
toolbarTopLeft = tester.getTopLeft(find.text('Select all'));
textFieldTopLeft = tester.getTopLeft(find.byType(TextField));
expect(toolbarTopLeft.dy, lessThan(textFieldTopLeft.dy));
},
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets(
'Toolbar appears in the right places in multiline inputs',
(WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/36749
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(30.0),
child: TextField(
controller: controller,
minLines: 6,
maxLines: 6,
),
),
),
));
expect(find.text('Select all'), findsNothing);
const String testValue = 'abc\ndef\nghi\njkl\nmno\npqr';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
// Show the selection menu on the first line and verify the selection
// toolbar position is below the first line.
await showSelectionMenuAt(tester, controller, testValue.indexOf('c'));
expect(find.text('Select all'), findsOneWidget);
final Offset firstLineToolbarTopLeft = tester.getTopLeft(find.text('Select all'));
final Offset firstLineTopLeft = textOffsetToPosition(tester, testValue.indexOf('a'));
expect(firstLineTopLeft.dy, lessThan(firstLineToolbarTopLeft.dy));
// Show the selection menu on the second to last line and verify the
// selection toolbar position is above that line and above the first
// line's toolbar.
await showSelectionMenuAt(tester, controller, testValue.indexOf('o'));
expect(find.text('Select all'), findsOneWidget);
final Offset penultimateLineToolbarTopLeft = tester.getTopLeft(find.text('Select all'));
final Offset penultimateLineTopLeft = textOffsetToPosition(tester, testValue.indexOf('p'));
expect(penultimateLineToolbarTopLeft.dy, lessThan(penultimateLineTopLeft.dy));
expect(penultimateLineToolbarTopLeft.dy, lessThan(firstLineToolbarTopLeft.dy));
// Show the selection menu on the last line and verify the selection
// toolbar position is above that line and below the position of the
// second to last line's toolbar.
await showSelectionMenuAt(tester, controller, testValue.indexOf('r'));
expect(find.text('Select all'), findsOneWidget);
final Offset lastLineToolbarTopLeft = tester.getTopLeft(find.text('Select all'));
final Offset lastLineTopLeft = textOffsetToPosition(tester, testValue.indexOf('p'));
expect(lastLineToolbarTopLeft.dy, lessThan(lastLineTopLeft.dy));
expect(lastLineToolbarTopLeft.dy, greaterThan(penultimateLineToolbarTopLeft.dy));
},
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets('Selection toolbar fades in', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
// Tap the selection handle to bring up the "paste / select all" menu.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pump();
// Allow time for the handle to appear and for a double tap to time out.
await tester.pump(const Duration(milliseconds: 600));
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
// Pump an extra frame to allow the selection menu to read the clipboard.
await tester.pump();
await tester.pump();
// Toolbar should fade in. Starting at 0% opacity.
final Element target = tester.element(find.text('Select all'));
final FadeTransition opacity = target.findAncestorWidgetOfExactType<FadeTransition>()!;
expect(opacity.opacity.value, equals(0.0));
// Still fading in.
await tester.pump(const Duration(milliseconds: 50));
final FadeTransition opacity2 = target.findAncestorWidgetOfExactType<FadeTransition>()!;
expect(opacity, same(opacity2));
expect(opacity.opacity.value, greaterThan(0.0));
expect(opacity.opacity.value, lessThan(1.0));
// End the test here to ensure the animation is properly disposed of.
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
testWidgets('An obscured TextField is selectable by default', (WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/32845
final TextEditingController controller = TextEditingController();
Widget buildFrame(bool obscureText) {
return overlay(
child: TextField(
controller: controller,
obscureText: obscureText,
),
);
}
// Obscure text and don't enable or disable selection.
await tester.pumpWidget(buildFrame(true));
await tester.enterText(find.byType(TextField), 'abcdefghi');
await skipPastScrollingAnimation(tester);
expect(controller.selection.isCollapsed, true);
// Long press does select text.
final Offset ePos = textOffsetToPosition(tester, 1);
await tester.longPressAt(ePos, pointer: 7);
await tester.pump();
expect(controller.selection.isCollapsed, false);
});
testWidgets('An obscured TextField is not selectable when disabled', (WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/32845
final TextEditingController controller = TextEditingController();
Widget buildFrame(bool obscureText, bool enableInteractiveSelection) {
return overlay(
child: TextField(
controller: controller,
obscureText: obscureText,
enableInteractiveSelection: enableInteractiveSelection,
),
);
}
// Explicitly disabled selection on obscured text.
await tester.pumpWidget(buildFrame(true, false));
await tester.enterText(find.byType(TextField), 'abcdefghi');
await skipPastScrollingAnimation(tester);
expect(controller.selection.isCollapsed, true);
// Long press doesn't select text.
final Offset ePos2 = textOffsetToPosition(tester, 1);
await tester.longPressAt(ePos2, pointer: 7);
await tester.pump();
expect(controller.selection.isCollapsed, true);
});
testWidgets('An obscured TextField is not selectable when read-only', (WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/32845
final TextEditingController controller = TextEditingController();
Widget buildFrame(bool obscureText, bool readOnly) {
return overlay(
child: TextField(
controller: controller,
obscureText: obscureText,
readOnly: readOnly,
),
);
}
// Explicitly disabled selection on obscured text that is read-only.
await tester.pumpWidget(buildFrame(true, true));
await tester.enterText(find.byType(TextField), 'abcdefghi');
await skipPastScrollingAnimation(tester);
expect(controller.selection.isCollapsed, true);
// Long press doesn't select text.
final Offset ePos2 = textOffsetToPosition(tester, 1);
await tester.longPressAt(ePos2, pointer: 7);
await tester.pump();
expect(controller.selection.isCollapsed, true);
});
testWidgets('An obscured TextField is selected as one word', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(overlay(
child: TextField(
controller: controller,
obscureText: true,
),
));
await tester.enterText(find.byType(TextField), 'abcde fghi');
await skipPastScrollingAnimation(tester);
// Long press does select text.
final Offset bPos = textOffsetToPosition(tester, 1);
await tester.longPressAt(bPos, pointer: 7);
await tester.pump();
final TextSelection selection = controller.selection;
expect(selection.isCollapsed, false);
expect(selection.baseOffset, 0);
expect(selection.extentOffset, 10);
});
testWidgets('An obscured TextField has correct default context menu', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(overlay(
child: TextField(
controller: controller,
obscureText: true,
),
));
await tester.enterText(find.byType(TextField), 'abcde fghi');
await skipPastScrollingAnimation(tester);
// Long press to select text.
final Offset bPos = textOffsetToPosition(tester, 1);
await tester.longPressAt(bPos, pointer: 7);
await tester.pumpAndSettle();
// Should only have paste option when whole obscure text is selected.
expect(find.text('Paste'), findsOneWidget);
expect(find.text('Copy'), findsNothing);
expect(find.text('Cut'), findsNothing);
expect(find.text('Select all'), findsNothing);
// Long press at the end
final Offset iPos = textOffsetToPosition(tester, 10);
final Offset slightRight = iPos + const Offset(30.0, 0.0);
await tester.longPressAt(slightRight, pointer: 7);
await tester.pumpAndSettle();
// Should have paste and select all options when collapse.
expect(find.text('Paste'), findsOneWidget);
expect(find.text('Select all'), findsOneWidget);
expect(find.text('Copy'), findsNothing);
expect(find.text('Cut'), findsNothing);
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
testWidgets('TextField height with minLines unset', (WidgetTester tester) async {
await tester.pumpWidget(textFieldBuilder());
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
final RenderBox inputBox = findInputBox();
final Size emptyInputSize = inputBox.size;
await tester.enterText(find.byType(TextField), 'No wrapping here.');
await tester.pumpWidget(textFieldBuilder());
expect(findInputBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize));
// Even when entering multiline text, TextField doesn't grow. It's a single
// line input.
await tester.enterText(find.byType(TextField), kThreeLines);
await tester.pumpWidget(textFieldBuilder());
expect(findInputBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize));
// maxLines: 3 makes the TextField 3 lines tall
await tester.enterText(find.byType(TextField), '');
await tester.pumpWidget(textFieldBuilder(maxLines: 3));
expect(findInputBox(), equals(inputBox));
expect(inputBox.size.height, greaterThan(emptyInputSize.height));
expect(inputBox.size.width, emptyInputSize.width);
final Size threeLineInputSize = inputBox.size;
// Filling with 3 lines of text stays the same size
await tester.enterText(find.byType(TextField), kThreeLines);
await tester.pumpWidget(textFieldBuilder(maxLines: 3));
expect(findInputBox(), equals(inputBox));
expect(inputBox.size, threeLineInputSize);
// An extra line won't increase the size because we max at 3.
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
await tester.pumpWidget(textFieldBuilder(maxLines: 3));
expect(findInputBox(), equals(inputBox));
expect(inputBox.size, threeLineInputSize);
// But now it will... but it will max at four
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
await tester.pumpWidget(textFieldBuilder(maxLines: 4));
expect(findInputBox(), equals(inputBox));
expect(inputBox.size.height, greaterThan(threeLineInputSize.height));
expect(inputBox.size.width, threeLineInputSize.width);
final Size fourLineInputSize = inputBox.size;
// Now it won't max out until the end
await tester.enterText(find.byType(TextField), '');
await tester.pumpWidget(textFieldBuilder(maxLines: null));
expect(findInputBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize));
await tester.enterText(find.byType(TextField), kThreeLines);
await tester.pump();
expect(inputBox.size, equals(threeLineInputSize));
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
await tester.pump();
expect(inputBox.size.height, greaterThan(fourLineInputSize.height));
expect(inputBox.size.width, fourLineInputSize.width);
});
testWidgets('TextField height with minLines and maxLines', (WidgetTester tester) async {
await tester.pumpWidget(textFieldBuilder());
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
final RenderBox inputBox = findInputBox();
final Size emptyInputSize = inputBox.size;
await tester.enterText(find.byType(TextField), 'No wrapping here.');
await tester.pumpWidget(textFieldBuilder());
expect(findInputBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize));
// min and max set to same value locks height to value.
await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 3));
expect(findInputBox(), equals(inputBox));
expect(inputBox.size.height, greaterThan(emptyInputSize.height));
expect(inputBox.size.width, emptyInputSize.width);
final Size threeLineInputSize = inputBox.size;
// maxLines: null with minLines set grows beyond minLines
await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: null));
expect(findInputBox(), equals(inputBox));
expect(inputBox.size, threeLineInputSize);
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
await tester.pump();
expect(inputBox.size.height, greaterThan(threeLineInputSize.height));
expect(inputBox.size.width, threeLineInputSize.width);
// With minLines and maxLines set, input will expand through the range
await tester.enterText(find.byType(TextField), '');
await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 4));
expect(findInputBox(), equals(inputBox));
expect(inputBox.size, equals(threeLineInputSize));
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
await tester.pump();
expect(inputBox.size.height, greaterThan(threeLineInputSize.height));
expect(inputBox.size.width, threeLineInputSize.width);
// minLines can't be greater than maxLines.
expect(() async {
await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 2));
}, throwsAssertionError);
// maxLines defaults to 1 and can't be less than minLines
expect(() async {
await tester.pumpWidget(textFieldBuilder(minLines: 3));
}, throwsAssertionError);
});
testWidgets('Multiline text when wrapped in Expanded', (WidgetTester tester) async {
Widget expandedTextFieldBuilder({
int? maxLines = 1,
int? minLines,
bool expands = false,
}) {
return boilerplate(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: TextField(
key: textFieldKey,
style: const TextStyle(color: Colors.black, fontSize: 34.0),
maxLines: maxLines,
minLines: minLines,
expands: expands,
decoration: const InputDecoration(
hintText: 'Placeholder',
),
),
),
],
),
);
}
await tester.pumpWidget(expandedTextFieldBuilder());
RenderBox findBorder() {
return tester.renderObject(find.descendant(
of: find.byType(InputDecorator),
matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_BorderContainer'),
));
}
final RenderBox border = findBorder();
// Without expanded: true and maxLines: null, the TextField does not expand
// to fill its parent when wrapped in an Expanded widget.
final Size unexpandedInputSize = border.size;
// It does expand to fill its parent when expands: true, maxLines: null, and
// it's wrapped in an Expanded widget.
await tester.pumpWidget(expandedTextFieldBuilder(expands: true, maxLines: null));
expect(border.size.height, greaterThan(unexpandedInputSize.height));
expect(border.size.width, unexpandedInputSize.width);
// min/maxLines that is not null and expands: true contradict each other.
expect(() async {
await tester.pumpWidget(expandedTextFieldBuilder(expands: true, maxLines: 4));
}, throwsAssertionError);
expect(() async {
await tester.pumpWidget(expandedTextFieldBuilder(expands: true, minLines: 1, maxLines: null));
}, throwsAssertionError);
});
// Regression test for https://github.com/flutter/flutter/pull/29093
testWidgets('Multiline text when wrapped in IntrinsicHeight', (WidgetTester tester) async {
final Key intrinsicHeightKey = UniqueKey();
Widget intrinsicTextFieldBuilder(bool wrapInIntrinsic) {
final TextFormField textField = TextFormField(
key: textFieldKey,
style: const TextStyle(color: Colors.black, fontSize: 34.0),
maxLines: null,
decoration: const InputDecoration(
counterText: 'I am counter',
),
);
final Widget widget = wrapInIntrinsic
? IntrinsicHeight(key: intrinsicHeightKey, child: textField)
: textField;
return boilerplate(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[widget],
),
);
}
await tester.pumpWidget(intrinsicTextFieldBuilder(false));
expect(find.byKey(intrinsicHeightKey), findsNothing);
RenderBox findEditableText() => tester.renderObject(find.byType(EditableText));
RenderBox editableText = findEditableText();
final Size unwrappedEditableTextSize = editableText.size;
// Wrapping in IntrinsicHeight should not affect the height of the input
await tester.pumpWidget(intrinsicTextFieldBuilder(true));
editableText = findEditableText();
expect(editableText.size.height, unwrappedEditableTextSize.height);
expect(editableText.size.width, unwrappedEditableTextSize.width);
});
// Regression test for https://github.com/flutter/flutter/pull/29093
testWidgets('errorText empty string', (WidgetTester tester) async {
Widget textFormFieldBuilder(String? errorText) {
return boilerplate(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
key: textFieldKey,
maxLength: 3,
maxLengthEnforcement: MaxLengthEnforcement.none,
decoration: InputDecoration(
counterText: '',
errorText: errorText,
),
),
],
),
);
}
await tester.pumpWidget(textFormFieldBuilder(null));
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
final RenderBox inputBox = findInputBox();
final Size errorNullInputSize = inputBox.size;
// Setting errorText causes the input's height to increase to accommodate it
await tester.pumpWidget(textFormFieldBuilder('im errorText'));
expect(inputBox, findInputBox());
expect(inputBox.size.height, greaterThan(errorNullInputSize.height));
expect(inputBox.size.width, errorNullInputSize.width);
final Size errorInputSize = inputBox.size;
// Setting errorText to an empty string causes the input's height to
// increase to accommodate it, even though it's not displayed.
// This may or may not be ideal behavior, but it is legacy behavior and
// there are visual tests that rely on it (see Github issue referenced at
// the top of this test). A counterText of empty string does not affect
// input height, however.
await tester.pumpWidget(textFormFieldBuilder(''));
expect(inputBox, findInputBox());
expect(inputBox.size.height, errorInputSize.height);
expect(inputBox.size.width, errorNullInputSize.width);
});
testWidgets('Growable TextField when content height exceeds parent', (WidgetTester tester) async {
const double height = 200.0;
const double padding = 24.0;
Widget containedTextFieldBuilder({
Widget? counter,
String? helperText,
String? labelText,
Widget? prefix,
}) {
return boilerplate(
child: SizedBox(
height: height,
child: TextField(
key: textFieldKey,
maxLines: null,
decoration: InputDecoration(
counter: counter,
helperText: helperText,
labelText: labelText,
prefix: prefix,
),
),
),
);
}
await tester.pumpWidget(containedTextFieldBuilder());
RenderBox findEditableText() => tester.renderObject(find.byType(EditableText));
final RenderBox inputBox = findEditableText();
// With no decoration and when overflowing with content, the EditableText
// takes up the full height minus the padding, so the input fits perfectly
// inside the parent.
await tester.enterText(find.byType(TextField), 'a\n' * 11);
await tester.pump();
expect(findEditableText(), equals(inputBox));
expect(inputBox.size.height, height - padding);
// Adding a counter causes the EditableText to shrink to fit the counter
// inside the parent as well.
const double counterHeight = 40.0;
const double subtextGap = 8.0;
const double counterSpace = counterHeight + subtextGap;
await tester.pumpWidget(containedTextFieldBuilder(
counter: Container(height: counterHeight),
));
expect(findEditableText(), equals(inputBox));
expect(inputBox.size.height, height - padding - counterSpace);
// Including helperText causes the EditableText to shrink to fit the text
// inside the parent as well.
await tester.pumpWidget(containedTextFieldBuilder(
helperText: 'I am helperText',
));
expect(findEditableText(), equals(inputBox));
const double helperTextSpace = 12.0;
expect(inputBox.size.height, height - padding - helperTextSpace - subtextGap);
// When both helperText and counter are present, EditableText shrinks by the
// height of the taller of the two in order to fit both within the parent.
await tester.pumpWidget(containedTextFieldBuilder(
counter: Container(height: counterHeight),
helperText: 'I am helperText',
));
expect(findEditableText(), equals(inputBox));
expect(inputBox.size.height, height - padding - counterSpace);
// When a label is present, EditableText shrinks to fit it at the top so
// that the bottom of the input still lines up perfectly with the parent.
await tester.pumpWidget(containedTextFieldBuilder(
labelText: 'I am labelText',
));
const double labelSpace = 16.0;
expect(findEditableText(), equals(inputBox));
expect(inputBox.size.height, height - padding - labelSpace);
// When decoration is present on the top and bottom, EditableText shrinks to
// fit both inside the parent independently.
await tester.pumpWidget(containedTextFieldBuilder(
counter: Container(height: counterHeight),
labelText: 'I am labelText',
));
expect(findEditableText(), equals(inputBox));
expect(inputBox.size.height, height - padding - counterSpace - labelSpace);
// When a prefix or suffix is present in an input that's full of content,
// it is ignored and allowed to expand beyond the top of the input. Other
// top and bottom decoration is still respected.
await tester.pumpWidget(containedTextFieldBuilder(
counter: Container(height: counterHeight),
labelText: 'I am labelText',
prefix: const SizedBox(
width: 10,
height: 60,
),
));
expect(findEditableText(), equals(inputBox));
expect(
inputBox.size.height,
height
- padding
- labelSpace
- counterSpace,
);
});
testWidgets('Multiline hint text will wrap up to maxLines', (WidgetTester tester) async {
final Key textFieldKey = UniqueKey();
Widget builder(int? maxLines, final String hintMsg) {
return boilerplate(
child: TextField(
key: textFieldKey,
style: const TextStyle(color: Colors.black, fontSize: 34.0),
maxLines: maxLines,
decoration: InputDecoration(
hintText: hintMsg,
),
),
);
}
const String hintPlaceholder = 'Placeholder';
const String multipleLineText = "Here's a text, which is more than one line, to demonstrate the multiple line hint text";
await tester.pumpWidget(builder(null, hintPlaceholder));
RenderBox findHintText(String hint) => tester.renderObject(find.text(hint));
final RenderBox hintTextBox = findHintText(hintPlaceholder);
final Size oneLineHintSize = hintTextBox.size;
await tester.pumpWidget(builder(null, hintPlaceholder));
expect(findHintText(hintPlaceholder), equals(hintTextBox));
expect(hintTextBox.size, equals(oneLineHintSize));
const int maxLines = 3;
await tester.pumpWidget(builder(maxLines, multipleLineText));
final Text hintTextWidget = tester.widget(find.text(multipleLineText));
expect(hintTextWidget.maxLines, equals(maxLines));
expect(findHintText(multipleLineText).size.width, greaterThanOrEqualTo(oneLineHintSize.width));
expect(findHintText(multipleLineText).size.height, greaterThanOrEqualTo(oneLineHintSize.height));
});
testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
style: const TextStyle(color: Colors.black, fontSize: 34.0),
maxLines: 3,
),
),
);
const String testValue = kThreeLines;
const String cutValue = 'First line of stuff';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
// Check that the text spans multiple lines.
final Offset firstPos = textOffsetToPosition(tester, testValue.indexOf('First'));
final Offset secondPos = textOffsetToPosition(tester, testValue.indexOf('Second'));
final Offset thirdPos = textOffsetToPosition(tester, testValue.indexOf('Third'));
final Offset middleStringPos = textOffsetToPosition(tester, testValue.indexOf('irst'));
expect(firstPos.dx, lessThan(middleStringPos.dx));
expect(firstPos.dx, secondPos.dx);
expect(firstPos.dx, thirdPos.dx);
expect(firstPos.dy, lessThan(secondPos.dy));
expect(secondPos.dy, lessThan(thirdPos.dy));
// Long press the 'n' in 'until' to select the word.
final Offset untilPos = textOffsetToPosition(tester, testValue.indexOf('until')+1);
TestGesture gesture = await tester.startGesture(untilPos, pointer: 7);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
expect(controller.selection.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);
if (!isContextMenuProvidedByPlatform) {
await tester.tap(find.text('Cut'));
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.text, cutValue);
}
});
testWidgets('Can scroll multiline input', (WidgetTester tester) async {
final Key textFieldKey = UniqueKey();
final TextEditingController controller = TextEditingController(
text: kMoreThanFourLines,
);
await tester.pumpWidget(
overlay(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
key: textFieldKey,
controller: controller,
style: const TextStyle(color: Colors.black, fontSize: 34.0),
maxLines: 2,
),
),
);
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
final RenderBox inputBox = findInputBox();
// Check that the last line of text is not displayed.
final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
expect(firstPos.dx, fourthPos.dx);
expect(firstPos.dy, lessThan(fourthPos.dy));
expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue);
expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(fourthPos)), isFalse);
TestGesture gesture = await tester.startGesture(firstPos, pointer: 7);
await tester.pump();
await gesture.moveBy(const Offset(0.0, -1000.0));
await tester.pump(const Duration(seconds: 1));
// Wait and drag again to trigger https://github.com/flutter/flutter/issues/6329
// (No idea why this is necessary, but the bug wouldn't repro without it.)
await gesture.moveBy(const Offset(0.0, -1000.0));
await tester.pump(const Duration(seconds: 1));
await gesture.up();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Now the first line is scrolled up, and the fourth line is visible.
Offset newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
Offset newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
expect(newFirstPos.dy, lessThan(firstPos.dy));
expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isFalse);
expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isTrue);
// Now try scrolling by dragging the selection handle.
// Long press the middle of the word "won't" in the fourth line.
final Offset selectedWordPos = textOffsetToPosition(
tester,
kMoreThanFourLines.indexOf('Fourth line') + 14,
);
gesture = await tester.startGesture(selectedWordPos, pointer: 7);
await tester.pump(const Duration(seconds: 1));
await gesture.up();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(controller.selection.base.offset, 77);
expect(controller.selection.extent.offset, 82);
// Sanity check for the word selected is the intended one.
expect(
controller.text.substring(controller.selection.baseOffset, controller.selection.extentOffset),
"won't",
);
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
expect(endpoints.length, 2);
// Drag the left handle to the first line, just after 'First'.
final Offset handlePos = endpoints[0].point + const Offset(-1, 1);
final Offset newHandlePos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First') + 5);
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump(const Duration(seconds: 1));
await gesture.moveTo(newHandlePos + const Offset(0.0, -10.0));
await tester.pump(const Duration(seconds: 1));
await gesture.up();
await tester.pump(const Duration(seconds: 1));
// The text should have scrolled up with the handle to keep the active
// cursor visible, back to its original position.
newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
expect(newFirstPos.dy, firstPos.dy);
expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue);
expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse);
});
testWidgets('TextField smoke test', (WidgetTester tester) async {
late String textFieldValue;
await tester.pumpWidget(
overlay(
child: TextField(
decoration: null,
onChanged: (String value) {
textFieldValue = value;
},
),
),
);
Future<void> checkText(String testValue) {
return TestAsyncUtils.guard(() async {
await tester.enterText(find.byType(TextField), testValue);
// Check that the onChanged event handler fired.
expect(textFieldValue, equals(testValue));
await tester.pump();
});
}
await checkText('Hello World');
});
testWidgets('TextField with global key', (WidgetTester tester) async {
final GlobalKey textFieldKey = GlobalKey(debugLabel: 'textFieldKey');
late String textFieldValue;
await tester.pumpWidget(
overlay(
child: TextField(
key: textFieldKey,
decoration: const InputDecoration(
hintText: 'Placeholder',
),
onChanged: (String value) { textFieldValue = value; },
),
),
);
Future<void> checkText(String testValue) async {
return TestAsyncUtils.guard(() async {
await tester.enterText(find.byType(TextField), testValue);
// Check that the onChanged event handler fired.
expect(textFieldValue, equals(testValue));
await tester.pump();
});
}
await checkText('Hello World');
});
testWidgets('TextField errorText trumps helperText', (WidgetTester tester) async {
await tester.pumpWidget(
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.bodySmall!.fontSize);
});
testWidgets('TextField with specified helperStyle', (WidgetTester tester) async {
final TextStyle style = TextStyle(
inherit: false,
color: Colors.pink[500],
fontSize: 10.0,
);
await tester.pumpWidget(
overlay(
child: TextField(
decoration: InputDecoration(
helperText: 'helper text',
helperStyle: style,
),
),
),
);
final Text helperText = tester.widget(find.text('helper text'));
expect(helperText.style, style);
});
testWidgets('TextField with default hintStyle', (WidgetTester tester) async {
final TextStyle style = TextStyle(
color: Colors.pink[500],
fontSize: 10.0,
);
final ThemeData themeData = ThemeData(
hintColor: Colors.blue[500],
);
await tester.pumpWidget(
overlay(
child: Theme(
data: themeData,
child: TextField(
decoration: const InputDecoration(
hintText: 'Placeholder',
),
style: style,
),
),
),
);
final Text hintText = tester.widget(find.text('Placeholder'));
expect(hintText.style!.color, themeData.hintColor);
expect(hintText.style!.fontSize, style.fontSize);
});
testWidgets('TextField with specified hintStyle', (WidgetTester tester) async {
final TextStyle hintStyle = TextStyle(
inherit: false,
color: Colors.pink[500],
fontSize: 10.0,
);
await tester.pumpWidget(
overlay(
child: TextField(
decoration: InputDecoration(
hintText: 'Placeholder',
hintStyle: hintStyle,
),
),
),
);
final Text hintText = tester.widget(find.text('Placeholder'));
expect(hintText.style, hintStyle);
});
testWidgets('TextField with specified prefixStyle', (WidgetTester tester) async {
final TextStyle prefixStyle = TextStyle(
inherit: false,
color: Colors.pink[500],
fontSize: 10.0,
);
await tester.pumpWidget(
overlay(
child: TextField(
decoration: InputDecoration(
prefixText: 'Prefix:',
prefixStyle: prefixStyle,
),
),
),
);
final Text prefixText = tester.widget(find.text('Prefix:'));
expect(prefixText.style, prefixStyle);
});
testWidgets('TextField 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(EditableText))));
});
testWidgets('Can align to center', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
child: const SizedBox(
width: 300.0,
child: TextField(
textAlign: TextAlign.center,
decoration: null,
),
),
),
);
final RenderEditable editable = findRenderEditable(tester);
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: const SizedBox(
width: 300.0,
child: Center(
child: TextField(
textAlign: TextAlign.center,
decoration: null,
),
),
),
),
);
final RenderEditable editable = findRenderEditable(tester);
Offset topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft,
);
// The overlay() function centers its child within a 800x600 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;
late StateSetter setState;
await tester.pumpWidget(
overlay(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return TextField(controller: currentController);
},
),
),
);
expect(tester.testTextInput.editingState, isNull);
// Initial state with null controller.
await tester.tap(find.byType(TextField));
await tester.pump();
expect(tester.testTextInput.editingState!['text'], isEmpty);
// Update the controller from null to controller1.
setState(() {
currentController = controller1;
});
await tester.pump();
expect(tester.testTextInput.editingState!['text'], equals('Initial Text'));
// Verify that updates to controller1 are handled.
controller1.text = 'Updated Text';
await tester.idle();
expect(tester.testTextInput.editingState!['text'], equals('Updated Text'));
// Verify that switching from controller1 to controller2 is handled.
setState(() {
currentController = controller2;
});
await tester.pump();
expect(tester.testTextInput.editingState!['text'], equals('More Text'));
// Verify that updates to controller1 are ignored.
controller1.text = 'Ignored Text';
await tester.idle();
expect(tester.testTextInput.editingState!['text'], equals('More Text'));
// Verify that updates to controller text are handled.
controller2.text = 'Additional Text';
await tester.idle();
expect(tester.testTextInput.editingState!['text'], equals('Additional Text'));
// Verify that updates to controller selection are handled.
controller2.selection = const TextSelection(baseOffset: 0, extentOffset: 5);
await tester.idle();
expect(tester.testTextInput.editingState!['selectionBase'], equals(0));
expect(tester.testTextInput.editingState!['selectionExtent'], equals(5));
// Verify that calling clear() clears the text.
controller2.clear();
await tester.idle();
expect(tester.testTextInput.editingState!['text'], equals(''));
// Verify that switching from controller2 to null preserves current text.
controller2.text = 'The Final Cut';
await tester.idle();
expect(tester.testTextInput.editingState!['text'], equals('The Final Cut'));
setState(() {
currentController = null;
});
await tester.pump();
expect(tester.testTextInput.editingState!['text'], equals('The Final Cut'));
// Verify that changes to controller2 are ignored.
controller2.text = 'Goodbye Cruel World';
expect(tester.testTextInput.editingState!['text'], equals('The Final Cut'));
});
testWidgets('Cannot enter new lines onto single line TextField', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(controller: textController, decoration: null),
));
await tester.enterText(find.byType(TextField), 'abc\ndef');
expect(textController.text, 'abcdef');
});
testWidgets('Injected formatters are chained', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
decoration: null,
inputFormatters: <TextInputFormatter> [
FilteringTextInputFormatter.deny(
RegExp(r'[a-z]'),
replacementString: '#',
),
],
),
));
await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六');
// The default single line formatter replaces \n with empty string.
expect(textController.text, '#一#二#三#四#五#六');
});
testWidgets('Injected formatters are chained (deprecated names)', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
decoration: null,
inputFormatters: <TextInputFormatter> [
FilteringTextInputFormatter.deny(
RegExp(r'[a-z]'),
replacementString: '#',
),
],
),
));
await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六');
// The default single line formatter replaces \n with empty string.
expect(textController.text, '#一#二#三#四#五#六');
});
testWidgets('Chained formatters are in sequence', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
decoration: null,
maxLines: 2,
inputFormatters: <TextInputFormatter> [
FilteringTextInputFormatter.deny(
RegExp(r'[a-z]'),
replacementString: '12\n',
),
FilteringTextInputFormatter.allow(RegExp(r'\n[0-9]')),
],
),
));
await tester.enterText(find.byType(TextField), 'a1b2c3');
// The first formatter turns it into
// 12\n112\n212\n3
// The second formatter turns it into
// \n1\n2\n3
// Multiline is allowed since maxLine != 1.
expect(textController.text, '\n1\n2\n3');
});
testWidgets('Chained formatters are in sequence (deprecated names)', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
decoration: null,
maxLines: 2,
inputFormatters: <TextInputFormatter> [
FilteringTextInputFormatter.deny(
RegExp(r'[a-z]'),
replacementString: '12\n',
),
FilteringTextInputFormatter.allow(RegExp(r'\n[0-9]')),
],
),
));
await tester.enterText(find.byType(TextField), 'a1b2c3');
// The first formatter turns it into
// 12\n112\n212\n3
// The second formatter turns it into
// \n1\n2\n3
// Multiline is allowed since maxLine != 1.
expect(textController.text, '\n1\n2\n3');
});
testWidgets('Pasted values are formatted', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: textController,
decoration: null,
inputFormatters: <TextInputFormatter> [
FilteringTextInputFormatter.digitsOnly,
],
),
),
);
await tester.enterText(find.byType(TextField), 'a1b\n2c3');
expect(textController.text, '123');
await skipPastScrollingAnimation(tester);
await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(textController.selection),
renderEditable,
);
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
Clipboard.setData(const ClipboardData(text: '一4二\n5三6'));
await tester.tap(find.text('Paste'));
await tester.pump();
// Puts 456 before the 2 in 123.
expect(textController.text, '145623');
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
testWidgets('Pasted values are formatted (deprecated names)', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: textController,
decoration: null,
inputFormatters: <TextInputFormatter> [
FilteringTextInputFormatter.digitsOnly,
],
),
),
);
await tester.enterText(find.byType(TextField), 'a1b\n2c3');
expect(textController.text, '123');
await skipPastScrollingAnimation(tester);
await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(textController.selection),
renderEditable,
);
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
Clipboard.setData(const ClipboardData(text: '一4二\n5三6'));
await tester.tap(find.text('Paste'));
await tester.pump();
// Puts 456 before the 2 in 123.
expect(textController.text, '145623');
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
testWidgets('Do not add LengthLimiting formatter to the user supplied list', (WidgetTester tester) async {
final List<TextInputFormatter> formatters = <TextInputFormatter>[];
await tester.pumpWidget(
overlay(
child: TextField(
decoration: null,
maxLength: 5,
inputFormatters: formatters,
),
),
);
expect(formatters.isEmpty, isTrue);
});
testWidgets('Text field scrolls the caret into view', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: SizedBox(
width: 100.0,
child: TextField(
controller: controller,
),
),
),
);
final String longText = 'a' * 20;
await tester.enterText(find.byType(TextField), longText);
await skipPastScrollingAnimation(tester);
ScrollableState scrollableState = tester.firstState(find.byType(Scrollable));
expect(scrollableState.position.pixels, equals(0.0));
// Move the caret to the end of the text and check that the text field
// scrolls to make the caret visible.
scrollableState = tester.firstState(find.byType(Scrollable));
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
editableTextState.userUpdateTextEditingValue(
editableTextState.textEditingValue.copyWith(
selection: TextSelection.collapsed(offset: longText.length),
),
null,
);
await tester.pump(); // TODO(ianh): Figure out why this extra pump is needed.
await skipPastScrollingAnimation(tester);
scrollableState = tester.firstState(find.byType(Scrollable));
// For a horizontal input, scrolls to the exact position of the caret.
expect(scrollableState.position.pixels, equals(222.0));
});
testWidgets('Multiline text field scrolls the caret into view', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
maxLines: 6,
),
),
);
const String tallText = 'a\nb\nc\nd\ne\nf\ng'; // One line over max
await tester.enterText(find.byType(TextField), tallText);
await skipPastScrollingAnimation(tester);
ScrollableState scrollableState = tester.firstState(find.byType(Scrollable));
expect(scrollableState.position.pixels, equals(0.0));
// Move the caret to the end of the text and check that the text field
// scrolls to make the caret visible.
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
editableTextState.userUpdateTextEditingValue(
editableTextState.textEditingValue.copyWith(
selection: const TextSelection.collapsed(offset: tallText.length),
),
null,
);
await tester.pump();
await skipPastScrollingAnimation(tester);
// Should have scrolled down exactly one line height (7 lines of text in 6
// line text field).
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
scrollableState = tester.firstState(find.byType(Scrollable));
expect(scrollableState.position.pixels, moreOrLessEquals(lineHeight, epsilon: 0.1));
});
testWidgets('haptic feedback', (WidgetTester tester) async {
final FeedbackTester feedback = FeedbackTester();
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: SizedBox(
width: 100.0,
child: TextField(
controller: controller,
),
),
),
);
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 0);
expect(feedback.hapticCount, 0);
await tester.longPress(find.byType(TextField));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 0);
expect(feedback.hapticCount, 1);
feedback.dispose();
});
testWidgets('Text field drops selection color when losing focus', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/103341.
final Key key1 = UniqueKey();
final Key key2 = UniqueKey();
final TextEditingController controller1 = TextEditingController();
const Color selectionColor = Colors.orange;
const Color cursorColor = Colors.red;
await tester.pumpWidget(
overlay(
child: DefaultSelectionStyle(
selectionColor: selectionColor,
cursorColor: cursorColor,
child: Column(
children: <Widget>[
TextField(
key: key1,
controller: controller1,
),
TextField(key: key2),
],
),
),
),
);
const TextSelection selection = TextSelection(baseOffset: 0, extentOffset: 4);
final EditableTextState state1 = tester.state<EditableTextState>(find.byType(EditableText).first);
final EditableTextState state2 = tester.state<EditableTextState>(find.byType(EditableText).last);
await tester.tap(find.byKey(key1));
await tester.enterText(find.byKey(key1), 'abcd');
await tester.pump();
await tester.tap(find.byKey(key2));
await tester.enterText(find.byKey(key2), 'dcba');
await tester.pump();
// Focus and selection is active on first TextField, so the second TextFields
// selectionColor should be dropped.
await tester.tap(find.byKey(key1));
controller1.selection = const TextSelection(baseOffset: 0, extentOffset: 4);
await tester.pump();
expect(controller1.selection, selection);
expect(state1.widget.selectionColor, selectionColor);
expect(state2.widget.selectionColor, null);
// Focus and selection is active on second TextField, so the first TextFields
// selectionColor should be dropped.
await tester.tap(find.byKey(key2));
await tester.pump();
expect(state1.widget.selectionColor, null);
expect(state2.widget.selectionColor, selectionColor);
});
testWidgets('Selection is consistent with text length', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
controller.text = 'abcde';
controller.selection = const TextSelection.collapsed(offset: 5);
controller.text = '';
expect(controller.selection.start, lessThanOrEqualTo(0));
expect(controller.selection.end, lessThanOrEqualTo(0));
late FlutterError error;
try {
controller.selection = const TextSelection.collapsed(offset: 10);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error.diagnostics.length, 1);
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' invalid text selection: TextSelection.collapsed(offset: 10,\n'
' affinity: TextAffinity.downstream, isDirectional: false)\n',
),
);
}
});
// Regression test for https://github.com/flutter/flutter/issues/35848
testWidgets('Clearing text field with suffixIcon does not cause text selection exception', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Prefilled text.',
);
await tester.pumpWidget(
boilerplate(
child: TextField(
controller: controller,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: const Icon(Icons.close),
onPressed: controller.clear,
),
),
),
),
);
await tester.tap(find.byType(IconButton));
expect(controller.text, '');
});
testWidgets('maxLength limits input.', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
maxLength: 10,
),
));
await tester.enterText(find.byType(TextField), '0123456789101112');
expect(textController.text, '0123456789');
});
testWidgets('maxLength limits input with surrogate pairs.', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
maxLength: 10,
),
));
const String surrogatePair = '😆';
await tester.enterText(find.byType(TextField), '${surrogatePair}0123456789101112');
expect(textController.text, '${surrogatePair}012345678');
});
testWidgets('maxLength limits input with grapheme clusters.', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
maxLength: 10,
),
));
const String graphemeCluster = '👨‍👩‍👦';
await tester.enterText(find.byType(TextField), '${graphemeCluster}0123456789101112');
expect(textController.text, '${graphemeCluster}012345678');
});
testWidgets('maxLength limits input in the center of a maxed-out field.', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/37420.
final TextEditingController textController = TextEditingController();
const String testValue = '0123456789';
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
maxLength: 10,
),
));
// Max out the character limit in the field.
await tester.enterText(find.byType(TextField), testValue);
expect(textController.text, testValue);
// Entering more characters at the end does nothing.
await tester.enterText(find.byType(TextField), '${testValue}9999999');
expect(textController.text, testValue);
// Entering text in the middle of the field also does nothing.
await tester.enterText(find.byType(TextField), '0123455555555556789');
expect(textController.text, testValue);
});
testWidgets(
'maxLength limits input in the center of a maxed-out field, with collapsed selection',
(WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
const String testValue = '0123456789';
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
maxLength: 10,
),
));
// Max out the character limit in the field.
await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: testValue,
selection: TextSelection.collapsed(offset: 10),
));
await tester.pump();
expect(textController.text, testValue);
// Entering more characters at the end does nothing.
await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: '${testValue}9999999',
selection: TextSelection.collapsed(offset: 10 + 7),
));
await tester.pump();
expect(textController.text, testValue);
// Entering text in the middle of the field also does nothing.
// Entering more characters at the end does nothing.
await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: '0123455555555556789',
selection: TextSelection.collapsed(offset: 19),
));
await tester.pump();
expect(textController.text, testValue);
},
);
testWidgets(
'maxLength limits input in the center of a maxed-out field, with non-collapsed selection',
(WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
const String testValue = '0123456789';
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
maxLength: 10,
maxLengthEnforcement: MaxLengthEnforcement.enforced,
),
));
// Max out the character limit in the field.
await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: testValue,
selection: TextSelection(baseOffset: 8, extentOffset: 10),
));
await tester.pump();
expect(textController.text, testValue);
// Entering more characters at the end does nothing.
await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: '01234569999999',
selection: TextSelection.collapsed(offset: 14),
));
await tester.pump();
expect(textController.text, '0123456999');
},
);
testWidgets('maxLength limits input length even if decoration is null.', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
decoration: null,
maxLength: 10,
),
));
await tester.enterText(find.byType(TextField), '0123456789101112');
expect(textController.text, '0123456789');
});
testWidgets('maxLength still works with other formatters', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
maxLength: 10,
inputFormatters: <TextInputFormatter> [
FilteringTextInputFormatter.deny(
RegExp(r'[a-z]'),
replacementString: '#',
),
],
),
));
await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六');
// The default single line formatter replaces \n with empty string.
expect(textController.text, '#一#二#三#四#五');
});
testWidgets('maxLength still works with other formatters (deprecated names)', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
maxLength: 10,
inputFormatters: <TextInputFormatter> [
FilteringTextInputFormatter.deny(
RegExp(r'[a-z]'),
replacementString: '#',
),
],
),
));
await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六');
// The default single line formatter replaces \n with empty string.
expect(textController.text, '#一#二#三#四#五');
});
testWidgets("maxLength isn't enforced when maxLengthEnforcement.none.", (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
maxLength: 10,
maxLengthEnforcement: MaxLengthEnforcement.none,
),
));
await tester.enterText(find.byType(TextField), '0123456789101112');
expect(textController.text, '0123456789101112');
});
testWidgets('maxLength shows warning when maxLengthEnforcement.none.', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
const TextStyle testStyle = TextStyle(color: Colors.deepPurpleAccent);
await tester.pumpWidget(boilerplate(
child: TextField(
decoration: const InputDecoration(errorStyle: testStyle),
controller: textController,
maxLength: 10,
maxLengthEnforcement: MaxLengthEnforcement.none,
),
));
await tester.enterText(find.byType(TextField), '0123456789101112');
await tester.pump();
expect(textController.text, '0123456789101112');
expect(find.text('16/10'), findsOneWidget);
Text counterTextWidget = tester.widget(find.text('16/10'));
expect(counterTextWidget.style!.color, equals(Colors.deepPurpleAccent));
await tester.enterText(find.byType(TextField), '0123456789');
await tester.pump();
expect(textController.text, '0123456789');
expect(find.text('10/10'), findsOneWidget);
counterTextWidget = tester.widget(find.text('10/10'));
expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent)));
});
testWidgets('maxLength shows warning in Material 3', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
final ThemeData theme = ThemeData.from(
colorScheme: const ColorScheme.light().copyWith(error: Colors.deepPurpleAccent),
useMaterial3: true,
);
await tester.pumpWidget(boilerplate(
theme: theme,
child: TextField(
controller: textController,
maxLength: 10,
maxLengthEnforcement: MaxLengthEnforcement.none,
),
));
await tester.enterText(find.byType(TextField), '0123456789101112');
await tester.pump();
expect(textController.text, '0123456789101112');
expect(find.text('16/10'), findsOneWidget);
Text counterTextWidget = tester.widget(find.text('16/10'));
expect(counterTextWidget.style!.color, equals(Colors.deepPurpleAccent));
await tester.enterText(find.byType(TextField), '0123456789');
await tester.pump();
expect(textController.text, '0123456789');
expect(find.text('10/10'), findsOneWidget);
counterTextWidget = tester.widget(find.text('10/10'));
expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent)));
});
testWidgets('maxLength shows warning when maxLengthEnforcement.none with surrogate pairs.', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
const TextStyle testStyle = TextStyle(color: Colors.deepPurpleAccent);
await tester.pumpWidget(boilerplate(
child: TextField(
decoration: const InputDecoration(errorStyle: testStyle),
controller: textController,
maxLength: 10,
maxLengthEnforcement: MaxLengthEnforcement.none,
),
));
await tester.enterText(find.byType(TextField), '😆012345678910111');
await tester.pump();
expect(textController.text, '😆012345678910111');
expect(find.text('16/10'), findsOneWidget);
Text counterTextWidget = tester.widget(find.text('16/10'));
expect(counterTextWidget.style!.color, equals(Colors.deepPurpleAccent));
await tester.enterText(find.byType(TextField), '😆012345678');
await tester.pump();
expect(textController.text, '😆012345678');
expect(find.text('10/10'), findsOneWidget);
counterTextWidget = tester.widget(find.text('10/10'));
expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent)));
});
testWidgets('maxLength shows warning when maxLengthEnforcement.none with grapheme clusters.', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
const TextStyle testStyle = TextStyle(color: Colors.deepPurpleAccent);
await tester.pumpWidget(boilerplate(
child: TextField(
decoration: const InputDecoration(errorStyle: testStyle),
controller: textController,
maxLength: 10,
maxLengthEnforcement: MaxLengthEnforcement.none,
),
));
await tester.enterText(find.byType(TextField), '👨‍👩‍👦012345678910111');
await tester.pump();
expect(textController.text, '👨‍👩‍👦012345678910111');
expect(find.text('16/10'), findsOneWidget);
Text counterTextWidget = tester.widget(find.text('16/10'));
expect(counterTextWidget.style!.color, equals(Colors.deepPurpleAccent));
await tester.enterText(find.byType(TextField), '👨‍👩‍👦012345678');
await tester.pump();
expect(textController.text, '👨‍👩‍👦012345678');
expect(find.text('10/10'), findsOneWidget);
counterTextWidget = tester.widget(find.text('10/10'));
expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent)));
});
testWidgets('maxLength limits input with surrogate pairs.', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
maxLength: 10,
),
));
const String surrogatePair = '😆';
await tester.enterText(find.byType(TextField), '${surrogatePair}0123456789101112');
expect(textController.text, '${surrogatePair}012345678');
});
testWidgets('maxLength limits input with grapheme clusters.', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
maxLength: 10,
),
));
const String graphemeCluster = '👨‍👩‍👦';
await tester.enterText(find.byType(TextField), '${graphemeCluster}0123456789101112');
expect(textController.text, '${graphemeCluster}012345678');
});
testWidgets('setting maxLength shows counter', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLength: 10,
),
),
),
),
);
expect(find.text('0/10'), findsOneWidget);
await tester.enterText(find.byType(TextField), '01234');
await tester.pump();
expect(find.text('5/10'), findsOneWidget);
});
testWidgets('maxLength counter measures surrogate pairs as one character', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLength: 10,
),
),
),
),
);
expect(find.text('0/10'), findsOneWidget);
const String surrogatePair = '😆';
await tester.enterText(find.byType(TextField), surrogatePair);
await tester.pump();
expect(find.text('1/10'), findsOneWidget);
});
testWidgets('maxLength counter measures grapheme clusters as one character', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLength: 10,
),
),
),
),
);
expect(find.text('0/10'), findsOneWidget);
const String familyEmoji = '👨‍👩‍👦';
await tester.enterText(find.byType(TextField), familyEmoji);
await tester.pump();
expect(find.text('1/10'), findsOneWidget);
});
testWidgets('setting maxLength to TextField.noMaxLength shows only entered length', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLength: TextField.noMaxLength,
),
),
),
),
);
expect(find.text('0'), findsOneWidget);
await tester.enterText(find.byType(TextField), '01234');
await tester.pump();
expect(find.text('5'), findsOneWidget);
});
testWidgets('passing a buildCounter shows returned widget', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: Center(
child: TextField(
buildCounter: (BuildContext context, { required int currentLength, int? maxLength, required bool isFocused }) {
return Text('$currentLength of $maxLength');
},
maxLength: 10,
),
),
),
),
);
expect(find.text('0 of 10'), findsOneWidget);
await tester.enterText(find.byType(TextField), '01234');
await tester.pump();
expect(find.text('5 of 10'), findsOneWidget);
});
testWidgets('TextField identifies as text field in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLength: 10,
),
),
),
),
);
expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField]));
semantics.dispose();
});
testWidgets('Disabled text field does not have tap action', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLength: 10,
enabled: false,
),
),
),
),
);
expect(semantics, isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.tap])));
semantics.dispose();
});
testWidgets('Readonly text field does not have tap action', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLength: 10,
readOnly: true,
),
),
),
),
);
expect(semantics, isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.tap])));
semantics.dispose();
});
testWidgets('Disabled text field hides helper and counter', (WidgetTester tester) async {
const String helperText = 'helper text';
const String counterText = 'counter text';
const String errorText = 'error text';
Widget buildFrame(bool enabled, bool hasError) {
return MaterialApp(
home: Material(
child: Center(
child: TextField(
decoration: InputDecoration(
labelText: 'label text',
helperText: helperText,
counterText: counterText,
errorText: hasError ? errorText : null,
enabled: enabled,
),
),
),
),
);
}
await tester.pumpWidget(buildFrame(true, false));
Text helperWidget = tester.widget(find.text(helperText));
Text counterWidget = tester.widget(find.text(counterText));
expect(helperWidget.style!.color, isNot(equals(Colors.transparent)));
expect(counterWidget.style!.color, isNot(equals(Colors.transparent)));
await tester.pumpWidget(buildFrame(true, true));
counterWidget = tester.widget(find.text(counterText));
Text errorWidget = tester.widget(find.text(errorText));
expect(helperWidget.style!.color, isNot(equals(Colors.transparent)));
expect(errorWidget.style!.color, isNot(equals(Colors.transparent)));
// When enabled is false, the helper/error and counter are not visible.
await tester.pumpWidget(buildFrame(false, false));
helperWidget = tester.widget(find.text(helperText));
counterWidget = tester.widget(find.text(counterText));
expect(helperWidget.style!.color, equals(Colors.transparent));
expect(counterWidget.style!.color, equals(Colors.transparent));
await tester.pumpWidget(buildFrame(false, true));
errorWidget = tester.widget(find.text(errorText));
counterWidget = tester.widget(find.text(counterText));
expect(counterWidget.style!.color, equals(Colors.transparent));
expect(errorWidget.style!.color, equals(Colors.transparent));
});
testWidgets('currentValueLength/maxValueLength are in the tree', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
maxLength: 10,
),
),
),
),
);
expect(semantics, includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
maxValueLength: 10,
currentValueLength: 0,
));
await tester.showKeyboard(find.byType(TextField));
const String testValue = '123';
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: testValue,
selection: TextSelection.collapsed(offset: 3),
composing: TextRange(start: 0, end: testValue.length),
));
await tester.pump();
expect(semantics, includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isFocused],
maxValueLength: 10,
currentValueLength: 3,
));
semantics.dispose();
});
testWidgets('Read only TextField identifies as read only text field in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLength: 10,
readOnly: true,
),
),
),
),
);
expect(
semantics,
includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isReadOnly]),
);
semantics.dispose();
});
testWidgets("Disabled TextField can't be traversed to when disabled.", (WidgetTester tester) async {
final FocusNode focusNode1 = FocusNode(debugLabel: 'TextField 1');
final FocusNode focusNode2 = FocusNode(debugLabel: 'TextField 2');
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: Column(
children: <Widget>[
TextField(
focusNode: focusNode1,
autofocus: true,
maxLength: 10,
enabled: true,
),
TextField(
focusNode: focusNode2,
maxLength: 10,
enabled: false,
),
],
),
),
),
),
);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
expect(focusNode1.nextFocus(), isTrue);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
});
group('Keyboard Tests', () {
late TextEditingController controller;
setUp( () {
controller = TextEditingController();
});
Future<void> setupWidget(WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RawKeyboardListener(
focusNode: focusNode,
child: TextField(
controller: controller,
maxLines: 3,
),
),
),
),
);
await tester.pump();
}
testWidgets('Shift test 1', (WidgetTester tester) async {
await setupWidget(tester);
const String testValue = 'a big house';
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
// Need to wait for selection to catch up.
await tester.pump();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Shift test 2', (WidgetTester tester) async {
await setupWidget(tester);
const String testValue = 'abcdefghi';
await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: testValue,
selection: TextSelection.collapsed(offset: 3),
composing: TextRange(start: 0, end: testValue.length),
));
await tester.pump();
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Control Shift test', (WidgetTester tester) async {
await setupWidget(tester);
const String testValue = 'their big house';
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
await tester.pumpAndSettle();
await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 5);
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Down and up test', (WidgetTester tester) async {
await setupWidget(tester);
const String testValue = 'a big house';
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
// Need to wait for selection to catch up.
await tester.pump();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowUp);
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, -11);
await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowUp);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pumpAndSettle();
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Down and up test 2', (WidgetTester tester) async {
await setupWidget(tester);
const String testValue = 'a big house\njumped over a mouse\nOne more line yay'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
}
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 12);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 32);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 12);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Read only keyboard selection test', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'readonly');
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
readOnly: true,
),
),
);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);
expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
}, variant: KeySimulatorTransitModeVariant.all());
}, skip: areKeyEventsHandledByPlatform); // [intended] only applies to platforms where we handle key events.
testWidgets('Copy paste test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
final TextField textField =
TextField(
controller: controller,
maxLines: 3,
);
String clipboardContent = '';
tester.binding.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.setData') {
// ignore: avoid_dynamic_calls
clipboardContent = methodCall.arguments['text'] as String;
} else if (methodCall.method == 'Clipboard.getData') {
return <String, dynamic>{'text': clipboardContent};
}
return null;
});
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RawKeyboardListener(
focusNode: focusNode,
child: textField,
),
),
),
);
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
// Select the first 5 characters
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
}
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
// Copy them
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.sendKeyEvent(LogicalKeyboardKey.keyC);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
expect(clipboardContent, 'a big');
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
// Paste them
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
const String expected = 'a biga big house\njumped over a mouse';
expect(find.text(expected), findsOneWidget, reason: 'Because text contains ${controller.text}');
},
skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
variant: KeySimulatorTransitModeVariant.all()
);
// Regressing test for https://github.com/flutter/flutter/issues/78219
testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
final TextField textField = TextField(
controller: controller,
obscureText: true,
);
const String clipboardContent = 'I love Flutter!';
tester.binding.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.getData') {
return <String, dynamic>{'text': clipboardContent};
}
return null;
});
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RawKeyboardListener(
focusNode: focusNode,
child: textField,
),
),
),
);
focusNode.requestFocus();
await tester.pump();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
// This setter will set `selection` invalid.
controller.text = '';
// Paste clipboardContent to the text field.
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
// Do nothing.
expect(find.text(clipboardContent), findsNothing);
expect(controller.selection, const TextSelection.collapsed(offset: -1));
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Cut test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
final TextField textField =
TextField(
controller: controller,
maxLines: 3,
);
String clipboardContent = '';
tester.binding.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.setData') {
// ignore: avoid_dynamic_calls
clipboardContent = methodCall.arguments['text'] as String;
} else if (methodCall.method == 'Clipboard.getData') {
return <String, dynamic>{'text': clipboardContent};
}
return null;
});
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RawKeyboardListener(
focusNode: focusNode,
child: textField,
),
),
),
);
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
// Select the first 5 characters
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pumpAndSettle();
}
// Cut them
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.sendKeyEvent(LogicalKeyboardKey.keyX);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
expect(clipboardContent, 'a big');
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
}
// Paste them
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
const String expected = ' housa bige\njumped over a mouse';
expect(find.text(expected), findsOneWidget);
},
skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
variant: KeySimulatorTransitModeVariant.all()
);
testWidgets('Select all test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
final TextField textField =
TextField(
controller: controller,
maxLines: 3,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RawKeyboardListener(
focusNode: focusNode,
child: textField,
),
),
),
);
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
// Select All
await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.control);
await tester.pumpAndSettle();
// Delete them
await tester.sendKeyDownEvent(LogicalKeyboardKey.delete);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
await tester.sendKeyUpEvent(LogicalKeyboardKey.delete);
await tester.pumpAndSettle();
const String expected = '';
expect(find.text(expected), findsOneWidget);
},
skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
variant: KeySimulatorTransitModeVariant.all()
);
testWidgets('Delete test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
final TextField textField =
TextField(
controller: controller,
maxLines: 3,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RawKeyboardListener(
focusNode: focusNode,
child: textField,
),
),
),
);
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
// Delete
for (int i = 0; i < 6; i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.delete);
await tester.pumpAndSettle();
}
const String expected = 'house\njumped over a mouse';
expect(find.text(expected), findsOneWidget);
await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.control);
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.delete);
await tester.pumpAndSettle();
const String expected2 = '';
expect(find.text(expected2), findsOneWidget);
},
skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
variant: KeySimulatorTransitModeVariant.all(),
);
testWidgets('Changing positions of text fields', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final List<RawKeyEvent> events = <RawKeyEvent>[];
final TextEditingController c1 = TextEditingController();
final TextEditingController c2 = TextEditingController();
final Key key1 = UniqueKey();
final Key key2 = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home:
Material(
child: RawKeyboardListener(
focusNode: focusNode,
onKey: events.add,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
key: key1,
controller: c1,
maxLines: 3,
),
TextField(
key: key2,
controller: c2,
maxLines: 3,
),
],
),
),
),
),
);
const String testValue = 'a big house';
await tester.enterText(find.byType(TextField).first, testValue);
await tester.idle();
// Need to wait for selection to catch up.
await tester.pump();
await tester.tap(find.byType(TextField).first);
await tester.pumpAndSettle();
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pumpAndSettle();
}
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(c1.selection.extentOffset - c1.selection.baseOffset, -5);
await tester.pumpWidget(
MaterialApp(
home:
Material(
child: RawKeyboardListener(
focusNode: focusNode,
onKey: events.add,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
key: key2,
controller: c2,
maxLines: 3,
),
TextField(
key: key1,
controller: c1,
maxLines: 3,
),
],
),
),
),
),
);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pumpAndSettle();
}
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(c1.selection.extentOffset - c1.selection.baseOffset, -10);
},
skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
variant: KeySimulatorTransitModeVariant.all()
);
testWidgets('Changing focus test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final List<RawKeyEvent> events = <RawKeyEvent>[];
final TextEditingController c1 = TextEditingController();
final TextEditingController c2 = TextEditingController();
final Key key1 = UniqueKey();
final Key key2 = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home:
Material(
child: RawKeyboardListener(
focusNode: focusNode,
onKey: events.add,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
key: key1,
controller: c1,
maxLines: 3,
),
TextField(
key: key2,
controller: c2,
maxLines: 3,
),
],
),
),
),
),
);
const String testValue = 'a big house';
await tester.enterText(find.byType(TextField).first, testValue);
await tester.idle();
await tester.pump();
await tester.idle();
await tester.tap(find.byType(TextField).first);
await tester.pumpAndSettle();
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pumpAndSettle();
}
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(c1.selection.extentOffset - c1.selection.baseOffset, -5);
expect(c2.selection.extentOffset - c2.selection.baseOffset, 0);
await tester.enterText(find.byType(TextField).last, testValue);
await tester.idle();
await tester.pump();
await tester.idle();
await tester.tap(find.byType(TextField).last);
await tester.pumpAndSettle();
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pumpAndSettle();
}
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(c1.selection.extentOffset - c1.selection.baseOffset, -5);
expect(c2.selection.extentOffset - c2.selection.baseOffset, -5);
},
skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
variant: KeySimulatorTransitModeVariant.all()
);
testWidgets('Caret works when maxLines is null', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
maxLines: null,
),
),
);
const String testValue = 'x';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.length);
// Tap the selection handle to bring up the "paste / select all" menu.
await tester.tapAt(textOffsetToPosition(tester, 0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is
// Confirm that the selection was updated.
expect(controller.selection.baseOffset, 0);
});
testWidgets('TextField baseline alignment no-strut', (WidgetTester tester) async {
final TextEditingController controllerA = TextEditingController(text: 'A');
final TextEditingController controllerB = TextEditingController(text: 'B');
final Key keyA = UniqueKey();
final Key keyB = UniqueKey();
await tester.pumpWidget(
overlay(
child: Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: <Widget>[
Expanded(
child: TextField(
key: keyA,
decoration: null,
controller: controllerA,
style: const TextStyle(fontSize: 10.0),
strutStyle: StrutStyle.disabled,
),
),
const Text(
'abc',
style: TextStyle(fontSize: 20.0),
),
Expanded(
child: TextField(
key: keyB,
decoration: null,
controller: controllerB,
style: const TextStyle(fontSize: 30.0),
strutStyle: StrutStyle.disabled,
),
),
],
),
),
);
// The Ahem font extends 0.2 * fontSize below the baseline.
// So the three row elements line up like this:
//
// A abc B
// --------- baseline
// 2 4 6 space below the baseline = 0.2 * fontSize
// --------- rowBottomY
final double rowBottomY = tester.getBottomLeft(find.byType(Row)).dy;
expect(tester.getBottomLeft(find.byKey(keyA)).dy, moreOrLessEquals(rowBottomY - 4.0, epsilon: 0.001));
expect(tester.getBottomLeft(find.text('abc')).dy, moreOrLessEquals(rowBottomY - 2.0, epsilon: 0.001));
expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY);
});
testWidgets('TextField baseline alignment', (WidgetTester tester) async {
final TextEditingController controllerA = TextEditingController(text: 'A');
final TextEditingController controllerB = TextEditingController(text: 'B');
final Key keyA = UniqueKey();
final Key keyB = UniqueKey();
await tester.pumpWidget(
overlay(
child: Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: <Widget>[
Expanded(
child: TextField(
key: keyA,
decoration: null,
controller: controllerA,
style: const TextStyle(fontSize: 10.0),
),
),
const Text(
'abc',
style: TextStyle(fontSize: 20.0),
),
Expanded(
child: TextField(
key: keyB,
decoration: null,
controller: controllerB,
style: const TextStyle(fontSize: 30.0),
),
),
],
),
),
);
// The Ahem font extends 0.2 * fontSize below the baseline.
// So the three row elements line up like this:
//
// A abc B
// --------- baseline
// 2 4 6 space below the baseline = 0.2 * fontSize
// --------- rowBottomY
final double rowBottomY = tester.getBottomLeft(find.byType(Row)).dy;
// The values here should match the version with strut disabled ('TextField baseline alignment no-strut')
expect(tester.getBottomLeft(find.byKey(keyA)).dy, moreOrLessEquals(rowBottomY - 4.0, epsilon: 0.001));
expect(tester.getBottomLeft(find.text('abc')).dy, moreOrLessEquals(rowBottomY - 2.0, epsilon: 0.001));
expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY);
});
testWidgets('TextField semantics include label when unfocused and label/hint when focused', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController(text: 'value');
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
decoration: const InputDecoration(
hintText: 'hint',
labelText: 'label',
),
),
),
);
final SemanticsNode node = tester.getSemantics(find.byKey(key));
expect(node.label, 'label');
expect(node.value, 'value');
// Focus text field.
await tester.tap(find.byKey(key));
await tester.pump();
expect(node.label, 'label\nhint');
expect(node.value, 'value');
semantics.dispose();
});
testWidgets('TextField semantics always include label when no hint is given', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController(text: 'value');
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
decoration: const InputDecoration(
labelText: 'label',
),
),
),
);
final SemanticsNode node = tester.getSemantics(find.byKey(key));
expect(node.label, 'label');
expect(node.value, 'value');
// Focus text field.
await tester.tap(find.byKey(key));
await tester.pump();
expect(node.label, 'label');
expect(node.value, 'value');
semantics.dispose();
});
testWidgets('TextField semantics always include hint when no label is given', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController(text: 'value');
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
decoration: const InputDecoration(
hintText: 'hint',
),
),
),
);
final SemanticsNode node = tester.getSemantics(find.byKey(key));
expect(node.label, 'hint');
expect(node.value, 'value');
// Focus text field.
await tester.tap(find.byKey(key));
await tester.pump();
expect(node.label, 'hint');
expect(node.value, 'value');
semantics.dispose();
});
testWidgets('TextField semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController();
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
),
),
);
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
],
),
],
), ignoreTransform: true, ignoreRect: true));
controller.text = 'Guten Tag';
await tester.pump();
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
textDirection: TextDirection.ltr,
value: 'Guten Tag',
actions: <SemanticsAction>[
SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
],
),
],
), ignoreTransform: true, ignoreRect: true));
await tester.tap(find.byKey(key));
await tester.pump();
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
textDirection: TextDirection.ltr,
value: 'Guten Tag',
textSelection: const TextSelection.collapsed(offset: 9),
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.setText,
SemanticsAction.paste,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
),
],
), ignoreTransform: true, ignoreRect: true));
controller.selection = const TextSelection.collapsed(offset: 4);
await tester.pump();
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
textDirection: TextDirection.ltr,
textSelection: const TextSelection.collapsed(offset: 4),
value: 'Guten Tag',
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
SemanticsAction.setText,
SemanticsAction.paste,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
),
],
), ignoreTransform: true, ignoreRect: true));
controller.text = 'Schönen Feierabend';
controller.selection = const TextSelection.collapsed(offset: 0);
await tester.pump();
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
textDirection: TextDirection.ltr,
textSelection: const TextSelection.collapsed(offset: 0),
value: 'Schönen Feierabend',
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
SemanticsAction.setText,
SemanticsAction.paste,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
),
],
), ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
// Regressing test for https://github.com/flutter/flutter/issues/99763
testWidgets('Update textField semantics when obscureText changes', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(_ObscureTextTestWidget(controller: controller));
controller.text = 'Hello';
await tester.pump();
expect(
semantics,
includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.tap],
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
],
value: 'Hello',
)
);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(
semantics,
includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.tap],
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isObscured,
],
)
);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(
semantics,
includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.tap],
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
],
value: 'Hello',
)
);
semantics.dispose();
});
testWidgets('TextField semantics, enableInteractiveSelection = false', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController();
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
enableInteractiveSelection: false,
),
),
);
await tester.tap(find.byKey(key));
await tester.pump();
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.setText,
// Absent the following because enableInteractiveSelection: false
// SemanticsAction.moveCursorBackwardByCharacter,
// SemanticsAction.moveCursorBackwardByWord,
// SemanticsAction.setSelection,
// SemanticsAction.paste,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
),
],
), ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('TextField semantics for selections', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController()
..text = 'Hello';
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
),
),
);
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
value: 'Hello',
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
],
),
],
), ignoreTransform: true, ignoreRect: true));
// Focus the text field
await tester.tap(find.byKey(key));
await tester.pump();
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
value: 'Hello',
textSelection: const TextSelection.collapsed(offset: 5),
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.setText,
SemanticsAction.paste,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
),
],
), ignoreTransform: true, ignoreRect: true));
controller.selection = const TextSelection(baseOffset: 5, extentOffset: 3);
await tester.pump();
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
value: 'Hello',
textSelection: const TextSelection(baseOffset: 5, extentOffset: 3),
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
SemanticsAction.setText,
SemanticsAction.paste,
SemanticsAction.cut,
SemanticsAction.copy,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
),
],
), ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('TextField change selection with semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final TextEditingController controller = TextEditingController()
..text = 'Hello';
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
),
),
);
// Focus the text field
await tester.tap(find.byKey(key));
await tester.pump();
const int inputFieldId = 1;
expect(controller.selection, const TextSelection.collapsed(offset: 5, affinity: TextAffinity.upstream));
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: inputFieldId,
value: 'Hello',
textSelection: const TextSelection.collapsed(offset: 5),
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.setText,
SemanticsAction.paste,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
),
],
), ignoreTransform: true, ignoreRect: true));
// move cursor back once
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <dynamic, dynamic>{
'base': 4,
'extent': 4,
});
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(offset: 4));
// move cursor to front
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <dynamic, dynamic>{
'base': 0,
'extent': 0,
});
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(offset: 0));
// select all
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <dynamic, dynamic>{
'base': 0,
'extent': 5,
});
await tester.pump();
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: inputFieldId,
value: 'Hello',
textSelection: const TextSelection(baseOffset: 0, extentOffset: 5),
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.setText,
SemanticsAction.paste,
SemanticsAction.cut,
SemanticsAction.copy,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
),
],
), ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('Can activate TextField with explicit controller via semantics ', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/17801
const String textInTextField = 'Hello';
final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final TextEditingController controller = TextEditingController()
..text = textInTextField;
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
),
),
);
const int inputFieldId = 1;
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: inputFieldId,
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
actions: <SemanticsAction>[SemanticsAction.tap],
value: textInTextField,
textDirection: TextDirection.ltr,
),
],
),
ignoreRect: true, ignoreTransform: true,
));
semanticsOwner.performAction(inputFieldId, SemanticsAction.tap);
await tester.pump();
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: inputFieldId,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.setText,
SemanticsAction.paste,
],
value: textInTextField,
textDirection: TextDirection.ltr,
textSelection: const TextSelection(
baseOffset: textInTextField.length,
extentOffset: textInTextField.length,
),
),
],
),
ignoreRect: true, ignoreTransform: true,
));
semantics.dispose();
});
testWidgets('When clipboard empty, no semantics paste option', (WidgetTester tester) async {
const String textInTextField = 'Hello';
final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final TextEditingController controller = TextEditingController()
..text = textInTextField;
final Key key = UniqueKey();
// Clear the clipboard.
await Clipboard.setData(const ClipboardData(text: ''));
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
),
),
);
const int inputFieldId = 1;
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: inputFieldId,
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
actions: <SemanticsAction>[SemanticsAction.tap],
value: textInTextField,
textDirection: TextDirection.ltr,
),
],
),
ignoreRect: true, ignoreTransform: true,
));
semanticsOwner.performAction(inputFieldId, SemanticsAction.tap);
await tester.pump();
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: inputFieldId,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.setText,
// No paste option.
],
value: textInTextField,
textDirection: TextDirection.ltr,
textSelection: const TextSelection(
baseOffset: textInTextField.length,
extentOffset: textInTextField.length,
),
),
],
),
ignoreRect: true, ignoreTransform: true,
));
semantics.dispose();
// On web, we don't check for pasteability because that triggers a
// permission dialog in the browser.
// https://github.com/flutter/flutter/pull/57139#issuecomment-629048058
}, skip: isBrowser); // [intended] see above.
testWidgets('TextField throws when not descended from a Material widget', (WidgetTester tester) async {
const Widget textField = TextField();
await tester.pumpWidget(textField);
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(exception.toString(), startsWith('No Material widget found.'));
expect(exception.toString(), endsWith(':\n $textField\nThe ancestors of this widget were:\n [root]'));
});
testWidgets('TextField loses focus when disabled', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'TextField Focus Node');
await tester.pumpWidget(
boilerplate(
child: TextField(
focusNode: focusNode,
autofocus: true,
enabled: true,
),
),
);
expect(focusNode.hasFocus, isTrue);
await tester.pumpWidget(
boilerplate(
child: TextField(
focusNode: focusNode,
autofocus: true,
enabled: false,
),
),
);
expect(focusNode.hasFocus, isFalse);
await tester.pumpWidget(
boilerplate(
child: Builder(builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
navigationMode: NavigationMode.directional,
),
child: TextField(
focusNode: focusNode,
autofocus: true,
enabled: true,
),
);
}),
),
);
focusNode.requestFocus();
await tester.pump();
expect(focusNode.hasFocus, isTrue);
await tester.pumpWidget(
boilerplate(
child: Builder(builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
navigationMode: NavigationMode.directional,
),
child: TextField(
focusNode: focusNode,
autofocus: true,
enabled: false,
),
);
}),
),
);
await tester.pump();
expect(focusNode.hasFocus, isTrue);
});
testWidgets('TextField displays text with text direction', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: TextField(
textDirection: TextDirection.rtl,
),
),
),
);
RenderEditable editable = findRenderEditable(tester);
await tester.enterText(find.byType(TextField), '0123456789101112');
await tester.pumpAndSettle();
Offset topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 10)).topLeft,
);
expect(topLeft.dx, equals(701));
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: TextField(
textDirection: TextDirection.ltr,
),
),
),
);
editable = findRenderEditable(tester);
await tester.enterText(find.byType(TextField), '0123456789101112');
await tester.pumpAndSettle();
topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 10)).topLeft,
);
expect(topLeft.dx, equals(160.0));
});
testWidgets('TextField semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController();
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
maxLength: 10,
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
helperText: 'helper',
),
),
),
);
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'label',
id: 1,
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
],
children: <TestSemantics>[
TestSemantics(
id: 2,
label: 'helper',
textDirection: TextDirection.ltr,
),
TestSemantics(
id: 3,
label: '10 characters remaining',
textDirection: TextDirection.ltr,
),
],
),
],
), ignoreTransform: true, ignoreRect: true));
await tester.tap(find.byType(TextField));
await tester.pump();
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'label\nhint',
id: 1,
textDirection: TextDirection.ltr,
textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.setSelection,
SemanticsAction.setText,
SemanticsAction.paste,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
children: <TestSemantics>[
TestSemantics(
id: 2,
label: 'helper',
textDirection: TextDirection.ltr,
),
TestSemantics(
id: 3,
label: '10 characters remaining',
flags: <SemanticsFlag>[
SemanticsFlag.isLiveRegion,
],
textDirection: TextDirection.ltr,
),
],
),
],
), ignoreTransform: true, ignoreRect: true));
controller.text = 'hello';
await tester.pump();
semantics.dispose();
});
testWidgets('InputDecoration counterText can have a semanticCounterText', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController();
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
helperText: 'helper',
counterText: '0/10',
semanticCounterText: '0 out of 10',
),
),
),
);
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'label',
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
],
children: <TestSemantics>[
TestSemantics(
label: 'helper',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: '0 out of 10',
textDirection: TextDirection.ltr,
),
],
),
],
), ignoreTransform: true, ignoreRect: true, ignoreId: true));
semantics.dispose();
});
testWidgets('InputDecoration errorText semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController();
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
errorText: 'oh no!',
),
),
),
);
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'label',
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
],
children: <TestSemantics>[
TestSemantics(
label: 'oh no!',
flags: <SemanticsFlag>[
SemanticsFlag.isLiveRegion,
],
textDirection: TextDirection.ltr,
),
],
),
],
), ignoreTransform: true, ignoreRect: true, ignoreId: true));
semantics.dispose();
});
testWidgets('floating label does not overlap with value at large textScaleFactors', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'Just some text');
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window).copyWith(textScaleFactor: 4.0),
child: Center(
child: TextField(
decoration: const InputDecoration(labelText: 'Label', border: UnderlineInputBorder()),
controller: controller,
),
),
),
),
),
);
await tester.tap(find.byType(TextField));
final Rect labelRect = tester.getRect(find.text('Label'));
final Rect fieldRect = tester.getRect(find.text('Just some text'));
expect(labelRect.bottom, lessThanOrEqualTo(fieldRect.top));
});
testWidgets('TextField scrolls into view but does not bounce (SingleChildScrollView)', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/20485
final Key textField1 = UniqueKey();
final Key textField2 = UniqueKey();
final ScrollController scrollController = ScrollController();
double? minOffset;
double? maxOffset;
scrollController.addListener(() {
final double offset = scrollController.offset;
minOffset = math.min(minOffset ?? offset, offset);
maxOffset = math.max(maxOffset ?? offset, offset);
});
Widget buildFrame(Axis scrollDirection) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
controller: scrollController,
child: Column(
children: <Widget>[
SizedBox( // visible when scrollOffset is 0.0
height: 100.0,
width: 100.0,
child: TextField(key: textField1, scrollPadding: const EdgeInsets.all(200.0)),
),
const SizedBox(
height: 600.0, // Same size as the frame. Initially
width: 800.0, // textField2 is not visible
),
SizedBox( // visible when scrollOffset is 200.0
height: 100.0,
width: 100.0,
child: TextField(key: textField2, scrollPadding: const EdgeInsets.all(200.0)),
),
],
),
),
),
),
);
}
await tester.pumpWidget(buildFrame(Axis.vertical));
await tester.enterText(find.byKey(textField1), '1');
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
await tester.pumpAndSettle();
expect(minOffset, 0.0);
expect(maxOffset, 200.0);
minOffset = null;
maxOffset = null;
await tester.pumpWidget(buildFrame(Axis.horizontal));
await tester.enterText(find.byKey(textField1), '1');
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
await tester.pumpAndSettle();
expect(minOffset, 0.0);
expect(maxOffset, 200.0);
});
testWidgets('TextField scrolls into view but does not bounce (ListView)', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/20485
final Key textField1 = UniqueKey();
final Key textField2 = UniqueKey();
final ScrollController scrollController = ScrollController();
double? minOffset;
double? maxOffset;
scrollController.addListener(() {
final double offset = scrollController.offset;
minOffset = math.min(minOffset ?? offset, offset);
maxOffset = math.max(maxOffset ?? offset, offset);
});
Widget buildFrame(Axis scrollDirection) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: ListView(
physics: const BouncingScrollPhysics(),
controller: scrollController,
children: <Widget>[
SizedBox( // visible when scrollOffset is 0.0
height: 100.0,
width: 100.0,
child: TextField(key: textField1, scrollPadding: const EdgeInsets.all(200.0)),
),
const SizedBox(
height: 450.0, // 50.0 smaller than the overall frame so that both
width: 650.0, // textfields are always partially visible.
),
SizedBox( // visible when scrollOffset = 50.0
height: 100.0,
width: 100.0,
child: TextField(key: textField2, scrollPadding: const EdgeInsets.all(200.0)),
),
],
),
),
),
);
}
await tester.pumpWidget(buildFrame(Axis.vertical));
await tester.enterText(find.byKey(textField1), '1'); // textfield1 is visible
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
await tester.pumpAndSettle();
expect(minOffset, 0.0);
expect(maxOffset, 50.0);
minOffset = null;
maxOffset = null;
await tester.pumpWidget(buildFrame(Axis.horizontal));
await tester.enterText(find.byKey(textField1), '1'); // textfield1 is visible
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
await tester.pumpAndSettle();
expect(minOffset, 0.0);
expect(maxOffset, 50.0);
});
testWidgets('onTap is called upon tap', (WidgetTester tester) async {
int tapCount = 0;
await tester.pumpWidget(
overlay(
child: TextField(
onTap: () {
tapCount += 1;
},
),
),
);
expect(tapCount, 0);
await tester.tap(find.byType(TextField));
// Wait a bit so they're all single taps and not double taps.
await tester.pump(const Duration(milliseconds: 300));
await tester.tap(find.byType(TextField));
await tester.pump(const Duration(milliseconds: 300));
await tester.tap(find.byType(TextField));
await tester.pump(const Duration(milliseconds: 300));
expect(tapCount, 3);
});
testWidgets('onTap is not called, field is disabled', (WidgetTester tester) async {
int tapCount = 0;
await tester.pumpWidget(
overlay(
child: TextField(
enabled: false,
onTap: () {
tapCount += 1;
},
),
),
);
expect(tapCount, 0);
await tester.tap(find.byType(TextField));
await tester.tap(find.byType(TextField));
await tester.tap(find.byType(TextField));
expect(tapCount, 0);
});
testWidgets('Includes cursor for TextField', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/24612
Widget buildFrame({
double? stepWidth,
required double cursorWidth,
required TextAlign textAlign,
}) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IntrinsicWidth(
stepWidth: stepWidth,
child: TextField(
textAlign: textAlign,
cursorWidth: cursorWidth,
),
),
],
),
),
),
);
}
// A cursor of default size doesn't cause the TextField to increase its
// width.
const String text = '1234';
double? stepWidth = 80.0;
await tester.pumpWidget(buildFrame(
stepWidth: 80.0,
cursorWidth: 2.0,
textAlign: TextAlign.left,
));
await tester.enterText(find.byType(TextField), text);
await tester.pumpAndSettle();
expect(tester.getSize(find.byType(TextField)).width, stepWidth);
// A wide cursor is counted in the width of the text and causes the
// TextField to increase to twice the stepWidth.
await tester.pumpWidget(buildFrame(
stepWidth: stepWidth,
cursorWidth: 18.0,
textAlign: TextAlign.left,
));
await tester.enterText(find.byType(TextField), text);
await tester.pumpAndSettle();
expect(tester.getSize(find.byType(TextField)).width, 2 * stepWidth);
// A null stepWidth causes the TextField to perfectly wrap the text plus
// the cursor regardless of alignment.
stepWidth = null;
const double WIDTH_OF_CHAR = 16.0;
const double CARET_GAP = 1.0;
await tester.pumpWidget(buildFrame(
stepWidth: stepWidth,
cursorWidth: 18.0,
textAlign: TextAlign.left,
));
await tester.enterText(find.byType(TextField), text);
await tester.pumpAndSettle();
expect(tester.getSize(find.byType(TextField)).width, WIDTH_OF_CHAR * text.length + 18.0 + CARET_GAP);
await tester.pumpWidget(buildFrame(
stepWidth: stepWidth,
cursorWidth: 18.0,
textAlign: TextAlign.right,
));
await tester.enterText(find.byType(TextField), text);
await tester.pumpAndSettle();
expect(tester.getSize(find.byType(TextField)).width, WIDTH_OF_CHAR * text.length + 18.0 + CARET_GAP);
});
testWidgets('TextField style is merged with theme', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/23994
final ThemeData themeData = ThemeData(
textTheme: TextTheme(
titleMedium: TextStyle(
color: Colors.blue[500],
),
),
);
Widget buildFrame(TextStyle style) {
return MaterialApp(
theme: themeData,
home: Material(
child: Center(
child: TextField(
style: style,
),
),
),
);
}
// Empty TextStyle is overridden by theme
await tester.pumpWidget(buildFrame(const TextStyle()));
EditableText editableText = tester.widget(find.byType(EditableText));
expect(editableText.style.color, themeData.textTheme.titleMedium!.color);
expect(editableText.style.background, themeData.textTheme.titleMedium!.background);
expect(editableText.style.shadows, themeData.textTheme.titleMedium!.shadows);
expect(editableText.style.decoration, themeData.textTheme.titleMedium!.decoration);
expect(editableText.style.locale, themeData.textTheme.titleMedium!.locale);
expect(editableText.style.wordSpacing, themeData.textTheme.titleMedium!.wordSpacing);
// Properties set on TextStyle override theme
const Color setColor = Colors.red;
await tester.pumpWidget(buildFrame(const TextStyle(color: setColor)));
editableText = tester.widget(find.byType(EditableText));
expect(editableText.style.color, setColor);
// inherit: false causes nothing to be merged in from theme
await tester.pumpWidget(buildFrame(const TextStyle(
fontSize: 24.0,
textBaseline: TextBaseline.alphabetic,
inherit: false,
)));
editableText = tester.widget(find.byType(EditableText));
expect(editableText.style.color, isNull);
});
testWidgets('TextField style is merged with theme in Material 3', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/23994
final ThemeData themeData = ThemeData(
useMaterial3: true,
textTheme: TextTheme(
bodyLarge: TextStyle(
color: Colors.blue[500],
),
),
);
Widget buildFrame(TextStyle style) {
return MaterialApp(
theme: themeData,
home: Material(
child: Center(
child: TextField(
style: style,
),
),
),
);
}
// Empty TextStyle is overridden by theme
await tester.pumpWidget(buildFrame(const TextStyle()));
EditableText editableText = tester.widget(find.byType(EditableText));
expect(editableText.style.color, themeData.textTheme.bodyLarge!.color);
expect(editableText.style.background, themeData.textTheme.bodyLarge!.background);
expect(editableText.style.shadows, themeData.textTheme.bodyLarge!.shadows);
expect(editableText.style.decoration, themeData.textTheme.bodyLarge!.decoration);
expect(editableText.style.locale, themeData.textTheme.bodyLarge!.locale);
expect(editableText.style.wordSpacing, themeData.textTheme.bodyLarge!.wordSpacing);
// Properties set on TextStyle override theme
const Color setColor = Colors.red;
await tester.pumpWidget(buildFrame(const TextStyle(color: setColor)));
editableText = tester.widget(find.byType(EditableText));
expect(editableText.style.color, setColor);
// inherit: false causes nothing to be merged in from theme
await tester.pumpWidget(buildFrame(const TextStyle(
fontSize: 24.0,
textBaseline: TextBaseline.alphabetic,
inherit: false,
)));
editableText = tester.widget(find.byType(EditableText));
expect(editableText.style.color, isNull);
});
testWidgets('style enforces required fields', (WidgetTester tester) async {
Widget buildFrame(TextStyle style) {
return MaterialApp(
home: Material(
child: TextField(
style: style,
),
),
);
}
await tester.pumpWidget(buildFrame(const TextStyle(
inherit: false,
fontSize: 12.0,
textBaseline: TextBaseline.alphabetic,
)));
expect(tester.takeException(), isNull);
// With inherit not set to false, will pickup required fields from theme
await tester.pumpWidget(buildFrame(const TextStyle(
fontSize: 12.0,
)));
expect(tester.takeException(), isNull);
await tester.pumpWidget(buildFrame(const TextStyle(
inherit: false,
fontSize: 12.0,
)));
expect(tester.takeException(), isNotNull);
});
testWidgets(
'tap moves cursor to the edge of the word it tapped',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump();
// We moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
);
// But don't trigger the toolbar.
expect(find.byType(CupertinoButton), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets(
'tap with a mouse does not move cursor to the edge of the word',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
final TestGesture gesture = await tester.startGesture(
textfieldStart + const Offset(50.0, 9.0),
pointer: 1,
kind: PointerDeviceKind.mouse,
);
await gesture.up();
// Cursor at tap position, not at word edge.
expect(
controller.selection,
const TextSelection.collapsed(offset: 3),
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets('tap moves cursor to the position tapped', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump();
// We moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 3),
);
// But don't trigger the toolbar.
expect(find.byType(TextButton), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));
testWidgets(
'two slow taps do not trigger a word selection',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset pos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'.
await tester.tapAt(pos);
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(pos);
await tester.pump();
// Plain collapsed selection.
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6);
// No toolbar.
expect(find.byType(CupertinoButton), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'double tap selects word and first tap of double tap moves cursor',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'.
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'.
// This tap just puts the cursor somewhere different than where the double
// tap will occur to test that the double tap moves the existing cursor first.
await tester.tapAt(wPos);
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
);
await tester.tapAt(pPos);
await tester.pumpAndSettle();
// Second tap selects the word around the cursor.
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
// Selected text shows 3 toolbar buttons.
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets(
'double tap does not select word on read-only obscured field',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
obscureText: true,
readOnly: true,
controller: controller,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
// This tap just puts the cursor somewhere different than where the double
// tap will occur to test that the double tap moves the existing cursor first.
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 35),
);
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pumpAndSettle();
// Second tap doesn't select anything.
expect(
controller.selection,
const TextSelection.collapsed(offset: 35),
);
// Selected text shows nothing.
expect(find.byType(CupertinoButton), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'double tap selects word and first tap of double tap moves cursor and shows toolbar',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
// This tap just puts the cursor somewhere different than where the double
// tap will occur to test that the double tap moves the existing cursor first.
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 9),
);
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pumpAndSettle();
// Second tap selects the word around the cursor.
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
// Selected text shows 4 toolbar buttons: cut, copy, paste, select all
expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }),
);
testWidgets(
'Custom toolbar test - Android text selection controls',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
selectionControls: materialTextSelectionControls,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pumpAndSettle();
// Selected text shows 4 toolbar buttons: cut, copy, paste, select all
expect(find.byType(TextButton), findsNWidgets(4));
},
variant: TargetPlatformVariant.all(),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.,
);
testWidgets(
'Custom toolbar test - Cupertino text selection controls',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
selectionControls: cupertinoTextSelectionControls,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pumpAndSettle();
// Selected text shows 3 toolbar buttons: cut, copy, paste
expect(find.byType(CupertinoButton), findsNWidgets(3));
},
variant: TargetPlatformVariant.all(),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.,
);
testWidgets('selectionControls is passed to EditableText', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Scaffold(
body: TextField(
selectionControls: materialTextSelectionControls,
),
),
),
),
);
final EditableText widget = tester.widget(find.byType(EditableText));
expect(widget.selectionControls, equals(materialTextSelectionControls));
});
testWidgets(
'double tap on top of cursor also selects word',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
// Tap to put the cursor after the "w".
const int index = 3;
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pump(const Duration(milliseconds: 500));
expect(
controller.selection,
const TextSelection.collapsed(offset: index),
);
// Double tap on the same location.
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pump(const Duration(milliseconds: 50));
// First tap doesn't change the selection
expect(
controller.selection,
const TextSelection.collapsed(offset: index),
);
// Second tap selects the word around the cursor.
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
// Selected text shows 4 toolbar buttons: cut, copy, paste, select all
expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }),
);
testWidgets(
'double double tap just shows the selection menu',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: '',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
// Double tap on the same location shows the selection menu.
await tester.tapAt(textOffsetToPosition(tester, 0));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, 0));
await tester.pumpAndSettle();
expect(find.text('Paste'), findsOneWidget);
// Double tap again keeps the selection menu visible.
await tester.tapAt(textOffsetToPosition(tester, 0));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, 0));
await tester.pumpAndSettle();
expect(find.text('Paste'), findsOneWidget);
},
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.,
);
testWidgets(
'double long press just shows the selection menu',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: '',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
// Long press shows the selection menu.
await tester.longPressAt(textOffsetToPosition(tester, 0));
await tester.pumpAndSettle();
expect(find.text('Paste'), findsOneWidget);
// Long press again keeps the selection menu visible.
await tester.longPressAt(textOffsetToPosition(tester, 0));
await tester.pump();
expect(find.text('Paste'), findsOneWidget);
},
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.,
);
testWidgets(
'A single tap hides the selection menu',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: '',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
// Long press shows the selection menu.
await tester.longPress(find.byType(TextField));
await tester.pumpAndSettle();
expect(find.text('Paste'), findsOneWidget);
// Tap hides the selection menu.
await tester.tap(find.byType(TextField));
await tester.pump();
expect(find.text('Paste'), findsNothing);
},
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.,
);
testWidgets('Drag selection hides the selection menu', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'blah1 blah2',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
// Initially, the menu is not shown and there is no selection.
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
final Offset midBlah1 = textOffsetToPosition(tester, 2);
final Offset midBlah2 = textOffsetToPosition(tester, 8);
// Right click the second word.
final TestGesture gesture = await tester.startGesture(
midBlah2,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// The toolbar is shown.
expect(find.text('Paste'), findsOneWidget);
// Drag the mouse to the first word.
final TestGesture gesture2 = await tester.startGesture(
midBlah1,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture2.moveTo(midBlah2);
await tester.pump();
await gesture2.up();
await tester.pumpAndSettle();
// The toolbar is hidden.
expect(find.text('Paste'), findsNothing);
},
variant: TargetPlatformVariant.desktop(),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets(
'Long press on an autofocused field shows the selection menu',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: '',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
autofocus: true,
controller: controller,
),
),
),
),
);
// This extra pump allows the selection set by autofocus to propagate to
// the RenderEditable.
await tester.pump();
// Long press shows the selection menu.
expect(find.text('Paste'), findsNothing);
await tester.longPress(find.byType(TextField));
await tester.pumpAndSettle();
expect(find.text('Paste'), findsOneWidget);
},
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.,
);
testWidgets(
'double tap hold selects word',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
final TestGesture gesture =
await tester.startGesture(textfieldStart + const Offset(150.0, 9.0));
// Hold the press.
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
// Selected text shows 3 toolbar buttons.
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
await gesture.up();
await tester.pump();
// Still selected.
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
// The toolbar is still showing.
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'tap after a double tap select is not affected',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'.
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
);
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(ePos);
await tester.pump();
// Plain collapsed selection at the edge of first word on iOS. In iOS 12,
// the first tap after a double tap ends up putting the cursor at where
// you tapped instead of the edge like every other single tap. This is
// likely a bug in iOS 12 and not present in other versions.
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6);
// No toolbar.
expect(find.byType(CupertinoButton), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'long press moves cursor to the exact long press position and shows toolbar',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pumpAndSettle();
// Collapsed cursor for iOS long press.
expect(
controller.selection,
const TextSelection.collapsed(offset: 3),
);
// Collapsed toolbar shows 2 buttons.
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'long press selects word and shows toolbar',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
// Collapsed toolbar shows 4 buttons: cut, copy, paste, select all
expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }),
);
testWidgets(
'long press tap cannot initiate a double tap',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
await tester.longPressAt(ePos);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(ePos);
await tester.pump();
// We ended up moving the cursor to the edge of the same word and dismissed
// the toolbar.
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6);
// Collapsed toolbar shows 2 buttons.
expect(find.byType(CupertinoButton), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'long press drag moves the cursor under the drag and shows toolbar on lift',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
final TestGesture gesture =
await tester.startGesture(textfieldStart + const Offset(50.0, 9.0));
await tester.pump(const Duration(milliseconds: 500));
// Long press on iOS shows collapsed selection cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 3),
);
// Cursor move doesn't trigger a toolbar initially.
expect(find.byType(CupertinoButton), findsNothing);
await gesture.moveBy(const Offset(50, 0));
await tester.pump();
// The selection position is now moved with the drag.
expect(
controller.selection,
const TextSelection.collapsed(offset: 6),
);
// Still no toolbar.
expect(find.byType(CupertinoButton), findsNothing);
await gesture.moveBy(const Offset(50, 0));
await tester.pump();
// The selection position is now moved with the drag.
expect(
controller.selection,
const TextSelection.collapsed(offset: 9),
);
// Still no toolbar.
expect(find.byType(CupertinoButton), findsNothing);
await gesture.up();
await tester.pumpAndSettle();
// The selection isn't affected by the gesture lift.
expect(
controller.selection,
const TextSelection.collapsed(offset: 9),
);
// The toolbar now shows up.
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets('long press drag can edge scroll', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final RenderEditable renderEditable = findRenderEditable(tester);
List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection(
const TextSelection.collapsed(offset: 66), // Last character's position.
);
expect(lastCharEndpoint.length, 1);
// Just testing the test and making sure that the last character is off
// the right side of the screen.
expect(lastCharEndpoint[0].point.dx, 1056);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
final TestGesture gesture =
await tester.startGesture(textfieldStart + const Offset(300, 5));
await tester.pump(const Duration(milliseconds: 500));
expect(
controller.selection,
const TextSelection.collapsed(offset: 19, affinity: TextAffinity.upstream),
);
expect(find.byType(CupertinoButton), findsNothing);
await gesture.moveBy(const Offset(600, 0));
// To the edge of the screen basically.
await tester.pump();
expect(
controller.selection,
const TextSelection.collapsed(offset: 56),
);
// Keep moving out.
await gesture.moveBy(const Offset(1, 0));
await tester.pump();
expect(
controller.selection,
const TextSelection.collapsed(offset: 62),
);
await gesture.moveBy(const Offset(1, 0));
await tester.pump();
expect(
controller.selection,
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
); // We're at the edge now.
expect(find.byType(CupertinoButton), findsNothing);
await gesture.up();
await tester.pumpAndSettle();
// The selection isn't affected by the gesture lift.
expect(
controller.selection,
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
);
// The toolbar now shows up.
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
lastCharEndpoint = renderEditable.getEndpointsForSelection(
const TextSelection.collapsed(offset: 66), // Last character's position.
);
expect(lastCharEndpoint.length, 1);
// The last character is now on screen near the right edge.
expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(798, epsilon: 1));
final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection(
const TextSelection.collapsed(offset: 0), // First character's position.
);
expect(firstCharEndpoint.length, 1);
// The first character is now offscreen to the left.
expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257.0, epsilon: 1));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('mouse click and drag can edge scroll', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Size screenSize = MediaQuery.of(tester.element(find.byType(TextField))).size;
// Just testing the test and making sure that the last character is off
// the right side of the screen.
expect(textOffsetToPosition(tester, 66).dx, greaterThan(screenSize.width));
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 19),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await gesture.moveTo(textOffsetToPosition(tester, 56));
// To the edge of the screen basically.
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 19, extentOffset: 56),
);
// Keep moving out.
await gesture.moveTo(textOffsetToPosition(tester, 62));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 19, extentOffset: 62),
);
await gesture.moveTo(textOffsetToPosition(tester, 66));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 19, extentOffset: 66),
); // We're at the edge now.
expect(find.byType(CupertinoButton), findsNothing);
await gesture.up();
await tester.pumpAndSettle();
// The selection isn't affected by the gesture lift.
expect(
controller.selection,
const TextSelection(baseOffset: 19, extentOffset: 66),
);
// The last character is now on screen near the right edge.
expect(
textOffsetToPosition(tester, 66).dx,
moreOrLessEquals(TestSemantics.fullScreen.width, epsilon: 2.0),
);
// The first character is now offscreen to the left.
expect(textOffsetToPosition(tester, 0).dx, lessThan(-100.0));
}, variant: TargetPlatformVariant.all());
testWidgets('keyboard selection change scrolls the field', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
// Just testing the test and making sure that the last character is off
// the right side of the screen.
expect(textOffsetToPosition(tester, 66).dx, 1056);
await tester.tapAt(textOffsetToPosition(tester, 13));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 13),
);
// Move to position 56 with the right arrow (near the edge of the screen).
for (int i = 0; i < (56 - 13); i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
}
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 56, affinity: TextAffinity.upstream),
);
// Keep moving out.
for (int i = 0; i < (62 - 56); i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
}
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 62, affinity: TextAffinity.upstream),
);
for (int i = 0; i < (66 - 62); i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
}
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
); // We're at the edge now.
await tester.pumpAndSettle();
// The last character is now on screen near the right edge.
expect(
textOffsetToPosition(tester, 66).dx,
moreOrLessEquals(TestSemantics.fullScreen.width, epsilon: 2.0),
);
// The first character is now offscreen to the left.
expect(textOffsetToPosition(tester, 0).dx, moreOrLessEquals(-257.0, epsilon: 1));
}, variant: TargetPlatformVariant.all(),
skip: isBrowser, // [intended] Browser handles arrow keys differently.
);
testWidgets('long press drag can edge scroll vertically', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neigse Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLines: 2,
controller: controller,
),
),
),
),
);
// Just testing the test and making sure that the last character is outside
// the bottom of the field.
final int textLength = controller.text.length;
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
final double firstCharY = textOffsetToPosition(tester, 0).dy;
expect(
textOffsetToPosition(tester, textLength).dy,
moreOrLessEquals(firstCharY + lineHeight * 2, epsilon: 1),
);
// Start long pressing on the first line.
final TestGesture gesture =
await tester.startGesture(textOffsetToPosition(tester, 19));
// TODO(justinmc): Make sure you've got all things torn down.
await tester.pump(const Duration(milliseconds: 500));
expect(
controller.selection,
const TextSelection.collapsed(offset: 19),
);
await tester.pumpAndSettle();
// Move down to the second line.
await gesture.moveBy(Offset(0.0, lineHeight));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 65),
);
// Still hasn't scrolled.
expect(
textOffsetToPosition(tester, 65).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Keep selecting down to the third and final line.
await gesture.moveBy(Offset(0.0, lineHeight));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 110),
);
// The last character is no longer three line heights down from the top of
// the field, it's now only two line heights down, because it has scrolled
// down by one line.
expect(
textOffsetToPosition(tester, 110).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Likewise, the first character is now scrolled out of the top of the field
// by one line.
expect(
textOffsetToPosition(tester, 0).dy,
moreOrLessEquals(firstCharY - lineHeight, epsilon: 1),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('keyboard selection change scrolls the field vertically', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLines: 2,
controller: controller,
),
),
),
),
);
// Just testing the test and making sure that the last character is outside
// the bottom of the field.
final int textLength = controller.text.length;
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
final double firstCharY = textOffsetToPosition(tester, 0).dy;
expect(
textOffsetToPosition(tester, textLength).dy,
moreOrLessEquals(firstCharY + lineHeight * 2, epsilon: 1),
);
await tester.tapAt(textOffsetToPosition(tester, 13));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 13),
);
// Move down to the second line.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 59),
);
// Still hasn't scrolled.
expect(
textOffsetToPosition(tester, 66).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Move down to the third and final line.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 104),
);
// The last character is no longer three line heights down from the top of
// the field, it's now only two line heights down, because it has scrolled
// down by one line.
expect(
textOffsetToPosition(tester, textLength).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Likewise, the first character is now scrolled out of the top of the field
// by one line.
expect(
textOffsetToPosition(tester, 0).dy,
moreOrLessEquals(firstCharY - lineHeight, epsilon: 1),
);
}, variant: TargetPlatformVariant.all(),
skip: isBrowser, // [intended] Browser handles arrow keys differently.
);
testWidgets('mouse click and drag can edge scroll vertically', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLines: 2,
controller: controller,
),
),
),
),
);
// Just testing the test and making sure that the last character is outside
// the bottom of the field.
final int textLength = controller.text.length;
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
final double firstCharY = textOffsetToPosition(tester, 0).dy;
expect(
textOffsetToPosition(tester, textLength).dy,
moreOrLessEquals(firstCharY + lineHeight * 2, epsilon: 1),
);
// Start selecting on the first line.
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 19),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
// Still hasn't scrolled.
expect(
textOffsetToPosition(tester, 60).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Select down to the second line.
await gesture.moveBy(Offset(0.0, lineHeight));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 19, extentOffset: 65),
);
// Still hasn't scrolled.
expect(
textOffsetToPosition(tester, 60).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Keep selecting down to the third and final line.
await gesture.moveBy(Offset(0.0, lineHeight));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 19, extentOffset: 110),
);
// The last character is no longer three line heights down from the top of
// the field, it's now only two line heights down, because it has scrolled
// down by one line.
expect(
textOffsetToPosition(tester, textLength).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Likewise, the first character is now scrolled out of the top of the field
// by one line.
expect(
textOffsetToPosition(tester, 0).dy,
moreOrLessEquals(firstCharY - lineHeight, epsilon: 1),
);
}, variant: TargetPlatformVariant.all());
testWidgets(
'long tap after a double tap select is not affected',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor to the beginning of the second word.
expect(
controller.selection,
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
);
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 500));
await tester.longPressAt(ePos);
await tester.pumpAndSettle();
// Plain collapsed selection at the exact tap position.
expect(
controller.selection,
const TextSelection.collapsed(offset: 6),
);
// Long press toolbar.
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'double tap after a long tap is not affected',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'
await tester.longPressAt(wPos);
await tester.pump(const Duration(milliseconds: 50));
expect(
controller.selection,
const TextSelection.collapsed(offset: 3),
);
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
);
await tester.tapAt(pPos);
await tester.pumpAndSettle();
// Double tap selection.
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'double click after a click on Mac',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(50.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 3),
);
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// First click moved the cursor to the precise location, not the start of
// the word.
expect(
controller.selection,
const TextSelection.collapsed(offset: 9),
);
// Double click selection.
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
// The text selection toolbar isn't shown on Mac without a right click.
expect(find.byType(CupertinoButton), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }),
);
testWidgets(
'double tap chains work',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
expect(
controller.selection,
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
);
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
// Double tap selecting the same word somewhere else is fine.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
);
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 8),
);
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets(
'double click chains work',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
// First click moves the cursor to the point of the click, not the edge of
// the clicked word.
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(50.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(
controller.selection,
const TextSelection.collapsed(offset: 3),
);
// Second click selects.
await gesture.down(textFieldStart + const Offset(50.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expect(find.byType(CupertinoButton), findsNothing);
// Double tap selecting the same word somewhere else is fine.
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 6),
);
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expect(find.byType(CupertinoButton), findsNothing);
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 9),
);
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
expect(find.byType(CupertinoButton), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }),
);
testWidgets('double tapping a space selects the previous word on iOS', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: ' blah blah \n blah',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, -1);
expect(controller.value.selection.extentOffset, -1);
// Put the cursor at the end of the field.
await tester.tapAt(textOffsetToPosition(tester, 19));
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 19);
expect(controller.value.selection.extentOffset, 19);
// Double tapping does the same thing.
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textOffsetToPosition(tester, 5));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.extentOffset, 5);
expect(controller.value.selection.baseOffset, 1);
// Put the cursor at the end of the field.
await tester.tapAt(textOffsetToPosition(tester, 19));
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 19);
expect(controller.value.selection.extentOffset, 19);
// Double tapping does the same thing for the first space.
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textOffsetToPosition(tester, 0));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, 0));
await tester.pumpAndSettle();
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 0);
expect(controller.value.selection.extentOffset, 1);
// Put the cursor at the end of the field.
await tester.tapAt(textOffsetToPosition(tester, 19));
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 19);
expect(controller.value.selection.extentOffset, 19);
// Double tapping the last space selects all previous contiguous spaces on
// both lines and the previous word.
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textOffsetToPosition(tester, 14));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, 14));
await tester.pumpAndSettle();
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 6);
expect(controller.value.selection.extentOffset, 14);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('selecting a space selects the space on non-iOS platforms', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: ' blah blah',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, -1);
expect(controller.value.selection.extentOffset, -1);
// Put the cursor at the end of the field.
await tester.tapAt(textOffsetToPosition(tester, 10));
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 10);
expect(controller.value.selection.extentOffset, 10);
// Double tapping the second space selects it.
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textOffsetToPosition(tester, 5));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 5);
expect(controller.value.selection.extentOffset, 6);
// Put the cursor at the end of the field.
await tester.tapAt(textOffsetToPosition(tester, 10));
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 10);
expect(controller.value.selection.extentOffset, 10);
// Double tapping the second space selects it.
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textOffsetToPosition(tester, 0));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, 0));
await tester.pumpAndSettle();
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 0);
expect(controller.value.selection.extentOffset, 1);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia, TargetPlatform.android }));
testWidgets('selecting a space selects the space on Mac', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: ' blah blah',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, -1);
expect(controller.value.selection.extentOffset, -1);
// Put the cursor at the end of the field.
final TestGesture gesture = await tester.startGesture(
textOffsetToPosition(tester, 10),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 10);
expect(controller.value.selection.extentOffset, 10);
// Double clicking the second space selects it.
await tester.pump(const Duration(milliseconds: 500));
await gesture.down(textOffsetToPosition(tester, 5));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
await gesture.down(textOffsetToPosition(tester, 5));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 5);
expect(controller.value.selection.extentOffset, 6);
// Put the cursor at the end of the field.
await gesture.down(textOffsetToPosition(tester, 10));
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 10);
expect(controller.value.selection.extentOffset, 10);
// Double tapping the second space selects it.
await tester.pump(const Duration(milliseconds: 500));
await gesture.down(textOffsetToPosition(tester, 0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
await gesture.down(textOffsetToPosition(tester, 0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 0);
expect(controller.value.selection.extentOffset, 1);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }));
testWidgets('force press does not select a word', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
final Offset offset = tester.getTopLeft(find.byType(TextField)) + const Offset(150.0, 9.0);
final int pointerValue = tester.nextPointer;
final TestGesture gesture = await tester.createGesture();
await gesture.downWithCustomEvent(
offset,
PointerDownEvent(
pointer: pointerValue,
position: offset,
pressure: 0.0,
pressureMax: 6.0,
pressureMin: 0.0,
),
);
await gesture.updateWithCustomEvent(PointerMoveEvent(
pointer: pointerValue,
position: offset + const Offset(150.0, 9.0),
pressure: 0.5,
pressureMin: 0,
));
// We don't want this gesture to select any word on Android.
expect(controller.selection, const TextSelection.collapsed(offset: -1));
await gesture.up();
await tester.pump();
expect(find.byType(TextButton), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));
testWidgets('force press selects word', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
final int pointerValue = tester.nextPointer;
final Offset offset = textfieldStart + const Offset(150.0, 9.0);
final TestGesture gesture = await tester.createGesture();
await gesture.downWithCustomEvent(
offset,
PointerDownEvent(
pointer: pointerValue,
position: offset,
pressure: 0.0,
pressureMax: 6.0,
pressureMin: 0.0,
),
);
await gesture.updateWithCustomEvent(
PointerMoveEvent(
pointer: pointerValue,
position: textfieldStart + const Offset(150.0, 9.0),
pressure: 0.5,
pressureMin: 0,
),
);
// We expect the force press to select a word at the given location.
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
await gesture.up();
await tester.pumpAndSettle();
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('tap on non-force-press-supported devices work', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(Container(key: GlobalKey()));
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
final int pointerValue = tester.nextPointer;
final Offset offset = textfieldStart + const Offset(150.0, 9.0);
final TestGesture gesture = await tester.createGesture();
await gesture.downWithCustomEvent(
offset,
PointerDownEvent(
pointer: pointerValue,
position: offset,
// iPhone 6 and below report 0 across the board.
pressure: 0,
pressureMax: 0,
pressureMin: 0,
),
);
await gesture.updateWithCustomEvent(
PointerMoveEvent(
pointer: pointerValue,
position: textfieldStart + const Offset(150.0, 9.0),
pressure: 0.5,
pressureMin: 0,
),
);
await gesture.up();
// The event should fallback to a normal tap and move the cursor.
// Single taps selects the edge of the word.
expect(
controller.selection,
const TextSelection.collapsed(offset: 8),
);
await tester.pump();
// Single taps shouldn't trigger the toolbar.
expect(find.byType(CupertinoButton), findsNothing);
// TODO(gspencergoog): Add in TargetPlatform.macOS in the line below when we figure out what global state is leaking.
// https://github.com/flutter/flutter/issues/43445
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
testWidgets('default TextField debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const TextField().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString()).toList();
expect(description, <String>[]);
});
testWidgets('TextField implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
// Not checking controller, inputFormatters, focusNode
const TextField(
decoration: InputDecoration(labelText: 'foo'),
keyboardType: TextInputType.text,
textInputAction: TextInputAction.done,
style: TextStyle(color: Color(0xff00ff00)),
textAlign: TextAlign.end,
textDirection: TextDirection.ltr,
autofocus: true,
autocorrect: false,
maxLines: 10,
maxLength: 100,
maxLengthEnforcement: MaxLengthEnforcement.none,
smartDashesType: SmartDashesType.disabled,
smartQuotesType: SmartQuotesType.disabled,
enabled: false,
cursorWidth: 1.0,
cursorHeight: 1.0,
cursorRadius: Radius.zero,
cursorColor: Color(0xff00ff00),
keyboardAppearance: Brightness.dark,
scrollPadding: EdgeInsets.zero,
scrollPhysics: ClampingScrollPhysics(),
enableInteractiveSelection: false,
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString()).toList();
expect(description, <String>[
'enabled: false',
'decoration: InputDecoration(labelText: "foo")',
'style: TextStyle(inherit: true, color: Color(0xff00ff00))',
'autofocus: true',
'autocorrect: false',
'smartDashesType: disabled',
'smartQuotesType: disabled',
'maxLines: 10',
'maxLength: 100',
'maxLengthEnforcement: none',
'textInputAction: done',
'textAlign: end',
'textDirection: ltr',
'cursorWidth: 1.0',
'cursorHeight: 1.0',
'cursorRadius: Radius.circular(0.0)',
'cursorColor: Color(0xff00ff00)',
'keyboardAppearance: Brightness.dark',
'scrollPadding: EdgeInsets.zero',
'selection disabled',
'scrollPhysics: ClampingScrollPhysics',
]);
});
testWidgets(
'strut basic single line',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: const Material(
child: Center(
child: TextField(),
),
),
),
);
expect(
tester.getSize(find.byType(TextField)),
// The TextField will be as tall as the decoration (24) plus the metrics
// from the default TextStyle of the theme (16), or 40 altogether.
// Because this is less than the kMinInteractiveDimension, it will be
// increased to that value (48).
const Size(800, kMinInteractiveDimension),
);
},
);
testWidgets(
'strut TextStyle increases height',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: const Material(
child: Center(
child: TextField(
style: TextStyle(fontSize: 20),
),
),
),
),
);
expect(
tester.getSize(find.byType(TextField)),
// Strut should inherit the TextStyle.fontSize by default and produce the
// same height as if it were disabled.
const Size(800, kMinInteractiveDimension), // Because 44 < 48.
);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: const Material(
child: Center(
child: TextField(
style: TextStyle(fontSize: 20),
strutStyle: StrutStyle.disabled,
),
),
),
),
);
expect(
tester.getSize(find.byType(TextField)),
// The height here should match the previous version with strut enabled.
const Size(800, kMinInteractiveDimension), // Because 44 < 48.
);
},
);
testWidgets(
'strut basic multi line',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: const Material(
child: Center(
child: TextField(
maxLines: 6,
),
),
),
),
);
expect(
tester.getSize(find.byType(TextField)),
// The height should be the input decoration (24) plus 6x the strut height (16).
const Size(800, 120),
);
},
);
testWidgets(
'strut no force small strut',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: const Material(
child: Center(
child: TextField(
maxLines: 6,
strutStyle: StrutStyle(
// The small strut is overtaken by the larger
// TextStyle fontSize.
fontSize: 5,
),
),
),
),
),
);
expect(
tester.getSize(find.byType(TextField)),
// When the strut's height is smaller than TextStyle's and forceStrutHeight
// is disabled, then the TextStyle takes precedence. Should be the same height
// as 'strut basic multi line'.
const Size(800, 120),
);
},
);
testWidgets(
'strut no force large strut',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: const Material(
child: Center(
child: TextField(
maxLines: 6,
strutStyle: StrutStyle(
fontSize: 25,
),
),
),
),
),
);
expect(
tester.getSize(find.byType(TextField)),
// When the strut's height is larger than TextStyle's and forceStrutHeight
// is disabled, then the StrutStyle takes precedence.
const Size(800, 174),
);
},
skip: isBrowser, // TODO(mdebbar): https://github.com/flutter/flutter/issues/32243
);
testWidgets(
'strut height override',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: const Material(
child: Center(
child: TextField(
maxLines: 3,
strutStyle: StrutStyle(
fontSize: 8,
forceStrutHeight: true,
),
),
),
),
),
);
expect(
tester.getSize(find.byType(TextField)),
// The smaller font size of strut make the field shorter than normal.
const Size(800, 48),
);
},
skip: isBrowser, // TODO(mdebbar): https://github.com/flutter/flutter/issues/32243
);
testWidgets(
'strut forces field taller',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: const Material(
child: Center(
child: TextField(
maxLines: 3,
style: TextStyle(fontSize: 10),
strutStyle: StrutStyle(
fontSize: 18,
forceStrutHeight: true,
),
),
),
),
),
);
expect(
tester.getSize(find.byType(TextField)),
// When the strut fontSize is larger than a provided TextStyle, the
// the strut's height takes precedence.
const Size(800, 78),
);
},
skip: isBrowser, // TODO(mdebbar): https://github.com/flutter/flutter/issues/32243
);
testWidgets('Caret center position', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
child: const SizedBox(
width: 300.0,
child: TextField(
textAlign: TextAlign.center,
decoration: null,
),
),
),
);
final RenderEditable editable = findRenderEditable(tester);
await tester.enterText(find.byType(TextField), 'abcd');
await tester.pump();
Offset topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 4)).topLeft,
);
expect(topLeft.dx, equals(431));
topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 3)).topLeft,
);
expect(topLeft.dx, equals(415));
topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
);
expect(topLeft.dx, equals(399));
topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 1)).topLeft,
);
expect(topLeft.dx, equals(383));
});
testWidgets('Caret indexes into trailing whitespace center align', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
child: const SizedBox(
width: 300.0,
child: TextField(
textAlign: TextAlign.center,
decoration: null,
),
),
),
);
final RenderEditable editable = findRenderEditable(tester);
await tester.enterText(find.byType(TextField), 'abcd ');
await tester.pump();
Offset topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 7)).topLeft,
);
expect(topLeft.dx, equals(479));
topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 8)).topLeft,
);
expect(topLeft.dx, equals(495));
topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 4)).topLeft,
);
expect(topLeft.dx, equals(431));
topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 3)).topLeft,
);
expect(topLeft.dx, equals(415)); // Should be same as equivalent in 'Caret center position'
topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
);
expect(topLeft.dx, equals(399)); // Should be same as equivalent in 'Caret center position'
topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 1)).topLeft,
);
expect(topLeft.dx, equals(383)); // Should be same as equivalent in 'Caret center position'
});
testWidgets('selection handles are rendered and not faded away', (WidgetTester tester) async {
const String testText = 'lorem ipsum';
final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
final RenderEditable renderEditable = state.renderEditable;
await tester.tapAt(const Offset(20, 10));
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
await tester.pumpAndSettle();
final List<FadeTransition> transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, 2);
final FadeTransition left = transitions[0];
final FadeTransition right = transitions[1];
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
});
testWidgets('iOS selection handles are rendered and not faded away', (WidgetTester tester) async {
const String testText = 'lorem ipsum';
final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
final RenderEditable renderEditable =
tester.state<EditableTextState>(find.byType(EditableText)).renderEditable;
await tester.tapAt(const Offset(20, 10));
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
await tester.pumpAndSettle();
final List<FadeTransition> transitions =
find.byType(FadeTransition).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, 2);
final FadeTransition left = transitions[0];
final FadeTransition right = transitions[1];
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('iPad Scribble selection change shows selection handles', (WidgetTester tester) async {
const String testText = 'lorem ipsum';
final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
await tester.showKeyboard(find.byType(EditableText));
await tester.testTextInput.startScribbleInteraction();
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: testText,
selection: TextSelection(baseOffset: 2, extentOffset: 7),
));
await tester.pumpAndSettle();
final List<FadeTransition> transitions =
find.byType(FadeTransition).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, 2);
final FadeTransition left = transitions[0];
final FadeTransition right = transitions[1];
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('Tap shows handles but not toolbar', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
// Tap to trigger the text field.
await tester.tap(find.byType(TextField));
await tester.pump();
final EditableTextState editableText = tester.state(find.byType(EditableText));
expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
});
testWidgets(
'Tap in empty text field does not show handles nor toolbar',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
// Tap to trigger the text field.
await tester.tap(find.byType(TextField));
await tester.pump();
final EditableTextState editableText = tester.state(find.byType(EditableText));
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
},
);
testWidgets('Long press shows handles and toolbar', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
// Long press to trigger the text field.
await tester.longPress(find.byType(TextField));
await tester.pump();
final EditableTextState editableText = tester.state(find.byType(EditableText));
expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue);
});
testWidgets(
'Long press in empty text field shows handles and toolbar',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
// Tap to trigger the text field.
await tester.longPress(find.byType(TextField));
await tester.pump();
final EditableTextState editableText = tester.state(find.byType(EditableText));
expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue);
},
);
testWidgets('Double tap shows handles and toolbar', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
// Double tap to trigger the text field.
await tester.tap(find.byType(TextField));
await tester.pump(const Duration(milliseconds: 50));
await tester.tap(find.byType(TextField));
await tester.pump();
final EditableTextState editableText = tester.state(find.byType(EditableText));
expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue);
});
testWidgets(
'Double tap in empty text field shows toolbar but not handles',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
// Double tap to trigger the text field.
await tester.tap(find.byType(TextField));
await tester.pump(const Duration(milliseconds: 50));
await tester.tap(find.byType(TextField));
await tester.pump();
final EditableTextState editableText = tester.state(find.byType(EditableText));
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue);
},
);
testWidgets(
'Mouse tap does not show handles nor toolbar',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
// Long press to trigger the text field.
final Offset textFieldPos = tester.getCenter(find.byType(TextField));
final TestGesture gesture = await tester.startGesture(
textFieldPos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
final EditableTextState editableText = tester.state(find.byType(EditableText));
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
},
);
testWidgets(
'Mouse long press does not show handles nor toolbar',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
// Long press to trigger the text field.
final Offset textFieldPos = tester.getCenter(find.byType(TextField));
final TestGesture gesture = await tester.startGesture(
textFieldPos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pump();
final EditableTextState editableText = tester.state(find.byType(EditableText));
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
},
);
testWidgets(
'Mouse double tap does not show handles nor toolbar',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
// Double tap to trigger the text field.
final Offset textFieldPos = tester.getCenter(find.byType(TextField));
final TestGesture gesture = await tester.startGesture(
textFieldPos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump(const Duration(milliseconds: 50));
await gesture.up();
await tester.pump();
await gesture.down(textFieldPos);
await tester.pump();
await gesture.up();
await tester.pump();
final EditableTextState editableText = tester.state(find.byType(EditableText));
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
},
);
testWidgets('Does not show handles when updated from the web engine', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
// Interact with the text field to establish the input connection.
final Offset topLeft = tester.getTopLeft(find.byType(EditableText));
final TestGesture gesture = await tester.startGesture(
topLeft + const Offset(0.0, 5.0),
kind: PointerDeviceKind.mouse,
);
await tester.pump(const Duration(milliseconds: 50));
await gesture.up();
await tester.pumpAndSettle();
final EditableTextState state = tester.state(find.byType(EditableText));
expect(state.selectionOverlay!.handlesAreVisible, isFalse);
expect(controller.selection, const TextSelection.collapsed(offset: 0));
if (kIsWeb) {
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: 'abc def ghi',
selection: TextSelection(baseOffset: 2, extentOffset: 7),
));
// Wait for all the `setState` calls to be flushed.
await tester.pumpAndSettle();
expect(
state.currentTextEditingValue.selection,
const TextSelection(baseOffset: 2, extentOffset: 7),
);
expect(state.selectionOverlay!.handlesAreVisible, isFalse);
}
});
testWidgets('Tapping selection handles toggles the toolbar', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
// Tap to position the cursor and show the selection handles.
final Offset ePos = textOffsetToPosition(tester, 5); // Index of 'e'.
await tester.tapAt(ePos, pointer: 7);
await tester.pumpAndSettle();
final EditableTextState editableText = tester.state(find.byType(EditableText));
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
expect(endpoints.length, 1);
// Tap the handle to show the toolbar.
final Offset handlePos = endpoints[0].point + const Offset(0.0, 1.0);
await tester.tapAt(handlePos, pointer: 7);
expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue);
// Tap the handle again to hide the toolbar.
await tester.tapAt(handlePos, pointer: 7);
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
});
testWidgets('when TextField would be blocked by keyboard, it is shown with enough space for the selection handle', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(MaterialApp(
theme: ThemeData(),
home: Scaffold(
body: Center(
child: ListView(
controller: scrollController,
children: <Widget>[
Container(height: 579), // Push field almost off screen.
const TextField(),
Container(height: 1000),
],
),
),
),
));
// Tap the TextField to put the cursor into it and bring it into view.
expect(scrollController.offset, 0.0);
await tester.tapAt(tester.getTopLeft(find.byType(TextField)));
await tester.pumpAndSettle();
// The ListView has scrolled to keep the TextField and cursor handle
// visible.
expect(scrollController.offset, 48.0);
});
// Regression test for https://github.com/flutter/flutter/issues/74566
testWidgets('TextField and last input character are visible on the screen when the cursor is not shown', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
final ScrollController textFieldScrollController = ScrollController();
await tester.pumpWidget(MaterialApp(
theme: ThemeData(),
home: Scaffold(
body: Center(
child: ListView(
controller: scrollController,
children: <Widget>[
Container(height: 579), // Push field almost off screen.
TextField(
scrollController: textFieldScrollController,
showCursor: false,
),
Container(height: 1000),
],
),
),
),
));
// Tap the TextField to bring it into view.
expect(scrollController.offset, 0.0);
await tester.tapAt(tester.getTopLeft(find.byType(TextField)));
await tester.pumpAndSettle();
// The ListView has scrolled to keep the TextField visible.
expect(scrollController.offset, 48.0);
expect(textFieldScrollController.offset, 0.0);
// After entering some long text, the last input character remains on the screen.
final String testValue = 'I love Flutter!' * 10;
tester.testTextInput.updateEditingValue(TextEditingValue(
text: testValue,
selection: TextSelection.collapsed(offset: testValue.length),
));
await tester.pump();
await tester.pumpAndSettle(); // Text scroll animation.
expect(textFieldScrollController.offset, 1602.0);
});
group('height', () {
testWidgets('By default, TextField is at least kMinInteractiveDimension high', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
theme: ThemeData(),
home: const Scaffold(
body: Center(
child: TextField(),
),
),
));
final RenderBox renderBox = tester.renderObject(find.byType(TextField));
expect(renderBox.size.height, greaterThanOrEqualTo(kMinInteractiveDimension));
});
testWidgets("When text is very small, TextField still doesn't go below kMinInteractiveDimension height", (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
theme: ThemeData(),
home: const Scaffold(
body: Center(
child: TextField(
style: TextStyle(fontSize: 2.0),
),
),
),
));
final RenderBox renderBox = tester.renderObject(find.byType(TextField));
expect(renderBox.size.height, kMinInteractiveDimension);
});
testWidgets('When isDense, TextField can go below kMinInteractiveDimension height', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
theme: ThemeData(),
home: const Scaffold(
body: Center(
child: TextField(
decoration: InputDecoration(
isDense: true,
),
),
),
),
));
final RenderBox renderBox = tester.renderObject(find.byType(TextField));
expect(renderBox.size.height, lessThan(kMinInteractiveDimension));
});
group('intrinsics', () {
Widget buildTest({ required bool isDense }) {
return MaterialApp(
home: Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverFillRemaining(
hasScrollBody: false,
child: Column(
children: <Widget>[
TextField(
decoration: InputDecoration(
isDense: isDense,
),
),
Container(
height: 1000,
),
],
),
),
],
),
),
);
}
testWidgets('By default, intrinsic height is at least kMinInteractiveDimension high', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/54729
// If the intrinsic height does not match that of the height after
// performLayout, this will fail.
await tester.pumpWidget(buildTest(isDense: false));
});
testWidgets('When isDense, intrinsic height can go below kMinInteractiveDimension height', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/54729
// If the intrinsic height does not match that of the height after
// performLayout, this will fail.
await tester.pumpWidget(buildTest(isDense: true));
});
});
});
testWidgets("Arrow keys don't move input focus", (WidgetTester tester) async {
final TextEditingController controller1 = TextEditingController();
final TextEditingController controller2 = TextEditingController();
final TextEditingController controller3 = TextEditingController();
final TextEditingController controller4 = TextEditingController();
final TextEditingController controller5 = TextEditingController();
final FocusNode focusNode1 = FocusNode(debugLabel: 'Field 1');
final FocusNode focusNode2 = FocusNode(debugLabel: 'Field 2');
final FocusNode focusNode3 = FocusNode(debugLabel: 'Field 3');
final FocusNode focusNode4 = FocusNode(debugLabel: 'Field 4');
final FocusNode focusNode5 = FocusNode(debugLabel: 'Field 5');
// Lay out text fields in a "+" formation, and focus the center one.
await tester.pumpWidget(MaterialApp(
theme: ThemeData(),
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(
width: 100.0,
child: TextField(
controller: controller1,
focusNode: focusNode1,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(
width: 100.0,
child: TextField(
controller: controller2,
focusNode: focusNode2,
),
),
SizedBox(
width: 100.0,
child: TextField(
controller: controller3,
focusNode: focusNode3,
),
),
SizedBox(
width: 100.0,
child: TextField(
controller: controller4,
focusNode: focusNode4,
),
),
],
),
SizedBox(
width: 100.0,
child: TextField(
controller: controller5,
focusNode: focusNode5,
),
),
],
),
),
),
));
focusNode3.requestFocus();
await tester.pump();
expect(focusNode3.hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(focusNode3.hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(focusNode3.hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusNode3.hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusNode3.hasPrimaryFocus, isTrue);
});
testWidgets('Scrolling shortcuts are disabled in text fields', (WidgetTester tester) async {
bool scrollInvoked = false;
await tester.pumpWidget(
MaterialApp(
home: Actions(
actions: <Type, Action<Intent>>{
ScrollIntent: CallbackAction<ScrollIntent>(onInvoke: (Intent intent) {
scrollInvoked = true;
return null;
}),
},
child: Material(
child: ListView(
children: const <Widget>[
Padding(padding: EdgeInsets.symmetric(vertical: 200)),
TextField(),
Padding(padding: EdgeInsets.symmetric(vertical: 800)),
],
),
),
),
),
);
await tester.pump();
expect(scrollInvoked, isFalse);
// Set focus on the text field.
await tester.tapAt(tester.getTopLeft(find.byType(TextField)));
await tester.sendKeyEvent(LogicalKeyboardKey.space);
expect(scrollInvoked, isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(scrollInvoked, isFalse);
});
testWidgets("A buildCounter that returns null doesn't affect the size of the TextField", (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/44909
final GlobalKey textField1Key = GlobalKey();
final GlobalKey textField2Key = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
children: <Widget>[
TextField(key: textField1Key),
TextField(
key: textField2Key,
maxLength: 1,
buildCounter: (BuildContext context, {required int currentLength, required bool isFocused, int? maxLength}) => null,
),
],
),
),
),
);
await tester.pumpAndSettle();
final Size textFieldSize1 = tester.getSize(find.byKey(textField1Key));
final Size textFieldSize2 = tester.getSize(find.byKey(textField2Key));
expect(textFieldSize1, equals(textFieldSize2));
});
testWidgets(
'The selection menu displays in an Overlay without error',
(WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/43787
final TextEditingController controller = TextEditingController(
text: 'This is a test that shows some odd behavior with Text Selection!',
);
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Container(
color: Colors.grey,
child: Center(
child: Container(
color: Colors.red,
width: 300,
height: 600,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) => Center(
child: TextField(
controller: controller,
),
),
),
],
),
),
),
),
),
));
await showSelectionMenuAt(tester, controller, controller.text.indexOf('test'));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
},
);
testWidgets('Web does not check the clipboard status', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
bool calledGetData = false;
bool calledHasStrings = false;
tester.binding.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
switch (methodCall.method) {
case 'Clipboard.getData':
calledGetData = true;
break;
case 'Clipboard.hasStrings':
calledHasStrings = true;
break;
default:
break;
}
return null;
});
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
// Double tap like when showing the text selection menu on Android/iOS.
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump();
// getData is not called unless something is pasted. hasStrings is used to
// check the status of the clipboard.
expect(calledGetData, false);
if (kIsWeb) {
// hasStrings is not checked because web doesn't show a custom text
// selection menu.
expect(calledHasStrings, false);
} else {
// hasStrings is checked in order to decide if the content can be pasted.
expect(calledHasStrings, true);
}
});
testWidgets('TextField changes mouse cursor when hovered', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: TextField(
mouseCursor: SystemMouseCursors.grab,
decoration: InputDecoration(
// Add an icon so that the left edge is not the text area
icon: Icon(Icons.person),
),
),
),
),
),
);
// Center, which is within the text area
final Offset center = tester.getCenter(find.byType(TextField));
// Top left, which is not the text area
final Offset edge = tester.getTopLeft(find.byType(TextField)) + const Offset(1, 1);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: center);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab);
// Test default cursor
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: TextField(
decoration: InputDecoration(
icon: Icon(Icons.person),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
await gesture.moveTo(edge);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
await gesture.moveTo(center);
// Test default cursor when disabled
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: TextField(
enabled: false,
decoration: InputDecoration(
icon: Icon(Icons.person),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
await gesture.moveTo(edge);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
await gesture.moveTo(center);
});
testWidgets('TextField icons change mouse cursor when hovered', (WidgetTester tester) async {
// Test default cursor in icons area.
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: TextField(
decoration: InputDecoration(
icon: Icon(Icons.label),
prefixIcon: Icon(Icons.cabin),
suffixIcon: Icon(Icons.person),
),
),
),
),
),
);
// Center, which is within the text area
final Offset center = tester.getCenter(find.byType(TextField));
// The Icon area
final Offset iconArea = tester.getCenter(find.byIcon(Icons.label));
// The prefix Icon area
final Offset prefixIconArea = tester.getCenter(find.byIcon(Icons.cabin));
// The suffix Icon area
final Offset suffixIconArea = tester.getCenter(find.byIcon(Icons.person));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: center);
await tester.pump();
await gesture.moveTo(center);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
await gesture.moveTo(iconArea);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
await gesture.moveTo(prefixIconArea);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
await gesture.moveTo(suffixIconArea);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
await gesture.moveTo(center);
// Test click cursor in icons area for buttons.
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: TextField(
decoration: InputDecoration(
icon: IconButton(
icon: const Icon(Icons.label),
onPressed: () {},
),
prefixIcon: IconButton(
icon: const Icon(Icons.cabin),
onPressed: () {},
),
suffixIcon: IconButton(
icon: const Icon(Icons.person),
onPressed: () {},
),
),
),
),
),
),
);
await tester.pump();
await gesture.moveTo(center);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
await gesture.moveTo(iconArea);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
await gesture.moveTo(prefixIconArea);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
await gesture.moveTo(suffixIconArea);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
});
testWidgets('Text selection menu does not change mouse cursor when hovered', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: TextField(
controller: controller,
),
),
),
),
);
expect(find.text('Copy'), findsNothing);
final TestGesture gesture = await tester.startGesture(
textOffsetToPosition(tester, 3),
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
expect(find.text('Paste'), findsOneWidget);
await gesture.moveTo(tester.getCenter(find.text('Paste')));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
},
variant: TargetPlatformVariant.desktop(),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets('Caret rtl with changing width', (WidgetTester tester) async {
late StateSetter setState;
bool isWide = false;
const double wideWidth = 300.0;
const double narrowWidth = 200.0;
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
boilerplate(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return SizedBox(
width: isWide ? wideWidth : narrowWidth,
child: TextField(
key: textFieldKey,
controller: controller,
textDirection: TextDirection.rtl,
),
);
},
),
),
);
// The cursor is on the right of the input because it's RTL.
RenderEditable editable = findRenderEditable(tester);
double cursorRight = editable.getLocalRectForCaret(
TextPosition(offset: controller.value.text.length),
).topRight.dx;
double inputWidth = editable.size.width;
expect(inputWidth, narrowWidth);
expect(cursorRight, inputWidth - kCaretGap);
// After entering some text, the cursor remains on the right of the input.
await tester.enterText(find.byType(TextField), '12345');
await tester.pump();
editable = findRenderEditable(tester);
cursorRight = editable.getLocalRectForCaret(
TextPosition(offset: controller.value.text.length),
).topRight.dx;
inputWidth = editable.size.width;
expect(cursorRight, inputWidth - kCaretGap);
// Since increasing the width of the input moves its right edge further to
// the right, the cursor has followed this change and still appears on the
// right of the input.
setState(() {
isWide = true;
});
await tester.pump();
editable = findRenderEditable(tester);
cursorRight = editable.getLocalRectForCaret(
TextPosition(offset: controller.value.text.length),
).topRight.dx;
inputWidth = editable.size.width;
expect(inputWidth, wideWidth);
expect(cursorRight, inputWidth - kCaretGap);
});
testWidgets('Text selection menu hides after select all on desktop', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
final String selectAll = defaultTargetPlatform == TargetPlatform.macOS
? 'Select All'
: 'Select all';
expect(find.text(selectAll), findsNothing);
expect(find.text('Copy'), findsNothing);
final TestGesture gesture = await tester.startGesture(
const Offset(10.0, 0.0) + textOffsetToPosition(tester, controller.text.length),
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.value.selection,
TextSelection.collapsed(
offset: controller.text.length,
affinity: TextAffinity.upstream,
),
);
expect(find.text(selectAll), findsOneWidget);
await tester.tapAt(tester.getCenter(find.text(selectAll)));
await tester.pump();
expect(find.text(selectAll), findsNothing);
expect(find.text('Copy'), findsNothing);
},
variant: TargetPlatformVariant.desktop(),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
// Regressing test for https://github.com/flutter/flutter/issues/70625
testWidgets('TextFields can inherit [FloatingLabelBehaviour] from InputDecorationTheme.', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
Widget textFieldBuilder({ FloatingLabelBehavior behavior = FloatingLabelBehavior.auto }) {
return MaterialApp(
theme: ThemeData(
inputDecorationTheme: InputDecorationTheme(
floatingLabelBehavior: behavior,
),
),
home: Scaffold(
body: TextField(
focusNode: focusNode,
decoration: const InputDecoration(
labelText: 'Label',
),
),
),
);
}
await tester.pumpWidget(textFieldBuilder());
// The label will be positioned within the content when unfocused.
expect(tester.getTopLeft(find.text('Label')).dy, 20.0);
focusNode.requestFocus();
await tester.pumpAndSettle(); // label animation.
// The label will float above the content when focused.
expect(tester.getTopLeft(find.text('Label')).dy, 12.0);
focusNode.unfocus();
await tester.pumpAndSettle(); // label animation.
await tester.pumpWidget(textFieldBuilder(behavior: FloatingLabelBehavior.never));
await tester.pumpAndSettle(); // theme animation.
// The label will be positioned within the content.
expect(tester.getTopLeft(find.text('Label')).dy, 20.0);
focusNode.requestFocus();
await tester.pumpAndSettle(); // label animation.
// The label will always be positioned within the content.
expect(tester.getTopLeft(find.text('Label')).dy, 20.0);
await tester.pumpWidget(textFieldBuilder(behavior: FloatingLabelBehavior.always));
await tester.pumpAndSettle(); // theme animation.
// The label will always float above the content.
expect(tester.getTopLeft(find.text('Label')).dy, 12.0);
focusNode.unfocus();
await tester.pumpAndSettle(); // label animation.
// The label will always float above the content.
expect(tester.getTopLeft(find.text('Label')).dy, 12.0);
});
group('MaxLengthEnforcement', () {
const int maxLength = 5;
Future<void> setupWidget(
WidgetTester tester,
MaxLengthEnforcement? enforcement,
) async {
final Widget widget = MaterialApp(
home: Material(
child: TextField(
maxLength: maxLength,
maxLengthEnforcement: enforcement,
),
),
);
await tester.pumpWidget(widget);
await tester.pumpAndSettle();
}
testWidgets('using none enforcement.', (WidgetTester tester) async {
const MaxLengthEnforcement enforcement = MaxLengthEnforcement.none;
await setupWidget(tester, enforcement);
final EditableTextState state = tester.state(find.byType(EditableText));
state.updateEditingValue(const TextEditingValue(text: 'abc'));
expect(state.currentTextEditingValue.text, 'abc');
expect(state.currentTextEditingValue.composing, TextRange.empty);
state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6)));
expect(state.currentTextEditingValue.text, 'abcdef');
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6));
state.updateEditingValue(const TextEditingValue(text: 'abcdef'));
expect(state.currentTextEditingValue.text, 'abcdef');
expect(state.currentTextEditingValue.composing, TextRange.empty);
});
testWidgets('using enforced.', (WidgetTester tester) async {
const MaxLengthEnforcement enforcement = MaxLengthEnforcement.enforced;
await setupWidget(tester, enforcement);
final EditableTextState state = tester.state(find.byType(EditableText));
state.updateEditingValue(const TextEditingValue(text: 'abc'));
expect(state.currentTextEditingValue.text, 'abc');
expect(state.currentTextEditingValue.composing, TextRange.empty);
state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5)));
expect(state.currentTextEditingValue.text, 'abcde');
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6)));
expect(state.currentTextEditingValue.text, 'abcde');
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
state.updateEditingValue(const TextEditingValue(text: 'abcdef'));
expect(state.currentTextEditingValue.text, 'abcde');
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
});
testWidgets('using truncateAfterCompositionEnds.', (WidgetTester tester) async {
const MaxLengthEnforcement enforcement = MaxLengthEnforcement.truncateAfterCompositionEnds;
await setupWidget(tester, enforcement);
final EditableTextState state = tester.state(find.byType(EditableText));
state.updateEditingValue(const TextEditingValue(text: 'abc'));
expect(state.currentTextEditingValue.text, 'abc');
expect(state.currentTextEditingValue.composing, TextRange.empty);
state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5)));
expect(state.currentTextEditingValue.text, 'abcde');
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6)));
expect(state.currentTextEditingValue.text, 'abcdef');
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6));
state.updateEditingValue(const TextEditingValue(text: 'abcdef'));
expect(state.currentTextEditingValue.text, 'abcde');
expect(state.currentTextEditingValue.composing, TextRange.empty);
});
testWidgets('using default behavior for different platforms.', (WidgetTester tester) async {
await setupWidget(tester, null);
final EditableTextState state = tester.state(find.byType(EditableText));
state.updateEditingValue(const TextEditingValue(text: '侬好啊'));
expect(state.currentTextEditingValue.text, '侬好啊');
expect(state.currentTextEditingValue.composing, TextRange.empty);
state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友', composing: TextRange(start: 3, end: 5)));
expect(state.currentTextEditingValue.text, '侬好啊旁友');
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友们', composing: TextRange(start: 3, end: 6)));
if (kIsWeb ||
defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.linux ||
defaultTargetPlatform == TargetPlatform.fuchsia
) {
expect(state.currentTextEditingValue.text, '侬好啊旁友们');
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6));
} else {
expect(state.currentTextEditingValue.text, '侬好啊旁友');
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
}
state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友'));
expect(state.currentTextEditingValue.text, '侬好啊旁友');
expect(state.currentTextEditingValue.composing, TextRange.empty);
});
});
testWidgets('prefix/suffix buttons do not leak touch events', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/39376.
int textFieldTapCount = 0;
int prefixTapCount = 0;
int suffixTapCount = 0;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: TextField(
onTap: () { textFieldTapCount += 1; },
decoration: InputDecoration(
labelText: 'Label',
prefix: ElevatedButton(
onPressed: () { prefixTapCount += 1; },
child: const Text('prefix'),
),
suffix: ElevatedButton(
onPressed: () { suffixTapCount += 1; },
child: const Text('suffix'),
),
),
),
),
),
);
await tester.tap(find.text('prefix'));
expect(textFieldTapCount, 0);
expect(prefixTapCount, 1);
expect(suffixTapCount, 0);
await tester.tap(find.text('suffix'));
expect(textFieldTapCount, 0);
expect(prefixTapCount, 1);
expect(suffixTapCount, 1);
});
testWidgets('autofill info has hint text', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: TextField(
decoration: InputDecoration(
hintText: 'placeholder text'
),
),
),
),
),
);
await tester.tap(find.byType(TextField));
expect(
tester.testTextInput.setClientArgs?['autofill'],
containsPair('hintText', 'placeholder text'),
);
});
testWidgets('TextField at rest does not push any layers with alwaysNeedsAddToScene', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: TextField(),
),
),
),
);
expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse);
});
testWidgets('Focused TextField does not push any layers with alwaysNeedsAddToScene', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(focusNode: focusNode),
),
),
),
);
await tester.showKeyboard(find.byType(TextField));
expect(focusNode.hasFocus, isTrue);
expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse);
});
testWidgets('TextField does not push any layers with alwaysNeedsAddToScene after toolbar is dismissed', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(focusNode: focusNode),
),
),
),
);
await tester.showKeyboard(find.byType(TextField));
// Bring up the toolbar.
const String testValue = 'A B C';
tester.testTextInput.updateEditingValue(
const TextEditingValue(
text: testValue,
),
);
await tester.pump();
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
state.renderEditable.selectWordsInRange(from: Offset.zero, cause: SelectionChangedCause.tap);
expect(state.showToolbar(), true);
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Copy'), findsOneWidget); // Toolbar is visible
// Hide the toolbar
focusNode.unfocus();
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Copy'), findsNothing); // Toolbar is not visible
expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse);
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
testWidgets('cursor blinking respects TickerMode', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
Widget builder({required bool tickerMode}) {
return MaterialApp(
home: Material(
child: Center(
child: TickerMode(enabled: tickerMode, child: TextField(focusNode: focusNode)),
),
),
);
}
// TickerMode is on, cursor is blinking.
await tester.pumpWidget(builder(tickerMode: true));
await tester.showKeyboard(find.byType(TextField));
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
final RenderEditable editable = state.renderEditable;
expect(editable.showCursor.value, isTrue);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isTrue);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
// TickerMode is off, cursor does not blink.
await tester.pumpWidget(builder(tickerMode: false));
expect(editable.showCursor.value, isFalse);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
// TickerMode is on, cursor blinks again.
await tester.pumpWidget(builder(tickerMode: true));
expect(editable.showCursor.value, isTrue);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isTrue);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
// Dismissing focus while tickerMode is off does not start cursor blinking
// when tickerMode is turned on again.
await tester.pumpWidget(builder(tickerMode: false));
focusNode.unfocus();
await tester.pump();
expect(editable.showCursor.value, isFalse);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
await tester.pumpWidget(builder(tickerMode: true));
expect(editable.showCursor.value, isFalse);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
// Focusing while tickerMode is off does not start cursor blinking...
await tester.pumpWidget(builder(tickerMode: false));
await tester.showKeyboard(find.byType(TextField));
expect(editable.showCursor.value, isFalse);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
// ... but it does start when tickerMode is switched on again.
await tester.pumpWidget(builder(tickerMode: true));
expect(editable.showCursor.value, isTrue);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isFalse);
await tester.pump(state.cursorBlinkInterval);
expect(editable.showCursor.value, isTrue);
});
testWidgets('can shift + tap to select with a keyboard (Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(controller: controller),
),
),
),
);
await tester.tapAt(textOffsetToPosition(tester, 13));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 13);
expect(controller.selection.extentOffset, 13);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.tapAt(textOffsetToPosition(tester, 20));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 13);
expect(controller.selection.extentOffset, 20);
await tester.pump(kDoubleTapTimeout);
await tester.tapAt(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 13);
expect(controller.selection.extentOffset, 23);
await tester.pump(kDoubleTapTimeout);
await tester.tapAt(textOffsetToPosition(tester, 4));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 4);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 4);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('can shift + tap to select with a keyboard (non-Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(controller: controller),
),
),
),
);
await tester.tapAt(textOffsetToPosition(tester, 13));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 13);
expect(controller.selection.extentOffset, 13);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.tapAt(textOffsetToPosition(tester, 20));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 13);
expect(controller.selection.extentOffset, 20);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.tapAt(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 13);
expect(controller.selection.extentOffset, 23);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.tapAt(textOffsetToPosition(tester, 4));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 13);
expect(controller.selection.extentOffset, 4);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 13);
expect(controller.selection.extentOffset, 4);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));
testWidgets('shift tapping an unfocused field', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
focusNode: focusNode,
),
),
),
),
);
expect(focusNode.hasFocus, isFalse);
// Put the cursor at the end of the field.
await tester.tapAt(textOffsetToPosition(tester, controller.text.length));
await tester.pump(kDoubleTapTimeout);
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isTrue);
expect(controller.selection.baseOffset, 35);
expect(controller.selection.extentOffset, 35);
// Unfocus the field, but the selection remains.
focusNode.unfocus();
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isFalse);
expect(controller.selection.baseOffset, 35);
expect(controller.selection.extentOffset, 35);
// Shift tap in the middle of the field.
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.tapAt(textOffsetToPosition(tester, 20));
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isTrue);
switch (defaultTargetPlatform) {
// Apple platforms start the selection from 0.
case TargetPlatform.iOS:
case TargetPlatform.macOS:
expect(controller.selection.baseOffset, 0);
break;
// Other platforms start from the previous selection.
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(controller.selection.baseOffset, 35);
break;
}
expect(controller.selection.extentOffset, 20);
}, variant: TargetPlatformVariant.all());
testWidgets('can shift + tap + drag to select with a keyboard (Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(controller: controller),
),
),
),
);
await tester.tapAt(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 23),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
if (isTargetPlatformMobile) {
await gesture.up();
}
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Expand the selection a bit.
if (isTargetPlatformMobile) {
await gesture.down(textOffsetToPosition(tester, 24));
}
await tester.pumpAndSettle();
await gesture.moveTo(textOffsetToPosition(tester, 28));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 28);
// Move back to the original selection.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Collapse the selection.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
// Invert the selection. The base jumps to the original extent.
await gesture.moveTo(textOffsetToPosition(tester, 7));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 7);
// Continuing to move in the inverted direction expands the selection.
await gesture.moveTo(textOffsetToPosition(tester, 4));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 4);
// Move back to the original base.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Continue to move past the original base, which will cause the selection
// to invert back to the original orientation.
await gesture.moveTo(textOffsetToPosition(tester, 9));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 9);
// Continuing to select in this direction selects just like it did
// originally.
await gesture.moveTo(textOffsetToPosition(tester, 24));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
// Releasing the shift key has no effect; the selection continues as the
// mouse continues to move.
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
await gesture.moveTo(textOffsetToPosition(tester, 26));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
await gesture.up();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('can shift + tap + drag to select with a keyboard (non-Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android
|| defaultTargetPlatform == TargetPlatform.fuchsia;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(controller: controller),
),
),
),
);
await tester.tapAt(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 23),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
if (isTargetPlatformMobile) {
await gesture.up();
}
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Expand the selection a bit.
if (isTargetPlatformMobile) {
await gesture.down(textOffsetToPosition(tester, 23));
}
await gesture.moveTo(textOffsetToPosition(tester, 28));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 28);
// Move back to the original selection.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Collapse the selection.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
// Invert the selection. The original selection is not restored like on iOS
// and Mac.
await gesture.moveTo(textOffsetToPosition(tester, 7));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 7);
// Continuing to move in the inverted direction expands the selection.
await gesture.moveTo(textOffsetToPosition(tester, 4));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 4);
// Move back to the original base.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
// Continue to move past the original base.
await gesture.moveTo(textOffsetToPosition(tester, 9));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 9);
// Continuing to select in this direction selects just like it did
// originally.
await gesture.moveTo(textOffsetToPosition(tester, 24));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
// Releasing the shift key has no effect; the selection continues as the
// mouse continues to move.
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
await gesture.moveTo(textOffsetToPosition(tester, 26));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
await gesture.up();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));
testWidgets('can shift + tap + drag to select with a keyboard, reversed (Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(controller: controller),
),
),
),
);
// Make a selection from right to left.
await tester.tapAt(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 8),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
if (isTargetPlatformMobile) {
await gesture.up();
}
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Expand the selection a bit.
if (isTargetPlatformMobile) {
await gesture.down(textOffsetToPosition(tester, 7));
}
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 5);
// Move back to the original selection.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Collapse the selection.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
// Invert the selection. The base jumps to the original extent.
await gesture.moveTo(textOffsetToPosition(tester, 24));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
// Continuing to move in the inverted direction expands the selection.
await gesture.moveTo(textOffsetToPosition(tester, 27));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 27);
// Move back to the original base.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Continue to move past the original base, which will cause the selection
// to invert back to the original orientation.
await gesture.moveTo(textOffsetToPosition(tester, 22));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 22);
// Continuing to select in this direction selects just like it did
// originally.
await gesture.moveTo(textOffsetToPosition(tester, 16));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 16);
// Releasing the shift key has no effect; the selection continues as the
// mouse continues to move.
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 16);
await gesture.moveTo(textOffsetToPosition(tester, 14));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
await gesture.up();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('can shift + tap + drag to select with a keyboard, reversed (non-Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android
|| defaultTargetPlatform == TargetPlatform.fuchsia;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(controller: controller),
),
),
),
);
// Make a selection from right to left.
await tester.tapAt(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 8),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
if (isTargetPlatformMobile) {
await gesture.up();
}
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Expand the selection a bit.
if (isTargetPlatformMobile) {
await gesture.down(textOffsetToPosition(tester, 8));
}
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 5);
// Move back to the original selection.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Collapse the selection.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
// Invert the selection. The selection is not restored like it would be on
// iOS and Mac.
await gesture.moveTo(textOffsetToPosition(tester, 24));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 24);
// Continuing to move in the inverted direction expands the selection.
await gesture.moveTo(textOffsetToPosition(tester, 27));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 27);
// Move back to the original base.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
// Continue to move past the original base.
await gesture.moveTo(textOffsetToPosition(tester, 22));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 22);
// Continuing to select in this direction selects just like it did
// originally.
await gesture.moveTo(textOffsetToPosition(tester, 16));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 16);
// Releasing the shift key has no effect; the selection continues as the
// mouse continues to move.
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 16);
await gesture.moveTo(textOffsetToPosition(tester, 14));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
await gesture.up();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));
// Regression test for https://github.com/flutter/flutter/issues/101587.
testWidgets('Right clicking menu behavior', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'blah1 blah2',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
// Initially, the menu is not shown and there is no selection.
expect(find.byType(CupertinoButton), findsNothing);
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
final Offset midBlah1 = textOffsetToPosition(tester, 2);
final Offset midBlah2 = textOffsetToPosition(tester, 8);
// Right click the second word.
final TestGesture gesture = await tester.startGesture(
midBlah2,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
expect(controller.selection, const TextSelection(baseOffset: 6, extentOffset: 11));
expect(find.text('Cut'), findsOneWidget);
expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsOneWidget);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(controller.selection, const TextSelection.collapsed(offset: 8));
expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsOneWidget);
expect(find.text('Select all'), findsOneWidget);
break;
}
// Right click the first word.
await gesture.down(midBlah1);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.text('Cut'), findsOneWidget);
expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsOneWidget);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(controller.selection, const TextSelection.collapsed(offset: 8));
expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsNothing);
expect(find.text('Select all'), findsNothing);
break;
}
},
variant: TargetPlatformVariant.all(),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
group('Right click focus', () {
testWidgets('Can right click to focus multiple times', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/pull/103228
final FocusNode focusNode1 = FocusNode();
final FocusNode focusNode2 = FocusNode();
final UniqueKey key1 = UniqueKey();
final UniqueKey key2 = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
TextField(
key: key1,
focusNode: focusNode1,
),
TextField(
key: key2,
focusNode: focusNode2,
),
],
),
),
),
);
// Interact with the field to establish the input connection.
await tester.tapAt(
tester.getCenter(find.byKey(key1)),
buttons: kSecondaryButton,
);
await tester.pump();
expect(focusNode1.hasFocus, isTrue);
expect(focusNode2.hasFocus, isFalse);
await tester.tapAt(
tester.getCenter(find.byKey(key2)),
buttons: kSecondaryButton,
);
await tester.pump();
expect(focusNode1.hasFocus, isFalse);
expect(focusNode2.hasFocus, isTrue);
await tester.tapAt(
tester.getCenter(find.byKey(key1)),
buttons: kSecondaryButton,
);
await tester.pump();
expect(focusNode1.hasFocus, isTrue);
expect(focusNode2.hasFocus, isFalse);
});
testWidgets('Can right click to focus on previously selected word on Apple platforms', (WidgetTester tester) async {
final FocusNode focusNode1 = FocusNode();
final FocusNode focusNode2 = FocusNode();
final TextEditingController controller = TextEditingController(
text: 'first second',
);
final UniqueKey key1 = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
TextField(
key: key1,
controller: controller,
focusNode: focusNode1,
),
Focus(
focusNode: focusNode2,
child: const Text('focusable'),
),
],
),
),
),
);
// Interact with the field to establish the input connection.
await tester.tapAt(
tester.getCenter(find.byKey(key1)),
buttons: kSecondaryButton,
);
await tester.pump();
expect(focusNode1.hasFocus, isTrue);
expect(focusNode2.hasFocus, isFalse);
// Select the second word.
controller.selection = const TextSelection(
baseOffset: 6,
extentOffset: 12,
);
await tester.pump();
expect(focusNode1.hasFocus, isTrue);
expect(focusNode2.hasFocus, isFalse);
expect(controller.selection.isCollapsed, isFalse);
expect(controller.selection.baseOffset, 6);
expect(controller.selection.extentOffset, 12);
// Unfocus the first field.
focusNode2.requestFocus();
await tester.pumpAndSettle();
expect(focusNode1.hasFocus, isFalse);
expect(focusNode2.hasFocus, isTrue);
// Right click the second word in the first field, which is still selected
// even though the selection is not visible.
await tester.tapAt(
textOffsetToPosition(tester, 8),
buttons: kSecondaryButton,
);
await tester.pump();
expect(focusNode1.hasFocus, isTrue);
expect(focusNode2.hasFocus, isFalse);
expect(controller.selection.baseOffset, 6);
expect(controller.selection.extentOffset, 12);
// Select everything.
controller.selection = const TextSelection(
baseOffset: 0,
extentOffset: 12,
);
await tester.pump();
expect(focusNode1.hasFocus, isTrue);
expect(focusNode2.hasFocus, isFalse);
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 12);
// Unfocus the first field.
focusNode2.requestFocus();
await tester.pumpAndSettle();
// Right click the first word in the first field.
await tester.tapAt(
textOffsetToPosition(tester, 2),
buttons: kSecondaryButton,
);
await tester.pump();
expect(focusNode1.hasFocus, isTrue);
expect(focusNode2.hasFocus, isFalse);
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 5);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
});
group('magnifier builder', () {
testWidgets('should build custom magnifier if given',
(WidgetTester tester) async {
final Widget customMagnifier = Container(
key: UniqueKey(),
);
final TextField textField = TextField(
magnifierConfiguration: TextMagnifierConfiguration(
magnifierBuilder: (_, __, ___) => customMagnifier,
),
);
await tester.pumpWidget(const MaterialApp(
home: Placeholder(),
));
final BuildContext context =
tester.firstElement(find.byType(Placeholder));
expect(
textField.magnifierConfiguration!.magnifierBuilder(
context,
MagnifierController(),
ValueNotifier<MagnifierInfo>(MagnifierInfo.empty),
),
isA<Widget>().having(
(Widget widget) => widget.key,
'built magnifier key equal to passed in magnifier key',
equals(customMagnifier.key)));
});
group('defaults', () {
testWidgets('should build Magnifier on Android', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(body: TextField()))
);
final BuildContext context = tester.firstElement(find.byType(TextField));
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(
editableText.magnifierConfiguration.magnifierBuilder(
context,
MagnifierController(),
ValueNotifier<MagnifierInfo>(MagnifierInfo.empty),
),
isA<TextMagnifier>());
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
testWidgets('should build CupertinoMagnifier on iOS',
(WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(body: TextField()))
);
final BuildContext context = tester.firstElement(find.byType(TextField));
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(
editableText.magnifierConfiguration.magnifierBuilder(
context,
MagnifierController(),
ValueNotifier<MagnifierInfo>(MagnifierInfo.empty),
),
isA<CupertinoTextMagnifier>());
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
testWidgets('should build nothing on Android and iOS',
(WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(body: TextField()))
);
final BuildContext context = tester.firstElement(find.byType(TextField));
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(
editableText.magnifierConfiguration.magnifierBuilder(
context,
MagnifierController(),
ValueNotifier<MagnifierInfo>(MagnifierInfo.empty),
),
isNull);
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{
TargetPlatform.iOS,
TargetPlatform.android
}));
});
});
group('magnifier', () {
late ValueNotifier<MagnifierInfo> magnifierInfo;
final Widget fakeMagnifier = Container(key: UniqueKey());
testWidgets(
'Can drag handles to show, unshow, and update magnifier',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
magnifierConfiguration: TextMagnifierConfiguration(
magnifierBuilder: (
_,
MagnifierController controller,
ValueNotifier<MagnifierInfo> localMagnifierInfo
) {
magnifierInfo = localMagnifierInfo;
return fakeMagnifier;
},
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
// Double tap the 'e' to select 'def'.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pump(const Duration(milliseconds: 30));
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pump(const Duration(milliseconds: 30));
final TextSelection selection = controller.selection;
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
// Drag the right handle 2 letters to the right.
final Offset handlePos = endpoints.last.point + const Offset(1.0, 1.0);
final TestGesture gesture = await tester.startGesture(handlePos);
await gesture.moveTo(textOffsetToPosition(tester, testValue.length - 2));
await tester.pump();
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
final Offset firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;
await gesture.moveTo(textOffsetToPosition(tester, testValue.length));
await tester.pump();
// Expect the position the magnifier gets to have moved.
expect(firstDragGesturePosition,
isNot(magnifierInfo.value.globalGesturePosition));
await gesture.up();
await tester.pump();
expect(find.byKey(fakeMagnifier.key!), findsNothing);
});
testWidgets('Can long press to show, unshow, and update magnifier', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
magnifierConfiguration: TextMagnifierConfiguration(
magnifierBuilder: (
_,
MagnifierController controller,
ValueNotifier<MagnifierInfo> localMagnifierInfo
) {
magnifierInfo = localMagnifierInfo;
return fakeMagnifier;
},
),
),
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
// Tap at 'e' to set the selection to position 5 on Android.
// Tap at 'e' to set the selection to the closest word edge, which is position 4 on iOS.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pumpAndSettle(const Duration(milliseconds: 300));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 4);
expect(find.byKey(fakeMagnifier.key!), findsNothing);
// Long press the 'e' to select 'def' on Android and show magnifier.
// Long press the 'e' to move the cursor in front of the 'e' on iOS and show the magnifier.
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pumpAndSettle(const Duration(milliseconds: 1000));
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 4 : 5);
expect(controller.selection.extentOffset, isTargetPlatformAndroid ? 7 : 5);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
final Offset firstLongPressGesturePosition = magnifierInfo.value.globalGesturePosition;
// Move the gesture to 'h' on Android to update the magnifier and select 'ghi'.
// Move the gesture to 'h' on iOS to update the magnifier and move the cursor to 'h'.
await gesture.moveTo(textOffsetToPosition(tester, testValue.indexOf('h')));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 4 : 9);
expect(controller.selection.extentOffset, isTargetPlatformAndroid ? 11 : 9);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
// Expect the position the magnifier gets to have moved.
expect(firstLongPressGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));
// End the long press to hide the magnifier.
await gesture.up();
await tester.pumpAndSettle();
expect(find.byKey(fakeMagnifier.key!), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }));
group('TapRegion integration', () {
testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 100,
height: 100,
child: Opacity(
opacity: 0.5,
child: TextField(
autofocus: true,
focusNode: focusNode,
decoration: const InputDecoration(
hintText: 'Placeholder',
border: OutlineInputBorder(),
),
),
),
),
),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
await tester.tapAt(const Offset(10, 10));
await tester.pump();
expect(focusNode.hasPrimaryFocus, isFalse);
}, variant: TargetPlatformVariant.desktop());
testWidgets("Tapping outside doesn't lose focus on mobile", (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 100,
height: 100,
child: Opacity(
opacity: 0.5,
child: TextField(
autofocus: true,
focusNode: focusNode,
decoration: const InputDecoration(
hintText: 'Placeholder',
border: OutlineInputBorder(),
),
),
),
),
),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
await tester.tapAt(const Offset(10, 10));
await tester.pump();
// Focus is lost on mobile browsers, but not mobile apps.
expect(focusNode.hasPrimaryFocus, kIsWeb ? isFalse : isTrue);
}, variant: TargetPlatformVariant.mobile());
testWidgets("Tapping on toolbar doesn't lose focus", (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
final TextEditingController controller = TextEditingController(text: 'A B C');
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 100,
height: 100,
child: Opacity(
opacity: 0.5,
child: TextField(
controller: controller,
focusNode: focusNode,
decoration: const InputDecoration(hintText: 'Placeholder'),
),
),
),
),
),
),
);
// The selectWordsInRange with SelectionChangedCause.tap seems to be needed to show the toolbar.
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
state.renderEditable.selectWordsInRange(from: Offset.zero, cause: SelectionChangedCause.tap);
final Offset aPosition = textOffsetToPosition(tester, 1);
// Right clicking shows the menu.
final TestGesture gesture = await tester.startGesture(
aPosition,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Sanity check that the toolbar widget exists.
expect(find.text('Copy'), findsOneWidget);
expect(focusNode.hasPrimaryFocus, isTrue);
// Now tap on it to see if we lose focus.
await tester.tap(find.text('Copy'));
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
},
variant: TargetPlatformVariant.all(),
skip: isBrowser, // [intended] On the web, the toolbar isn't rendered by Flutter.
);
testWidgets("Tapping on input decorator doesn't lose focus", (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 100,
height: 100,
child: Opacity(
opacity: 0.5,
child: TextField(
autofocus: true,
focusNode: focusNode,
decoration: const InputDecoration(
hintText: 'Placeholder',
border: OutlineInputBorder(),
),
),
),
),
),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
final Rect decorationBox = tester.getRect(find.byType(TextField));
// Tap just inside the decoration, but not inside the EditableText.
await tester.tapAt(decorationBox.topLeft + const Offset(1, 1));
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
}, variant: TargetPlatformVariant.all());
// PointerDownEvents can't be trackpad events, apparently, so we skip that one.
for (final PointerDeviceKind pointerDeviceKind in PointerDeviceKind.values.toSet()..remove(PointerDeviceKind.trackpad)) {
testWidgets('Default TextField handling of onTapOutside follows platform conventions for ${pointerDeviceKind.name}', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test');
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
children: <Widget>[
const Text('Outside'),
TextField(
autofocus: true,
focusNode: focusNode,
),
],
),
),
),
);
await tester.pump();
Future<void> click(Finder finder) async {
final TestGesture gesture = await tester.startGesture(
tester.getCenter(finder),
kind: pointerDeviceKind,
);
await gesture.up();
await gesture.removePointer();
}
expect(focusNode.hasPrimaryFocus, isTrue);
await click(find.text('Outside'));
switch(pointerDeviceKind) {
case PointerDeviceKind.touch:
switch(defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.android:
case TargetPlatform.fuchsia:
expect(focusNode.hasPrimaryFocus, equals(!kIsWeb));
break;
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
expect(focusNode.hasPrimaryFocus, isFalse);
break;
}
break;
case PointerDeviceKind.mouse:
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.trackpad:
case PointerDeviceKind.unknown:
expect(focusNode.hasPrimaryFocus, isFalse);
break;
}
}, variant: TargetPlatformVariant.all());
}
});
});
}
/// A Simple widget for testing the obscure text.
class _ObscureTextTestWidget extends StatefulWidget {
const _ObscureTextTestWidget({ required this.controller });
final TextEditingController controller;
@override
_ObscureTextTestWidgetState createState() => _ObscureTextTestWidgetState();
}
class _ObscureTextTestWidgetState extends State<_ObscureTextTestWidget> {
bool _obscureText = false;
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Builder(
builder: (_) {
return Column(
children: <Widget>[
TextField(
obscureText: _obscureText,
controller: widget.controller,
),
ElevatedButton(
onPressed: () => setState(() {_obscureText = !_obscureText;}),
child: const SizedBox.shrink(),
),
],
);
},
),
),
);
}
}