| // 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 '../rendering/mock_canvas.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('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 3 toolbar buttons. |
| expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4)); |
| |
| // 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(4)); |
| |
| // 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 3 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(4)); |
| } |
| } |
| }, |
| 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(4) : 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), |