blob: 3c8ebd855cd31a7b18ac58975238a0c14cd7ab56 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
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,