
This PR adds framework support for the Search Web feature in iOS. https://github.com/flutter/flutter/assets/36148254/c159f0d9-8f14-45e7-b295-e065b0826fab The corresponding merged engine PR can be found [here](https://github.com/flutter/engine/pull/43324). This PR addresses https://github.com/flutter/flutter/issues/82907 More details are available in this [design doc](https://docs.google.com/document/d/1QizXwBiO-2REIcEovl5pK06BaLPOWYmNwOE5jactJZA/edit?resourcekey=0-1pb9mJiAq29Gesmt25GAug)
9805 lines
341 KiB
Dart
9805 lines
341 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, 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;
|
|
import '../widgets/live_text_utils.dart';
|
|
import '../widgets/semantics_tester.dart';
|
|
|
|
// On web, the context menu (aka toolbar) is provided by the browser.
|
|
const bool isContextMenuProvidedByPlatform = isBrowser;
|
|
|
|
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: '');
|
|
const Key key = ValueKey<String>('TextField');
|
|
final FocusNode focusNode = FocusNode();
|
|
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 (CupertinoTextField)', (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',
|
|
);
|
|
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.pump();
|
|
|
|
// 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('Look Up shows up on iOS only (TextField)', (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 ',
|
|
);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: TextField(
|
|
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.pump();
|
|
|
|
// 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 (CupertinoTextField)', (WidgetTester tester) async {
|
|
String? lastLookUp;
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
|
if (methodCall.method == 'SearchWeb.invoke') {
|
|
expect(methodCall.arguments, isA<String>());
|
|
lastLookUp = methodCall.arguments as String;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Test',
|
|
);
|
|
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.pump();
|
|
|
|
// 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(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 (TextField)', (WidgetTester tester) async {
|
|
String? lastLookUp;
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
|
if (methodCall.method == 'SearchWeb.invoke') {
|
|
expect(methodCall.arguments, isA<String>());
|
|
lastLookUp = methodCall.arguments as String;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Test ',
|
|
);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: TextField(
|
|
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.pump();
|
|
|
|
// 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(lastLookUp, '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',
|
|
);
|
|
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();
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'blah1 blah2',
|
|
);
|
|
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();
|
|
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();
|
|
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.pump();
|
|
|
|
// Focus and selection is active on first TextField, so the second TextFields
|
|
// selectionColor should be dropped.
|
|
await tester.tap(find.byKey(key1));
|
|
controller1.selection = const TextSelection(baseOffset: 0, extentOffset: 4);
|
|
await tester.pump();
|
|
expect(controller1.selection, selection);
|
|
expect(state1.widget.selectionColor, selectionColor);
|
|
expect(state2.widget.selectionColor, null);
|
|
|
|
// Focus and selection is active on second TextField, so the first TextFields
|
|
// selectionColor should be dropped.
|
|
await tester.tap(find.byKey(key2));
|
|
await tester.pump();
|
|
expect(state1.widget.selectionColor, null);
|
|
expect(state2.widget.selectionColor, selectionColor);
|
|
});
|
|
|
|
testWidgets(
|
|
'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 {
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
home: Center(
|
|
child: CupertinoTextField(
|
|
controller: TextEditingController(text: 'initial'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
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();
|
|
|
|
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();
|
|
expect(find.text('placeholder'), findsNothing);
|
|
},
|
|
);
|
|
|
|
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();
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
home: Center(
|
|
child: CupertinoTextField(
|
|
focusNode: focusNode,
|
|
prefix: const Icon(CupertinoIcons.add),
|
|
controller: TextEditingController(text: 'input'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
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();
|
|
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();
|
|
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();
|
|
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();
|
|
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();
|
|
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();
|
|
|
|
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",
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
home: Column(
|
|
children: <Widget>[
|
|
CupertinoTextField(
|
|
controller: controller,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.longPressAt(
|
|
tester.getTopRight(find.text("j'aime la poutine")),
|
|
);
|
|
await tester.pump();
|
|
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.pump();
|
|
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',
|
|
);
|
|
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.pump();
|
|
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',
|
|
);
|
|
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.pump();
|
|
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');
|
|
|
|
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.pump();
|
|
|
|
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.pump();
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
|
|
await tester.tap(find.text('Select All'));
|
|
await tester.pump();
|
|
|
|
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);
|
|
expect(find.text('field 2'), findsNothing);
|
|
}, 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',
|
|
);
|
|
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',
|
|
);
|
|
// 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.
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformIOS ? findsNWidgets(2) : findsNothing);
|
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
|
|
|
testWidgets(
|
|
'Tapping on a collapsed selection toggles the toolbar',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neigse Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
|
|
);
|
|
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
|
|
await tester.pumpWidget(
|
|
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);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
|
|
|
|
// Tap the 'v' position again to hide the toolbar.
|
|
await tester.tapAt(vPos);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.selection.isCollapsed, true);
|
|
expect(controller.selection.baseOffset, 35);
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
|
|
// Long press at the end of the first line to move the cursor to the end of the first line
|
|
// where the word wrap is. Since there is a word wrap here, and the direction of the text is LTR,
|
|
// the TextAffinity will be upstream and against the natural direction. The toolbar is also
|
|
// shown after a long press.
|
|
await tester.longPressAt(endPos);
|
|
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
|
expect(controller.selection.isCollapsed, true);
|
|
expect(controller.selection.baseOffset, 46);
|
|
expect(controller.selection.affinity, TextAffinity.upstream);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
|
|
|
|
// Tap at the same position to toggle the toolbar.
|
|
await tester.tapAt(endPos);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.selection.isCollapsed, true);
|
|
expect(controller.selection.baseOffset, 46);
|
|
expect(controller.selection.affinity, TextAffinity.upstream);
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
|
|
// Tap at the beginning of the second line to move the cursor to the front of the first word on the
|
|
// second line, where the word wrap is. Since there is a word wrap here, and the direction of the text is LTR,
|
|
// the TextAffinity will be downstream and following the natural direction. The toolbar will be hidden after this tap.
|
|
await tester.tapAt(begPos + Offset(0.0, lineHeight));
|
|
await tester.pumpAndSettle();
|
|
expect(controller.selection.isCollapsed, true);
|
|
expect(controller.selection.baseOffset, 46);
|
|
expect(controller.selection.affinity, TextAffinity.downstream);
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
|
|
);
|
|
|
|
testWidgets(
|
|
'Tapping on a non-collapsed selection toggles the toolbar and retains the selection',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
|
);
|
|
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
|
|
await tester.pumpWidget(
|
|
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),
|
|
);
|
|
|
|
// Selected text shows 5 toolbar buttons.
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(5));
|
|
|
|
// Tap the selected word to hide the toolbar and retain the selection.
|
|
await tester.tapAt(vPos);
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 24, extentOffset: 35),
|
|
);
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
|
|
// Tap the selected word to show the toolbar and retain the selection.
|
|
await tester.tapAt(vPos);
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 24, extentOffset: 35),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(5));
|
|
|
|
// 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',
|
|
);
|
|
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.pump();
|
|
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),
|
|
);
|
|
|
|
// Selected text shows 4 toolbar buttons.
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
|
|
|
|
// 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',
|
|
);
|
|
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.pump();
|
|
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),
|
|
);
|
|
|
|
if (isContextMenuProvidedByPlatform) {
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
} else {
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.macOS:
|
|
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
expect(find.byType(CupertinoButton), findsNWidgets(5));
|
|
}
|
|
}
|
|
},
|
|
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',
|
|
);
|
|
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.pump();
|
|
|
|
// 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();
|
|
|
|
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();
|
|
|
|
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();
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
|
|
},
|
|
);
|
|
|
|
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',
|
|
);
|
|
// 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),
|
|
);
|
|
|
|
// Selected text shows 3 toolbar buttons.
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(5) : findsNWidgets(3)));
|
|
}, 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',
|
|
);
|
|
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),
|
|
);
|
|
|
|
final Matcher matchToolbarButtons;
|
|
if (isContextMenuProvidedByPlatform) {
|
|
matchToolbarButtons = findsNothing;
|
|
} else {
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.macOS:
|
|
case TargetPlatform.iOS:
|
|
matchToolbarButtons = findsNWidgets(3);
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
matchToolbarButtons = findsNWidgets(4);
|
|
}
|
|
}
|
|
expect(find.byType(CupertinoButton), matchToolbarButtons);
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
// Still selected.
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 8, extentOffset: 12),
|
|
);
|
|
expect(find.byType(CupertinoButton), matchToolbarButtons);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'tap after a double tap select is not affected',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
|
);
|
|
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
|
|
// On macOS, we select the precise position of the tap.
|
|
final bool 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',
|
|
);
|
|
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',
|
|
);
|
|
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',
|
|
);
|
|
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',
|
|
);
|
|
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.
|
|
expect(find.byType(CupertinoButton), findsNWidgets(0));
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
// Still nothing selected and no selection menu.
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 35, extentOffset: 35),
|
|
);
|
|
expect(find.byType(CupertinoButton), findsNWidgets(0));
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'A read-only obscured CupertinoTextField is not selectable',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
|
);
|
|
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.
|
|
expect(find.byType(CupertinoButton), findsNWidgets(0));
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
// Still nothing selected and no selection menu.
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection.collapsed(offset: 35),
|
|
);
|
|
expect(find.byType(CupertinoButton), findsNWidgets(0));
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'An obscured CupertinoTextField is selectable by default',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
|
);
|
|
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 buttons.
|
|
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',
|
|
);
|
|
|
|
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.pump();
|
|
|
|
// 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.pump();
|
|
|
|
// 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',
|
|
);
|
|
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),
|
|
);
|
|
|
|
// Non-Collapsed toolbar shows 4 buttons.
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
|
|
},
|
|
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',
|
|
);
|
|
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),
|
|
);
|
|
|
|
// Collapsed toolbar shows 2 buttons.
|
|
expect(
|
|
find.byType(CupertinoButton),
|
|
isContextMenuProvidedByPlatform
|
|
? findsNothing
|
|
: defaultTargetPlatform == TargetPlatform.iOS
|
|
? findsNWidgets(2)
|
|
: findsNWidgets(1),
|
|
);
|
|
},
|
|
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',
|
|
);
|
|
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.pump(const Duration(milliseconds: 50));
|
|
|
|
final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
|
|
if (kIsWeb) {
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
} else {
|
|
expect(find.byType(CupertinoButton), findsNWidgets(isTargetPlatformIOS ? 2 : 1));
|
|
}
|
|
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 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.
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
}, 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',
|
|
);
|
|
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.
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
|
|
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),
|
|
);
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
|
|
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),
|
|
);
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// The selection isn't affected by the gesture lift.
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 0, extentOffset: 23, affinity: TextAffinity.upstream),
|
|
);
|
|
// The toolbar now shows up.
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
|
|
},
|
|
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
|
);
|
|
|
|
testWidgets(
|
|
'long press drag on a focused TextField moves the cursor under the drag and shows toolbar on lift on Apple platforms',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
|
);
|
|
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.
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
|
|
await gesture.moveBy(const Offset(50, 0));
|
|
await tester.pump();
|
|
|
|
// The selection position is now moved with the drag.
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection.collapsed(offset: 6, affinity: TextAffinity.upstream),
|
|
);
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
|
|
await gesture.moveBy(const Offset(50, 0));
|
|
await tester.pump();
|
|
|
|
// The selection position is now moved with the drag.
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection.collapsed(offset: 9, affinity: TextAffinity.upstream),
|
|
);
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// The selection isn't affected by the gesture lift.
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection.collapsed(offset: 9, affinity: TextAffinity.upstream),
|
|
);
|
|
// The toolbar now shows up.
|
|
expect(
|
|
find.byType(CupertinoButton),
|
|
isContextMenuProvidedByPlatform
|
|
? findsNothing
|
|
: defaultTargetPlatform == TargetPlatform.iOS
|
|
? findsNWidgets(2)
|
|
: findsNWidgets(1),
|
|
);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
|
);
|
|
|
|
testWidgets('long press drag can edge scroll on non-Apple platforms', (WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
|
|
);
|
|
await tester.pumpWidget(
|
|
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.
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
|
|
|
|
lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
|
const TextSelection.collapsed(offset: 66), // Last character's position.
|
|
);
|
|
|
|
expect(lastCharEndpoint.length, 1);
|
|
// The last character is now on screen near the right edge.
|
|
expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(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',
|
|
);
|
|
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.
|
|
final int toolbarButtons;
|
|
if (isContextMenuProvidedByPlatform) {
|
|
toolbarButtons = 0;
|
|
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
|
|
toolbarButtons = 2;
|
|
} else {
|
|
// MacOS has no 'Select all' button.
|
|
toolbarButtons = 1;
|
|
}
|
|
expect(find.byType(CupertinoButton), findsNWidgets(toolbarButtons));
|
|
|
|
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',
|
|
);
|
|
// 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.
|
|
final int toolbarButtons;
|
|
if (isContextMenuProvidedByPlatform) {
|
|
toolbarButtons = 0;
|
|
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
|
|
toolbarButtons = 2;
|
|
} else {
|
|
// MacOS has no 'Select all' button.
|
|
toolbarButtons = 1;
|
|
}
|
|
expect(find.byType(CupertinoButton), findsNWidgets(toolbarButtons));
|
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
|
|
|
testWidgets(
|
|
'double tap after a long tap is not affected',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
|
);
|
|
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
|
|
// On macOS, we select the precise position of the tap.
|
|
final bool 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.pump(const Duration(milliseconds: 50));
|
|
|
|
expect(controller.selection.isCollapsed, isTrue);
|
|
expect(controller.selection.baseOffset, 3);
|
|
if (isContextMenuProvidedByPlatform) {
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
} else {
|
|
expect(find.byType(CupertinoButton), isTargetPlatformIOS ? findsNWidgets(2) : findsNWidgets(1));
|
|
}
|
|
|
|
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),
|
|
);
|
|
// Shows toolbar.
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(5) : findsNWidgets(3)));
|
|
}, 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',
|
|
);
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
home: Center(
|
|
child: CupertinoTextField(
|
|
controller: controller,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
|
final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
|
|
|
|
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),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(5));
|
|
|
|
// 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),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(5) : findsNWidgets(3)));
|
|
|
|
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),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(5) : findsNWidgets(3)));
|
|
}, 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();
|
|
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();
|
|
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 tap chains work on Non-Apple mobile platforms',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
|
);
|
|
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),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
|
|
|
|
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 0, extentOffset: 35),
|
|
);
|
|
// Triple tap selecting the same paragraph somewhere else is fine.
|
|
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
// First tap hides the toolbar and moves the selection.
|
|
expect(controller.selection.isCollapsed, true);
|
|
expect(controller.selection.baseOffset, 6);
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
// Second tap shows the toolbar and selects the word.
|
|
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
|
await tester.pump();
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 0, extentOffset: 7),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
|
|
|
|
// Third tap shows the toolbar and selects the paragraph.
|
|
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 0, extentOffset: 35),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
|
|
|
|
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
// First tap moved the cursor and hid the toolbar.
|
|
expect(controller.selection.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),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
|
|
|
|
// Third tap selects the paragraph and shows the toolbar.
|
|
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 0, extentOffset: 35),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }),
|
|
);
|
|
|
|
testWidgets(
|
|
'triple tap chains work on Apple platforms',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Atwater Peel Sherbrooke Bonaventure\nThe fox jumped over the fence.',
|
|
);
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
home: Center(
|
|
child: Center(
|
|
child: CupertinoTextField(
|
|
controller: controller,
|
|
maxLines: null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
|
final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
|
|
|
|
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
expect(controller.selection.isCollapsed, true);
|
|
expect(controller.selection.baseOffset, 7);
|
|
|
|
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
|
await tester.pump();
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 0, extentOffset: 7),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(5));
|
|
|
|
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 0, extentOffset: 36),
|
|
);
|
|
|
|
// Triple tap selecting the same paragraph somewhere else is fine.
|
|
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
// First tap hides the toolbar and retains the selection.
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 0, extentOffset: 36),
|
|
);
|
|
expect(find.byType(CupertinoButton), findsNothing);
|
|
|
|
// Second tap shows the toolbar and selects the word.
|
|
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
|
await tester.pump();
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 0, extentOffset: 7),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(5) : findsNWidgets(3)));
|
|
|
|
// Third tap shows the toolbar and selects the paragraph.
|
|
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
expect(
|
|
controller.selection,
|
|
const TextSelection(baseOffset: 0, extentOffset: 36),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(5) : findsNWidgets(3)));
|
|
|
|
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),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(5) : findsNWidgets(3)));
|
|
|
|
// 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),
|
|
);
|
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(5) : findsNWidgets(3)));
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
|
|
);
|
|
|
|
testWidgets(
|
|
'triple click chains work',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(
|
|
text: testValueA,
|
|
);
|
|
await tester.pumpWidget(
|
|
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,
|
|
);
|
|
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,
|
|
);
|
|
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,
|
|
);
|
|
|
|
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();
|
|
|
|
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();
|
|
|
|
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();
|
|
|
|
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();
|
|
|
|
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,
|
|
);
|
|
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,
|
|
);
|
|
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 positio.
|
|
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,
|
|
);
|
|
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',
|
|
);
|
|
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.
|
|
final Matcher matchToolbarButtons;
|
|
if (isContextMenuProvidedByPlatform) {
|
|
matchToolbarButtons = findsNothing;
|
|
} else {
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.macOS:
|
|
case TargetPlatform.iOS:
|
|
matchToolbarButtons = findsNWidgets(3);
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
matchToolbarButtons = findsNWidgets(4);
|
|
}
|
|
}
|
|
expect(find.byType(CupertinoButton), matchToolbarButtons);
|
|
});
|
|
|
|
testWidgets('force press on unsupported devices falls back to tap', (WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
|
);
|
|
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
|
|
// On macOS, we select the precise position of the tap.
|
|
final bool 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',
|
|
);
|
|
// 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',
|
|
);
|
|
|
|
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',
|
|
);
|
|
|
|
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();
|
|
|
|
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();
|
|
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();
|
|
|
|
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();
|
|
|
|
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();
|
|
|
|
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'));
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
|
|
);
|
|
|
|
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - multiline', (WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController();
|
|
|
|
await tester.pumpWidget(
|
|
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'));
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
|
|
);
|
|
|
|
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - ListView', (WidgetTester tester) async {
|
|
// This is a regression test for
|
|
// https://github.com/flutter/flutter/issues/122519
|
|
final TextEditingController controller = TextEditingController();
|
|
|
|
await tester.pumpWidget(
|
|
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();
|
|
|
|
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'));
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }),
|
|
);
|
|
|
|
testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async {
|
|
int selectionChangedCount = 0;
|
|
const String testValue = 'abc def ghi';
|
|
final TextEditingController controller = TextEditingController(text: testValue);
|
|
|
|
controller.addListener(() {
|
|
selectionChangedCount++;
|
|
});
|
|
|
|
await tester.pumpWidget(
|
|
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',
|
|
);
|
|
|
|
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',
|
|
);
|
|
|
|
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',
|
|
);
|
|
|
|
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',
|
|
);
|
|
|
|
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',
|
|
);
|
|
|
|
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',
|
|
);
|
|
|
|
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();
|
|
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();
|
|
|
|
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);
|
|
|
|
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();
|
|
final TextEditingController controller = TextEditingController();
|
|
|
|
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 {
|
|
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: TextEditingController(text: 'lorem'),
|
|
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);
|
|
final ScrollController scrollController = ScrollController();
|
|
|
|
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);
|
|
final ScrollController scrollController = ScrollController();
|
|
|
|
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.
|
|
Offset(26.0, bottomLeftSelectionPosition.dy + 8 + 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 + 45, 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 = 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 + 45, 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 = TextEditingController(text: List<String>.filled(200, 'a').join());
|
|
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 - 45, 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(' '));
|
|
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 - 45, 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'));
|
|
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 - 45, 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,
|
|
);
|
|
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',
|
|
);
|
|
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();
|
|
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();
|
|
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();
|
|
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();
|
|
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();
|
|
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();
|
|
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();
|
|
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();
|
|
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');
|
|
|
|
// 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);
|
|
}, 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);
|
|
}, 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('text selection style 1', (WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwassssup!',
|
|
);
|
|
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.pump(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!',
|
|
);
|
|
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.pump(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(
|
|
enabled: true,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
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 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: OverflowWidgetTextEditingController(),
|
|
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',
|
|
);
|
|
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',
|
|
);
|
|
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',
|
|
);
|
|
final FocusNode focusNode = FocusNode();
|
|
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',
|
|
);
|
|
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',
|
|
);
|
|
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',
|
|
);
|
|
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',
|
|
);
|
|
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',
|
|
);
|
|
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();
|
|
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();
|
|
final TextEditingController controller = TextEditingController(
|
|
text: 'first second',
|
|
);
|
|
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: '');
|
|
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: '');
|
|
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));
|
|
|
|
expect(
|
|
defaultCupertinoTextField.magnifierConfiguration!.magnifierBuilder(
|
|
context,
|
|
MagnifierController(),
|
|
ValueNotifier<MagnifierInfo>(MagnifierInfo.empty),
|
|
),
|
|
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));
|
|
|
|
expect(
|
|
editableText.magnifierConfiguration.magnifierBuilder(
|
|
context,
|
|
MagnifierController(),
|
|
ValueNotifier<MagnifierInfo>(MagnifierInfo.empty),
|
|
),
|
|
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));
|
|
|
|
expect(
|
|
editableText.magnifierConfiguration.magnifierBuilder(
|
|
context,
|
|
MagnifierController(),
|
|
ValueNotifier<MagnifierInfo>(MagnifierInfo.empty),
|
|
),
|
|
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();
|
|
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();
|
|
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();
|
|
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();
|
|
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');
|
|
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');
|
|
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');
|
|
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
|
|
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');
|
|
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();
|
|
|
|
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',
|
|
);
|
|
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',
|
|
);
|
|
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 }),
|
|
);
|
|
}
|