flutter/packages/flutter/test/cupertino/text_field_test.dart
Anis Alibegić 81d80c587d
Fixed a lot of typos (#141431)
Fair amount of typos spotted and fixed. Some of them are in comments, some of them are in code and some of them are in nondart files.

There is no need for issues since it's a typo fix.

I have doubts about [packages/flutter_tools/lib/src/ios/core_devices.dart](https://github.com/flutter/flutter/compare/master...anisalibegic:flutter:master#diff-fdbc1496b4bbe7e2b445a567fd385677af861c0093774e3d8cc460fdd5b794fa), I have a feeling it might broke some things on the other end, even though it's a typo.
2024-01-12 22:10:25 +00:00

10140 lines
351 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// reduced-test-set:
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
library;
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Color;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior, PointerDeviceKind, kDoubleTapTimeout, kLongPressTimeout, kSecondaryMouseButton;
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, isContextMenuProvidedByPlatform;
import '../widgets/live_text_utils.dart';
import '../widgets/semantics_tester.dart';
import '../widgets/text_selection_toolbar_utils.dart';
class MockTextSelectionControls extends TextSelectionControls {
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]) {
throw UnimplementedError();
}
@override
Widget buildToolbar(
BuildContext context,
Rect globalEditableRegion,
double textLineHeight,
Offset position,
List<TextSelectionPoint> endpoints,
TextSelectionDelegate delegate,
ValueListenable<ClipboardStatus>? clipboardStatus,
Offset? lastSecondaryTapDownPosition,
) {
throw UnimplementedError();
}
@override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
throw UnimplementedError();
}
@override
Size getHandleSize(double textLineHeight) {
throw UnimplementedError();
}
}
class PathBoundsMatcher extends Matcher {
const PathBoundsMatcher({
this.rectMatcher,
this.topMatcher,
this.leftMatcher,
this.rightMatcher,
this.bottomMatcher,
}) : super();
final Matcher? rectMatcher;
final Matcher? topMatcher;
final Matcher? leftMatcher;
final Matcher? rightMatcher;
final Matcher? bottomMatcher;
@override
bool matches(covariant Path item, Map<dynamic, dynamic> matchState) {
final Rect bounds = item.getBounds();
final List<Matcher?> matchers = <Matcher?> [rectMatcher, topMatcher, leftMatcher, rightMatcher, bottomMatcher];
final List<dynamic> values = <dynamic> [bounds, bounds.top, bounds.left, bounds.right, bounds.bottom];
final Map<Matcher, dynamic> failedMatcher = <Matcher, dynamic> {};
for (int idx = 0; idx < matchers.length; idx++) {
if (!(matchers[idx]?.matches(values[idx], matchState) ?? true)) {
failedMatcher[matchers[idx]!] = values[idx];
}
}
matchState['failedMatcher'] = failedMatcher;
return failedMatcher.isEmpty;
}
@override
Description describe(Description description) => description.add('The actual Rect does not match');
@override
Description describeMismatch(covariant Path item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
final Description description = super.describeMismatch(item, mismatchDescription, matchState, verbose);
final Map<Matcher, dynamic> map = matchState['failedMatcher'] as Map<Matcher, dynamic>;
final Iterable<String> descriptions = map.entries
.map<String>(
(MapEntry<Matcher, dynamic> entry) => entry.key.describeMismatch(entry.value, StringDescription(), matchState, verbose).toString(),
);
// description is guaranteed to be non-null.
return description
..add('mismatch Rect: ${item.getBounds()}')
.addAll(': ', ', ', '. ', descriptions);
}
}
class PathPointsMatcher extends Matcher {
const PathPointsMatcher({
this.includes = const <Offset>[],
this.excludes = const <Offset>[],
}) : super();
final Iterable<Offset> includes;
final Iterable<Offset> excludes;
@override
bool matches(covariant Path item, Map<dynamic, dynamic> matchState) {
final Offset? notIncluded = includes.cast<Offset?>().firstWhere((Offset? offset) => !item.contains(offset!), orElse: () => null);
final Offset? notExcluded = excludes.cast<Offset?>().firstWhere((Offset? offset) => item.contains(offset!), orElse: () => null);
matchState['notIncluded'] = notIncluded;
matchState['notExcluded'] = notExcluded;
return (notIncluded ?? notExcluded) == null;
}
@override
Description describe(Description description) => description.add('must include these points $includes and must not include $excludes');
@override
Description describeMismatch(covariant Path item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
final Offset? notIncluded = matchState['notIncluded'] as Offset?;
final Offset? notExcluded = matchState['notExcluded'] as Offset?;
final Description desc = super.describeMismatch(item, mismatchDescription, matchState, verbose);
if ((notExcluded ?? notIncluded) != null) {
desc.add('Within the bounds of the path ${item.getBounds()}: ');
}
if (notIncluded != null) {
desc.add('$notIncluded is not included. ');
}
if (notExcluded != null) {
desc.add('$notExcluded is not excluded. ');
}
return desc;
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final MockClipboard mockClipboard = MockClipboard();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);
// Returns the first RenderEditable.
RenderEditable findRenderEditable(WidgetTester tester) {
final RenderObject root = tester.renderObject(find.byType(EditableText));
expect(root, isNotNull);
RenderEditable? renderEditable;
void recursiveFinder(RenderObject child) {
if (child is RenderEditable) {
renderEditable = child;
return;
}
child.visitChildren(recursiveFinder);
}
root.visitChildren(recursiveFinder);
expect(renderEditable, isNotNull);
return renderEditable!;
}
List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) {
return points.map<TextSelectionPoint>((TextSelectionPoint point) {
return TextSelectionPoint(
box.localToGlobal(point.point),
point.direction,
);
}).toList();
}
Offset textOffsetToBottomLeftPosition(WidgetTester tester, int offset) {
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(
TextSelection.collapsed(offset: offset),
),
renderEditable,
);
expect(endpoints.length, 1);
return endpoints[0].point;
}
// Web has a less threshold for downstream/upstream text position.
Offset textOffsetToPosition(WidgetTester tester, int offset) => textOffsetToBottomLeftPosition(tester, offset) + const Offset(kIsWeb ? 1 : 0, -2);
setUp(() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);
EditableText.debugDeterministicCursor = false;
// Fill the clipboard so that the Paste option is available in the text
// selection menu.
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
});
testWidgets(
'Live Text button shows and hides correctly when LiveTextStatus changes',
(WidgetTester tester) async {
final LiveTextInputTester liveTextInputTester = LiveTextInputTester();
addTearDown(liveTextInputTester.dispose);
final TextEditingController controller = TextEditingController(text: '');
addTearDown(controller.dispose);
const Key key = ValueKey<String>('TextField');
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
final Widget app = MaterialApp(
theme: ThemeData(platform: TargetPlatform.iOS),
home: Scaffold(
body: Center(
child: CupertinoTextField(
key: key,
controller: controller,
focusNode: focusNode,
),
),
),
);
liveTextInputTester.mockLiveTextInputEnabled = true;
await tester.pumpWidget(app);
focusNode.requestFocus();
await tester.pumpAndSettle();
final Finder textFinder = find.byType(EditableText);
await tester.longPress(textFinder);
await tester.pumpAndSettle();
expect(
findLiveTextButton(),
kIsWeb ? findsNothing : findsOneWidget,
);
liveTextInputTester.mockLiveTextInputEnabled = false;
await tester.longPress(textFinder);
await tester.pumpAndSettle();
expect(findLiveTextButton(), findsNothing);
},
);
testWidgets('Look Up shows up on iOS only', (WidgetTester tester) async {
String? lastLookUp;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'LookUp.invoke') {
expect(methodCall.arguments, isA<String>());
lastLookUp = methodCall.arguments as String;
}
return null;
});
final TextEditingController controller = TextEditingController(
text: 'Test',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final bool isTargetPlatformiOS = defaultTargetPlatform == TargetPlatform.iOS;
// Long press to put the cursor after the "s".
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
// Double tap on the same location to select the word around the cursor.
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4));
expect(find.text('Look Up'), isTargetPlatformiOS? findsOneWidget : findsNothing);
if (isTargetPlatformiOS) {
await tester.tap(find.text('Look Up'));
expect(lastLookUp, 'Test');
}
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets('Search Web shows up on iOS only', (WidgetTester tester) async {
String? lastSearch;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'SearchWeb.invoke') {
expect(methodCall.arguments, isA<String>());
lastSearch = methodCall.arguments as String;
}
return null;
});
final TextEditingController controller = TextEditingController(
text: 'Test',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final bool isTargetPlatformiOS = defaultTargetPlatform == TargetPlatform.iOS;
// Long press to put the cursor after the "s".
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
// Double tap on the same location to select the word around the cursor.
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4));
expect(find.text('Search Web'), isTargetPlatformiOS? findsOneWidget : findsNothing);
if (isTargetPlatformiOS) {
await tester.tap(find.text('Search Web'));
expect(lastSearch, 'Test');
}
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets('Share shows up on iOS and Android', (WidgetTester tester) async {
String? lastShare;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'Share.invoke') {
expect(methodCall.arguments, isA<String>());
lastShare = methodCall.arguments as String;
}
return null;
});
final TextEditingController controller = TextEditingController(
text: 'Test',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
// Long press to put the cursor after the "s".
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
// Double tap on the same location to select the word around the cursor.
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4));
expect(find.text('Share...'), findsOneWidget);
await tester.tap(find.text('Share...'));
expect(lastShare, 'Test');
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets('can use the desktop cut/copy/paste buttons on Mac', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'blah1 blah2',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(400, 200)),
child: CupertinoTextField(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('Copy'), findsOneWidget);
expect(find.text('Cut'), 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(baseOffset: 11, extentOffset: 11, affinity: TextAffinity.upstream));
expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsOneWidget);
await tester.tap(find.text('Paste'));
await tester.pumpAndSettle();
expect(controller.text, 'blah1 blah2blah1');
expect(controller.selection, const TextSelection.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: kIsWeb, // [intended] the web handles this on its own.
);
testWidgets('can get text selection color initially on desktop', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
final TextEditingController controller = TextEditingController(
text: 'blah1 blah2',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: RepaintBoundary(
child: CupertinoTextField(
key: const ValueKey<int>(1),
controller: controller,
focusNode: focusNode,
),
),
),
),
);
controller.selection = const TextSelection(baseOffset: 0, extentOffset: 11);
focusNode.requestFocus();
await tester.pump();
expect(focusNode.hasFocus, true);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_golden.text_selection_color.0.png'),
);
});
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();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTextField(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,
SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled,],
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(
'takes available space horizontally and takes intrinsic space vertically no-strut',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(200, 200)),
child: const CupertinoTextField(strutStyle: StrutStyle.disabled),
),
),
),
);
expect(
tester.getSize(find.byType(CupertinoTextField)),
const Size(200, 31), // 31 is the height of the default font + padding etc.
);
},
);
testWidgets('sets cursorOpacityAnimates on EditableText correctly', (WidgetTester tester) async {
// True
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoTextField(autofocus: true),
),
);
await tester.pump();
EditableText editableText = tester.widget(find.byType(EditableText));
expect(editableText.cursorOpacityAnimates, true);
// False
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoTextField(autofocus: true, cursorOpacityAnimates: false),
),
);
await tester.pump();
editableText = tester.widget(find.byType(EditableText));
expect(editableText.cursorOpacityAnimates, false);
});
testWidgets(
'takes available space horizontally and takes intrinsic space vertically',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(200, 200)),
child: const CupertinoTextField(),
),
),
),
);
expect(
tester.getSize(find.byType(CupertinoTextField)),
const Size(200, 31), // 31 is the height of the default font (17) + decoration (12).
);
},
);
testWidgets(
'uses DefaultSelectionStyle for selection and cursor colors if provided',
(WidgetTester tester) async {
const Color selectionColor = Colors.black;
const Color cursorColor = Colors.white;
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: DefaultSelectionStyle(
selectionColor: selectionColor,
cursorColor: cursorColor,
child: CupertinoTextField(
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('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();
addTearDown(controller1.dispose);
const Color selectionColor = Colors.orange;
const Color cursorColor = Colors.red;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: DefaultSelectionStyle(
selectionColor: selectionColor,
cursorColor: cursorColor,
child: Column(
children: <Widget>[
CupertinoTextField(
key: key1,
controller: controller1,
),
CupertinoTextField(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.pumpAndSettle();
// 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 TextField
// selectionColor should be dropped.
await tester.tap(find.byKey(key2));
await tester.pump();
expect(state1.widget.selectionColor, null);
expect(state2.widget.selectionColor, selectionColor);
});
testWidgets(
'multi-lined text fields are intrinsically taller no-strut',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(200, 200)),
child: const CupertinoTextField(
maxLines: 3,
strutStyle: StrutStyle.disabled,
),
),
),
),
);
expect(
tester.getSize(find.byType(CupertinoTextField)),
const Size(200, 65), // 65 is the height of the default font (17) * maxlines (3) + decoration height (12).
);
},
);
testWidgets(
'multi-lined text fields are intrinsically taller',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(200, 200)),
child: const CupertinoTextField(maxLines: 3),
),
),
),
);
expect(
tester.getSize(find.byType(CupertinoTextField)),
const Size(200, 65),
);
},
);
testWidgets(
'strut height override',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(200, 200)),
child: const CupertinoTextField(
maxLines: 3,
strutStyle: StrutStyle(
fontSize: 8,
forceStrutHeight: true,
),
),
),
),
),
);
expect(
tester.getSize(find.byType(CupertinoTextField)),
const Size(200, 38),
);
},
// TODO(mdebbar): Strut styles support.
skip: isBrowser, // https://github.com/flutter/flutter/issues/32243
);
testWidgets(
'strut forces field taller',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(200, 200)),
child: const CupertinoTextField(
maxLines: 3,
style: TextStyle(fontSize: 10),
strutStyle: StrutStyle(
fontSize: 18,
forceStrutHeight: true,
),
),
),
),
),
);
expect(
tester.getSize(find.byType(CupertinoTextField)),
const Size(200, 68),
);
},
// TODO(mdebbar): Strut styles support.
skip: isBrowser, // https://github.com/flutter/flutter/issues/32243
);
testWidgets(
'default text field has a border',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(),
),
),
);
BoxDecoration decoration = tester.widget<DecoratedBox>(
find.descendant(
of: find.byType(CupertinoTextField),
matching: find.byType(DecoratedBox),
),
).decoration as BoxDecoration;
expect(
decoration.borderRadius,
const BorderRadius.all(Radius.circular(5)),
);
expect(
decoration.border!.bottom.color.value,
0x33000000,
);
// Dark mode.
await tester.pumpWidget(
const CupertinoApp(
theme: CupertinoThemeData(brightness: Brightness.dark),
home: Center(
child: CupertinoTextField(),
),
),
);
decoration = tester.widget<DecoratedBox>(
find.descendant(
of: find.byType(CupertinoTextField),
matching: find.byType(DecoratedBox),
),
).decoration as BoxDecoration;
expect(
decoration.borderRadius,
const BorderRadius.all(Radius.circular(5)),
);
expect(
decoration.border!.bottom.color.value,
0x33FFFFFF,
);
},
);
testWidgets(
'decoration can be overridden',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
decoration: null,
),
),
),
);
expect(
find.descendant(
of: find.byType(CupertinoTextField),
matching: find.byType(DecoratedBox),
),
findsNothing,
);
},
);
testWidgets(
'text entries are padded by default',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'initial');
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
expect(
tester.getTopLeft(find.text('initial')) - tester.getTopLeft(find.byType(CupertinoTextField)),
const Offset(7.0, 7.0),
);
},
);
testWidgets('iOS cursor has offset', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoTextField(),
),
);
final EditableText editableText = tester.firstWidget(find.byType(EditableText));
expect(editableText.cursorOffset, const Offset(-2.0 / 3.0, 0));
});
testWidgets('Cursor radius is 2.0', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoTextField(),
),
);
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('Cupertino cursor android golden', (WidgetTester tester) async {
final Widget widget = CupertinoApp(
home: Center(
child: RepaintBoundary(
key: const ValueKey<int>(1),
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(400, 400)),
child: const CupertinoTextField(),
),
),
),
);
await tester.pumpWidget(widget);
const String testValue = 'A short phrase';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pump();
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
await tester.pumpAndSettle();
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_cursor_test.cupertino.0.png'),
);
});
testWidgets('Cupertino cursor golden', (WidgetTester tester) async {
final Widget widget = CupertinoApp(
home: Center(
child: RepaintBoundary(
key: const ValueKey<int>(1),
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(400, 400)),
child: const CupertinoTextField(),
),
),
),
);
await tester.pumpWidget(widget);
const String testValue = 'A short phrase';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pump();
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
await tester.pumpAndSettle();
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile(
'text_field_cursor_test.cupertino_${debugDefaultTargetPlatformOverride!.name.toLowerCase()}.1.png',
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets(
'can control text content via controller',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
controller.text = 'controller text';
await tester.pump();
expect(find.text('controller text'), findsOneWidget);
controller.text = '';
await tester.pump();
expect(find.text('controller text'), findsNothing);
},
);
testWidgets(
'placeholder respects textAlign',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
placeholder: 'placeholder',
textAlign: TextAlign.right,
),
),
),
);
final Text placeholder = tester.widget(find.text('placeholder'));
expect(placeholder.textAlign, TextAlign.right);
await tester.enterText(find.byType(CupertinoTextField), 'input');
await tester.pump();
final EditableText inputText = tester.widget(find.text('input'));
expect(placeholder.textAlign, inputText.textAlign);
},
);
testWidgets('placeholder dark mode', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
theme: CupertinoThemeData(brightness: Brightness.dark),
home: Center(
child: CupertinoTextField(
placeholder: 'placeholder',
textAlign: TextAlign.right,
),
),
),
);
final Text placeholder = tester.widget(find.text('placeholder'));
expect(placeholder.style!.color!.value, CupertinoColors.placeholderText.darkColor.value);
});
testWidgets(
'placeholders are lightly colored and disappears once typing starts',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
placeholder: 'placeholder',
),
),
),
);
final Text placeholder = tester.widget(find.text('placeholder'));
expect(placeholder.style!.color!.value, CupertinoColors.placeholderText.color.value);
await tester.enterText(find.byType(CupertinoTextField), 'input');
await tester.pump();
final Element element = tester.element(find.text('placeholder'));
expect(Visibility.of(element), false);
},
);
testWidgets(
"placeholderStyle modifies placeholder's style and doesn't affect text's style",
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
placeholder: 'placeholder',
style: TextStyle(
color: Color(0x00FFFFFF),
fontWeight: FontWeight.w300,
),
placeholderStyle: TextStyle(
color: Color(0xAAFFFFFF),
fontWeight: FontWeight.w600,
),
),
),
),
);
final Text placeholder = tester.widget(find.text('placeholder'));
expect(placeholder.style!.color, const Color(0xAAFFFFFF));
expect(placeholder.style!.fontWeight, FontWeight.w600);
await tester.enterText(find.byType(CupertinoTextField), 'input');
await tester.pump();
final EditableText inputText = tester.widget(find.text('input'));
expect(inputText.style.color, const Color(0x00FFFFFF));
expect(inputText.style.fontWeight, FontWeight.w300);
},
);
testWidgets(
'prefix widget is in front of the text',
(WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
final TextEditingController controller = TextEditingController(text: 'input');
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
focusNode: focusNode,
prefix: const Icon(CupertinoIcons.add),
controller: controller,
),
),
),
);
expect(
tester.getTopRight(find.byIcon(CupertinoIcons.add)).dx + 7.0, // 7px standard padding around input.
tester.getTopLeft(find.byType(EditableText)).dx,
);
expect(
tester.getTopLeft(find.byType(EditableText)).dx,
tester.getTopLeft(find.byType(CupertinoTextField)).dx
+ tester.getSize(find.byIcon(CupertinoIcons.add)).width
+ 7.0,
);
},
);
testWidgets(
'prefix widget respects visibility mode',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
prefix: Icon(CupertinoIcons.add),
prefixMode: OverlayVisibilityMode.editing,
),
),
),
);
expect(find.byIcon(CupertinoIcons.add), findsNothing);
// The position should just be the edge of the whole text field plus padding.
expect(
tester.getTopLeft(find.byType(EditableText)).dx,
tester.getTopLeft(find.byType(CupertinoTextField)).dx + 7.0,
);
await tester.enterText(find.byType(CupertinoTextField), 'text input');
await tester.pump();
expect(find.text('text input'), findsOneWidget);
expect(find.byIcon(CupertinoIcons.add), findsOneWidget);
// Text is now moved to the right.
expect(
tester.getTopLeft(find.byType(EditableText)).dx,
tester.getTopLeft(find.byType(CupertinoTextField)).dx
+ tester.getSize(find.byIcon(CupertinoIcons.add)).width
+ 7.0,
);
},
);
testWidgets(
'suffix widget is after the text',
(WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
focusNode: focusNode,
suffix: const Icon(CupertinoIcons.add),
),
),
),
);
expect(
tester.getTopRight(find.byType(EditableText)).dx + 7.0,
tester.getTopLeft(find.byIcon(CupertinoIcons.add)).dx, // 7px standard padding around input.
);
expect(
tester.getTopRight(find.byType(EditableText)).dx,
tester.getTopRight(find.byType(CupertinoTextField)).dx
- tester.getSize(find.byIcon(CupertinoIcons.add)).width
- 7.0,
);
},
);
testWidgets(
'suffix widget respects visibility mode',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
suffix: Icon(CupertinoIcons.add),
suffixMode: OverlayVisibilityMode.notEditing,
),
),
),
);
expect(find.byIcon(CupertinoIcons.add), findsOneWidget);
await tester.enterText(find.byType(CupertinoTextField), 'text input');
await tester.pump();
expect(find.text('text input'), findsOneWidget);
expect(find.byIcon(CupertinoIcons.add), findsNothing);
},
);
testWidgets(
'can customize padding',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
padding: EdgeInsets.zero,
),
),
),
);
expect(
tester.getSize(find.byType(EditableText)),
tester.getSize(find.byType(CupertinoTextField)),
);
},
);
testWidgets(
'padding is in between prefix and suffix no-strut',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
padding: EdgeInsets.all(20.0),
prefix: SizedBox(height: 100.0, width: 100.0),
suffix: SizedBox(height: 50.0, width: 50.0),
strutStyle: StrutStyle.disabled,
),
),
),
);
expect(
tester.getTopLeft(find.byType(EditableText)).dx,
// Size of prefix + padding.
100.0 + 20.0,
);
expect(tester.getTopLeft(find.byType(EditableText)).dy, 291.5);
expect(
tester.getTopRight(find.byType(EditableText)).dx,
800.0 - 50.0 - 20.0,
);
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
padding: EdgeInsets.all(30.0),
prefix: SizedBox(height: 100.0, width: 100.0),
suffix: SizedBox(height: 50.0, width: 50.0),
strutStyle: StrutStyle.disabled,
),
),
),
);
expect(
tester.getTopLeft(find.byType(EditableText)).dx,
100.0 + 30.0,
);
// Since the highest component, the prefix box, is higher than
// the text + paddings, the text's vertical position isn't affected.
expect(tester.getTopLeft(find.byType(EditableText)).dy, 291.5);
expect(
tester.getTopRight(find.byType(EditableText)).dx,
800.0 - 50.0 - 30.0,
);
},
);
testWidgets(
'padding is in between prefix and suffix',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
padding: EdgeInsets.all(20.0),
prefix: SizedBox(height: 100.0, width: 100.0),
suffix: SizedBox(height: 50.0, width: 50.0),
),
),
),
);
expect(
tester.getTopLeft(find.byType(EditableText)).dx,
// Size of prefix + padding.
100.0 + 20.0,
);
expect(tester.getTopLeft(find.byType(EditableText)).dy, 291.5);
expect(
tester.getTopRight(find.byType(EditableText)).dx,
800.0 - 50.0 - 20.0,
);
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
padding: EdgeInsets.all(30.0),
prefix: SizedBox(height: 100.0, width: 100.0),
suffix: SizedBox(height: 50.0, width: 50.0),
),
),
),
);
expect(
tester.getTopLeft(find.byType(EditableText)).dx,
100.0 + 30.0,
);
// Since the highest component, the prefix box, is higher than
// the text + paddings, the text's vertical position isn't affected.
expect(tester.getTopLeft(find.byType(EditableText)).dy, 291.5);
expect(
tester.getTopRight(find.byType(EditableText)).dx,
800.0 - 50.0 - 30.0,
);
},
);
testWidgets(
'clear button shows with right visibility mode',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
placeholder: 'placeholder does not affect clear button',
clearButtonMode: OverlayVisibilityMode.always,
),
),
),
);
expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsOneWidget);
expect(
tester.getTopRight(find.byType(EditableText)).dx,
800.0 - 30.0 /* size of button */ - 7.0 /* padding */,
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
placeholder: 'placeholder does not affect clear button',
clearButtonMode: OverlayVisibilityMode.editing,
),
),
),
);
expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsNothing);
expect(
tester.getTopRight(find.byType(EditableText)).dx,
800.0 - 7.0 /* padding */,
);
await tester.enterText(find.byType(CupertinoTextField), 'text input');
await tester.pump();
expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsOneWidget);
expect(find.text('text input'), findsOneWidget);
expect(
tester.getTopRight(find.byType(EditableText)).dx,
800.0 - 30.0 - 7.0,
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
placeholder: 'placeholder does not affect clear button',
clearButtonMode: OverlayVisibilityMode.notEditing,
),
),
),
);
expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsNothing);
controller.text = '';
await tester.pump();
expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsOneWidget);
},
);
testWidgets(
'clear button removes text',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
placeholder: 'placeholder',
clearButtonMode: OverlayVisibilityMode.editing,
),
),
),
);
controller.text = 'text entry';
await tester.pump();
await tester.tap(find.byIcon(CupertinoIcons.clear_thick_circled));
await tester.pump();
expect(controller.text, '');
expect(find.text('placeholder'), findsOneWidget);
expect(find.text('text entry'), findsNothing);
expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsNothing);
},
);
testWidgets(
'tapping clear button also calls onChanged when text not empty',
(WidgetTester tester) async {
String value = 'text entry';
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
placeholder: 'placeholder',
onChanged: (String newValue) => value = newValue,
clearButtonMode: OverlayVisibilityMode.always,
),
),
),
);
controller.text = value;
await tester.pump();
await tester.tap(find.byIcon(CupertinoIcons.clear_thick_circled));
await tester.pump();
expect(controller.text, isEmpty);
expect(find.text('text entry'), findsNothing);
expect(value, isEmpty);
},
);
testWidgets(
'clear button yields precedence to suffix',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
clearButtonMode: OverlayVisibilityMode.always,
suffix: const Icon(CupertinoIcons.add_circled_solid),
suffixMode: OverlayVisibilityMode.editing,
),
),
),
);
expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsOneWidget);
expect(find.byIcon(CupertinoIcons.add_circled_solid), findsNothing);
expect(
tester.getTopRight(find.byType(EditableText)).dx,
800.0 - 30.0 /* size of button */ - 7.0 /* padding */,
);
controller.text = 'non empty text';
await tester.pump();
expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsNothing);
expect(find.byIcon(CupertinoIcons.add_circled_solid), findsOneWidget);
// Still just takes the space of one widget.
expect(
tester.getTopRight(find.byType(EditableText)).dx,
800.0 - 24.0 /* size of button */ - 7.0 /* padding */,
);
},
);
testWidgets(
'font style controls intrinsic height no-strut',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
strutStyle: StrutStyle.disabled,
),
),
),
);
expect(
tester.getSize(find.byType(CupertinoTextField)).height,
31.0,
);
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
style: TextStyle(
// A larger font.
fontSize: 50.0,
),
strutStyle: StrutStyle.disabled,
),
),
),
);
expect(
tester.getSize(find.byType(CupertinoTextField)).height,
64.0,
);
},
);
testWidgets(
'font style controls intrinsic height',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(),
),
),
);
expect(
tester.getSize(find.byType(CupertinoTextField)).height,
31.0,
);
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
style: TextStyle(
// A larger font.
fontSize: 50.0,
),
),
),
),
);
expect(
tester.getSize(find.byType(CupertinoTextField)).height,
64.0,
);
},
);
testWidgets(
'RTL puts attachments to the right places',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Center(
child: CupertinoTextField(
padding: EdgeInsets.all(20.0),
prefix: Icon(CupertinoIcons.book),
clearButtonMode: OverlayVisibilityMode.always,
),
),
),
),
);
expect(
tester.getTopLeft(find.byIcon(CupertinoIcons.book)).dx,
800.0 - 24.0,
);
expect(
tester.getTopRight(find.byIcon(CupertinoIcons.clear_thick_circled)).dx,
24.0,
);
},
);
testWidgets(
'text fields with no max lines can grow no-strut',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
maxLines: null,
strutStyle: StrutStyle.disabled,
),
),
),
);
expect(
tester.getSize(find.byType(CupertinoTextField)).height,
31.0, // Initially one line high.
);
await tester.enterText(find.byType(CupertinoTextField), '\n');
await tester.pump();
expect(
tester.getSize(find.byType(CupertinoTextField)).height,
48.0, // Initially one line high.
);
},
);
testWidgets(
'text fields with no max lines can grow',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
maxLines: null,
),
),
),
);
expect(
tester.getSize(find.byType(CupertinoTextField)).height,
31.0, // Initially one line high.
);
await tester.enterText(find.byType(CupertinoTextField), '\n');
await tester.pump();
expect(
tester.getSize(find.byType(CupertinoTextField)).height,
48.0, // Initially one line high.
);
},
);
testWidgets('cannot enter new lines onto single line TextField', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), 'abc\ndef');
expect(controller.text, 'abcdef');
});
testWidgets('toolbar colors change with theme brightness, but nothing else', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: "j'aime la poutine",
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Column(
children: <Widget>[
CupertinoTextField(
controller: controller,
),
],
),
),
);
await tester.longPressAt(
tester.getTopRight(find.text("j'aime la poutine")),
);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
Text text = tester.widget<Text>(find.text('Paste'));
expect(text.style!.color!.value, CupertinoColors.black.value);
expect(text.style!.fontSize, 15);
expect(text.style!.letterSpacing, -0.15);
expect(text.style!.fontWeight, FontWeight.w400);
// Change the theme.
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(
brightness: Brightness.dark,
textTheme: CupertinoTextThemeData(
textStyle: TextStyle(fontSize: 100, fontWeight: FontWeight.w800),
),
),
home: Column(
children: <Widget>[
CupertinoTextField(
controller: controller,
),
],
),
),
);
await tester.longPressAt(
tester.getTopRight(find.text("j'aime la poutine")),
);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
text = tester.widget<Text>(find.text('Paste'));
// The toolbar buttons' text are still the same style.
expect(text.style!.color!.value, CupertinoColors.white.value);
expect(text.style!.fontSize, 15);
expect(text.style!.letterSpacing, -0.15);
expect(text.style!.fontWeight, FontWeight.w400);
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
testWidgets('text field toolbar options correctly changes options on Apple Platforms', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Column(
children: <Widget>[
CupertinoTextField(
autofocus: true,
controller: controller,
toolbarOptions: const ToolbarOptions(copy: true),
),
],
),
),
);
// This extra pump is so autofocus can propagate to renderEditable.
await tester.pump();
// Long press to put the cursor after the "w".
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: index),
);
// Double tap on the same location to select the word around the cursor.
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
// Selected text shows 'Copy'.
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 field toolbar options correctly changes options on non-Apple platforms', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Column(
children: <Widget>[
CupertinoTextField(
controller: controller,
toolbarOptions: const ToolbarOptions(copy: true),
),
],
),
),
);
// Long press to select 'Atwater'
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
// Tap elsewhere to hide the context menu so that subsequent taps don't
// collide with it.
await tester.tapAt(textOffsetToPosition(tester, controller.text.length));
await tester.pump();
expect(
controller.selection,
const TextSelection.collapsed(offset: 35, affinity: TextAffinity.upstream),
);
// Double tap on the same location to select the word around the cursor.
await tester.tapAt(textOffsetToPosition(tester, 10));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, 10));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
// Selected text shows 'Copy'.
expect(find.text('Paste'), findsNothing);
expect(find.text('Copy'), findsOneWidget);
expect(find.text('Cut'), findsNothing);
expect(find.text('Select All'), findsNothing);
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets('Read only text field', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'readonly');
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Column(
children: <Widget>[
CupertinoTextField(
controller: controller,
readOnly: true,
),
],
),
),
);
// Read only text field cannot open keyboard.
await tester.showKeyboard(find.byType(CupertinoTextField));
expect(tester.testTextInput.hasAnyClients, false);
await tester.longPressAt(
tester.getTopRight(find.text('readonly')),
);
await tester.pumpAndSettle();
expect(find.text('Paste'), findsNothing);
expect(find.text('Cut'), findsNothing);
expect(find.text('Select All'), findsOneWidget);
await tester.tap(find.text('Select All'));
await tester.pump();
expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsNothing);
expect(find.text('Cut'), findsNothing);
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
testWidgets('copy paste', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Column(
children: <Widget>[
CupertinoTextField(
placeholder: 'field 1',
),
CupertinoTextField(
placeholder: 'field 2',
),
],
),
),
);
await tester.enterText(
find.widgetWithText(CupertinoTextField, 'field 1'),
"j'aime la poutine",
);
await tester.pump();
// Tap an area inside the EditableText but with no text.
await tester.longPressAt(
tester.getTopRight(find.text("j'aime la poutine")),
);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
await tester.tap(find.text('Select All'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
await tester.tap(find.text('Cut'));
await tester.pump();
// Placeholder 1 is back since the text is cut.
expect(find.text('field 1'), findsOneWidget);
expect(find.text('field 2'), findsOneWidget);
await tester.longPress(find.text('field 2'), warnIfMissed: false); // can't actually hit placeholder
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
await tester.tap(find.text('Paste'));
await tester.pump();
expect(find.text('field 1'), findsOneWidget);
expect(find.text("j'aime la poutine"), findsOneWidget);
final Element placeholder2Element = tester.element(find.text('field 2'));
expect(Visibility.of(placeholder2Element), false);
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
testWidgets(
'tap moves cursor to the edge of the word it tapped on',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
await tester.tapAt(textFieldStart + const Offset(50.0, 5.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(
'slow double tap does not trigger double tap',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
// 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 isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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, isTargetPlatformIOS ? 7 : 6);
// Toolbar shows on mobile.
if (isTargetPlatformIOS) {
expectCupertinoToolbarForCollapsedSelection();
} else {
// After a tap, macOS does not show a selection toolbar for a collapsed selection.
expectNoCupertinoToolbar();
}
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets(
'Tapping on a collapsed selection toggles the toolbar',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neigse Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
);
addTearDown(controller.dispose);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
maxLines: 2,
),
),
),
);
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
final Offset begPos = textOffsetToPosition(tester, 0);
final Offset endPos = textOffsetToPosition(tester, 35) + const Offset(200.0, 0.0); // Index of 'Bonaventure|' + Offset(200.0,0), which is at the end of the first line.
final Offset vPos = textOffsetToPosition(tester, 29); // Index of 'Bonav|enture'.
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'.
// This tap just puts the cursor somewhere different than where the double
// tap will occur to test that the double tap moves the existing cursor first.
await tester.tapAt(wPos);
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(vPos);
await tester.pump(const Duration(milliseconds: 500));
// First tap moved the cursor. Here we tap the position where 'v' is located.
// On iOS this will select the closest word edge, in this case the cursor is placed
// at the end of the word 'Bonaventure|'.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 35);
expect(find.byType(CupertinoButton), findsNothing);
await tester.tapAt(vPos);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
// Second tap toggles the toolbar. Here we tap on 'v' again, and select the word edge. Since
// the selection has not changed we toggle the toolbar.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 35);
expectCupertinoToolbarForCollapsedSelection();
// Tap the 'v' position again to hide the toolbar.
await tester.tapAt(vPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 35);
expect(find.byType(CupertinoButton), findsNothing);
// Long press at the end of the first line to move the cursor to the end of the first line
// where the word wrap is. Since there is a word wrap here, and the direction of the text is LTR,
// the TextAffinity will be upstream and against the natural direction. The toolbar is also
// shown after a long press.
await tester.longPressAt(endPos);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 46);
expect(controller.selection.affinity, TextAffinity.upstream);
expectCupertinoToolbarForCollapsedSelection();
// Tap at the same position to toggle the toolbar.
await tester.tapAt(endPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 46);
expect(controller.selection.affinity, TextAffinity.upstream);
expectNoCupertinoToolbar();
// Tap at the beginning of the second line to move the cursor to the front of the first word on the
// second line, where the word wrap is. Since there is a word wrap here, and the direction of the text is LTR,
// the TextAffinity will be downstream and following the natural direction. The toolbar will be hidden after this tap.
await tester.tapAt(begPos + Offset(0.0, lineHeight));
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 46);
expect(controller.selection.affinity, TextAffinity.downstream);
expectNoCupertinoToolbar();
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets(
'Tapping on a non-collapsed selection toggles the toolbar and retains the selection',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final Offset vPos = textOffsetToPosition(tester, 29); // Index of 'Bonav|enture'.
final Offset ePos = textOffsetToPosition(tester, 35) + const Offset(7.0, 0.0); // Index of 'Bonaventure|' + Offset(7.0,0), which taps slightly to the right of the end of the text.
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'.
// This tap just puts the cursor somewhere different than where the double
// tap will occur to test that the double tap moves the existing cursor first.
await tester.tapAt(wPos);
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(vPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(controller.selection.isCollapsed, true);
expect(
controller.selection.baseOffset,
35,
);
await tester.tapAt(vPos);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
// Second tap selects the word around the cursor.
expect(
controller.selection,
const TextSelection(baseOffset: 24, extentOffset: 35),
);
expectCupertinoToolbarForPartialSelection();
// Tap the selected word to hide the toolbar and retain the selection.
await tester.tapAt(vPos);
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 24, extentOffset: 35),
);
expect(find.byType(CupertinoButton), findsNothing);
// Tap the selected word to show the toolbar and retain the selection.
await tester.tapAt(vPos);
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 24, extentOffset: 35),
);
expectCupertinoToolbarForPartialSelection();
// Tap past the selected word to move the cursor and hide the toolbar.
await tester.tapAt(ePos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 35);
expect(find.byType(CupertinoButton), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets(
'double tap selects word for non-Apple platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
// Long press to select 'Atwater'.
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
// Tap elsewhere to hide the context menu so that subsequent taps don't
// collide with it.
await tester.tapAt(textOffsetToPosition(tester, controller.text.length));
await tester.pump();
expect(
controller.selection,
const TextSelection.collapsed(offset: 35, affinity: TextAffinity.upstream),
);
// Double tap in the middle of 'Peel' to select the word.
await tester.tapAt(textOffsetToPosition(tester, 10));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, 10));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
// The toolbar now shows up.
expectCupertinoToolbarForPartialSelection();
// Tap somewhere else to move the cursor.
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
expect(controller.selection, const TextSelection.collapsed(offset: index));
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS}),
);
testWidgets(
'double tap selects word for Apple platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
autofocus: true,
controller: controller,
),
),
),
);
// This extra pump is so autofocus can propagate to renderEditable.
await tester.pump();
// Long press to put the cursor after the "w".
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: index),
);
// Double tap to select the word around the cursor. Move slightly left of
// the previous tap in order to avoid hitting the text selection toolbar
// on Mac.
await tester.tapAt(textOffsetToPosition(tester, index) - const Offset(1.0, 0.0));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expectCupertinoToolbarForPartialSelection();
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'double tap does not select word on read-only obscured field',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
readOnly: true,
obscureText: true,
controller: controller,
),
),
),
);
// Long press to put the cursor after the "w".
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
// Second tap doesn't select anything.
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 35),
);
// Selected text shows nothing.
expect(find.byType(CupertinoButton), findsNothing);
},
);
testWidgets(
'Can double click + drag with a mouse to select word by word',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
final Offset hPos = textOffsetToPosition(tester, testValue.indexOf('h'));
// Tap on text field to gain focus, and set selection to '|e'.
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('e'));
// Here we tap on '|e' again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(ePos);
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 7);
// Drag, right after the double tap, to select word by word.
// Moving to the position of 'h', will extend the selection to 'ghi'.
await gesture.moveTo(hPos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, testValue.indexOf('d'));
expect(controller.selection.extentOffset, testValue.indexOf('i') + 1);
},
);
testWidgets(
'Can double tap + drag to select word by word',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
final Offset hPos = textOffsetToPosition(tester, testValue.indexOf('h'));
// Tap on text field to gain focus, and set selection to '|e'.
final TestGesture gesture = await tester.startGesture(ePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('e'));
// Here we tap on '|e' again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(ePos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 7);
// Drag, right after the double tap, to select word by word.
// Moving to the position of 'h', will extend the selection to 'ghi'.
await gesture.moveTo(hPos);
await tester.pumpAndSettle();
// Toolbar should be hidden during a drag.
expect(find.byType(CupertinoButton), findsNothing);
expect(controller.selection.baseOffset, testValue.indexOf('d'));
expect(controller.selection.extentOffset, testValue.indexOf('i') + 1);
// Toolbar should re-appear after a drag.
await gesture.up();
await tester.pump();
expectCupertinoToolbarForPartialSelection();
// Skip the magnifier hide animation, so it can release resources.
await tester.pump(const Duration(milliseconds: 150));
},
);
testWidgets('Readonly text field does not have tap action', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
maxLength: 10,
readOnly: true,
),
),
),
);
expect(semantics, isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.tap])));
semantics.dispose();
});
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',
);
addTearDown(controller.dispose);
// 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 isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'.
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'.
await tester.tapAt(ePos);
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.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 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),
);
expectCupertinoToolbarForPartialSelection();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets(
'double tap hold selects word',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
await tester.pump(const Duration(milliseconds: 50));
final TestGesture gesture =
await tester.startGesture(textFieldStart + const Offset(150.0, 5.0));
// Hold the press.
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
expectCupertinoToolbarForPartialSelection();
await gesture.up();
await tester.pump();
// Still selected.
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
expectCupertinoToolbarForPartialSelection();
}, variant: TargetPlatformVariant.all());
testWidgets(
'tap after a double tap select is not affected',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
// 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 isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 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. 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, isTargetPlatformIOS ? 7 : 6);
// No toolbar.
expect(find.byType(CupertinoButton), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('double tapping a space selects the previous word on iOS', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: ' blah blah \n blah',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
maxLines: 2,
),
),
),
);
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 the second space selects the previous word.
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, 1);
expect(controller.value.selection.extentOffset, 5);
// 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 first space selects the 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('double tapping a space selects the space on Mac', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: ' blah blah',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, -1);
expect(controller.value.selection.extentOffset, -1);
// Put the cursor at the end of the field.
await tester.tapAt(textOffsetToPosition(tester, 10));
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 10);
expect(controller.value.selection.extentOffset, 10);
// Double tapping the second space selects it.
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textOffsetToPosition(tester, 5));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 5);
expect(controller.value.selection.extentOffset, 6);
// Tap at the end of the text to move the selection to the end. On some
// platforms, the context menu "Cut" button blocks this tap, so move it out
// of the way by an Offset.
await tester.tapAt(textOffsetToPosition(tester, 10) + const Offset(200.0, 0.0));
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 10);
expect(controller.value.selection.extentOffset, 10);
// Double tapping the first 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 }));
testWidgets('double clicking a space selects the space on Mac', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: ' blah blah',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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 tapping 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(kDoubleTapTimeout);
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 5);
expect(controller.value.selection.extentOffset, 6);
// Put the cursor at the end of the field.
await gesture.down(textOffsetToPosition(tester, 10));
await tester.pump();
await gesture.up();
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 10);
expect(controller.value.selection.extentOffset, 10);
// Double tapping the first 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 }));
testWidgets(
'An obscured CupertinoTextField is not selectable when disabled',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
obscureText: true,
enableInteractiveSelection: false,
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
await tester.pump(const Duration(milliseconds: 50));
final TestGesture gesture =
await tester.startGesture(textFieldStart + const Offset(150.0, 5.0));
// Hold the press.
await tester.pump(const Duration(milliseconds: 500));
// Nothing is selected despite the double tap long press gesture.
expect(
controller.selection,
const TextSelection(baseOffset: 35, extentOffset: 35),
);
// The selection menu is not present.
expectNoCupertinoToolbar();
await gesture.up();
await tester.pump();
// Still nothing selected and no selection menu.
expect(
controller.selection,
const TextSelection(baseOffset: 35, extentOffset: 35),
);
expectNoCupertinoToolbar();
},
);
testWidgets(
'A read-only obscured CupertinoTextField is not selectable',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
obscureText: true,
readOnly: true,
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
await tester.pump(const Duration(milliseconds: 50));
final TestGesture gesture =
await tester.startGesture(textFieldStart + const Offset(150.0, 5.0));
// Hold the press.
await tester.pump(const Duration(milliseconds: 500));
// Nothing is selected despite the double tap long press gesture.
expect(
controller.selection,
const TextSelection(baseOffset: 35, extentOffset: 35),
);
// The selection menu is not present.
expectNoCupertinoToolbar();
await gesture.up();
await tester.pump();
// Still nothing selected and no selection menu.
expect(
controller.selection,
const TextSelection.collapsed(offset: 35),
);
expectNoCupertinoToolbar();
},
);
testWidgets(
'An obscured CupertinoTextField is selectable by default',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
obscureText: true,
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
await tester.pump(const Duration(milliseconds: 50));
final TestGesture gesture =
await tester.startGesture(textFieldStart + const Offset(150.0, 5.0));
// Hold the press.
await tester.pumpAndSettle();
// The obscured text is treated as one word, should select all
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 35),
);
// Selected text shows paste toolbar button.
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(1));
await gesture.up();
await tester.pump();
// Still selected.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 35),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(1));
},
);
testWidgets('An obscured TextField has correct default context menu', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
obscureText: true,
),
),
),
);
final Offset textFieldStart = tester.getCenter(find.byType(CupertinoTextField));
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
await tester.pump(const Duration(milliseconds: 50));
await tester.longPressAt(textFieldStart + const Offset(150.0, 5.0));
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);
// Tap to cancel selection.
final Offset textFieldEnd = tester.getTopRight(find.byType(CupertinoTextField));
await tester.tapAt(textFieldEnd + const Offset(-10.0, 5.0));
await tester.pump(const Duration(milliseconds: 50));
// Long tap at the end.
await tester.longPressAt(textFieldEnd + const Offset(-10.0, 5.0));
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(
'long press selects the word at the long press position and shows toolbar on non-Apple platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
await tester.longPressAt(textFieldStart + const Offset(50.0, 5.0));
await tester.pumpAndSettle();
// Select word, 'Atwater, on long press.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.upstream),
);
expectCupertinoToolbarForPartialSelection();
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS}),
);
testWidgets(
'long press moves cursor to the exact long press position and shows toolbar on Apple platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
autofocus: true,
controller: controller,
),
),
),
);
// This extra pump is so autofocus can propagate to renderEditable.
await tester.pump();
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
await tester.longPressAt(textFieldStart + const Offset(50.0, 5.0));
await tester.pumpAndSettle();
// Collapsed cursor for iOS long press.
expect(
controller.selection,
const TextSelection.collapsed(offset: 3, affinity: TextAffinity.upstream),
);
expectCupertinoToolbarForCollapsedSelection();
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'long press tap cannot initiate a double tap',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
autofocus: true,
controller: controller,
),
),
),
);
// This extra pump is so autofocus can propagate to renderEditable.
await tester.pump();
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
await tester.longPressAt(ePos);
await tester.pumpAndSettle(const Duration(milliseconds: 50));
expectCupertinoToolbarForCollapsedSelection();
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 6);
// Tap in a slightly different position to avoid hitting the context menu
// on desktop.
final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
final Offset secondTapPos = isTargetPlatformIOS
? ePos
: ePos + const Offset(-1.0, 0.0);
await tester.tapAt(secondTapPos);
await tester.pump();
// The cursor does not move and the toolbar is toggled.
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 6);
// The toolbar from the long press is now dismissed by the second tap.
expectNoCupertinoToolbar();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets(
'long press drag selects word by word and shows toolbar on lift on non-Apple platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final TestGesture gesture =
await tester.startGesture(textFieldStart + const Offset(50.0, 5.0));
await tester.pump(const Duration(milliseconds: 500));
// Long press on non-Apple platforms selects the word at the long press position.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.upstream),
);
// Toolbar only shows up on long press up.
expectNoCupertinoToolbar();
await gesture.moveBy(const Offset(100, 0));
await tester.pump();
// The selection is extended word by word to the drag position.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 12, affinity: TextAffinity.upstream),
);
expectNoCupertinoToolbar();
await gesture.moveBy(const Offset(200, 0));
await tester.pump();
// The selection is extended word by word to the drag position.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 23, affinity: TextAffinity.upstream),
);
expectNoCupertinoToolbar();
await gesture.up();
await tester.pumpAndSettle();
// The selection isn't affected by the gesture lift.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 23, affinity: TextAffinity.upstream),
);
// The toolbar now shows up.
expectCupertinoToolbarForPartialSelection();
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'long press drag on a focused TextField moves the cursor under the drag and shows toolbar on lift on Apple platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
autofocus: true,
controller: controller,
),
),
),
);
// This extra pump is so autofocus can propagate to renderEditable.
await tester.pump();
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final TestGesture gesture =
await tester.startGesture(textFieldStart + const Offset(50.0, 5.0));
await tester.pump(const Duration(milliseconds: 500));
// Long press on iOS shows collapsed selection cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 3, affinity: TextAffinity.upstream),
);
// Toolbar only shows up on long press up.
expectNoCupertinoToolbar();
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, affinity: TextAffinity.upstream),
);
expectNoCupertinoToolbar();
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, affinity: TextAffinity.upstream),
);
expectNoCupertinoToolbar();
await gesture.up();
await tester.pumpAndSettle();
// The selection isn't affected by the gesture lift.
expect(
controller.selection,
const TextSelection.collapsed(offset: 9, affinity: TextAffinity.upstream),
);
// The toolbar now shows up.
expectCupertinoToolbarForCollapsedSelection();
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets('long press drag can edge scroll on non-Apple platforms', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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, moreOrLessEquals(1094.73, epsilon: 0.25));
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final TestGesture gesture =
await tester.startGesture(textfieldStart);
await tester.pump(const Duration(milliseconds: 500));
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.upstream),
);
expect(find.byType(CupertinoButton), findsNothing);
await gesture.moveBy(const Offset(950, 5));
// To the edge of the screen basically.
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 59),
);
// Keep moving out.
await gesture.moveBy(const Offset(1, 0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 66),
);
await gesture.moveBy(const Offset(1, 0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream),
); // We're at the edge now.
expect(find.byType(CupertinoButton), findsNothing);
await gesture.up();
await tester.pumpAndSettle();
// The selection isn't affected by the gesture lift.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream),
);
// The toolbar now shows up.
expectCupertinoToolbarForFullSelection();
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(785.40, 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(-310.30, epsilon: 1));
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('long press drag can edge scroll on Apple platforms', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
autofocus: true,
controller: controller,
),
),
),
);
// This extra pump is so autofocus can propagate to renderEditable.
await tester.pump();
final RenderEditable renderEditable = tester.renderObject<RenderEditable>(
find.byElementPredicate((Element element) => element.renderObject is RenderEditable).last,
);
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, moreOrLessEquals(1094.73, epsilon: 0.25));
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
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: 18, 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: 54, affinity: TextAffinity.upstream),
);
// Keep moving out.
await gesture.moveBy(const Offset(1, 0));
await tester.pump();
expect(
controller.selection,
const TextSelection.collapsed(offset: 61, affinity: TextAffinity.upstream),
);
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.
expectCupertinoToolbarForCollapsedSelection();
lastCharEndpoint = renderEditable.getEndpointsForSelection(
const TextSelection.collapsed(offset: 66), // Last character's position.
);
expect(lastCharEndpoint.length, 1);
// The last character is now on screen.
expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(784.73, epsilon: 0.25));
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(-310.20, epsilon: 0.25));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets(
'long tap after a double tap select is not affected',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
// 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 isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 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.
expectCupertinoToolbarForCollapsedSelection();
}, 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',
);
addTearDown(controller.dispose);
// 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 isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
autofocus: true,
controller: controller,
),
),
),
);
// This extra pump is so autofocus can propagate to renderEditable.
await tester.pump();
// Use a position higher than wPos to avoid tapping the context menu on
// desktop.
final Offset pPos = textOffsetToPosition(tester, 9) + const Offset(0.0, -20.0); // Index of 'P|eel'
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'
await tester.longPressAt(wPos);
await tester.pumpAndSettle(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 3);
expectCupertinoToolbarForCollapsedSelection();
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(find.byType(CupertinoButton), findsNothing);
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 9);
await tester.tapAt(pPos);
await tester.pumpAndSettle();
// Double tap selection.
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
expectCupertinoToolbarForPartialSelection();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets(
'double tap chains work',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
await tester.tapAt(textFieldStart + const Offset(50.0, 5.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, 5.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expectCupertinoToolbarForPartialSelection();
// Double tap selecting the same word somewhere else is fine.
await tester.tapAt(textFieldStart + const Offset(100.0, 5.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap hides the toolbar, and retains the selection.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expect(find.byType(CupertinoButton), findsNothing);
// Second tap shows the toolbar, and retains the selection.
await tester.tapAt(textFieldStart + const Offset(100.0, 5.0));
// Wait for the consecutive tap timer to timeout so the next
// tap is not detected as a triple tap.
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expectCupertinoToolbarForPartialSelection();
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor and hides the toolbar.
expect(
controller.selection,
const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
);
expect(find.byType(CupertinoButton), findsNothing);
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
expectCupertinoToolbarForPartialSelection();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
group('Triple tap/click', () {
const String testValueA = 'Now is the time for\n' // 20
'all good people\n' // 20 + 16 => 36
'to come to the aid\n' // 36 + 19 => 55
'of their country.'; // 55 + 17 => 72
const String testValueB = 'Today is the time for\n' // 22
'all good people\n' // 22 + 16 => 38
'to come to the aid\n' // 38 + 19 => 57
'of their country.'; // 57 + 17 => 74
testWidgets(
'Can triple tap to select a paragraph on mobile platforms when tapping at a word edge',
(WidgetTester tester) async {
// TODO(Renzo-Olivares): Enable, currently broken because selection overlay blocks the TextSelectionGestureDetector.
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueA);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueA);
final Offset firstLinePos = tester.getTopLeft(find.byType(CupertinoTextField)) + const Offset(110.0, 9.0);
// Tap on text field to gain focus, and set selection to 'is|' on the first line.
final TestGesture gesture = await tester.startGesture(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 6);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position. On iOS, tapping a whitespace selects the previous word.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 6);
expect(controller.selection.extentOffset, isTargetPlatformApple ? 6 : 7);
// Here we tap on same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 20);
},
variant: TargetPlatformVariant.mobile(),
skip: true, // https://github.com/flutter/flutter/issues/123415
);
testWidgets(
'Can triple tap to select a paragraph on mobile platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueB);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueB);
final Offset firstLinePos = tester.getTopLeft(find.byType(CupertinoTextField)) + const Offset(50.0, 9.0);
// Tap on text field to gain focus, and move the selection.
final TestGesture gesture = await tester.startGesture(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 5);
// Here we tap on same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 22);
},
variant: TargetPlatformVariant.mobile(),
);
testWidgets(
'Triple click at the beginning of a line should not select the previous paragraph',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/132126
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueB);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueB);
final Offset thirdLinePos = textOffsetToPosition(tester, 38);
// Click on text field to gain focus, and move the selection.
final TestGesture gesture = await tester.startGesture(thirdLinePos, kind: PointerDeviceKind.mouse);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 38);
// Here we click on same position again, to register a double click. This will select
// the word at the clicked position.
await gesture.down(thirdLinePos);
await gesture.up();
expect(controller.selection.baseOffset, 38);
expect(controller.selection.extentOffset, 40);
// Here we click on same position again, to register a triple click. This will select
// the paragraph at the clicked position.
await gesture.down(thirdLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 38);
expect(controller.selection.extentOffset, 57);
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }),
);
testWidgets(
'Triple click at the end of text should select the previous paragraph',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/132126.
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueB);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueB);
final Offset endOfTextPos = textOffsetToPosition(tester, 74);
// Click on text field to gain focus, and move the selection.
final TestGesture gesture = await tester.startGesture(endOfTextPos, kind: PointerDeviceKind.mouse);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 74);
// Here we click on same position again, to register a double click.
await gesture.down(endOfTextPos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 74);
expect(controller.selection.extentOffset, 74);
// Here we click on same position again, to register a triple click. This will select
// the paragraph at the clicked position.
await gesture.down(endOfTextPos);
await tester.pump();
await gesture.up();
await tester.pump();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 57);
expect(controller.selection.extentOffset, 74);
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }),
);
testWidgets(
'triple tap chains work on Non-Apple mobile platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 3);
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expectCupertinoToolbarForPartialSelection();
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 35),
);
// Triple tap selecting the same paragraph somewhere else is fine.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap hides the toolbar and moves the selection.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 6);
expectNoCupertinoToolbar();
// Second tap shows the toolbar and selects the word.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expectCupertinoToolbarForPartialSelection();
// Third tap shows the toolbar and selects the paragraph.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 35),
);
expectCupertinoToolbarForFullSelection();
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor and hid the toolbar.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 9);
expect(find.byType(CupertinoButton), findsNothing);
// Second tap selects the word.
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
expectCupertinoToolbarForPartialSelection();
// Third tap selects the paragraph and shows the toolbar.
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 35),
);
expectCupertinoToolbarForFullSelection();
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }),
);
testWidgets(
'triple tap chains work on Apple platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure\nThe fox jumped over the fence.',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 7);
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expectCupertinoToolbarForPartialSelection();
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 36),
);
// Triple tap selecting the same paragraph somewhere else is fine.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap hides the toolbar and retains the selection.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 36),
);
expect(find.byType(CupertinoButton), findsNothing);
// Second tap shows the toolbar and selects the word.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expectCupertinoToolbarForPartialSelection();
// Third tap shows the toolbar and selects the paragraph.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 36),
);
expectCupertinoToolbarForPartialSelection();
await tester.tapAt(textfieldStart + const Offset(150.0, 25.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor and hid the toolbar.
expect(
controller.selection,
const TextSelection.collapsed(offset: 50, affinity: TextAffinity.upstream),
);
expect(find.byType(CupertinoButton), findsNothing);
// Second tap selects the word.
await tester.tapAt(textfieldStart + const Offset(150.0, 25.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 44, extentOffset: 50),
);
expectCupertinoToolbarForPartialSelection();
// Third tap selects the paragraph and shows the toolbar.
await tester.tapAt(textfieldStart + const Offset(150.0, 25.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 36, extentOffset: 66),
);
expectCupertinoToolbarForPartialSelection();
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets(
'triple click chains work',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;
// First click moves the cursor to the point of the click, not the edge of
// the clicked word.
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(200.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 12);
// Second click selects the word.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
// Triple click selects the paragraph.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
// Wait for the consecutive tap timer to timeout so the next
// tap is not detected as a triple tap.
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
// Triple click selecting the same paragraph somewhere else is fine.
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// First click moved the cursor.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 6);
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Second click selected the word.
expect(
controller.selection,
const TextSelection(baseOffset: 4, extentOffset: 6),
);
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
await tester.pump();
await gesture.up();
// Wait for the consecutive tap timer to timeout so the tap count
// is reset.
await tester.pumpAndSettle(kDoubleTapTimeout);
// Third click selected the paragraph.
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// First click moved the cursor.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 9);
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Second click selected the word.
expect(
controller.selection,
const TextSelection(baseOffset: 7, extentOffset: 10),
);
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Third click selects the paragraph.
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
},
variant: TargetPlatformVariant.desktop(),
);
testWidgets(
'triple click after a click on desktop platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(50.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 3);
// First click moves the selection.
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 9);
// Double click selection to select a word.
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 7, extentOffset: 10),
);
// Triple click selection to select a paragraph.
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
},
variant: TargetPlatformVariant.desktop(),
);
testWidgets(
'Can triple tap to select all on a single-line textfield on mobile platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueB,
);
addTearDown(controller.dispose);
final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final Offset firstLinePos = tester.getTopLeft(find.byType(CupertinoTextField)) + const Offset(50.0, 9.0);
// Tap on text field to gain focus, and set selection somewhere on the first word.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 5);
// Here we tap on same position again, to register a triple tap. This will select
// the entire text field if it is a single-line field.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 74);
},
variant: TargetPlatformVariant.mobile(),
);
testWidgets(
'Can triple click to select all on a single-line textfield on desktop platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on same position again, to register a triple tap. This will select
// the entire text field if it is a single-line field.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 72);
},
variant: TargetPlatformVariant.desktop(),
);
testWidgets(
'Can triple click to select a line on Linux',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueA);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueA);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 19);
},
variant: TargetPlatformVariant.only(TargetPlatform.linux),
);
testWidgets(
'Can triple click to select a paragraph',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueA);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueA);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 20);
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }),
);
testWidgets(
'Can triple click + drag to select line by line on Linux',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueA);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueA);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on the same position again, to register a triple tap. This will select
// the line at the tapped position.
await gesture.down(firstLinePos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 19);
// Drag, down after the triple tap, to select line by line.
// Moving down will extend the selection to the second line.
await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 35);
// Moving down will extend the selection to the third line.
await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 54);
// Moving down will extend the selection to the last line.
await gesture.moveTo(firstLinePos + const Offset(0, 40.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 72);
// Moving up will extend the selection to the third line.
await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 54);
// Moving up will extend the selection to the second line.
await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 35);
// Moving up will extend the selection to the first line.
await gesture.moveTo(firstLinePos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 19);
},
variant: TargetPlatformVariant.only(TargetPlatform.linux),
);
testWidgets(
'Can triple click + drag to select paragraph by paragraph',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueA);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueA);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on the same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 20);
// Drag, down after the triple tap, to select paragraph by paragraph.
// Moving down will extend the selection to the second line.
await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 36);
// Moving down will extend the selection to the third line.
await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 55);
// Moving down will extend the selection to the last line.
await gesture.moveTo(firstLinePos + const Offset(0, 40.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 72);
// Moving up will extend the selection to the third line.
await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 55);
// Moving up will extend the selection to the second line.
await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 36);
// Moving up will extend the selection to the first line.
await gesture.moveTo(firstLinePos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 20);
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }),
);
testWidgets(
'Going past triple click retains the selection on Apple platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
// 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(200.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 12);
// Second click selects the word.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
// Triple click selects the paragraph.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
// Clicking again retains the selection.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again retains the selection.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again retains the selection.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'Tap count resets when going past a triple tap on Android, Fuchsia, and Linux',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;
// First click moves the cursor to the point of the click, not the edge of
// the clicked word.
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(200.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 12);
// Second click selects the word.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
// Triple click selects the paragraph.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
// Clicking again moves the caret to the tapped position.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 12);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again selects the word.
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again selects the paragraph.
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// Clicking again moves the caret to the tapped position.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 12);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again selects the word.
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again selects the paragraph.
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux }),
);
testWidgets(
'Double click and triple click alternate on Windows',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
// 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(200.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 12);
// Second click selects the word.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
// Triple click selects the paragraph.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
// Clicking again selects the word.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again selects the paragraph.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again selects the word.
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// Clicking again selects the paragraph.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again selects the word.
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again selects the paragraph.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
},
variant: TargetPlatformVariant.only(TargetPlatform.windows),
);
});
testWidgets('force press selects word', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final int pointerValue = tester.nextPointer;
final TestGesture gesture = await tester.createGesture();
await gesture.downWithCustomEvent(
textFieldStart + const Offset(150.0, 5.0),
PointerDownEvent(
pointer: pointerValue,
position: textFieldStart + const Offset(150.0, 5.0),
pressure: 3.0,
pressureMax: 6.0,
pressureMin: 0.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();
// Shows toolbar.
expectCupertinoToolbarForPartialSelection();
});
testWidgets('force press on unsupported devices falls back to tap', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
// 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 isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
final int pointerValue = tester.nextPointer;
final TestGesture gesture = await tester.createGesture();
await gesture.downWithCustomEvent(
pPos,
PointerDownEvent(
pointer: pointerValue,
position: pPos,
// iPhone 6 and below report 0 across the board.
pressure: 0,
pressureMax: 0,
pressureMin: 0,
),
);
await gesture.up();
// Fall back to a single tap which selects the edge of the word on iOS, and
// a precise position on macOS.
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 9);
await tester.pump();
// Falling back to a single tap doesn't trigger a toolbar.
expect(find.byType(CupertinoButton), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
addTearDown(controller.dispose);
// 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 isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
style: const TextStyle(fontSize: 10.0),
),
),
),
);
// Double tap on 'e' to select 'def'.
final Offset ePos = textOffsetToPosition(tester, 5);
await tester.tapAt(ePos, pointer: 7);
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformIOS ? 7 : 5);
await tester.tapAt(ePos, pointer: 7);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 7);
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
expect(endpoints.length, 2);
// On Mac, the toolbar blocks the drag on the right handle, so hide it.
final EditableTextState editableTextState = tester.state(find.byType(EditableText));
editableTextState.hideToolbar(false);
await tester.pumpAndSettle();
// 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;
Offset newHandlePos = textOffsetToPosition(tester, 5); // Position of 'e'.
final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 5);
newHandlePos = textOffsetToPosition(tester, 2); // Position of 'c'.
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
// The selection doesn't move beyond the left handle. There's always at
// least 1 char selected.
expect(controller.selection.extentOffset, 5);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('Dragging between multiple lines keeps the contact point at the same place on the handle on Android', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
// 11 first line, 19 second line, 17 third line = length 49
text: 'a big house\njumped over a mouse\nOne more line yay',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: 3,
minLines: 3,
),
),
),
);
// Double tap to select 'over'.
final Offset pos = textOffsetToPosition(tester, controller.text.indexOf('v'));
// The first tap.
TestGesture gesture = await tester.startGesture(pos, pointer: 7);
await tester.pump();
await gesture.up();
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
// The second tap.
await gesture.down(pos);
await tester.pump();
await gesture.up();
await tester.pump();
final TextSelection selection = controller.selection;
expect(
controller.selection,
const TextSelection(
baseOffset: 19,
extentOffset: 23,
),
);
final RenderEditable renderEditable = findRenderEditable(tester);
List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 2);
// Drag the right handle 4 letters to the right.
// The adjustment moves the tap from the text position to the handle.
const Offset endHandleAdjustment = Offset(1.0, 6.0);
Offset handlePos = endpoints[1].point + endHandleAdjustment;
Offset newHandlePos = textOffsetToPosition(tester, 27) + endHandleAdjustment;
await tester.pump();
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(
baseOffset: 19,
extentOffset: 27,
),
);
// Drag the right handle 1 line down.
endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
handlePos = endpoints[1].point + endHandleAdjustment;
final Offset toNextLine = Offset(
0.0,
findRenderEditable(tester).preferredLineHeight + 3.0,
);
newHandlePos = handlePos + toNextLine;
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(
baseOffset: 19,
extentOffset: 47,
),
);
// Drag the right handle back up 1 line.
endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
handlePos = endpoints[1].point + endHandleAdjustment;
newHandlePos = handlePos - toNextLine;
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(
baseOffset: 19,
extentOffset: 27,
),
);
// Drag the left handle 4 letters to the left.
// The adjustment moves the tap from the text position to the handle.
const Offset startHandleAdjustment = Offset(-1.0, 6.0);
endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
handlePos = endpoints[0].point + startHandleAdjustment;
newHandlePos = textOffsetToPosition(tester, 15) + startHandleAdjustment;
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(
baseOffset: 15,
extentOffset: 27,
),
);
// Drag the left handle 1 line up.
endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
handlePos = endpoints[0].point + startHandleAdjustment;
// Move handle a sufficient global distance so it can be considered a drag
// by the selection handle's [PanGestureRecognizer].
newHandlePos = handlePos - (toNextLine * 2);
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(
baseOffset: 3,
extentOffset: 27,
),
);
// Drag the left handle 1 line back down.
endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
handlePos = endpoints[0].point + startHandleAdjustment;
newHandlePos = handlePos + toNextLine;
gesture = await tester.startGesture(handlePos, pointer: 7);
// Move handle up a small amount before dragging it down so the total global
// distance travelled can be accepted by the selection handle's [PanGestureRecognizer] as a drag.
// This way it can declare itself the winner before the [TapAndDragGestureRecognizer] that
// is on the selection overlay.
await tester.pump();
await gesture.moveTo(handlePos - toNextLine);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(
baseOffset: 15,
extentOffset: 27,
),
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
);
testWidgets('Dragging between multiple lines keeps the contact point at the same place on the handle on iOS', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
// 11 first line, 19 second line, 17 third line = length 49
text: 'a big house\njumped over a mouse\nOne more line yay',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: 3,
minLines: 3,
),
),
),
);
// Double tap to select 'over'.
final Offset pos = textOffsetToPosition(tester, controller.text.indexOf('v'));
// The first tap.
TestGesture gesture = await tester.startGesture(pos, pointer: 7);
await tester.pump();
await gesture.up();
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
// The second tap.
await gesture.down(pos);
await tester.pump();
await gesture.up();
await tester.pump();
final TextSelection selection = controller.selection;
expect(
controller.selection,
const TextSelection(
baseOffset: 19,
extentOffset: 23,
),
);
final RenderEditable renderEditable = findRenderEditable(tester);
List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 2);
// Drag the right handle 4 letters to the right.
// The adjustment moves the tap from the text position to the handle.
const Offset endHandleAdjustment = Offset(1.0, 6.0);
Offset handlePos = endpoints[1].point + endHandleAdjustment;
Offset newHandlePos = textOffsetToPosition(tester, 27) + endHandleAdjustment;
await tester.pump();
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(
baseOffset: 19,
extentOffset: 27,
),
);
// Drag the right handle 1 line down.
endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
handlePos = endpoints[1].point + endHandleAdjustment;
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
final Offset toNextLine = Offset(0.0, lineHeight + 3.0);
newHandlePos = handlePos + toNextLine;
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(
baseOffset: 19,
extentOffset: 47,
),
);
// Drag the right handle back up 1 line.
endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
handlePos = endpoints[1].point + endHandleAdjustment;
newHandlePos = handlePos - toNextLine;
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(
baseOffset: 19,
extentOffset: 27,
),
);
// Drag the left handle 4 letters to the left.
// The adjustment moves the tap from the text position to the handle.
final Offset startHandleAdjustment = Offset(-1.0, -lineHeight + 6.0);
endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
handlePos = endpoints[0].point + startHandleAdjustment;
newHandlePos = textOffsetToPosition(tester, 15) + startHandleAdjustment;
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
// On Apple platforms, dragging the base handle makes it the extent.
expect(
controller.selection,
const TextSelection(
baseOffset: 27,
extentOffset: 15,
),
);
// Drag the left handle 1 line up.
endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
handlePos = endpoints[0].point + startHandleAdjustment;
newHandlePos = handlePos - toNextLine;
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(
baseOffset: 27,
extentOffset: 3,
),
);
// Drag the left handle 1 line back down.
endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
handlePos = endpoints[0].point + startHandleAdjustment;
newHandlePos = handlePos + toNextLine;
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(
baseOffset: 27,
extentOffset: 15,
),
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Selection updates on tap down (Desktop platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(controller: controller),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
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();
addTearDown(controller.dispose);
final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(controller: controller),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
final Offset ePos = textOffsetToPosition(tester, 5);
final Offset gPos = textOffsetToPosition(tester, 8);
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
await gesture.down(gPos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
final TestGesture touchGesture = await tester.startGesture(ePos);
await touchGesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
// On iOS, a tap to select, selects the word edge instead of the exact tap position.
expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5);
expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5);
// Selection should stay the same since it is set on tap up for mobile platforms.
await touchGesture.down(gPos);
await tester.pump();
expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5);
expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5);
await touchGesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
},
variant: TargetPlatformVariant.mobile(),
);
testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
style: const TextStyle(fontSize: 10.0),
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
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('Cursor should not move on a quick touch drag when touch does not begin on previous selection (iOS)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
// Tap on text field to gain focus, and set selection to '|a'. On iOS
// the selection is set to the word edge closest to the tap position.
// We await for [kDoubleTapTimeout] after the up event, so our next down
// event does not register as a double tap.
final TestGesture gesture = await tester.startGesture(aPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
// The position we tap during a drag start is not on the collapsed selection,
// so the cursor should not move.
await gesture.down(textOffsetToPosition(tester, 7));
await gesture.moveTo(iPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
// Tap on text field to gain focus, and set selection to '|g'. On iOS
// the selection is set to the word edge closest to the tap position.
// We await for [kDoubleTapTimeout] after the up event, so our next down
// event does not register as a double tap.
final TestGesture gesture = await tester.startGesture(ePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 7);
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|g', where our selection was previously, and move to '|i'.
await gesture.down(textOffsetToPosition(tester, 7));
await tester.pump();
await gesture.moveTo(iPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('i'));
// End gesture and skip the magnifier hide animation, so it can release
// resources.
await gesture.up();
await tester.pump();
await tester.pump(const Duration(milliseconds: 150));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - multiline', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
const String testValue = 'abc\ndef\nghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
// Tap on text field to gain focus, and set selection to '|a'. On iOS
// the selection is set to the word edge closest to the tap position.
// We await for kDoubleTapTimeout after the up event, so our next down event
// does not register as a double tap.
final TestGesture gesture = await tester.startGesture(aPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and move to '|i'.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(iPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('i'));
// End gesture and skip the magnifier hide animation, so it can release
// resources.
await gesture.up();
await tester.pump();
await tester.pump(const Duration(milliseconds: 150));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - ListView', (WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/122519
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
const String testValue = 'abc\ndef\nghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
// Tap on text field to gain focus, and set selection to '|a'. On iOS
// the selection is set to the word edge closest to the tap position.
// We await for kDoubleTapTimeout after the up event, so our next down event
// does not register as a double tap.
final TestGesture gesture = await tester.startGesture(aPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and attempt move
// to '|g'. The cursor will not move because the `VerticalDragGestureRecognizer`
// in the scrollable will beat the `TapAndHorizontalDragGestureRecognizer`
// in the TextField. This is because moving from `|a` to `|g` is a completely
// vertical movement.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(gPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
// Release the pointer.
await gesture.up();
await tester.pumpAndSettle();
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and move to '|i'.
// Unlike our previous attempt to drag to `|g`, this works because moving
// to `|i` includes a horizontal movement so the `TapAndHorizontalDragGestureRecognizer`
// in TextField can beat the `VerticalDragGestureRecognizer` in the scrollable.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(iPos);
await tester.pumpAndSettle();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('i'));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Can move cursor when dragging (Android)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
// Tap on text field to gain focus, and set selection to '|e'.
// We await for [kDoubleTapTimeout] after the up event, so our
// next down event does not register as a double tap.
final TestGesture gesture = await tester.startGesture(ePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('e'));
// Here we tap on '|d', and move to '|g'.
await gesture.down(textOffsetToPosition(tester, testValue.indexOf('d')));
await tester.pump();
await gesture.moveTo(gPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('g'));
// End gesture and skip the magnifier hide animation, so it can release
// resources.
await gesture.up();
await tester.pump();
await tester.pump(const Duration(milliseconds: 150));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }),
);
testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async {
int selectionChangedCount = 0;
const String testValue = 'abc def ghi';
final TextEditingController controller = TextEditingController(text: testValue);
addTearDown(controller.dispose);
controller.addListener(() {
selectionChangedCount++;
});
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
style: const TextStyle(fontSize: 10.0),
),
),
),
);
final Offset cPos = textOffsetToPosition(tester, 2); // Index of 'c'.
final Offset gPos = textOffsetToPosition(tester, 8); // Index of 'g'.
final Offset hPos = textOffsetToPosition(tester, 9); // Index of 'h'.
// Drag from 'c' to 'g'.
final TestGesture gesture = await tester.startGesture(cPos, kind: PointerDeviceKind.mouse);
await tester.pump();
await gesture.moveTo(gPos);
await tester.pumpAndSettle();
expect(selectionChangedCount, isNonZero);
selectionChangedCount = 0;
expect(controller.selection.baseOffset, 2);
expect(controller.selection.extentOffset, 8);
// Tiny movement shouldn't cause text selection to change.
await gesture.moveTo(gPos + const Offset(2.0, 0.0));
await tester.pumpAndSettle();
expect(selectionChangedCount, 0);
// Now a text selection change will occur after a significant movement.
await gesture.moveTo(hPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(selectionChangedCount, 1);
expect(controller.selection.baseOffset, 2);
expect(controller.selection.extentOffset, 9);
});
testWidgets('Tap does not show handles nor toolbar', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(controller: controller),
),
),
);
// Tap to trigger the text field.
await tester.tap(find.byType(CupertinoTextField));
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 toolbar but not handles', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(controller: controller),
),
),
);
// Long press to trigger the text field.
await tester.longPress(find.byType(CupertinoTextField));
await tester.pump();
// A long press in Cupertino should position the cursor without any selection.
expect(controller.selection.isCollapsed, isTrue);
final EditableTextState editableText = tester.state(find.byType(EditableText));
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue);
});
testWidgets(
'Double tap shows handles and toolbar if selection is not collapsed',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(controller: controller),
),
),
);
final Offset hPos = textOffsetToPosition(tester, 9); // Position of 'h'.
// Double tap on 'h' to select 'ghi'.
await tester.tapAt(hPos);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(hPos);
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 toolbar but not handles if selection is collapsed',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(controller: controller),
),
),
);
final Offset textEndPos = textOffsetToPosition(tester, 11); // Position at the end of text.
// Double tap to place the cursor at the end.
await tester.tapAt(textEndPos);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textEndPos);
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 long press does not show handles nor toolbar',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(controller: controller),
),
),
);
// Long press to trigger the text field.
final Offset textFieldPos = tester.getCenter(find.byType(CupertinoTextField));
final TestGesture gesture = await tester.startGesture(
textFieldPos,
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',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(controller: controller),
),
),
);
final EditableTextState editableText = tester.state(find.byType(EditableText));
// Double tap at the end of text.
final Offset textEndPos = textOffsetToPosition(tester, 11); // Position at the end of text.
final TestGesture gesture = await tester.startGesture(
textEndPos,
kind: PointerDeviceKind.mouse,
);
await tester.pump(const Duration(milliseconds: 50));
await gesture.up();
await tester.pump();
await gesture.down(textEndPos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
final Offset hPos = textOffsetToPosition(tester, 9); // Position of 'h'.
// Double tap on 'h' to select 'ghi'.
await gesture.down(hPos);
await tester.pump(const Duration(milliseconds: 50));
await gesture.up();
await tester.pump();
await gesture.down(hPos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
},
);
testWidgets('onTap is called upon tap', (WidgetTester tester) async {
int tapCount = 0;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
onTap: () => tapCount++,
),
),
),
);
expect(tapCount, 0);
await tester.tap(find.byType(CupertinoTextField));
await tester.pump();
expect(tapCount, 1);
// Wait out the double tap interval so the next tap doesn't end up being
// recognized as a double tap.
await tester.pump(const Duration(seconds: 1));
// Double tap count as one single tap.
await tester.tap(find.byType(CupertinoTextField));
await tester.pump(const Duration(milliseconds: 100));
await tester.tap(find.byType(CupertinoTextField));
await tester.pump();
expect(tapCount, 2);
});
testWidgets(
'onTap does not work when the text field is disabled',
(WidgetTester tester) async {
int tapCount = 0;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
enabled: false,
onTap: () => tapCount++,
),
),
),
);
expect(tapCount, 0);
await tester.tap(find.byType(CupertinoTextField), warnIfMissed: false); // disabled
await tester.pump();
expect(tapCount, 0);
// Wait out the double tap interval so the next tap doesn't end up being
// recognized as a double tap.
await tester.pump(const Duration(seconds: 1));
// Enabling the text field, now it should accept taps.
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
onTap: () => tapCount++,
),
),
),
);
await tester.tap(find.byType(CupertinoTextField));
expect(tapCount, 1);
await tester.pump(const Duration(seconds: 1));
// Disable it again.
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
enabled: false,
onTap: () => tapCount++,
),
),
),
);
await tester.tap(find.byType(CupertinoTextField), warnIfMissed: false); // disabled
await tester.pump();
expect(tapCount, 1);
},
);
testWidgets('Focus test when the text field is disabled', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
focusNode: focusNode,
),
),
),
);
expect(focusNode.hasFocus, false); // initial status
// Should accept requestFocus.
focusNode.requestFocus();
await tester.pump();
expect(focusNode.hasFocus, true);
// Disable the text field, now it should not accept requestFocus.
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
enabled: false,
focusNode: focusNode,
),
),
),
);
// Should not accept requestFocus.
focusNode.requestFocus();
await tester.pump();
expect(focusNode.hasFocus, false);
});
testWidgets(
'text field respects theme',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
theme: CupertinoThemeData(
brightness: Brightness.dark,
),
home: Center(
child: CupertinoTextField(),
),
),
);
final BoxDecoration decoration = tester.widget<DecoratedBox>(
find.descendant(
of: find.byType(CupertinoTextField),
matching: find.byType(DecoratedBox),
),
).decoration as BoxDecoration;
expect(
decoration.border!.bottom.color.value,
0x33FFFFFF,
);
await tester.enterText(find.byType(CupertinoTextField), 'smoked meat');
await tester.pump();
expect(
tester.renderObject<RenderEditable>(
find.byElementPredicate((Element element) => element.renderObject is RenderEditable).last,
).text!.style!.color,
isSameColorAs(CupertinoColors.white),
);
},
);
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
const String testValue = 'abc def ghi';
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Container(
padding: const EdgeInsets.all(30),
child: CupertinoTextField(
controller: controller,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValue);
// Tap the selection handle to bring up the "paste / select all" menu.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
RenderEditable renderEditable = findRenderEditable(tester);
List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300)); // skip past the frame where the opacity is zero
// Verify the selection toolbar position
Offset toolbarTopLeft = tester.getTopLeft(find.text('Paste'));
Offset textFieldTopLeft = tester.getTopLeft(find.byType(CupertinoTextField));
expect(textFieldTopLeft.dy, lessThan(toolbarTopLeft.dy));
await tester.pumpWidget(
CupertinoApp(
home: Container(
padding: const EdgeInsets.all(150),
child: CupertinoTextField(
controller: controller,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValue);
// 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 = 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
// Verify the selection toolbar position
toolbarTopLeft = tester.getTopLeft(find.text('Paste'));
textFieldTopLeft = tester.getTopLeft(find.byType(CupertinoTextField));
expect(toolbarTopLeft.dy, lessThan(textFieldTopLeft.dy));
},
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
);
testWidgets('text field respects keyboardAppearance from theme', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async {
log.add(methodCall);
return null;
});
await tester.pumpWidget(
const CupertinoApp(
theme: CupertinoThemeData(
brightness: Brightness.dark,
),
home: Center(
child: CupertinoTextField(),
),
),
);
await tester.showKeyboard(find.byType(EditableText));
final MethodCall setClient = log.first;
expect(setClient.method, 'TextInput.setClient');
expect(((setClient.arguments as List<dynamic>).last as Map<String, dynamic>)['keyboardAppearance'], 'Brightness.dark');
});
testWidgets('text field can override keyboardAppearance from theme', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async {
log.add(methodCall);
return null;
});
await tester.pumpWidget(
const CupertinoApp(
theme: CupertinoThemeData(
brightness: Brightness.dark,
),
home: Center(
child: CupertinoTextField(
keyboardAppearance: Brightness.light,
),
),
),
);
await tester.showKeyboard(find.byType(EditableText));
final MethodCall setClient = log.first;
expect(setClient.method, 'TextInput.setClient');
expect(((setClient.arguments as List<dynamic>).last as Map<String, dynamic>)['keyboardAppearance'], 'Brightness.light');
});
testWidgets('cursorColor respects theme', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoTextField(),
),
);
final Finder textFinder = find.byType(CupertinoTextField);
await tester.tap(textFinder);
await tester.pump();
final EditableTextState editableTextState =
tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorColor, CupertinoColors.activeBlue.color);
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoTextField(),
theme: CupertinoThemeData(
brightness: Brightness.dark,
),
),
);
await tester.pump();
expect(renderEditable.cursorColor, CupertinoColors.activeBlue.darkColor);
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoTextField(),
theme: CupertinoThemeData(
primaryColor: Color(0xFFF44336),
),
),
);
await tester.pump();
expect(renderEditable.cursorColor, const Color(0xFFF44336));
});
testWidgets('cursor can override color from theme', (WidgetTester tester) async {
const CupertinoDynamicColor cursorColor = CupertinoDynamicColor.withBrightness(
color: Color(0x12345678),
darkColor: Color(0x87654321),
);
await tester.pumpWidget(
const CupertinoApp(
theme: CupertinoThemeData(),
home: Center(
child: CupertinoTextField(
cursorColor: cursorColor,
),
),
),
);
EditableText editableText = tester.firstWidget(find.byType(EditableText));
expect(editableText.cursorColor.value, 0x12345678);
await tester.pumpWidget(
const CupertinoApp(
theme: CupertinoThemeData(brightness: Brightness.dark),
home: Center(
child: CupertinoTextField(
cursorColor: cursorColor,
),
),
),
);
editableText = tester.firstWidget(find.byType(EditableText));
expect(editableText.cursorColor.value, 0x87654321);
});
testWidgets('shows selection handles', (WidgetTester tester) async {
const String testText = 'lorem ipsum';
final TextEditingController controller = TextEditingController(text: testText);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(),
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final RenderEditable renderEditable =
tester.state<EditableTextState>(find.byType(EditableText)).renderEditable;
await tester.tapAt(textOffsetToPosition(tester, 5));
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
await tester.pumpAndSettle();
final List<Widget> transitions =
find.byType(FadeTransition).evaluate().map((Element e) => e.widget).toList();
expect(transitions.length, 2);
final FadeTransition left = transitions[0] as FadeTransition;
final FadeTransition right = transitions[1] as FadeTransition;
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('when CupertinoTextField would be blocked by keyboard, it is shown with enough space for the selection handle', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
addTearDown(scrollController.dispose);
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(CupertinoApp(
theme: const CupertinoThemeData(),
home: Center(
child: ListView(
controller: scrollController,
children: <Widget>[
Container(height: 583), // Push field almost off screen.
CupertinoTextField(controller: controller),
Container(height: 1000),
],
),
),
));
// Tap the TextField to put the cursor into it and bring it into view.
expect(scrollController.offset, 0.0);
await tester.tap(find.byType(CupertinoTextField));
await tester.pumpAndSettle();
// The ListView has scrolled to keep the TextField and cursor handle
// visible.
expect(scrollController.offset, 27.0);
});
testWidgets('disabled state golden', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'lorem');
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: DecoratedBox(
decoration: const BoxDecoration(color: Color(0xFFFFFFFF)),
child: Center(
child: SizedBox(
width: 200,
height: 200,
child: RepaintBoundary(
key: const ValueKey<int>(1),
child: CupertinoTextField(
controller: controller,
enabled: false,
),
),
),
),
),
),
);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_test.disabled.png'),
);
});
testWidgets(
'Can drag the left handle while the right handle remains off-screen',
(WidgetTester tester) async {
// Text is longer than textfield width.
const String testValue = 'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb';
final TextEditingController controller = TextEditingController(text: testValue);
addTearDown(controller.dispose);
final ScrollController scrollController = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
scrollController: scrollController,
),
),
),
);
// Double tap 'b' to show handles.
final Offset bPos = textOffsetToPosition(tester, testValue.indexOf('b'));
await tester.tapAt(bPos);
await tester.pump(kDoubleTapTimeout ~/ 2);
await tester.tapAt(bPos);
await tester.pumpAndSettle();
final TextSelection selection = controller.selection;
expect(selection.baseOffset, 28);
expect(selection.extentOffset, testValue.length);
// Move to the left edge.
scrollController.jumpTo(0);
await tester.pumpAndSettle();
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 2);
// Left handle should appear between textfield's left and right position.
final Offset textFieldLeftPosition =
tester.getTopLeft(find.byType(CupertinoTextField));
expect(endpoints[0].point.dx - textFieldLeftPosition.dx, isPositive);
final Offset textFieldRightPosition =
tester.getTopRight(find.byType(CupertinoTextField));
expect(textFieldRightPosition.dx - endpoints[0].point.dx, isPositive);
// Right handle should remain off-screen.
expect(endpoints[1].point.dx - textFieldRightPosition.dx, isPositive);
// Drag the left handle to the right by 25 offset.
const int toOffset = 25;
final double beforeScrollOffset = scrollController.offset;
final Offset handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
final Offset newHandlePos = textOffsetToPosition(tester, toOffset);
final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
// On Apple platforms, dragging the base handle makes it the extent.
expect(controller.selection.baseOffset, testValue.length);
expect(controller.selection.extentOffset, toOffset);
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(controller.selection.baseOffset, toOffset);
expect(controller.selection.extentOffset, testValue.length);
}
// The scroll area of text field should not move.
expect(scrollController.offset, beforeScrollOffset);
},
);
testWidgets(
'Can drag the right handle while the left handle remains off-screen',
(WidgetTester tester) async {
// Text is longer than textfield width.
const String testValue = 'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb';
final TextEditingController controller = TextEditingController(text: testValue);
addTearDown(controller.dispose);
final ScrollController scrollController = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
scrollController: scrollController,
),
),
),
);
// Double tap 'a' to show handles.
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
await tester.tapAt(aPos);
await tester.pump(kDoubleTapTimeout ~/ 2);
await tester.tapAt(aPos);
await tester.pumpAndSettle();
final TextSelection selection = controller.selection;
expect(selection.baseOffset, 0);
expect(selection.extentOffset, 27);
// Move to the right edge.
scrollController.jumpTo(800);
await tester.pumpAndSettle();
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 2);
// Right handle should appear between textfield's left and right position.
final Offset textFieldLeftPosition =
tester.getTopLeft(find.byType(CupertinoTextField));
expect(endpoints[1].point.dx - textFieldLeftPosition.dx, isPositive);
final Offset textFieldRightPosition =
tester.getTopRight(find.byType(CupertinoTextField));
expect(textFieldRightPosition.dx - endpoints[1].point.dx, isPositive);
// Left handle should remain off-screen.
expect(endpoints[0].point.dx, isNegative);
// Drag the right handle to the left by 50 offset.
const int toOffset = 50;
final double beforeScrollOffset = scrollController.offset;
final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
final Offset newHandlePos = textOffsetToPosition(tester, toOffset);
final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, toOffset);
// The scroll area of text field should not move.
expect(scrollController.offset, beforeScrollOffset);
},
);
group('Text selection toolbar', () {
testWidgets('Collapsed selection works', (WidgetTester tester) async {
tester.view.physicalSize = const Size(400, 400);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
EditableText.debugDeterministicCursor = true;
TextEditingController controller;
EditableTextState state;
Offset bottomLeftSelectionPosition;
controller = TextEditingController(text: 'a');
// Top left collapsed selection. The toolbar should flip vertically, and
// the arrow should not point exactly to the caret because the caret is
// too close to the left.
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 200,
height: 200,
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
),
);
state = tester.state<EditableTextState>(find.byType(EditableText));
final double lineHeight = state.renderEditable.preferredLineHeight;
state.renderEditable.selectPositionAt(from: textOffsetToPosition(tester, 0), cause: SelectionChangedCause.tap);
expect(state.showToolbar(), true);
await tester.pumpAndSettle();
bottomLeftSelectionPosition = textOffsetToBottomLeftPosition(tester, 0);
expect(
find.byType(CupertinoTextSelectionToolbar),
paints..clipPath(
pathMatcher: PathPointsMatcher(
excludes: <Offset> [
// Arrow should not point to the selection handle.
bottomLeftSelectionPosition.translate(0, 8 + 0.1),
],
includes: <Offset> [
// Expected center of the arrow. The arrow should stay clear of
// the edges of the selection toolbar.
Offset(26.0, bottomLeftSelectionPosition.dy + 8.0 + 0.1),
],
),
),
);
expect(
find.byType(CupertinoTextSelectionToolbar),
paints..clipPath(
pathMatcher: PathBoundsMatcher(
topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01),
leftMatcher: moreOrLessEquals(8),
rightMatcher: lessThanOrEqualTo(400 - 8),
bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 44, epsilon: 0.01),
),
),
);
// Top Right collapsed selection. The toolbar should flip vertically, and
// the arrow should not point exactly to the caret because the caret is
// too close to the right.
controller.dispose();
controller = TextEditingController(text: List<String>.filled(200, 'a').join());
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
alignment: Alignment.topRight,
child: SizedBox(
width: 200,
height: 200,
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
),
);
state = tester.state<EditableTextState>(find.byType(EditableText));
state.renderEditable.selectPositionAt(
from: tester.getTopRight(find.byType(CupertinoApp)),
cause: SelectionChangedCause.tap,
);
await tester.pumpAndSettle();
// -1 because we want to reach the end of the line, not the start of a new line.
bottomLeftSelectionPosition = textOffsetToBottomLeftPosition(tester, state.renderEditable.selection!.baseOffset - 1);
expect(
find.byType(CupertinoTextSelectionToolbar),
paints..clipPath(
pathMatcher: PathPointsMatcher(
excludes: <Offset> [
// Arrow should not point to the selection handle.
bottomLeftSelectionPosition.translate(0, 8 + 0.1),
],
includes: <Offset> [
// Expected center of the arrow.
Offset(400 - 26.0, bottomLeftSelectionPosition.dy + 8 + 0.1),
],
),
),
);
expect(
find.byType(CupertinoTextSelectionToolbar),
paints..clipPath(
pathMatcher: PathBoundsMatcher(
topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01),
rightMatcher: moreOrLessEquals(400.0 - 8),
bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 44, epsilon: 0.01),
leftMatcher: greaterThanOrEqualTo(8),
),
),
);
// Normal centered collapsed selection. The toolbar arrow should point down, and
// it should point exactly to the caret.
controller.dispose();
controller = TextEditingController(text: List<String>.filled(200, 'a').join());
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
child: SizedBox(
width: 200,
height: 200,
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
),
);
state = tester.state<EditableTextState>(find.byType(EditableText));
state.renderEditable.selectPositionAt(
from: tester.getCenter(find.byType(EditableText)),
cause: SelectionChangedCause.tap,
);
await tester.pumpAndSettle();
bottomLeftSelectionPosition = textOffsetToBottomLeftPosition(tester, state.renderEditable.selection!.baseOffset);
expect(
find.byType(CupertinoTextSelectionToolbar),
paints..clipPath(
pathMatcher: PathPointsMatcher(
includes: <Offset> [
// Expected center of the arrow.
bottomLeftSelectionPosition.translate(0, -lineHeight - 8 - 0.1),
],
),
),
);
expect(
find.byType(CupertinoTextSelectionToolbar),
paints..clipPath(
pathMatcher: PathBoundsMatcher(
bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight, epsilon: 0.01),
topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight - 44, epsilon: 0.01),
rightMatcher: lessThanOrEqualTo(400 - 8),
leftMatcher: greaterThanOrEqualTo(8),
),
),
);
});
testWidgets('selecting multiple words works', (WidgetTester tester) async {
tester.view.physicalSize = const Size(400, 400);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
EditableText.debugDeterministicCursor = true;
final TextEditingController controller;
final EditableTextState state;
// Normal multiword collapsed selection. The toolbar arrow should point down, and
// it should point exactly to the caret.
controller = TextEditingController(text: List<String>.filled(20, 'a').join(' '));
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
child: SizedBox(
width: 200,
height: 200,
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
),
);
state = tester.state<EditableTextState>(find.byType(EditableText));
final double lineHeight = state.renderEditable.preferredLineHeight;
// Select the first 2 words.
state.renderEditable.selectPositionAt(
from: textOffsetToPosition(tester, 0),
to: textOffsetToPosition(tester, 4),
cause: SelectionChangedCause.tap,
);
expect(state.showToolbar(), true);
await tester.pumpAndSettle();
final Offset selectionPosition = (textOffsetToBottomLeftPosition(tester, 0) + textOffsetToBottomLeftPosition(tester, 4)) / 2;
expect(
find.byType(CupertinoTextSelectionToolbar),
paints..clipPath(
pathMatcher: PathPointsMatcher(
includes: <Offset> [
// Expected center of the arrow.
selectionPosition.translate(0, -lineHeight - 8 - 0.1),
],
),
),
);
expect(
find.byType(CupertinoTextSelectionToolbar),
paints..clipPath(
pathMatcher: PathBoundsMatcher(
bottomMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight, epsilon: 0.01),
topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 44, epsilon: 0.01),
rightMatcher: lessThanOrEqualTo(400 - 8),
leftMatcher: greaterThanOrEqualTo(8),
),
),
);
});
testWidgets('selecting multiline works', (WidgetTester tester) async {
tester.view.physicalSize = const Size(400, 400);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
EditableText.debugDeterministicCursor = true;
final TextEditingController controller;
final EditableTextState state;
// Normal multiline collapsed selection. The toolbar arrow should point down, and
// it should point exactly to the horizontal center of the text field.
controller = TextEditingController(text: List<String>.filled(20, 'a a ').join('\n'));
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
child: SizedBox(
width: 200,
height: 200,
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
),
);
state = tester.state<EditableTextState>(find.byType(EditableText));
final double lineHeight = state.renderEditable.preferredLineHeight;
// Select the first 2 words.
state.renderEditable.selectPositionAt(
from: textOffsetToPosition(tester, 0),
to: textOffsetToPosition(tester, 10),
cause: SelectionChangedCause.tap,
);
expect(state.showToolbar(), true);
await tester.pumpAndSettle();
final Offset selectionPosition = Offset(
// Toolbar should be centered.
200,
textOffsetToBottomLeftPosition(tester, 0).dy,
);
expect(
find.byType(CupertinoTextSelectionToolbar),
paints..clipPath(
pathMatcher: PathPointsMatcher(
includes: <Offset> [
// Expected center of the arrow.
selectionPosition.translate(0, -lineHeight - 8 - 0.1),
],
),
),
);
expect(
find.byType(CupertinoTextSelectionToolbar),
paints..clipPath(
pathMatcher: PathBoundsMatcher(
bottomMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight, epsilon: 0.01),
topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 44, epsilon: 0.01),
rightMatcher: lessThanOrEqualTo(400 - 8),
leftMatcher: greaterThanOrEqualTo(8),
),
),
);
});
// This is a regression test for
// https://github.com/flutter/flutter/issues/37046.
testWidgets('No exceptions when showing selection menu inside of nested Navigators', (WidgetTester tester) async {
const String testValue = '123456';
final TextEditingController controller = TextEditingController(
text: testValue,
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: Center(
child: Column(
children: <Widget>[
Container(
height: 100,
color: CupertinoColors.black,
),
Expanded(
child: Navigator(
onGenerateRoute: (_) => CupertinoPageRoute<void>(
builder: (_) => CupertinoTextField(
controller: controller,
),
),
),
),
],
),
),
),
),
);
// No text selection toolbar.
expect(find.byType(CupertinoTextSelectionToolbar), findsNothing);
// Double tap on the text in the input.
await tester.pumpAndSettle();
await tester.tapAt(textOffsetToPosition(tester, testValue.length ~/ 2));
await tester.pump(const Duration(milliseconds: 100));
await tester.tapAt(textOffsetToPosition(tester, testValue.length ~/ 2));
await tester.pumpAndSettle();
// Now the text selection toolbar is showing and there were no exceptions.
expect(find.byType(CupertinoTextSelectionToolbar), findsOneWidget);
expect(tester.takeException(), null);
});
testWidgets('Drag selection hides the selection menu', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'blah1 blah2',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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.
group('textAlignVertical position', () {
group('simple case', () {
testWidgets('align top (default)', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
const Size size = Size(200.0, 200.0);
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
child: SizedBox(
width: size.width,
height: size.height,
child: CupertinoTextField(
focusNode: focusNode,
expands: true,
maxLines: null,
),
),
),
),
),
);
// Fills the whole container since expands is true.
expect(tester.getSize(find.byType(CupertinoTextField)), size);
// Tapping anywhere inside focuses it.
expect(focusNode.hasFocus, false);
await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
await tester.pumpAndSettle();
expect(focusNode.hasFocus, true);
focusNode.unfocus();
await tester.pumpAndSettle();
expect(focusNode.hasFocus, false);
final Offset justInside = tester
.getBottomLeft(find.byType(CupertinoTextField))
.translate(0.0, -1.0);
await tester.tapAt(justInside);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 300));
expect(focusNode.hasFocus, true);
// The EditableText is at the top.
expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(207.0, epsilon: .0001));
});
testWidgets('align center', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
const Size size = Size(200.0, 200.0);
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
child: SizedBox(
width: size.width,
height: size.height,
child: CupertinoTextField(
textAlignVertical: TextAlignVertical.center,
focusNode: focusNode,
expands: true,
maxLines: null,
),
),
),
),
),
);
// Fills the whole container since expands is true.
expect(tester.getSize(find.byType(CupertinoTextField)), size);
// Tapping anywhere inside focuses it.
expect(focusNode.hasFocus, false);
await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
await tester.pumpAndSettle();
expect(focusNode.hasFocus, true);
focusNode.unfocus();
await tester.pumpAndSettle();
expect(focusNode.hasFocus, false);
final Offset justInside = tester
.getBottomLeft(find.byType(CupertinoTextField))
.translate(0.0, -1.0);
await tester.tapAt(justInside);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 300));
expect(focusNode.hasFocus, true);
// The EditableText is at the center.
expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(291.5, epsilon: .0001));
});
testWidgets('align bottom', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
const Size size = Size(200.0, 200.0);
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
child: SizedBox(
width: size.width,
height: size.height,
child: CupertinoTextField(
textAlignVertical: TextAlignVertical.bottom,
focusNode: focusNode,
expands: true,
maxLines: null,
),
),
),
),
),
);
// Fills the whole container since expands is true.
expect(tester.getSize(find.byType(CupertinoTextField)), size);
// Tapping anywhere inside focuses it.
expect(focusNode.hasFocus, false);
await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
await tester.pumpAndSettle();
expect(focusNode.hasFocus, true);
focusNode.unfocus();
await tester.pumpAndSettle();
expect(focusNode.hasFocus, false);
final Offset justInside = tester
.getBottomLeft(find.byType(CupertinoTextField))
.translate(0.0, -1.0);
await tester.tapAt(justInside);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 300));
expect(focusNode.hasFocus, true);
// The EditableText is at the bottom.
expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(376.0, epsilon: .0001));
});
testWidgets('align as a double', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
const Size size = Size(200.0, 200.0);
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
child: SizedBox(
width: size.width,
height: size.height,
child: CupertinoTextField(
textAlignVertical: const TextAlignVertical(y: 0.75),
focusNode: focusNode,
expands: true,
maxLines: null,
),
),
),
),
),
);
// Fills the whole container since expands is true.
expect(tester.getSize(find.byType(CupertinoTextField)), size);
// Tapping anywhere inside focuses it.
expect(focusNode.hasFocus, false);
await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
await tester.pumpAndSettle();
expect(focusNode.hasFocus, true);
focusNode.unfocus();
await tester.pumpAndSettle();
expect(focusNode.hasFocus, false);
final Offset justInside = tester
.getBottomLeft(find.byType(CupertinoTextField))
.translate(0.0, -1.0);
await tester.tapAt(justInside);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 300));
expect(focusNode.hasFocus, true);
// The EditableText is near the bottom.
expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(354.875, epsilon: .0001));
});
});
group('tall prefix', () {
testWidgets('align center (default when prefix)', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
const Size size = Size(200.0, 200.0);
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
child: SizedBox(
width: size.width,
height: size.height,
child: CupertinoTextField(
focusNode: focusNode,
expands: true,
maxLines: null,
prefix: const SizedBox(
height: 100,
width: 10,
),
),
),
),
),
),
);
// Fills the whole container since expands is true.
expect(tester.getSize(find.byType(CupertinoTextField)), size);
// Tapping anywhere inside focuses it. This includes tapping on the
// prefix, because in this case it is transparent.
expect(focusNode.hasFocus, false);
await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
await tester.pumpAndSettle();
expect(focusNode.hasFocus, true);
focusNode.unfocus();
await tester.pumpAndSettle();
expect(focusNode.hasFocus, false);
final Offset justInside = tester
.getBottomLeft(find.byType(CupertinoTextField))
.translate(0.0, -1.0);
await tester.tapAt(justInside);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 300));
expect(focusNode.hasFocus, true);
// The EditableText is at the center. Same as without prefix.
expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(291.5, epsilon: .0001));
});
testWidgets('align top', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
const Size size = Size(200.0, 200.0);
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
child: SizedBox(
width: size.width,
height: size.height,
child: CupertinoTextField(
textAlignVertical: TextAlignVertical.top,
focusNode: focusNode,
expands: true,
maxLines: null,
prefix: const SizedBox(
height: 100,
width: 10,
),
),
),
),
),
),
);
// Fills the whole container since expands is true.
expect(tester.getSize(find.byType(CupertinoTextField)), size);
// Tapping anywhere inside focuses it. This includes tapping on the
// prefix, because in this case it is transparent.
expect(focusNode.hasFocus, false);
await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
await tester.pumpAndSettle();
expect(focusNode.hasFocus, true);
focusNode.unfocus();
await tester.pumpAndSettle();
expect(focusNode.hasFocus, false);
final Offset justInside = tester
.getBottomLeft(find.byType(CupertinoTextField))
.translate(0.0, -1.0);
await tester.tapAt(justInside);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 300));
expect(focusNode.hasFocus, true);
// The prefix is at the top, and the EditableText is centered within its
// height.
expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(241.5, epsilon: .0001));
});
testWidgets('align bottom', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
const Size size = Size(200.0, 200.0);
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
child: SizedBox(
width: size.width,
height: size.height,
child: CupertinoTextField(
textAlignVertical: TextAlignVertical.bottom,
focusNode: focusNode,
expands: true,
maxLines: null,
prefix: const SizedBox(
height: 100,
width: 10,
),
),
),
),
),
),
);
// Fills the whole container since expands is true.
expect(tester.getSize(find.byType(CupertinoTextField)), size);
// Tapping anywhere inside focuses it. This includes tapping on the
// prefix, because in this case it is transparent.
expect(focusNode.hasFocus, false);
await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
await tester.pumpAndSettle();
expect(focusNode.hasFocus, true);
focusNode.unfocus();
await tester.pumpAndSettle();
expect(focusNode.hasFocus, false);
final Offset justInside = tester
.getBottomLeft(find.byType(CupertinoTextField))
.translate(0.0, -1.0);
await tester.tapAt(justInside);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 300));
expect(focusNode.hasFocus, true);
// The prefix is at the bottom, and the EditableText is centered within
// its height.
expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(341.5, epsilon: .0001));
});
testWidgets('align as a double', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
const Size size = Size(200.0, 200.0);
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
child: SizedBox(
width: size.width,
height: size.height,
child: CupertinoTextField(
textAlignVertical: const TextAlignVertical(y: 0.75),
focusNode: focusNode,
expands: true,
maxLines: null,
prefix: const SizedBox(
height: 100,
width: 10,
),
),
),
),
),
),
);
// Fills the whole container since expands is true.
expect(tester.getSize(find.byType(CupertinoTextField)), size);
// Tapping anywhere inside focuses it. This includes tapping on the
// prefix, because in this case it is transparent.
expect(focusNode.hasFocus, false);
await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
await tester.pumpAndSettle();
expect(focusNode.hasFocus, true);
focusNode.unfocus();
await tester.pumpAndSettle();
expect(focusNode.hasFocus, false);
final Offset justInside = tester
.getBottomLeft(find.byType(CupertinoTextField))
.translate(0.0, -1.0);
await tester.tapAt(justInside);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 300));
expect(focusNode.hasFocus, true);
// The EditableText is near the bottom.
expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(329.0, epsilon: .0001));
});
});
testWidgets(
'Long press on an autofocused field shows the selection menu',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(200, 200)),
child: const CupertinoTextField(
autofocus: true,
),
),
),
),
);
// This extra pump allows the selection set by autofocus to propagate to
// the RenderEditable.
await tester.pump();
// Long press shows the selection menu.
await tester.longPressAt(textOffsetToPosition(tester, 0));
await tester.pumpAndSettle();
expect(find.text('Paste'), isContextMenuProvidedByPlatform ? findsNothing : findsOneWidget);
},
);
});
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');
addTearDown(focusNode1.dispose);
addTearDown(focusNode2.dispose);
addTearDown(focusNode3.dispose);
addTearDown(focusNode4.dispose);
addTearDown(focusNode5.dispose);
addTearDown(controller1.dispose);
addTearDown(controller2.dispose);
addTearDown(controller3.dispose);
addTearDown(controller4.dispose);
addTearDown(controller5.dispose);
// Lay out text fields in a "+" formation, and focus the center one.
await tester.pumpWidget(CupertinoApp(
home: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(
width: 100.0,
child: CupertinoTextField(
controller: controller1,
focusNode: focusNode1,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(
width: 100.0,
child: CupertinoTextField(
controller: controller2,
focusNode: focusNode2,
),
),
SizedBox(
width: 100.0,
child: CupertinoTextField(
controller: controller3,
focusNode: focusNode3,
),
),
SizedBox(
width: 100.0,
child: CupertinoTextField(
controller: controller4,
focusNode: focusNode4,
),
),
],
),
SizedBox(
width: 100.0,
child: CupertinoTextField(
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);
// TODO(gspencergoog): Remove the variant when the deprecated
// KeySimulatorTransitModeVariant API is removed.
// ignore: deprecated_member_use
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Scrolling shortcuts are disabled in text fields', (WidgetTester tester) async {
bool scrollInvoked = false;
await tester.pumpWidget(
CupertinoApp(
home: Actions(
actions: <Type, Action<Intent>>{
ScrollIntent: CallbackAction<ScrollIntent>(onInvoke: (Intent intent) {
scrollInvoked = true;
return null;
}),
},
child: ListView(
children: const <Widget>[
Padding(padding: EdgeInsets.symmetric(vertical: 200)),
CupertinoTextField(),
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(CupertinoTextField)));
await tester.sendKeyEvent(LogicalKeyboardKey.space);
expect(scrollInvoked, isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(scrollInvoked, isFalse);
// TODO(gspencergoog): Remove the variant when the deprecated
// KeySimulatorTransitModeVariant API is removed.
// ignore: deprecated_member_use
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Cupertino text field semantics', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(200, 200)),
child: const CupertinoTextField(),
),
),
),
);
expect(
tester.getSemantics(
find.descendant(
of: find.byType(CupertinoTextField),
matching: find.byType(Semantics),
).first,
),
matchesSemantics(
isTextField: true,
isEnabled: true,
hasEnabledState: true,
hasTapAction: true,
),
);
});
testWidgets('Disabled Cupertino text field semantics', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(200, 200)),
child: const CupertinoTextField(
enabled: false,
),
),
),
),
);
expect(
tester.getSemantics(
find.descendant(
of: find.byType(CupertinoTextField),
matching: find.byType(Semantics),
).first,
),
matchesSemantics(
hasEnabledState: true,
isTextField: true,
isReadOnly: true,
),
);
});
testWidgets('Cupertino text field clear button semantics', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(200, 200)),
child: const CupertinoTextField(
clearButtonMode: OverlayVisibilityMode.always,
),
),
),
),
);
expect(find.bySemanticsLabel('Clear'), findsOneWidget);
expect(
tester.getSemantics(
find.bySemanticsLabel('Clear').first,
),
matchesSemantics(
isButton: true,
hasTapAction: true,
label: 'Clear'
),
);
});
testWidgets('Cupertino text field clear semantic label', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(200, 200)),
child: const CupertinoTextField(
clearButtonMode: OverlayVisibilityMode.always,
clearButtonSemanticLabel: 'Delete Text'
),
),
),
),
);
expect(find.bySemanticsLabel('Clear'), findsNothing);
expect(find.bySemanticsLabel('Delete Text'), findsOneWidget);
expect(
tester.getSemantics(
find.bySemanticsLabel('Delete Text').first,
),
matchesSemantics(
isButton: true,
hasTapAction: true,
label: 'Delete Text'
),
);
});
testWidgets('text selection style 1', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwassssup!',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: RepaintBoundary(
child: Container(
width: 650.0,
height: 600.0,
decoration: const BoxDecoration(
color: Color(0xff00ff00),
),
child: Column(
children: <Widget>[
CupertinoTextField(
autofocus: true,
key: const Key('field0'),
controller: controller,
style: const TextStyle(height: 4, color: ui.Color.fromARGB(100, 0, 0, 0)),
toolbarOptions: const ToolbarOptions(selectAll: true),
selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingTop,
selectionWidthStyle: ui.BoxWidthStyle.max,
maxLines: 3,
),
],
),
),
),
),
),
);
// This extra pump is so autofocus can propagate to renderEditable.
await tester.pump();
final Offset textFieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
await tester.longPressAt(textFieldStart + const Offset(50.0, 2.0));
await tester.pumpAndSettle(const Duration(milliseconds: 150));
// Tap the Select All button.
await tester.tapAt(textFieldStart + const Offset(20.0, 100.0));
await tester.pump(const Duration(milliseconds: 300));
await expectLater(
find.byType(CupertinoApp),
matchesGoldenFile('text_field_golden.TextSelectionStyle.1.png'),
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
skip: kIsWeb, // [intended] the web has its own Select All.
);
testWidgets('text selection style 2', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwassssup!',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: RepaintBoundary(
child: Container(
width: 650.0,
height: 600.0,
decoration: const BoxDecoration(
color: Color(0xff00ff00),
),
child: Column(
children: <Widget>[
CupertinoTextField(
autofocus: true,
key: const Key('field0'),
controller: controller,
style: const TextStyle(height: 4, color: ui.Color.fromARGB(100, 0, 0, 0)),
toolbarOptions: const ToolbarOptions(selectAll: true),
selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingBottom,
maxLines: 3,
),
],
),
),
),
),
),
);
// This extra pump is so autofocus can propagate to renderEditable.
await tester.pump();
final Offset textFieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
await tester.longPressAt(textFieldStart + const Offset(50.0, 2.0));
await tester.pumpAndSettle(const Duration(milliseconds: 150));
// Tap the Select All button.
await tester.tapAt(textFieldStart + const Offset(20.0, 100.0));
await tester.pump(const Duration(milliseconds: 300));
await expectLater(
find.byType(CupertinoApp),
matchesGoldenFile('text_field_golden.TextSelectionStyle.2.png'),
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
skip: kIsWeb, // [intended] the web has its own Select All.
);
testWidgets('textSelectionControls is passed to EditableText', (WidgetTester tester) async {
final MockTextSelectionControls selectionControl = MockTextSelectionControls();
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
selectionControls: selectionControl,
),
),
),
);
final EditableText widget = tester.widget(find.byType(EditableText));
expect(widget.selectionControls, equals(selectionControl));
});
testWidgets('Do not add LengthLimiting formatter to the user supplied list', (WidgetTester tester) async {
final List<TextInputFormatter> formatters = <TextInputFormatter>[];
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTextField(maxLength: 5, inputFormatters: formatters),
),
);
expect(formatters.isEmpty, isTrue);
});
group('MaxLengthEnforcement', () {
const int maxLength = 5;
Future<void> setupWidget(
WidgetTester tester,
MaxLengthEnforcement? enforcement,
) async {
final Widget widget = CupertinoApp(
home: Center(
child: CupertinoTextField(
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('disabled widget changes background color', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
enabled: false,
),
),
),
);
BoxDecoration decoration = tester
.widget<DecoratedBox>(
find.descendant(
of: find.byType(CupertinoTextField),
matching: find.byType(DecoratedBox),
),
)
.decoration as BoxDecoration;
expect(
decoration.color!.value,
0xFFFAFAFA,
);
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(),
),
),
);
decoration = tester
.widget<DecoratedBox>(
find.descendant(
of: find.byType(CupertinoTextField),
matching: find.byType(DecoratedBox),
),
)
.decoration as BoxDecoration;
expect(
decoration.color!.value,
CupertinoColors.white.value,
);
await tester.pumpWidget(
const CupertinoApp(
theme: CupertinoThemeData(
brightness: Brightness.dark,
),
home: Center(
child: CupertinoTextField(
enabled: false,
),
),
),
);
decoration = tester
.widget<DecoratedBox>(
find.descendant(
of: find.byType(CupertinoTextField),
matching: find.byType(DecoratedBox),
),
)
.decoration as BoxDecoration;
expect(
decoration.color!.value,
0xFF050505,
);
});
// Regression test for https://github.com/flutter/flutter/issues/78097.
testWidgets(
'still gets disabled background color when decoration is null',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
decoration: null,
enabled: false,
),
),
),
);
final Color disabledColor = tester.widget<ColoredBox>(
find.descendant(
of: find.byType(CupertinoTextField),
matching: find.byType(ColoredBox),
),
).color;
expect(disabledColor, isSameColorAs(const Color(0xFFFAFAFA)));
},
);
testWidgets('autofill info has placeholder text', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoTextField(
placeholder: 'placeholder text',
),
),
);
await tester.tap(find.byType(CupertinoTextField));
expect(
tester.testTextInput.setClientArgs?['autofill'],
containsPair('hintText', 'placeholder text'),
);
});
testWidgets('textDirection is passed to EditableText', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
textDirection: TextDirection.ltr,
),
),
),
);
final EditableText ltrWidget = tester.widget(find.byType(EditableText));
expect(ltrWidget.textDirection, TextDirection.ltr);
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextField(
textDirection: TextDirection.rtl,
),
),
),
);
final EditableText rtlWidget = tester.widget(find.byType(EditableText));
expect(rtlWidget.textDirection, TextDirection.rtl);
});
testWidgets('clipBehavior has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoTextField(
),
),
);
final CupertinoTextField textField = tester.firstWidget(find.byType(CupertinoTextField));
expect(textField.clipBehavior, Clip.hardEdge);
});
testWidgets('Overflow clipBehavior none golden', (WidgetTester tester) async {
final OverflowWidgetTextEditingController controller = OverflowWidgetTextEditingController();
addTearDown(controller.dispose);
final Widget widget = CupertinoApp(
home: RepaintBoundary(
key: const ValueKey<int>(1),
child: SizedBox(
height: 200.0,
width: 200.0,
child: Center(
child: SizedBox(
// Make sure the input field is not high enough for the WidgetSpan.
height: 50,
child: CupertinoTextField(
controller: controller,
clipBehavior: Clip.none,
),
),
),
),
),
);
await tester.pumpWidget(widget);
final CupertinoTextField textField = tester.firstWidget(find.byType(CupertinoTextField));
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.cupertino.0.png'),
);
});
testWidgets('can shift + tap to select with a keyboard (Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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',
);
addTearDown(controller.dispose);
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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);
// Other platforms start from the previous selection.
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(controller.selection.baseOffset, 35);
}
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',
);
addTearDown(controller.dispose);
final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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,
);
await tester.pumpAndSettle();
if (isTargetPlatformIOS) {
await gesture.up();
// Not a double tap + drag.
await tester.pumpAndSettle(kDoubleTapTimeout);
}
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Expand the selection a bit.
if (isTargetPlatformIOS) {
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',
);
addTearDown(controller.dispose);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android
|| defaultTargetPlatform == TargetPlatform.fuchsia;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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,
);
await tester.pumpAndSettle();
if (isTargetPlatformMobile) {
await gesture.up();
// Not a double tap + drag.
await tester.pumpAndSettle(kDoubleTapTimeout);
}
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 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',
);
addTearDown(controller.dispose);
final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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,
);
await tester.pumpAndSettle();
if (isTargetPlatformIOS) {
await gesture.up();
// Not a double tap + drag.
await tester.pumpAndSettle(kDoubleTapTimeout);
}
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Expand the selection a bit.
if (isTargetPlatformIOS) {
await gesture.down(textOffsetToPosition(tester, 7));
await tester.pumpAndSettle();
}
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',
);
addTearDown(controller.dispose);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android
|| defaultTargetPlatform == TargetPlatform.fuchsia;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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,
);
await tester.pumpAndSettle();
if (isTargetPlatformMobile) {
await gesture.up();
// Not a double tap + drag.
await tester.pumpAndSettle(kDoubleTapTimeout);
}
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Expand the selection a bit.
if (isTargetPlatformMobile) {
await gesture.down(textOffsetToPosition(tester, 7));
await tester.pumpAndSettle();
}
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',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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);
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);
}
// 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);
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);
}
},
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();
addTearDown(focusNode1.dispose);
addTearDown(focusNode2.dispose);
final UniqueKey key1 = UniqueKey();
final UniqueKey key2 = UniqueKey();
await tester.pumpWidget(
CupertinoApp(
home: Column(
children: <Widget>[
CupertinoTextField(
key: key1,
focusNode: focusNode1,
),
// This spacer prevents the context menu in one field from
// overlapping with the other field.
const SizedBox(height: 100.0),
CupertinoTextField(
key: key2,
focusNode: focusNode2,
),
],
),
),
);
// Interact with the field to establish the input connection.
await tester.tapAt(
tester.getCenter(find.byKey(key1)),
buttons: kSecondaryMouseButton,
);
await tester.pump();
expect(focusNode1.hasFocus, isTrue);
expect(focusNode2.hasFocus, isFalse);
await tester.tapAt(
tester.getCenter(find.byKey(key2)),
buttons: kSecondaryMouseButton,
);
await tester.pump();
expect(focusNode1.hasFocus, isFalse);
expect(focusNode2.hasFocus, isTrue);
await tester.tapAt(
tester.getCenter(find.byKey(key1)),
buttons: kSecondaryMouseButton,
);
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();
addTearDown(focusNode1.dispose);
addTearDown(focusNode2.dispose);
final TextEditingController controller = TextEditingController(
text: 'first second',
);
addTearDown(controller.dispose);
final UniqueKey key1 = UniqueKey();
await tester.pumpWidget(
CupertinoApp(
home: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CupertinoTextField(
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: kSecondaryMouseButton,
);
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: kSecondaryMouseButton,
);
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: kSecondaryMouseButton,
);
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('context menu', () {
testWidgets('builds CupertinoAdaptiveTextSelectionToolbar by default', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: '');
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CupertinoTextField(
controller: controller,
),
],
),
),
);
await tester.pump(); // Wait for autofocus to take effect.
expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsNothing);
// Long-press to bring up the context menu.
final Finder textFinder = find.byType(EditableText);
await tester.longPress(textFinder);
tester.state<EditableTextState>(textFinder).showToolbar();
await tester.pumpAndSettle();
expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsOneWidget);
},
skip: kIsWeb, // [intended] on web the browser handles the context menu.
);
testWidgets('contextMenuBuilder is used in place of the default text selection toolbar', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final TextEditingController controller = TextEditingController(text: '');
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CupertinoTextField(
controller: controller,
contextMenuBuilder: (
BuildContext context,
EditableTextState editableTextState,
) {
return Placeholder(key: key);
},
),
],
),
),
);
await tester.pump(); // Wait for autofocus to take effect.
expect(find.byKey(key), findsNothing);
// Long-press to bring up the context menu.
final Finder textFinder = find.byType(EditableText);
await tester.longPress(textFinder);
tester.state<EditableTextState>(textFinder).showToolbar();
await tester.pumpAndSettle();
expect(find.byKey(key), findsOneWidget);
},
skip: kIsWeb, // [intended] on web the browser handles the context menu.
);
});
group('magnifier', () {
late ValueNotifier<MagnifierInfo> magnifierInfo;
final Widget fakeMagnifier = Container(key: UniqueKey());
group('magnifier builder', () {
testWidgets('should build custom magnifier if given', (WidgetTester tester) async {
final Widget customMagnifier = Container(
key: UniqueKey(),
);
final CupertinoTextField defaultCupertinoTextField = CupertinoTextField(
magnifierConfiguration: TextMagnifierConfiguration(magnifierBuilder: (_, __, ___) => customMagnifier),
);
await tester.pumpWidget(const CupertinoApp(
home: Placeholder(),
));
final BuildContext context =
tester.firstElement(find.byType(Placeholder));
final ValueNotifier<MagnifierInfo> magnifierInfo = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
addTearDown(magnifierInfo.dispose);
expect(
defaultCupertinoTextField.magnifierConfiguration!.magnifierBuilder(
context,
MagnifierController(),
magnifierInfo,
),
isA<Widget>().having((Widget widget) => widget.key, 'key', equals(customMagnifier.key)));
});
group('defaults', () {
testWidgets('should build CupertinoMagnifier on iOS and Android', (WidgetTester tester) async {
await tester.pumpWidget(const CupertinoApp(
home: CupertinoTextField(),
));
final BuildContext context = tester.firstElement(find.byType(CupertinoTextField));
final EditableText editableText = tester.widget(find.byType(EditableText));
final ValueNotifier<MagnifierInfo> magnifierInfo = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
addTearDown(magnifierInfo.dispose);
expect(
editableText.magnifierConfiguration.magnifierBuilder(
context,
MagnifierController(),
magnifierInfo,
),
isA<CupertinoTextMagnifier>());
},
variant: const TargetPlatformVariant(
<TargetPlatform>{TargetPlatform.iOS, TargetPlatform.android}));
});
testWidgets('should build nothing on all platforms but iOS and Android', (WidgetTester tester) async {
await tester.pumpWidget(const CupertinoApp(
home: CupertinoTextField(),
));
final BuildContext context = tester.firstElement(find.byType(CupertinoTextField));
final EditableText editableText = tester.widget(find.byType(EditableText));
final ValueNotifier<MagnifierInfo> magnifierInfo = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
addTearDown(magnifierInfo.dispose);
expect(
editableText.magnifierConfiguration.magnifierBuilder(
context,
MagnifierController(),
magnifierInfo,
),
isNull);
},
variant: TargetPlatformVariant.all(
excluding: <TargetPlatform>{TargetPlatform.iOS, TargetPlatform.android}));
});
testWidgets('Can drag handles to show, unshow, and update magnifier',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: Builder(
builder: (BuildContext context) => CupertinoTextField(
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(CupertinoTextField), testValue);
// 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, pointer: 7);
Offset? firstDragGesturePosition;
await gesture.moveTo(textOffsetToPosition(tester, testValue.length - 2));
await tester.pump();
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
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);
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
testWidgets('Can drag to show, unshow, and update magnifier', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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(CupertinoTextField), testValue);
await tester.pumpAndSettle();
// Tap at '|a' to move the selection to position 0.
await tester.tapAt(textOffsetToPosition(tester, 0));
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
expect(find.byKey(fakeMagnifier.key!), findsNothing);
// Start a drag gesture to move the selection to the dragged position, showing
// the magnifier.
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, 0));
await tester.pump();
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
Offset firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;
await gesture.moveTo(textOffsetToPosition(tester, 10));
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 10);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
// Expect the position the magnifier gets to have moved.
expect(firstDragGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));
// The magnifier should hide when the drag ends.
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 10);
expect(find.byKey(fakeMagnifier.key!), findsNothing);
// Start a double-tap select the word at the tapped position.
await gesture.down(textOffsetToPosition(tester, 1));
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(tester, 1));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 3);
// Start a drag gesture to extend the selection word-by-word, showing the
// magnifier.
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 7);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;
await gesture.moveTo(textOffsetToPosition(tester, 10));
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 11);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
// Expect the position the magnifier gets to have moved.
expect(firstDragGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));
// The magnifier should hide when the drag ends.
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 11);
expect(find.byKey(fakeMagnifier.key!), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }));
testWidgets('Can long press to show, unshow, and update magnifier on non-Apple platforms', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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(CupertinoTextField), testValue);
await tester.pumpAndSettle();
// Tap at 'e' to move the cursor before the 'e'.
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' 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, 4);
expect(controller.selection.extentOffset, 7);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
final Offset firstLongPressGesturePosition = magnifierInfo.value.globalGesturePosition;
// Move the gesture to 'h' to extend the selection to 'ghi'.
await gesture.moveTo(textOffsetToPosition(tester, testValue.indexOf('h')));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 11);
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 }));
testWidgets('Can long press to show, unshow, and update magnifier on iOS', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
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(CupertinoTextField), testValue);
await tester.pumpAndSettle();
// 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 : 7);
expect(find.byKey(fakeMagnifier.key!), findsNothing);
// Long press the 'e' to move the cursor in front of the 'e' 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, 5);
expect(controller.selection.extentOffset, 5);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
final Offset firstLongPressGesturePosition = magnifierInfo.value.globalGesturePosition;
// Move the gesture to 'h' 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, 9);
expect(controller.selection.extentOffset, 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.iOS }));
});
group('TapRegion integration', () {
testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
addTearDown(focusNode.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: SizedBox(
width: 100,
height: 100,
child: CupertinoTextField(
autofocus: true,
focusNode: focusNode,
),
),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
// Tap outside the border.
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');
addTearDown(focusNode.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: SizedBox(
width: 100,
height: 100,
child: CupertinoTextField(
autofocus: true,
focusNode: focusNode,
),
),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
// Tap just outside the border, but not inside the EditableText.
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 TextEditingController controller;
final EditableTextState state;
controller = TextEditingController(text: 'A B C');
addTearDown(controller.dispose);
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
addTearDown(focusNode.dispose);
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: Align(
child: SizedBox(
width: 200,
height: 200,
child: CupertinoTextField(
autofocus: true,
focusNode: focusNode,
controller: controller,
),
),
),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
state = tester.state<EditableTextState>(find.byType(EditableText));
// Select the first 2 words.
state.renderEditable.selectPositionAt(
from: textOffsetToPosition(tester, 0),
to: textOffsetToPosition(tester, 2),
cause: SelectionChangedCause.tap,
);
final Offset midSelection = textOffsetToPosition(tester, 2);
// Right click the selection.
final TestGesture gesture = await tester.startGesture(
midSelection,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(find.text('Copy'), findsOneWidget);
// Copy the first word.
await tester.tap(find.text('Copy'));
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
},
variant: TargetPlatformVariant.all(),
skip: kIsWeb, // [intended] The toolbar isn't rendered by Flutter on the web, it's rendered by the browser.
);
testWidgets("Tapping on border doesn't lose focus",
(WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
addTearDown(focusNode.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: SizedBox(
width: 100,
height: 100,
child: CupertinoTextField(
autofocus: true,
focusNode: focusNode,
),
),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
final Rect borderBox = tester.getRect(find.byType(CupertinoTextField));
// Tap just inside the border, but not inside the EditableText.
await tester.tapAt(borderBox.topLeft + const Offset(1, 1));
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
}, variant: TargetPlatformVariant.all());
});
testWidgets('Can drag handles to change selection correctly in multiline', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
style: const TextStyle(color: Colors.black, fontSize: 34.0),
maxLines: 3,
),
),
),
);
const String testValue =
'First line of text is\n'
'Second line goes until\n'
'Third line of stuff';
const String cutValue =
'First line of text is\n'
'Second until\n'
'Third line of stuff';
await tester.enterText(find.byType(CupertinoTextField), testValue);
// Skip past scrolling animation.
await tester.pump();
await tester.pumpAndSettle(const Duration(milliseconds: 200));
// 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'));
expect(firstPos.dx, secondPos.dx);
expect(firstPos.dx, thirdPos.dx);
expect(firstPos.dy, lessThan(secondPos.dy));
expect(secondPos.dy, lessThan(thirdPos.dy));
// Double tap on the 'n' in 'until' to select the word.
final Offset untilPos = textOffsetToPosition(tester, testValue.indexOf('until')+1);
await tester.tapAt(untilPos);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(untilPos);
await tester.pumpAndSettle();
// Skip past the frame where the opacity is zero.
await tester.pump(const Duration(milliseconds: 200));
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);
final Offset offsetFromEndPointToMiddlePoint = Offset(0.0, -renderEditable.preferredLineHeight / 2);
// Drag the left handle to just after 'Second', still on the second line.
Offset handlePos = endpoints[0].point + offsetFromEndPointToMiddlePoint;
Offset newHandlePos = textOffsetToPosition(tester, testValue.indexOf('Second') + 6) + offsetFromEndPointToMiddlePoint;
TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 28);
expect(controller.selection.extentOffset, 44);
// Drag the right handle to just after 'goes', still on the second line.
handlePos = endpoints[1].point + offsetFromEndPointToMiddlePoint;
newHandlePos = textOffsetToPosition(tester, testValue.indexOf('goes') + 4) + offsetFromEndPointToMiddlePoint;
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, 28);
expect(controller.selection.extentOffset, 38);
if (!isContextMenuProvidedByPlatform) {
await tester.tap(find.text('Cut'));
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.text, cutValue);
}
});
testWidgets('placeholder style overflow works', (WidgetTester tester) async {
final String placeholder = 'hint text' * 20;
const TextStyle placeholderStyle = TextStyle(
fontSize: 14.0,
overflow: TextOverflow.fade,
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
placeholder: placeholder,
placeholderStyle: placeholderStyle,
),
),
),
);
await tester.pumpAndSettle();
final Finder placeholderFinder = find.text(placeholder);
final Text placeholderWidget = tester.widget(placeholderFinder);
expect(placeholderWidget.overflow, placeholderStyle.overflow);
expect(placeholderWidget.style!.overflow, placeholderStyle.overflow);
});
testWidgets('tapping on a misspelled word on iOS hides the handles and shows red selection', (WidgetTester tester) async {
tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue =
true;
// The default derived color for the iOS text selection highlight.
const Color defaultSelectionColor = Color(0x33007aff);
final TextEditingController controller = TextEditingController(
text: 'test test testt',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
spellCheckConfiguration:
const SpellCheckConfiguration(
misspelledTextStyle: CupertinoTextField.cupertinoMisspelledTextStyle,
spellCheckSuggestionsToolbarBuilder: CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder,
),
),
),
),
);
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
state.spellCheckResults = SpellCheckResults(
controller.value.text,
const <SuggestionSpan>[
SuggestionSpan(TextRange(start: 10, end: 15), <String>['test']),
]);
// Double tapping a non-misspelled word shows the normal blue selection and
// the selection handles.
expect(state.selectionOverlay, isNull);
await tester.tapAt(textOffsetToPosition(tester, 2));
await tester.pump(const Duration(milliseconds: 50));
expect(state.selectionOverlay!.handlesAreVisible, isFalse);
await tester.tapAt(textOffsetToPosition(tester, 2));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 4),
);
expect(state.selectionOverlay!.handlesAreVisible, isTrue);
expect(state.renderEditable.selectionColor, defaultSelectionColor);
// Single tapping a non-misspelled word shows a collpased cursor.
await tester.tapAt(textOffsetToPosition(tester, 7));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 9, affinity: TextAffinity.upstream),
);
expect(state.selectionOverlay!.handlesAreVisible, isFalse);
expect(state.renderEditable.selectionColor, defaultSelectionColor);
// Single tapping a misspelled word selects it in red with no handles.
await tester.tapAt(textOffsetToPosition(tester, 13));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 10, extentOffset: 15),
);
expect(state.selectionOverlay!.handlesAreVisible, isFalse);
expect(
state.renderEditable.selectionColor,
CupertinoTextField.kMisspelledSelectionColor,
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
skip: kIsWeb, // [intended]
);
testWidgets('text selection toolbar is hidden on tap down', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'blah1 blah2',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsNothing);
TestGesture gesture = await tester.startGesture(
textOffsetToPosition(tester, 8),
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsOneWidget);
gesture = await tester.startGesture(
textOffsetToPosition(tester, 2),
kind: PointerDeviceKind.mouse,
);
await tester.pump();
// After the gesture is down but not up, the toolbar is already gone.
expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsNothing);
await gesture.up();
await tester.pumpAndSettle();
expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsNothing);
},
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Does not shrink in height when enters text when there is large single-line placeholder', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/133241.
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Align(
alignment: Alignment.topCenter,
child: CupertinoTextField(
placeholderStyle: const TextStyle(fontSize: 100),
placeholder: 'p',
controller: controller,
),
),
),
);
final Rect rectWithPlaceholder = tester.getRect(find.byType(CupertinoTextField));
controller.value = const TextEditingValue(text: 'input');
await tester.pump();
final Rect rectWithText = tester.getRect(find.byType(CupertinoTextField));
expect(rectWithPlaceholder, rectWithText);
});
testWidgets('Does not match the height of a multiline placeholder', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
CupertinoApp(
home: Align(
alignment: Alignment.topCenter,
child: CupertinoTextField(
placeholderStyle: const TextStyle(fontSize: 100),
placeholder: 'p' * 50,
maxLines: null,
controller: controller,
),
),
),
);
final Rect rectWithPlaceholder = tester.getRect(find.byType(CupertinoTextField));
controller.value = const TextEditingValue(text: 'input');
await tester.pump();
final Rect rectWithText = tester.getRect(find.byType(CupertinoTextField));
// The text field is still top aligned.
expect(rectWithPlaceholder.top, rectWithText.top);
// But after entering text the text field should shrink since the
// placeholder text is huge and multiline.
expect(rectWithPlaceholder.height, greaterThan(rectWithText.height));
// But still should be taller than or the same height of the first line of
// placeholder.
expect(rectWithText.height, greaterThan(100));
});
testWidgets('Start the floating cursor on long tap', (WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
final TextEditingController controller = TextEditingController(
text: 'abcd',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RepaintBoundary(
key: const ValueKey<int>(1),
child: CupertinoTextField(
autofocus: true,
controller: controller,
),
)
),
),
),
);
// Wait for autofocus.
await tester.pumpAndSettle();
final Offset textFieldCenter = tester.getCenter(find.byType(CupertinoTextField));
final TestGesture gesture = await tester.startGesture(textFieldCenter);
await tester.pump(kLongPressTimeout);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_floating_cursor.regular_and_floating_both.cupertino.0.png'),
);
await gesture.moveTo(Offset(10, textFieldCenter.dy));
await tester.pump();
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_floating_cursor.only_floating_cursor.cupertino.0.png'),
);
await gesture.up();
EditableText.debugDeterministicCursor = false;
},
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
}