| // 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. |
| |
| import 'dart:convert' show jsonDecode; |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../rendering/mock_canvas.dart'; |
| import '../widgets/clipboard_utils.dart'; |
| import 'editable_text_utils.dart'; |
| import 'live_text_utils.dart'; |
| import 'semantics_tester.dart'; |
| |
| Matcher matchesMethodCall(String method, { dynamic args }) => _MatchesMethodCall(method, arguments: args == null ? null : wrapMatcher(args)); |
| |
| class _MatchesMethodCall extends Matcher { |
| const _MatchesMethodCall(this.name, {this.arguments}); |
| |
| final String name; |
| final Matcher? arguments; |
| |
| @override |
| bool matches(dynamic item, Map<dynamic, dynamic> matchState) { |
| if (item is MethodCall && item.method == name) { |
| return arguments?.matches(item.arguments, matchState) ?? true; |
| } |
| return false; |
| } |
| |
| @override |
| Description describe(Description description) { |
| final Description newDescription = description.add('has method name: ').addDescriptionOf(name); |
| if (arguments != null) { |
| newDescription.add(' with arguments: ').addDescriptionOf(arguments); |
| } |
| return newDescription; |
| } |
| } |
| |
| late TextEditingController controller; |
| final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Node'); |
| final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'EditableText Scope Node'); |
| const TextStyle textStyle = TextStyle(); |
| const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00); |
| |
| enum HandlePositionInViewport { |
| leftEdge, rightEdge, within, |
| } |
| |
| typedef _VoidFutureCallback = Future<void> Function(); |
| |
| TextEditingValue collapsedAtEnd(String text) { |
| return TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: text.length), |
| ); |
| } |
| |
| void main() { |
| final MockClipboard mockClipboard = MockClipboard(); |
| TestWidgetsFlutterBinding.ensureInitialized() |
| .defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall); |
| |
| setUp(() async { |
| debugResetSemanticsIdCounter(); |
| controller = TextEditingController(); |
| // Fill the clipboard so that the Paste option is available in the text |
| // selection menu. |
| await Clipboard.setData(const ClipboardData(text: 'Clipboard data')); |
| }); |
| |
| tearDown(() { |
| controller.dispose(); |
| }); |
| |
| // Tests that the desired keyboard action button is requested. |
| // |
| // More technically, when an EditableText is given a particular [action], Flutter |
| // requests [serializedActionName] when attaching to the platform's input |
| // system. |
| Future<void> desiredKeyboardActionIsRequested({ |
| required WidgetTester tester, |
| TextInputAction? action, |
| String serializedActionName = '', |
| }) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| textInputAction: action, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect(tester.testTextInput.setClientArgs!['inputAction'], equals(serializedActionName)); |
| } |
| |
| testWidgets( |
| 'Tapping the Live Text button calls onLiveTextInput', |
| (WidgetTester tester) async { |
| bool invokedLiveTextInputSuccessfully = false; |
| final GlobalKey key = GlobalKey(); |
| final TextEditingController controller = TextEditingController(text: ''); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Align( |
| alignment: Alignment.topLeft, |
| child: SizedBox( |
| width: 400, |
| child: EditableText( |
| maxLines: 10, |
| controller: controller, |
| showSelectionHandles: true, |
| autofocus: true, |
| focusNode: FocusNode(), |
| style: Typography.material2018().black.subtitle1!, |
| cursorColor: Colors.blue, |
| backgroundCursorColor: Colors.grey, |
| keyboardType: TextInputType.text, |
| textAlign: TextAlign.right, |
| selectionControls: materialTextSelectionHandleControls, |
| contextMenuBuilder: ( |
| BuildContext context, |
| EditableTextState editableTextState, |
| ) { |
| return CupertinoAdaptiveTextSelectionToolbar.editable( |
| key: key, |
| clipboardStatus: ClipboardStatus.pasteable, |
| onCopy: null, |
| onCut: null, |
| onPaste: null, |
| onSelectAll: null, |
| onLookUp: null, |
| onLiveTextInput: () { |
| invokedLiveTextInputSuccessfully = true; |
| }, |
| anchors: const TextSelectionToolbarAnchors(primaryAnchor: Offset.zero), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pump(); // Wait for autofocus to take effect. |
| |
| expect(find.byKey(key), findsNothing); |
| |
| // Long-press to bring up the context menu. |
| final Finder textFinder = find.byType(EditableText); |
| await tester.longPress(textFinder); |
| tester.state<EditableTextState>(textFinder).showToolbar(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byKey(key), findsOneWidget); |
| expect(findLiveTextButton(), findsOneWidget); |
| await tester.tap(findLiveTextButton()); |
| await tester.pump(); |
| expect(invokedLiveTextInputSuccessfully, isTrue); |
| }, |
| skip: kIsWeb, // [intended] |
| ); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/126312. |
| testWidgets('when open input connection in didUpdateWidget, should not throw', (WidgetTester tester) async { |
| final Key key = GlobalKey(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| key: key, |
| backgroundCursorColor: Colors.grey, |
| controller: TextEditingController(text: 'blah blah'), |
| focusNode: focusNode, |
| readOnly: true, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| focusNode.requestFocus(); |
| await tester.pump(); |
| |
| // Reparent the EditableText, so that the parent has not yet been laid |
| // out when didUpdateWidget is called. |
| await tester.pumpWidget( |
| MaterialApp( |
| home: FractionalTranslation( |
| translation: const Offset(0.1, 0.1), |
| child: EditableText( |
| key: key, |
| backgroundCursorColor: Colors.grey, |
| controller: TextEditingController(text: 'blah blah'), |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ), |
| ); |
| }); |
| |
| testWidgets('Text with selection can be shown on the screen when the keyboard shown', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/119628 |
| addTearDown(tester.view.reset); |
| |
| final ScrollController scrollController = ScrollController(); |
| final TextEditingController textController = TextEditingController.fromValue( |
| const TextEditingValue(text: 'I love flutter'), |
| ); |
| |
| final Widget widget = MaterialApp( |
| home: Scaffold( |
| body: SingleChildScrollView( |
| controller: scrollController, |
| child: Column( |
| children: <Widget>[ |
| const SizedBox(height: 1000.0), |
| SizedBox( |
| height: 20.0, |
| child: EditableText( |
| controller: textController, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: const TextStyle(), |
| cursorColor: Colors.red, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpWidget(widget); |
| |
| await tester.showKeyboard(find.byType(EditableText)); |
| tester.view.viewInsets = const FakeViewPadding(bottom: 500); |
| textController.selection = TextSelection( |
| baseOffset: 0, |
| extentOffset: textController.text.length, |
| ); |
| |
| await tester.pump(); |
| |
| // The offset of the scrollController should change immediately after view changes its metrics. |
| final double offsetAfter = scrollController.offset; |
| expect(offsetAfter, isNot(0.0)); |
| }); |
| |
| // Related issue: https://github.com/flutter/flutter/issues/98115 |
| testWidgets('ScheduleShowCaretOnScreen with no animation when the view changes metrics', (WidgetTester tester) async { |
| addTearDown(tester.view.reset); |
| |
| final ScrollController scrollController = ScrollController(); |
| final Widget widget = MaterialApp( |
| home: Scaffold( |
| body: SingleChildScrollView( |
| controller: scrollController, |
| child: Column( |
| children: <Widget>[ |
| Column( |
| children: List<Widget>.generate( |
| 5, |
| (_) { |
| return Container( |
| height: 1200.0, |
| color: Colors.black12, |
| ); |
| }, |
| ), |
| ), |
| SizedBox( |
| height: 20, |
| child: EditableText( |
| controller: TextEditingController(), |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: const TextStyle(), |
| cursorColor: Colors.red, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpWidget(widget); |
| await tester.showKeyboard(find.byType(EditableText)); |
| tester.view.viewInsets = const FakeViewPadding(bottom: 500); |
| await tester.pump(); |
| |
| // The offset of the scrollController should change immediately after view changes its metrics. |
| final double offsetAfter = scrollController.offset; |
| expect(offsetAfter, isNot(0.0)); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/34538. |
| testWidgets('RTL arabic correct caret placement after trailing whitespace', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.rtl, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| backgroundCursorColor: Colors.blue, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.idle(); |
| |
| final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| // Simulates Gboard Persian input. |
| state.updateEditingValue(const TextEditingValue(text: 'Ú¯', selection: TextSelection.collapsed(offset: 1))); |
| await tester.pump(); |
| double previousCaretXPosition = state.renderEditable.getLocalRectForCaret(state.textEditingValue.selection.base).left; |
| |
| state.updateEditingValue(const TextEditingValue(text: 'گی', selection: TextSelection.collapsed(offset: 2))); |
| await tester.pump(); |
| double caretXPosition = state.renderEditable.getLocalRectForCaret(state.textEditingValue.selection.base).left; |
| expect(caretXPosition, lessThan(previousCaretXPosition)); |
| previousCaretXPosition = caretXPosition; |
| |
| state.updateEditingValue(const TextEditingValue(text: 'گیگ', selection: TextSelection.collapsed(offset: 3))); |
| await tester.pump(); |
| caretXPosition = state.renderEditable.getLocalRectForCaret(state.textEditingValue.selection.base).left; |
| expect(caretXPosition, lessThan(previousCaretXPosition)); |
| previousCaretXPosition = caretXPosition; |
| |
| // Enter a whitespace in a RTL input field moves the caret to the left. |
| state.updateEditingValue(const TextEditingValue(text: 'گیگ ', selection: TextSelection.collapsed(offset: 4))); |
| await tester.pump(); |
| caretXPosition = state.renderEditable.getLocalRectForCaret(state.textEditingValue.selection.base).left; |
| expect(caretXPosition, lessThan(previousCaretXPosition)); |
| |
| expect(state.currentTextEditingValue.text, equals('گیگ ')); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/78550. |
| |
| testWidgets('has expected defaults', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ); |
| |
| final EditableText editableText = |
| tester.firstWidget(find.byType(EditableText)); |
| expect(editableText.maxLines, equals(1)); |
| expect(editableText.obscureText, isFalse); |
| expect(editableText.autocorrect, isTrue); |
| expect(editableText.enableSuggestions, isTrue); |
| expect(editableText.enableIMEPersonalizedLearning, isTrue); |
| expect(editableText.textAlign, TextAlign.start); |
| expect(editableText.cursorWidth, 2.0); |
| expect(editableText.cursorHeight, isNull); |
| expect(editableText.textHeightBehavior, isNull); |
| }); |
| |
| testWidgets('when backgroundCursorColor is updated, RenderEditable should be updated', (WidgetTester tester) async { |
| Widget buildWidget(Color backgroundCursorColor) { |
| return MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: backgroundCursorColor, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildWidget(Colors.red)); |
| await tester.pumpWidget(buildWidget(Colors.green)); |
| |
| final RenderEditable render = tester.allRenderObjects.whereType<RenderEditable>().first; |
| expect(render.backgroundCursorColor, Colors.green); |
| }); |
| |
| testWidgets('text keyboard is requested when maxLines is default', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| final EditableText editableText = |
| tester.firstWidget(find.byType(EditableText)); |
| expect(editableText.maxLines, equals(1)); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect((tester.testTextInput.setClientArgs!['inputType'] as Map<String, dynamic>)['name'], equals('TextInputType.text')); |
| expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.done')); |
| }); |
| |
| testWidgets('Keyboard is configured for "unspecified" action when explicitly requested', (WidgetTester tester) async { |
| await desiredKeyboardActionIsRequested( |
| tester: tester, |
| action: TextInputAction.unspecified, |
| serializedActionName: 'TextInputAction.unspecified', |
| ); |
| }); |
| |
| testWidgets('Keyboard is configured for "none" action when explicitly requested', (WidgetTester tester) async { |
| await desiredKeyboardActionIsRequested( |
| tester: tester, |
| action: TextInputAction.none, |
| serializedActionName: 'TextInputAction.none', |
| ); |
| }); |
| |
| testWidgets('Keyboard is configured for "done" action when explicitly requested', (WidgetTester tester) async { |
| await desiredKeyboardActionIsRequested( |
| tester: tester, |
| action: TextInputAction.done, |
| serializedActionName: 'TextInputAction.done', |
| ); |
| }); |
| |
| testWidgets('Keyboard is configured for "send" action when explicitly requested', (WidgetTester tester) async { |
| await desiredKeyboardActionIsRequested( |
| tester: tester, |
| action: TextInputAction.send, |
| serializedActionName: 'TextInputAction.send', |
| ); |
| }); |
| |
| testWidgets('Keyboard is configured for "go" action when explicitly requested', (WidgetTester tester) async { |
| await desiredKeyboardActionIsRequested( |
| tester: tester, |
| action: TextInputAction.go, |
| serializedActionName: 'TextInputAction.go', |
| ); |
| }); |
| |
| testWidgets('Keyboard is configured for "search" action when explicitly requested', (WidgetTester tester) async { |
| await desiredKeyboardActionIsRequested( |
| tester: tester, |
| action: TextInputAction.search, |
| serializedActionName: 'TextInputAction.search', |
| ); |
| }); |
| |
| testWidgets('Keyboard is configured for "send" action when explicitly requested', (WidgetTester tester) async { |
| await desiredKeyboardActionIsRequested( |
| tester: tester, |
| action: TextInputAction.send, |
| serializedActionName: 'TextInputAction.send', |
| ); |
| }); |
| |
| testWidgets('Keyboard is configured for "next" action when explicitly requested', (WidgetTester tester) async { |
| await desiredKeyboardActionIsRequested( |
| tester: tester, |
| action: TextInputAction.next, |
| serializedActionName: 'TextInputAction.next', |
| ); |
| }); |
| |
| testWidgets('Keyboard is configured for "previous" action when explicitly requested', (WidgetTester tester) async { |
| await desiredKeyboardActionIsRequested( |
| tester: tester, |
| action: TextInputAction.previous, |
| serializedActionName: 'TextInputAction.previous', |
| ); |
| }); |
| |
| testWidgets('Keyboard is configured for "continue" action when explicitly requested', (WidgetTester tester) async { |
| await desiredKeyboardActionIsRequested( |
| tester: tester, |
| action: TextInputAction.continueAction, |
| serializedActionName: 'TextInputAction.continueAction', |
| ); |
| }); |
| |
| testWidgets('Keyboard is configured for "join" action when explicitly requested', (WidgetTester tester) async { |
| await desiredKeyboardActionIsRequested( |
| tester: tester, |
| action: TextInputAction.join, |
| serializedActionName: 'TextInputAction.join', |
| ); |
| }); |
| |
| testWidgets('Keyboard is configured for "route" action when explicitly requested', (WidgetTester tester) async { |
| await desiredKeyboardActionIsRequested( |
| tester: tester, |
| action: TextInputAction.route, |
| serializedActionName: 'TextInputAction.route', |
| ); |
| }); |
| |
| testWidgets('Keyboard is configured for "emergencyCall" action when explicitly requested', (WidgetTester tester) async { |
| await desiredKeyboardActionIsRequested( |
| tester: tester, |
| action: TextInputAction.emergencyCall, |
| serializedActionName: 'TextInputAction.emergencyCall', |
| ); |
| }); |
| |
| testWidgets('insertContent does not throw and parses data correctly', (WidgetTester tester) async { |
| String? latestUri; |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| contentInsertionConfiguration: ContentInsertionConfiguration( |
| onContentInserted: (KeyboardInsertedContent content) { |
| latestUri = content.uri; |
| }, |
| allowedMimeTypes: const <String>['image/gif'], |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.enterText(find.byType(EditableText), 'test'); |
| await tester.idle(); |
| |
| const String uri = 'content://com.google.android.inputmethod.latin.fileprovider/test.gif'; |
| final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{ |
| 'args': <dynamic>[ |
| -1, |
| 'TextInputAction.commitContent', |
| jsonDecode('{"mimeType": "image/gif", "data": [0,1,0,1,0,1,0,0,0], "uri": "$uri"}'), |
| ], |
| 'method': 'TextInputClient.performAction', |
| }); |
| |
| Object? error; |
| try { |
| await tester.binding.defaultBinaryMessenger.handlePlatformMessage( |
| 'flutter/textinput', |
| messageBytes, |
| (ByteData? _) {}, |
| ); |
| } catch (e) { |
| error = e; |
| } |
| expect(error, isNull); |
| expect(latestUri, equals(uri)); |
| }); |
| |
| testWidgets('onAppPrivateCommand does not throw', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| |
| final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{ |
| 'args': <dynamic>[ |
| -1, // The magic clint id that points to the current client. |
| jsonDecode('{"action": "actionCommand", "data": {"input_context" : "abcdefg"}}'), |
| ], |
| 'method': 'TextInputClient.performPrivateCommand', |
| }); |
| |
| Object? error; |
| try { |
| await tester.binding.defaultBinaryMessenger.handlePlatformMessage( |
| 'flutter/textinput', |
| messageBytes, |
| (ByteData? _) {}, |
| ); |
| } catch (e) { |
| error = e; |
| } |
| expect(error, isNull); |
| }); |
| |
| group('Infer keyboardType from autofillHints', () { |
| testWidgets( |
| 'infer keyboard types from autofillHints: ios', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| autofillHints: const <String>[AutofillHints.streetAddressLine1], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect( |
| (tester.testTextInput.setClientArgs!['inputType'] as Map<String, dynamic>)['name'], |
| // On web, we don't infer the keyboard type as "name". We only infer |
| // on iOS and macOS. |
| kIsWeb ? equals('TextInputType.address') : equals('TextInputType.name'), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'infer keyboard types from autofillHints: non-ios', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| autofillHints: const <String>[AutofillHints.streetAddressLine1], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect((tester.testTextInput.setClientArgs!['inputType'] as Map<String, dynamic>)['name'], equals('TextInputType.address')); |
| }, |
| ); |
| |
| testWidgets( |
| 'inferred keyboard types can be overridden: ios', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| keyboardType: TextInputType.text, |
| autofillHints: const <String>[AutofillHints.streetAddressLine1], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect((tester.testTextInput.setClientArgs!['inputType'] as Map<String, dynamic>)['name'], equals('TextInputType.text')); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'inferred keyboard types can be overridden: non-ios', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| keyboardType: TextInputType.text, |
| autofillHints: const <String>[AutofillHints.streetAddressLine1], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect((tester.testTextInput.setClientArgs!['inputType'] as Map<String, dynamic>)['name'], equals('TextInputType.text')); |
| }, |
| ); |
| }); |
| |
| testWidgets('multiline keyboard is requested when set explicitly', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| keyboardType: TextInputType.multiline, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect((tester.testTextInput.setClientArgs!['inputType'] as Map<String, dynamic>)['name'], equals('TextInputType.multiline')); |
| expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.newline')); |
| }); |
| |
| testWidgets('EditableText sends enableInteractiveSelection to config', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| enableInteractiveSelection: true, |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| keyboardType: TextInputType.multiline, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); |
| expect(state.textInputConfiguration.enableInteractiveSelection, isTrue); |
| |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| enableInteractiveSelection: false, |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| keyboardType: TextInputType.multiline, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| state = tester.state<EditableTextState>(find.byType(EditableText)); |
| expect(state.textInputConfiguration.enableInteractiveSelection, isFalse); |
| }); |
| |
| testWidgets('selection persists when unfocused', (WidgetTester tester) async { |
| const TextEditingValue value = TextEditingValue( |
| text: 'test test', |
| selection: TextSelection(affinity: TextAffinity.upstream, baseOffset: 5, extentOffset: 7), |
| ); |
| controller.value = value; |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| keyboardType: TextInputType.multiline, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(controller.value, value); |
| expect(focusNode.hasFocus, isFalse); |
| |
| focusNode.requestFocus(); |
| await tester.pump(); |
| |
| // On web, focusing a single-line input selects the entire field. |
| final TextEditingValue webValue = value.copyWith( |
| selection: TextSelection( |
| baseOffset: 0, |
| extentOffset: controller.value.text.length, |
| ), |
| ); |
| if (kIsWeb) { |
| expect(controller.value, webValue); |
| } else { |
| expect(controller.value, value); |
| } |
| expect(focusNode.hasFocus, isTrue); |
| |
| focusNode.unfocus(); |
| await tester.pump(); |
| |
| if (kIsWeb) { |
| expect(controller.value, webValue); |
| } else { |
| expect(controller.value, value); |
| } |
| expect(focusNode.hasFocus, isFalse); |
| }); |
| |
| testWidgets('selection rects re-sent when refocused', (WidgetTester tester) async { |
| final List<List<SelectionRect>> log = <List<SelectionRect>>[]; |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { |
| if (methodCall.method == 'TextInput.setSelectionRects') { |
| final List<dynamic> args = methodCall.arguments as List<dynamic>; |
| final List<SelectionRect> selectionRects = <SelectionRect>[]; |
| for (final dynamic rect in args) { |
| selectionRects.add(SelectionRect( |
| position: (rect as List<dynamic>)[4] as int, |
| bounds: Rect.fromLTWH(rect[0] as double, rect[1] as double, rect[2] as double, rect[3] as double), |
| )); |
| } |
| log.add(selectionRects); |
| } |
| return null; |
| }); |
| |
| final TextEditingController controller = TextEditingController(); |
| final ScrollController scrollController = ScrollController(); |
| controller.text = 'Text1'; |
| |
| Future<void> pumpEditableText({ double? width, double? height, TextAlign textAlign = TextAlign.start }) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Center( |
| child: SizedBox( |
| width: width, |
| height: height, |
| child: EditableText( |
| controller: controller, |
| textAlign: textAlign, |
| scrollController: scrollController, |
| maxLines: null, |
| focusNode: focusNode, |
| cursorWidth: 0, |
| style: Typography.material2018().black.titleMedium!, |
| cursorColor: Colors.blue, |
| backgroundCursorColor: Colors.grey, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| const List<SelectionRect> expectedRects = <SelectionRect>[ |
| SelectionRect(position: 0, bounds: Rect.fromLTRB(0.0, 0.0, 14.0, 14.0)), |
| SelectionRect(position: 1, bounds: Rect.fromLTRB(14.0, 0.0, 28.0, 14.0)), |
| SelectionRect(position: 2, bounds: Rect.fromLTRB(28.0, 0.0, 42.0, 14.0)), |
| SelectionRect(position: 3, bounds: Rect.fromLTRB(42.0, 0.0, 56.0, 14.0)), |
| SelectionRect(position: 4, bounds: Rect.fromLTRB(56.0, 0.0, 70.0, 14.0)) |
| ]; |
| |
| await pumpEditableText(); |
| expect(log, isEmpty); |
| |
| await tester.showKeyboard(find.byType(EditableText)); |
| // First update. |
| expect(log.single, expectedRects); |
| log.clear(); |
| |
| await tester.pumpAndSettle(); |
| expect(log, isEmpty); |
| |
| focusNode.unfocus(); |
| await tester.pumpAndSettle(); |
| expect(log, isEmpty); |
| |
| focusNode.requestFocus(); |
| //await tester.showKeyboard(find.byType(EditableText)); |
| await tester.pumpAndSettle(); |
| // Should re-receive the same rects. |
| expect(log.single, expectedRects); |
| log.clear(); |
| |
| // On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects. |
| }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended] |
| |
| testWidgets('EditableText does not derive selection color from DefaultSelectionStyle', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/103341. |
| const TextEditingValue value = TextEditingValue( |
| text: 'test test', |
| selection: TextSelection(affinity: TextAffinity.upstream, baseOffset: 5, extentOffset: 7), |
| ); |
| const Color selectionColor = Colors.orange; |
| controller.value = value; |
| await tester.pumpWidget( |
| DefaultSelectionStyle( |
| selectionColor: selectionColor, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| keyboardType: TextInputType.multiline, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ) |
| ), |
| ); |
| final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); |
| expect(state.renderEditable.selectionColor, null); |
| }); |
| |
| testWidgets('visiblePassword keyboard is requested when set explicitly', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| keyboardType: TextInputType.visiblePassword, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect((tester.testTextInput.setClientArgs!['inputType'] as Map<String, dynamic>)['name'], equals('TextInputType.visiblePassword')); |
| expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.done')); |
| }); |
| |
| testWidgets('enableSuggestions flag is sent to the engine properly', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| const bool enableSuggestions = false; |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| enableSuggestions: enableSuggestions, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.idle(); |
| expect(tester.testTextInput.setClientArgs!['enableSuggestions'], enableSuggestions); |
| }); |
| |
| testWidgets('enableIMEPersonalizedLearning flag is sent to the engine properly', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| const bool enableIMEPersonalizedLearning = false; |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.idle(); |
| expect(tester.testTextInput.setClientArgs!['enableIMEPersonalizedLearning'], enableIMEPersonalizedLearning); |
| }); |
| |
| group('smartDashesType and smartQuotesType', () { |
| testWidgets('sent to the engine properly', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| const SmartDashesType smartDashesType = SmartDashesType.disabled; |
| const SmartQuotesType smartQuotesType = SmartQuotesType.disabled; |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| smartDashesType: smartDashesType, |
| smartQuotesType: smartQuotesType, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.idle(); |
| expect(tester.testTextInput.setClientArgs!['smartDashesType'], smartDashesType.index.toString()); |
| expect(tester.testTextInput.setClientArgs!['smartQuotesType'], smartQuotesType.index.toString()); |
| }); |
| |
| testWidgets('default to true when obscureText is false', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.idle(); |
| expect(tester.testTextInput.setClientArgs!['smartDashesType'], '1'); |
| expect(tester.testTextInput.setClientArgs!['smartQuotesType'], '1'); |
| }); |
| |
| testWidgets('default to false when obscureText is true', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(); |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| obscureText: true, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.idle(); |
| expect(tester.testTextInput.setClientArgs!['smartDashesType'], '0'); |
| expect(tester.testTextInput.setClientArgs!['smartQuotesType'], '0'); |
| }); |
| }); |
| |
| testWidgets('selection overlay will update when text grow bigger', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController.fromValue( |
| const TextEditingValue( |
| text: 'initial value', |
| ), |
| ); |
| Future<void> pumpEditableTextWithTextStyle(TextStyle style) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: style, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| showSelectionHandles: true, |
| ), |
| ), |
| ); |
| } |
| |
| await pumpEditableTextWithTextStyle(const TextStyle(fontSize: 18)); |
| final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); |
| state.renderEditable.selectWordsInRange( |
| from: Offset.zero, |
| cause: SelectionChangedCause.longPress, |
| ); |
| await tester.pumpAndSettle(); |
| await tester.idle(); |
| |
| List<RenderBox> handles = List<RenderBox>.from( |
| tester.renderObjectList<RenderBox>( |
| find.descendant( |
| of: find.byType(CompositedTransformFollower), |
| matching: find.byType(Padding), |
| ), |
| ), |
| ); |
| |
| expect(handles[0].localToGlobal(Offset.zero), const Offset(-35.0, 5.0)); |
| expect(handles[1].localToGlobal(Offset.zero), const Offset(113.0, 5.0)); |
| |
| await pumpEditableTextWithTextStyle(const TextStyle(fontSize: 30)); |
| await tester.pumpAndSettle(); |
| |
| // Handles should be updated with bigger font size. |
| handles = List<RenderBox>.from( |
| tester.renderObjectList<RenderBox>( |
| find.descendant( |
| of: find.byType(CompositedTransformFollower), |
| matching: find.byType(Padding), |
| ), |
| ), |
| ); |
| // First handle should have the same dx but bigger dy. |
| expect(handles[0].localToGlobal(Offset.zero), const Offset(-35.0, 17.0)); |
| expect(handles[1].localToGlobal(Offset.zero), const Offset(197.0, 17.0)); |
| }); |
| |
| testWidgets('can update style of previous activated EditableText', (WidgetTester tester) async { |
| final Key key1 = UniqueKey(); |
| final Key key2 = UniqueKey(); |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: Column( |
| children: <Widget>[ |
| EditableText( |
| key: key1, |
| controller: TextEditingController(), |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: const TextStyle(fontSize: 9), |
| cursorColor: cursorColor, |
| ), |
| EditableText( |
| key: key2, |
| controller: TextEditingController(), |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: const TextStyle(fontSize: 9), |
| cursorColor: cursorColor, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byKey(key1)); |
| await tester.showKeyboard(find.byKey(key1)); |
| controller.text = 'test'; |
| await tester.idle(); |
| RenderBox renderEditable = tester.renderObject(find.byKey(key1)); |
| expect(renderEditable.size.height, 9.0); |
| // Taps the other EditableText to deactivate the first one. |
| await tester.tap(find.byKey(key2)); |
| await tester.showKeyboard(find.byKey(key2)); |
| // Updates the style. |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: Column( |
| children: <Widget>[ |
| EditableText( |
| key: key1, |
| controller: TextEditingController(), |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: const TextStyle(fontSize: 20), |
| cursorColor: cursorColor, |
| ), |
| EditableText( |
| key: key2, |
| controller: TextEditingController(), |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: const TextStyle(fontSize: 9), |
| cursorColor: cursorColor, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| renderEditable = tester.renderObject(find.byKey(key1)); |
| expect(renderEditable.size.height, 20.0); |
| expect(tester.takeException(), null); |
| }); |
| |
| testWidgets('Multiline keyboard with newline action is requested when maxLines = null', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| maxLines: null, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect((tester.testTextInput.setClientArgs!['inputType'] as Map<String, dynamic>)['name'], equals('TextInputType.multiline')); |
| expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.newline')); |
| }); |
| |
| testWidgets('Text keyboard is requested when explicitly set and maxLines = null', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| maxLines: null, |
| keyboardType: TextInputType.text, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect((tester.testTextInput.setClientArgs!['inputType'] as Map<String, dynamic>)['name'], equals('TextInputType.text')); |
| expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.done')); |
| }); |
| |
| testWidgets('Correct keyboard is requested when set explicitly and maxLines > 1', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| keyboardType: TextInputType.phone, |
| maxLines: 3, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect((tester.testTextInput.setClientArgs!['inputType'] as Map<String, dynamic>)['name'], equals('TextInputType.phone')); |
| expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.done')); |
| }); |
| |
| testWidgets('multiline keyboard is requested when set implicitly', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| maxLines: 3, // Sets multiline keyboard implicitly. |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect((tester.testTextInput.setClientArgs!['inputType'] as Map<String, dynamic>)['name'], equals('TextInputType.multiline')); |
| expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.newline')); |
| }); |
| |
| testWidgets('single line inputs have correct default keyboard', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect((tester.testTextInput.setClientArgs!['inputType'] as Map<String, dynamic>)['name'], equals('TextInputType.text')); |
| expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.done')); |
| }); |
| |
| // Test case for https://github.com/flutter/flutter/issues/123523. |
| testWidgets( |
| 'The focus and callback behavior are correct when TextInputClient.onConnectionClosed message received', |
| (WidgetTester tester) async { |
| bool onSubmittedInvoked = false; |
| bool onEditingCompleteInvoked = false; |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| autofocus: true, |
| cursorColor: cursorColor, |
| onSubmitted: (String text) { |
| onSubmittedInvoked = true; |
| }, |
| onEditingComplete: () { |
| onEditingCompleteInvoked = true; |
| }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(focusNode.hasFocus, isTrue); |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| editableText.connectionClosed(); |
| await tester.pump(); |
| |
| if (kIsWeb) { |
| expect(onSubmittedInvoked, isTrue); |
| expect(onEditingCompleteInvoked, isTrue); |
| // Because we add the onEditingComplete and we didn't unfocus there, so focus still exists. |
| expect(focusNode.hasFocus, isTrue); |
| } else { |
| // For mobile and other platforms, calling connectionClosed will only unfocus. |
| expect(focusNode.hasFocus, isFalse); |
| expect(onEditingCompleteInvoked, isFalse); |
| expect(onSubmittedInvoked, isFalse); |
| } |
| }); |
| |
| testWidgets('connection is closed when TextInputClient.onConnectionClosed message received', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test'; |
| await tester.idle(); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| expect(state.wantKeepAlive, true); |
| |
| tester.testTextInput.log.clear(); |
| tester.testTextInput.closeConnection(); |
| await tester.idle(); |
| |
| // Widget does not have focus anymore. |
| expect(state.wantKeepAlive, false); |
| // No method calls are sent from the framework. |
| // This makes sure hide/clearClient methods are not called after connection |
| // closed. |
| expect(tester.testTextInput.log, isEmpty); |
| }); |
| |
| testWidgets('closed connection reopened when user focused', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| controller.text = 'test3'; |
| await tester.idle(); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| expect(tester.testTextInput.editingState!['text'], equals('test3')); |
| expect(state.wantKeepAlive, true); |
| |
| tester.testTextInput.log.clear(); |
| tester.testTextInput.closeConnection(); |
| await tester.pumpAndSettle(); |
| |
| // Widget does not have focus anymore. |
| expect(state.wantKeepAlive, false); |
| // No method calls are sent from the framework. |
| // This makes sure hide/clearClient methods are not called after connection |
| // closed. |
| expect(tester.testTextInput.log, isEmpty); |
| |
| await tester.tap(find.byType(EditableText)); |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.pump(); |
| controller.text = 'test2'; |
| expect(tester.testTextInput.editingState!['text'], equals('test2')); |
| // Widget regained the focus. |
| expect(state.wantKeepAlive, true); |
| }); |
| |
| testWidgets('closed connection reopened when user focused on another field', (WidgetTester tester) async { |
| final EditableText testNameField = |
| EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| maxLines: null, |
| keyboardType: TextInputType.text, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ); |
| |
| final EditableText testPhoneField = |
| EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| keyboardType: TextInputType.phone, |
| maxLines: 3, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ); |
| |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: ListView( |
| children: <Widget>[ |
| testNameField, |
| testPhoneField, |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Tap, enter text. |
| await tester.tap(find.byWidget(testNameField)); |
| await tester.showKeyboard(find.byWidget(testNameField)); |
| controller.text = 'test'; |
| await tester.idle(); |
| |
| expect(tester.testTextInput.editingState!['text'], equals('test')); |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byWidget(testNameField)); |
| expect(state.wantKeepAlive, true); |
| |
| tester.testTextInput.log.clear(); |
| tester.testTextInput.closeConnection(); |
| // A pump is needed to allow the focus change (unfocus) to be resolved. |
| await tester.pump(); |
| |
| // Widget does not have focus anymore. |
| expect(state.wantKeepAlive, false); |
| // No method calls are sent from the framework. |
| // This makes sure hide/clearClient methods are not called after connection |
| // closed. |
| expect(tester.testTextInput.log, isEmpty); |
| |
| // For the next fields, tap, enter text. |
| await tester.tap(find.byWidget(testPhoneField)); |
| await tester.showKeyboard(find.byWidget(testPhoneField)); |
| controller.text = '650123123'; |
| await tester.idle(); |
| expect(tester.testTextInput.editingState!['text'], equals('650123123')); |
| // Widget regained the focus. |
| expect(state.wantKeepAlive, true); |
| }); |
| |
| testWidgets( |
| 'kept-alive EditableText does not crash when layout is skipped', |
| (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/84896. |
| EditableText.debugDeterministicCursor = true; |
| const Key key = ValueKey<String>('EditableText'); |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: ListView( |
| children: <Widget>[ |
| EditableText( |
| key: key, |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| autofocus: true, |
| maxLines: null, |
| keyboardType: TextInputType.text, |
| style: textStyle, |
| textAlign: TextAlign.left, |
| cursorColor: cursorColor, |
| showCursor: false, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| // Wait for autofocus. |
| await tester.pump(); |
| expect(focusNode.hasFocus, isTrue); |
| |
| // Prepend an additional item to make EditableText invisible. It's still |
| // kept in the tree via the keepalive mechanism. Change the text alignment |
| // and showCursor. The RenderEditable now needs to relayout and repaint. |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: ListView( |
| children: <Widget>[ |
| const SizedBox(height: 6000), |
| EditableText( |
| key: key, |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| autofocus: true, |
| maxLines: null, |
| keyboardType: TextInputType.text, |
| style: textStyle, |
| textAlign: TextAlign.right, |
| cursorColor: cursorColor, |
| showCursor: true, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| EditableText.debugDeterministicCursor = false; |
| expect(tester.takeException(), isNull); |
| }); |
| |
| // Toolbar is not used in Flutter Web unless the browser context menu is |
| // explicitly disabled. Skip this check. |
| // |
| // Web is using native DOM elements (it is also used as platform input) |
| // to enable clipboard functionality of the toolbar: copy, paste, select, |
| // cut. It might also provide additional functionality depending on the |
| // browser (such as translation). Due to this, in browsers, we should not |
| // show a Flutter toolbar for the editable text elements. |
| testWidgets('can show toolbar when there is text and a selection', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| // Can't show the toolbar when there's no focus. |
| expect(state.showToolbar(), false); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), findsNothing); |
| |
| // Can show the toolbar when focused even though there's no text. |
| state.renderEditable.selectWordsInRange( |
| from: Offset.zero, |
| cause: SelectionChangedCause.tap, |
| ); |
| await tester.pump(); |
| // On web, we don't let Flutter show the toolbar. |
| expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget); |
| |
| // Hide the menu again. |
| state.hideToolbar(); |
| await tester.pump(); |
| expect(find.text('Paste'), findsNothing); |
| |
| // Can show the menu with text and a selection. |
| controller.text = 'blah'; |
| await tester.pump(); |
| // On web, we don't let Flutter show the toolbar. |
| expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget); |
| }); |
| |
| group('BrowserContextMenu', () { |
| setUp(() async { |
| TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( |
| SystemChannels.contextMenu, |
| (MethodCall call) { |
| // Just complete successfully, so that BrowserContextMenu thinks that |
| // the engine successfully received its call. |
| return Future<void>.value(); |
| }, |
| ); |
| await BrowserContextMenu.disableContextMenu(); |
| }); |
| |
| tearDown(() async { |
| await BrowserContextMenu.enableContextMenu(); |
| TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, null); |
| }); |
| |
| testWidgets('web can show flutter context menu when the browser context menu is disabled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| // Can't show the toolbar when there's no focus. |
| expect(state.showToolbar(), false); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), findsNothing); |
| |
| // Can show the toolbar when focused even though there's no text. |
| state.renderEditable.selectWordsInRange( |
| from: Offset.zero, |
| cause: SelectionChangedCause.tap, |
| ); |
| await tester.pump(); |
| expect(state.showToolbar(), isTrue); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), findsOneWidget); |
| |
| // Hide the menu again. |
| state.hideToolbar(); |
| await tester.pump(); |
| expect(find.text('Paste'), findsNothing); |
| |
| // Can show the menu with text and a selection. |
| controller.text = 'blah'; |
| await tester.pump(); |
| expect(state.showToolbar(), isTrue); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), findsOneWidget); |
| }, |
| skip: !kIsWeb, // [intended] |
| ); |
| }); |
| |
| testWidgets('can hide toolbar with DismissIntent', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| // Show the toolbar |
| state.renderEditable.selectWordsInRange( |
| from: Offset.zero, |
| cause: SelectionChangedCause.tap, |
| ); |
| await tester.pump(); |
| |
| // On web, we don't let Flutter show the toolbar. |
| expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget); |
| |
| // Hide the menu using the DismissIntent. |
| await tester.sendKeyEvent(LogicalKeyboardKey.escape); |
| await tester.pump(); |
| expect(find.text('Paste'), findsNothing); |
| }); |
| |
| testWidgets('toolbar hidden on mobile when orientation changes', (WidgetTester tester) async { |
| addTearDown(tester.view.reset); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| // Show the toolbar |
| state.renderEditable.selectWordsInRange( |
| from: Offset.zero, |
| cause: SelectionChangedCause.tap, |
| ); |
| await tester.pump(); |
| |
| expect(state.showToolbar(), true); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), findsOneWidget); |
| |
| // Hide the menu by changing orientation. |
| tester.view.physicalSize = const Size(1800.0, 2400.0); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), findsNothing); |
| |
| // Handles should be hidden as well on Android |
| expect( |
| find.descendant( |
| of: find.byType(CompositedTransformFollower), |
| matching: find.byType(Padding), |
| ), |
| defaultTargetPlatform == TargetPlatform.android ? findsNothing : findsOneWidget, |
| ); |
| |
| // On web, we don't show the Flutter toolbar and instead rely on the browser |
| // toolbar. Until we change that, this test should remain skipped. |
| }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android })); // [intended] |
| |
| testWidgets('Paste is shown only when there is something to paste', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| // Make sure the clipboard has a valid string on it. |
| await Clipboard.setData(const ClipboardData(text: 'Clipboard data')); |
| |
| // Show the toolbar. |
| state.renderEditable.selectWordsInRange( |
| from: Offset.zero, |
| cause: SelectionChangedCause.tap, |
| ); |
| await tester.pump(); |
| |
| // The Paste button is shown (except on web, which doesn't show the Flutter |
| // toolbar). |
| expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget); |
| |
| // Hide the menu again. |
| state.hideToolbar(); |
| await tester.pump(); |
| expect(find.text('Paste'), findsNothing); |
| |
| // Clear the clipboard |
| await Clipboard.setData(const ClipboardData(text: '')); |
| |
| // Show the toolbar again. |
| expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); |
| await tester.pumpAndSettle(); |
| |
| // Paste is not shown. |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), findsNothing); |
| }); |
| |
| testWidgets('Copy selection does not collapse selection on desktop and iOS', (WidgetTester tester) async { |
| final TextEditingController localController = TextEditingController(text: 'Hello world'); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: localController, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| // Show the toolbar. |
| state.renderEditable.selectWordsInRange( |
| from: Offset.zero, |
| cause: SelectionChangedCause.tap, |
| ); |
| await tester.pump(); |
| |
| final TextSelection copySelectionRange = localController.selection; |
| |
| state.showToolbar(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Copy'), findsOneWidget); |
| |
| await tester.tap(find.text('Copy')); |
| await tester.pumpAndSettle(); |
| expect(copySelectionRange, localController.selection); |
| expect(find.text('Copy'), findsNothing); |
| }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS, TargetPlatform.linux, TargetPlatform.windows })); // [intended] |
| |
| testWidgets('Copy selection collapses selection and hides the toolbar on Android and Fuchsia', (WidgetTester tester) async { |
| final TextEditingController localController = TextEditingController(text: 'Hello world'); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: localController, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| // Show the toolbar. |
| state.renderEditable.selectWordsInRange( |
| from: Offset.zero, |
| cause: SelectionChangedCause.tap, |
| ); |
| await tester.pump(); |
| |
| final TextSelection copySelectionRange = localController.selection; |
| |
| state.showToolbar(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Copy'), findsOneWidget); |
| |
| await tester.tap(find.text('Copy')); |
| await tester.pumpAndSettle(); |
| expect(localController.selection, TextSelection.collapsed(offset: copySelectionRange.extentOffset)); |
| expect(find.text('Copy'), findsNothing); |
| }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia })); // [intended] |
| |
| testWidgets('can show the toolbar after clearing all text', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/35998. |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| // Add text and an empty selection. |
| controller.text = 'blah'; |
| await tester.pump(); |
| state.renderEditable.selectWordsInRange( |
| from: Offset.zero, |
| cause: SelectionChangedCause.tap, |
| ); |
| await tester.pump(); |
| |
| // Clear the text and selection. |
| expect(find.text('Paste'), findsNothing); |
| state.updateEditingValue(TextEditingValue.empty); |
| await tester.pump(); |
| |
| // Should be able to show the toolbar. |
| // On web, we don't let Flutter show the toolbar. |
| expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); |
| await tester.pumpAndSettle(); |
| expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget); |
| }); |
| |
| testWidgets('can dynamically disable options in toolbar', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: TextEditingController(text: 'blah blah'), |
| focusNode: focusNode, |
| toolbarOptions: const ToolbarOptions( |
| copy: true, |
| selectAll: true, |
| ), |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| // Select something. Doesn't really matter what. |
| state.renderEditable.selectWordsInRange( |
| from: Offset.zero, |
| cause: SelectionChangedCause.tap, |
| ); |
| await tester.pump(); |
| // On web, we don't let Flutter show the toolbar. |
| expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); |
| await tester.pump(); |
| expect(find.text('Select all'), kIsWeb ? findsNothing : findsOneWidget); |
| expect(find.text('Copy'), kIsWeb ? findsNothing : findsOneWidget); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Cut'), findsNothing); |
| }); |
| |
| testWidgets('can dynamically disable select all option in toolbar - cupertino', (WidgetTester tester) async { |
| // Regression test: https://github.com/flutter/flutter/issues/40711 |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: TextEditingController(text: 'blah blah'), |
| focusNode: focusNode, |
| toolbarOptions: ToolbarOptions.empty, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: cupertinoTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| await tester.tap(find.byType(EditableText)); |
| await tester.pump(); |
| // On web, we don't let Flutter show the toolbar. |
| expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); |
| await tester.pump(); |
| expect(find.text('Select All'), findsNothing); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Cut'), findsNothing); |
| }); |
| |
| testWidgets('can dynamically disable select all option in toolbar - material', (WidgetTester tester) async { |
| // Regression test: https://github.com/flutter/flutter/issues/40711 |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: TextEditingController(text: 'blah blah'), |
| focusNode: focusNode, |
| toolbarOptions: const ToolbarOptions( |
| copy: true, |
| ), |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| // Select something. Doesn't really matter what. |
| state.renderEditable.selectWordsInRange( |
| from: Offset.zero, |
| cause: SelectionChangedCause.tap, |
| ); |
| await tester.pump(); |
| // On web, we don't let Flutter show the toolbar. |
| expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); |
| await tester.pump(); |
| expect(find.text('Select all'), findsNothing); |
| expect(find.text('Copy'), kIsWeb ? findsNothing : findsOneWidget); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Cut'), findsNothing); |
| }); |
| |
| testWidgets('cut and paste are disabled in read only mode even if explicitly set', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: TextEditingController(text: 'blah blah'), |
| focusNode: focusNode, |
| readOnly: true, |
| toolbarOptions: const ToolbarOptions( |
| copy: true, |
| cut: true, |
| paste: true, |
| selectAll: true, |
| ), |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| // Select something. Doesn't really matter what. |
| state.renderEditable.selectWordsInRange( |
| from: Offset.zero, |
| cause: SelectionChangedCause.tap, |
| ); |
| await tester.pump(); |
| // On web, we don't let Flutter show the toolbar. |
| expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); |
| await tester.pump(); |
| expect(find.text('Select all'), kIsWeb ? findsNothing : findsOneWidget); |
| expect(find.text('Copy'), kIsWeb ? findsNothing : findsOneWidget); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Cut'), findsNothing); |
| }); |
| |
| testWidgets('cut and copy are disabled in obscured mode even if explicitly set', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: TextEditingController(text: 'blah blah'), |
| focusNode: focusNode, |
| obscureText: true, |
| toolbarOptions: const ToolbarOptions( |
| copy: true, |
| cut: true, |
| paste: true, |
| selectAll: true, |
| ), |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| await tester.tap(find.byType(EditableText)); |
| await tester.pump(); |
| // Select something, but not the whole thing. |
| state.renderEditable.selectWord(cause: SelectionChangedCause.tap); |
| await tester.pump(); |
| expect(state.selectAllEnabled, isTrue); |
| expect(state.pasteEnabled, isTrue); |
| expect(state.cutEnabled, isFalse); |
| expect(state.copyEnabled, isFalse); |
| |
| // On web, we don't let Flutter show the toolbar. |
| expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); |
| await tester.pump(); |
| expect(find.text('Select all'), kIsWeb ? findsNothing : findsOneWidget); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget); |
| expect(find.text('Cut'), findsNothing); |
| }); |
| |
| testWidgets('cut and copy do nothing in obscured mode even if explicitly called', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: TextEditingController(text: 'blah blah'), |
| focusNode: focusNode, |
| obscureText: true, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| expect(state.selectAllEnabled, isTrue); |
| expect(state.pasteEnabled, isTrue); |
| expect(state.cutEnabled, isFalse); |
| expect(state.copyEnabled, isFalse); |
| |
| // Select all. |
| state.selectAll(SelectionChangedCause.toolbar); |
| await tester.pump(); |
| await Clipboard.setData(const ClipboardData(text: '')); |
| state.cutSelection(SelectionChangedCause.toolbar); |
| ClipboardData? data = await Clipboard.getData('text/plain'); |
| expect(data, isNotNull); |
| expect(data!.text, isEmpty); |
| |
| state.selectAll(SelectionChangedCause.toolbar); |
| await tester.pump(); |
| await Clipboard.setData(const ClipboardData(text: '')); |
| state.copySelection(SelectionChangedCause.toolbar); |
| data = await Clipboard.getData('text/plain'); |
| expect(data, isNotNull); |
| expect(data!.text, isEmpty); |
| }); |
| |
| testWidgets('select all does nothing if obscured and read-only, even if explicitly called', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: TextEditingController(text: 'blah blah'), |
| focusNode: focusNode, |
| obscureText: true, |
| readOnly: true, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = |
| tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| // Select all. |
| state.selectAll(SelectionChangedCause.toolbar); |
| expect(state.selectAllEnabled, isFalse); |
| expect(state.textEditingValue.selection.isCollapsed, isTrue); |
| }); |
| |
| group('buttonItemsForToolbarOptions', () { |
| testWidgets('returns null when toolbarOptions are empty', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| controller: TextEditingController(text: 'TEXT'), |
| toolbarOptions: ToolbarOptions.empty, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| backgroundCursorColor: Colors.grey, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = tester.state<EditableTextState>( |
| find.byType(EditableText), |
| ); |
| |
| expect(state.buttonItemsForToolbarOptions(), isNull); |
| }); |
| |
| testWidgets('returns empty array when only cut is selected in toolbarOptions but cut is not enabled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| controller: TextEditingController(text: 'TEXT'), |
| toolbarOptions: const ToolbarOptions(cut: true), |
| readOnly: true, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| backgroundCursorColor: Colors.grey, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = tester.state<EditableTextState>( |
| find.byType(EditableText), |
| ); |
| |
| expect(state.cutEnabled, isFalse); |
| expect(state.buttonItemsForToolbarOptions(), isEmpty); |
| }); |
| |
| testWidgets('returns only cut button when only cut is selected in toolbarOptions and cut is enabled', (WidgetTester tester) async { |
| const String text = 'TEXT'; |
| final TextEditingController controller = TextEditingController(text: text); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| controller: controller, |
| toolbarOptions: const ToolbarOptions(cut: true), |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| backgroundCursorColor: Colors.grey, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = tester.state<EditableTextState>( |
| find.byType(EditableText), |
| ); |
| |
| // Selecting all. |
| controller.selection = TextSelection( |
| baseOffset: 0, |
| extentOffset: controller.text.length, |
| ); |
| expect(state.cutEnabled, isTrue); |
| |
| final List<ContextMenuButtonItem>? items = state.buttonItemsForToolbarOptions(); |
| |
| expect(items, isNotNull); |
| expect(items, hasLength(1)); |
| |
| final ContextMenuButtonItem cutButton = items!.first; |
| expect(cutButton.type, ContextMenuButtonType.cut); |
| |
| cutButton.onPressed?.call(); |
| await tester.pump(); |
| |
| expect(controller.text, isEmpty); |
| final ClipboardData? data = await Clipboard.getData('text/plain'); |
| expect(data, isNotNull); |
| expect(data!.text, equals(text)); |
| }); |
| |
| testWidgets('returns empty array when only copy is selected in toolbarOptions but copy is not enabled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| controller: TextEditingController(text: 'TEXT'), |
| toolbarOptions: const ToolbarOptions(copy: true), |
| obscureText: true, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| backgroundCursorColor: Colors.grey, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = tester.state<EditableTextState>( |
| find.byType(EditableText), |
| ); |
| |
| expect(state.copyEnabled, isFalse); |
| expect(state.buttonItemsForToolbarOptions(), isEmpty); |
| }); |
| |
| testWidgets('returns only copy button when only copy is selected in toolbarOptions and copy is enabled', (WidgetTester tester) async { |
| const String text = 'TEXT'; |
| final TextEditingController controller = TextEditingController(text: text); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| controller: controller, |
| toolbarOptions: const ToolbarOptions(copy: true), |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| backgroundCursorColor: Colors.grey, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = tester.state<EditableTextState>( |
| find.byType(EditableText), |
| ); |
| |
| // Selecting all. |
| controller.selection = TextSelection( |
| baseOffset: 0, |
| extentOffset: controller.text.length, |
| ); |
| expect(state.copyEnabled, isTrue); |
| |
| final List<ContextMenuButtonItem>? items = state.buttonItemsForToolbarOptions(); |
| |
| expect(items, isNotNull); |
| expect(items, hasLength(1)); |
| |
| final ContextMenuButtonItem copyButton = items!.first; |
| expect(copyButton.type, ContextMenuButtonType.copy); |
| |
| copyButton.onPressed?.call(); |
| await tester.pump(); |
| |
| expect(controller.text, equals(text)); |
| final ClipboardData? data = await Clipboard.getData('text/plain'); |
| expect(data, isNotNull); |
| expect(data!.text, equals(text)); |
| }); |
| |
| testWidgets('returns empty array when only paste is selected in toolbarOptions but paste is not enabled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| controller: TextEditingController(text: 'TEXT'), |
| toolbarOptions: const ToolbarOptions(paste: true), |
| readOnly: true, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| backgroundCursorColor: Colors.grey, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = tester.state<EditableTextState>( |
| find.byType(EditableText), |
| ); |
| |
| expect(state.pasteEnabled, isFalse); |
| expect(state.buttonItemsForToolbarOptions(), isEmpty); |
| }); |
| |
| testWidgets('returns only paste button when only paste is selected in toolbarOptions and paste is enabled', (WidgetTester tester) async { |
| const String text = 'TEXT'; |
| final TextEditingController controller = TextEditingController(text: text); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| controller: controller, |
| toolbarOptions: const ToolbarOptions(paste: true), |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| backgroundCursorColor: Colors.grey, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = tester.state<EditableTextState>( |
| find.byType(EditableText), |
| ); |
| |
| // Moving caret to the end. |
| controller.selection = TextSelection.collapsed(offset: controller.text.length); |
| expect(state.pasteEnabled, isTrue); |
| |
| final List<ContextMenuButtonItem>? items = state.buttonItemsForToolbarOptions(); |
| |
| expect(items, isNotNull); |
| expect(items, hasLength(1)); |
| |
| final ContextMenuButtonItem pasteButton = items!.first; |
| expect(pasteButton.type, ContextMenuButtonType.paste); |
| |
| // Setting data which will be pasted into the clipboard. |
| await Clipboard.setData(const ClipboardData(text: text)); |
| |
| pasteButton.onPressed?.call(); |
| await tester.pump(); |
| |
| expect(controller.text, equals(text + text)); |
| }); |
| |
| testWidgets('returns empty array when only selectAll is selected in toolbarOptions but selectAll is not enabled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| controller: TextEditingController(text: 'TEXT'), |
| toolbarOptions: const ToolbarOptions(selectAll: true), |
| readOnly: true, |
| obscureText: true, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| backgroundCursorColor: Colors.grey, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = tester.state<EditableTextState>( |
| find.byType(EditableText), |
| ); |
| |
| expect(state.selectAllEnabled, isFalse); |
| expect(state.buttonItemsForToolbarOptions(), isEmpty); |
| }); |
| |
| testWidgets('returns only selectAll button when only selectAll is selected in toolbarOptions and selectAll is enabled', (WidgetTester tester) async { |
| const String text = 'TEXT'; |
| final TextEditingController controller = TextEditingController(text: text); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| controller: controller, |
| toolbarOptions: const ToolbarOptions(selectAll: true), |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| backgroundCursorColor: Colors.grey, |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = tester.state<EditableTextState>( |
| find.byType(EditableText), |
| ); |
| |
| final List<ContextMenuButtonItem>? items = state.buttonItemsForToolbarOptions(); |
| |
| expect(items, isNotNull); |
| expect(items, hasLength(1)); |
| |
| final ContextMenuButtonItem selectAllButton = items!.first; |
| expect(selectAllButton.type, ContextMenuButtonType.selectAll); |
| |
| selectAllButton.onPressed?.call(); |
| await tester.pump(); |
| |
| expect(controller.text, equals(text)); |
| expect(state.textEditingValue.selection.textInside(text), equals(text)); |
| }); |
| }); |
| |
| testWidgets('Handles the read-only flag correctly', (WidgetTester tester) async { |
| final TextEditingController controller = |
| TextEditingController(text: 'Lorem ipsum dolor sit amet'); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| readOnly: true, |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ); |
| |
| // Interact with the field to establish the input connection. |
| final Offset topLeft = tester.getTopLeft(find.byType(EditableText)); |
| await tester.tapAt(topLeft + const Offset(0.0, 5.0)); |
| await tester.pump(); |
| |
| controller.selection = const TextSelection(baseOffset: 0, extentOffset: 5); |
| await tester.pump(); |
| |
| if (kIsWeb) { |
| // On the web, a regular connection to the platform should've been made |
| // with the `readOnly` flag set to true. |
| expect(tester.testTextInput.hasAnyClients, isTrue); |
| expect(tester.testTextInput.setClientArgs!['readOnly'], isTrue); |
| expect( |
| tester.testTextInput.editingState!['text'], |
| 'Lorem ipsum dolor sit amet', |
| ); |
| expect(tester.testTextInput.editingState!['selectionBase'], 0); |
| expect(tester.testTextInput.editingState!['selectionExtent'], 5); |
| } else { |
| // On non-web platforms, a read-only field doesn't need a connection with |
| // the platform. |
| expect(tester.testTextInput.hasAnyClients, isFalse); |
| } |
| }); |
| |
| testWidgets('Does not accept updates when read-only', (WidgetTester tester) async { |
| final TextEditingController controller = |
| TextEditingController(text: 'Lorem ipsum dolor sit amet'); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| readOnly: true, |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ); |
| |
| // Interact with the field to establish the input connection. |
| final Offset topLeft = tester.getTopLeft(find.byType(EditableText)); |
| await tester.tapAt(topLeft + const Offset(0.0, 5.0)); |
| await tester.pump(); |
| |
| expect(tester.testTextInput.hasAnyClients, kIsWeb ? isTrue : isFalse); |
| if (kIsWeb) { |
| // On the web, the input connection exists, but text updates should be |
| // ignored. |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: 'Foo bar', |
| selection: TextSelection(baseOffset: 0, extentOffset: 3), |
| composing: TextRange(start: 3, end: 4), |
| )); |
| // Only selection should change. |
| expect( |
| controller.value, |
| const TextEditingValue( |
| text: 'Lorem ipsum dolor sit amet', |
| selection: TextSelection(baseOffset: 0, extentOffset: 3), |
| ), |
| ); |
| } |
| }); |
| |
| testWidgets('Read-only fields do not format text', (WidgetTester tester) async { |
| late SelectionChangedCause selectionCause; |
| |
| final TextEditingController controller = |
| TextEditingController(text: 'Lorem ipsum dolor sit amet'); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| readOnly: true, |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) { |
| selectionCause = cause!; |
| }, |
| ), |
| ), |
| ); |
| |
| // Interact with the field to establish the input connection. |
| final Offset topLeft = tester.getTopLeft(find.byType(EditableText)); |
| await tester.tapAt(topLeft + const Offset(0.0, 5.0)); |
| await tester.pump(); |
| |
| expect(tester.testTextInput.hasAnyClients, kIsWeb ? isTrue : isFalse); |
| if (kIsWeb) { |
| tester.testTextInput.updateEditingValue(const TextEditingValue( |
| text: 'Foo bar', |
| selection: TextSelection(baseOffset: 0, extentOffset: 3), |
| )); |
| // On web, the only way a text field can be updated from the engine is if |
| // a keyboard is used. |
| expect(selectionCause, SelectionChangedCause.keyboard); |
| } |
| }); |
| |
| testWidgets('Selection changes during Scribble interaction should have the scribble cause', (WidgetTester tester) async { |
| late SelectionChangedCause selectionCause; |
| |
| final TextEditingController controller = |
| TextEditingController(text: 'Lorem ipsum dolor sit amet'); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) { |
| if (cause != null) { |
| selectionCause = cause; |
| } |
| }, |
| ), |
| ), |
| ); |
| |
| await tester.showKeyboard(find.byType(EditableText)); |
| |
| // A normal selection update from the framework has 'keyboard' as the cause. |
| tester.testTextInput.updateEditingValue(TextEditingValue( |
| text: controller.text, |
| selection: const TextSelection(baseOffset: 2, extentOffset: 3), |
| )); |
| await tester.pumpAndSettle(); |
| |
| expect(selectionCause, SelectionChangedCause.keyboard); |
| |
| // A selection update during a scribble interaction has 'scribble' as the cause. |
| await tester.testTextInput.startScribbleInteraction(); |
| tester.testTextInput.updateEditingValue(TextEditingValue( |
| text: controller.text, |
| selection: const TextSelection(baseOffset: 3, extentOffset: 4), |
| )); |
| await tester.pumpAndSettle(); |
| |
| expect(selectionCause, SelectionChangedCause.scribble); |
| |
| await tester.testTextInput.finishScribbleInteraction(); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); |
| |
| testWidgets('Requests focus and changes the selection when onScribbleFocus is called', (WidgetTester tester) async { |
| final TextEditingController controller = |
| TextEditingController(text: 'Lorem ipsum dolor sit amet'); |
| late SelectionChangedCause selectionCause; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) { |
| if (cause != null) { |
| selectionCause = cause; |
| } |
| }, |
| ), |
| ), |
| ); |
| |
| await tester.testTextInput.scribbleFocusElement(TextInput.scribbleClients.keys.first, Offset.zero); |
| |
| expect(focusNode.hasFocus, true); |
| expect(selectionCause, SelectionChangedCause.scribble); |
| |
| // On web, we should rely on the browser's implementation of Scribble, so the selection changed cause |
| // will never be SelectionChangedCause.scribble. |
| }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended] |
| |
| testWidgets('Declares itself for Scribble interaction if the bounds overlap the scribble rect and the widget is touchable', (WidgetTester tester) async { |
| final TextEditingController controller = |
| TextEditingController(text: 'Lorem ipsum dolor sit amet'); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| final List<dynamic> elementEntry = <dynamic>[TextInput.scribbleClients.keys.first, 0.0, 0.0, 800.0, 600.0]; |
| |
| List<List<dynamic>> elements = await tester.testTextInput.scribbleRequestElementsInRect(const Rect.fromLTWH(0, 0, 1, 1)); |
| expect(elements.first, containsAll(elementEntry)); |
| |
| // Touch is outside the bounds of the widget. |
| elements = await tester.testTextInput.scribbleRequestElementsInRect(const Rect.fromLTWH(-1, -1, 1, 1)); |
| expect(elements.length, 0); |
| |
| // Widget is read only. |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| readOnly: true, |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ); |
| |
| elements = await tester.testTextInput.scribbleRequestElementsInRect(const Rect.fromLTWH(0, 0, 1, 1)); |
| expect(elements.length, 0); |
| |
| // Widget is not touchable. |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Stack(children: <Widget>[ |
| EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| ), |
| Positioned( |
| left: 0, |
| top: 0, |
| right: 0, |
| bottom: 0, |
| child: Container(color: Colors.black), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| elements = await tester.testTextInput.scribbleRequestElementsInRect(const Rect.fromLTWH(0, 0, 1, 1)); |
| expect(elements.length, 0); |
| |
| // Widget has scribble disabled. |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| controller: controller, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| selectionControls: materialTextSelectionControls, |
| scribbleEnabled: false, |
|
|