blob: 43e127103234618e5ba1fc13d3a7567caa0ca7b7 [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:async';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';
import 'package:mockito/mockito.dart';
import 'package:flutter/foundation.dart';
import 'editable_text_utils.dart';
import 'semantics_tester.dart';
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,
}
class MockClipboard {
Object _clipboardData = <String, dynamic>{
'text': null,
};
Future<dynamic> handleMethodCall(MethodCall methodCall) async {
switch (methodCall.method) {
case 'Clipboard.getData':
return _clipboardData;
case 'Clipboard.setData':
_clipboardData = methodCall.arguments;
break;
}
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final MockClipboard mockClipboard = MockClipboard();
SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall);
setUp(() {
debugResetSemanticsIdCounter();
controller = TextEditingController();
});
tearDown(() {
controller.dispose();
controller = null;
});
// 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({
WidgetTester tester,
TextInputAction action,
String serializedActionName,
}) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
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('has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
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.textAlign, TextAlign.start);
expect(editableText.cursorWidth, 2.0);
});
testWidgets('text keyboard is requested when maxLines is default', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
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']['name'],
equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done'));
}, skip: isBrowser);
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',
);
}, skip: isBrowser);
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',
);
}, skip: isBrowser);
testWidgets('Keyboard is configured for "next" action when explicitly requested', (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.next,
serializedActionName: 'TextInputAction.next',
);
}, skip: isBrowser);
testWidgets('Keyboard is configured for "previous" action when explicitly requested', (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.previous,
serializedActionName: 'TextInputAction.previous',
);
}, skip: isBrowser);
testWidgets('Keyboard is configured for "continue" action when explicitly requested', (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.continueAction,
serializedActionName: 'TextInputAction.continueAction',
);
}, skip: isBrowser);
testWidgets('Keyboard is configured for "join" action when explicitly requested', (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.join,
serializedActionName: 'TextInputAction.join',
);
}, skip: isBrowser);
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('multiline keyboard is requested when set explicitly', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
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']['name'],
equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.newline'));
});
testWidgets('visiblePassword keyboard is requested when set explicitly', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
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']['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(devicePixelRatio: 1.0),
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);
});
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(devicePixelRatio: 1.0),
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(devicePixelRatio: 1.0),
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: false,
),
),
),
),
);
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(devicePixelRatio: 1.0),
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: const Offset(0, 0),
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(GestureDetector),
),
),
);
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(GestureDetector),
),
),
);
// 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(devicePixelRatio: 1.0),
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(devicePixelRatio: 1.0),
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(devicePixelRatio: 1.0),
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']['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(devicePixelRatio: 1.0),
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']['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(devicePixelRatio: 1.0),
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']['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(devicePixelRatio: 1.0),
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']['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(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
maxLines: 1, // Sets text 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']['name'],
equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done'));
});
testWidgets('connection is closed when TextInputClient.onConnectionClosed message received', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
maxLines: 1, // Sets text keyboard implicitly.
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(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
maxLines: 1, // Sets text keyboard implicitly.
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(devicePixelRatio: 1.0),
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);
});
/// Toolbar is not used in Flutter Web. 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.pump();
expect(find.text('PASTE'), findsNothing);
// Can show the toolbar when focused even though there's no text.
state.renderEditable.selectWordsInRange(
from: const Offset(0, 0),
cause: SelectionChangedCause.tap,
);
await tester.pump();
expect(state.showToolbar(), true);
await tester.pump();
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(), true);
await tester.pump();
expect(find.text('PASTE'), findsOneWidget);
}, skip: isBrowser);
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: const Offset(0, 0),
cause: SelectionChangedCause.tap,
);
await tester.pump();
// Clear the text and selection.
expect(find.text('PASTE'), findsNothing);
state.updateEditingValue(const TextEditingValue(
text: '',
));
await tester.pump();
// Should be able to show the toolbar.
expect(state.showToolbar(), true);
await tester.pump();
expect(find.text('PASTE'), 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: const Offset(0, 0),
cause: SelectionChangedCause.tap,
);
await tester.pump();
expect(state.showToolbar(), true);
await tester.pump();
expect(find.text('SELECT ALL'), findsOneWidget);
expect(find.text('COPY'), 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: const ToolbarOptions(
selectAll: false,
),
style: textStyle,
cursorColor: cursorColor,
selectionControls: cupertinoTextSelectionControls,
),
),
);
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
await tester.tap(find.byType(EditableText));
await tester.pump();
expect(state.showToolbar(), true);
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,
selectAll: false,
),
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: const Offset(0, 0),
cause: SelectionChangedCause.tap,
);
await tester.pump();
expect(state.showToolbar(), true);
await tester.pump();
expect(find.text('SELECT ALL'), findsNothing);
expect(find.text('COPY'), findsOneWidget);
expect(find.text('PASTE'), findsNothing);
expect(find.text('CUT'), findsNothing);
});
testWidgets('cut and paste are disabled in read only mode even if explicit 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(
paste: true,
cut: true,
selectAll: true,
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: const Offset(0, 0),
cause: SelectionChangedCause.tap,
);
await tester.pump();
expect(state.showToolbar(), true);
await tester.pump();
expect(find.text('SELECT ALL'), findsOneWidget);
expect(find.text('COPY'), findsOneWidget);
expect(find.text('PASTE'), findsNothing);
expect(find.text('CUT'), findsNothing);
});
testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async {
String changedValue;
final Widget widget = MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: TextEditingController(),
focusNode: FocusNode(),
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {
changedValue = value;
},
),
);
await tester.pumpWidget(widget);
// Populate a fake clipboard.
const String clipboardContent = 'Dobunezumi mitai ni utsukushiku naritai';
Clipboard.setData(const ClipboardData(text: clipboardContent));
// Long-press to bring up the text editing controls.
final Finder textFinder = find.byType(EditableText);
await tester.longPress(textFinder);
tester.state<EditableTextState>(textFinder).showToolbar();
await tester.pump();
await tester.tap(find.text('PASTE'));
await tester.pump();
expect(changedValue, clipboardContent);
});
testWidgets('Does not lose focus by default when "next" action is pressed', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final Widget widget = MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: TextEditingController(),
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byType(EditableText);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
await tester.testTextInput.receiveAction(TextInputAction.next);
await tester.pump();
// Still has focus after pressing "next".
expect(focusNode.hasFocus, true);
});
testWidgets('Does not lose focus by default when "done" action is pressed and onEditingComplete is provided', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final Widget widget = MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: TextEditingController(),
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onEditingComplete: () {
// This prevents the default focus change behavior on submission.
},
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byType(EditableText);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
// Still has focus even though "done" was pressed because onEditingComplete
// was provided and it overrides the default behavior.
expect(focusNode.hasFocus, true);
});
testWidgets('When "done" is pressed callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
bool onEditingCompleteCalled = false;
bool onSubmittedCalled = false;
final Widget widget = MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: TextEditingController(),
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
cursorColor: Colors.blue,
onEditingComplete: () {
onEditingCompleteCalled = true;
expect(onSubmittedCalled, false);
},
onSubmitted: (String value) {
onSubmittedCalled = true;
expect(onEditingCompleteCalled, true);
},
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byType(EditableText);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
// The execution path starting with receiveAction() will trigger the
// onEditingComplete and onSubmission callbacks.
await tester.testTextInput.receiveAction(TextInputAction.done);
// The expectations we care about are up above in the onEditingComplete
// and onSubmission callbacks.
});
testWidgets('When "next" is pressed callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
bool onEditingCompleteCalled = false;
bool onSubmittedCalled = false;
final Widget widget = MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: TextEditingController(),
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
cursorColor: Colors.blue,
onEditingComplete: () {
onEditingCompleteCalled = true;
assert(!onSubmittedCalled);
},
onSubmitted: (String value) {
onSubmittedCalled = true;
assert(onEditingCompleteCalled);
},
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byType(EditableText);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
// The execution path starting with receiveAction() will trigger the
// onEditingComplete and onSubmission callbacks.
await tester.testTextInput.receiveAction(TextInputAction.done);
// The expectations we care about are up above in the onEditingComplete
// and onSubmission callbacks.
});
testWidgets('When "newline" action is called on a Editable text with maxLines == 1 callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
bool onEditingCompleteCalled = false;
bool onSubmittedCalled = false;
final Widget widget = MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: TextEditingController(),
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
cursorColor: Colors.blue,
maxLines: 1,
onEditingComplete: () {
onEditingCompleteCalled = true;
assert(!onSubmittedCalled);
},
onSubmitted: (String value) {
onSubmittedCalled = true;
assert(onEditingCompleteCalled);
},
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byType(EditableText);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
// The execution path starting with receiveAction() will trigger the
// onEditingComplete and onSubmission callbacks.
await tester.testTextInput.receiveAction(TextInputAction.newline);
// The expectations we care about are up above in the onEditingComplete
// and onSubmission callbacks.
});
testWidgets('When "newline" action is called on a Editable text with maxLines != 1, onEditingComplete and onSubmitted callbacks are not invoked.', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
bool onEditingCompleteCalled = false;
bool onSubmittedCalled = false;
final Widget widget = MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: TextEditingController(),
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
cursorColor: Colors.blue,
maxLines: 3,
onEditingComplete: () {
onEditingCompleteCalled = true;
},
onSubmitted: (String value) {
onSubmittedCalled = true;
},
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byType(EditableText);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
// The execution path starting with receiveAction() will trigger the
// onEditingComplete and onSubmission callbacks.
await tester.testTextInput.receiveAction(TextInputAction.newline);
// These callbacks shouldn't have been triggered.
assert(!onSubmittedCalled);
assert(!onEditingCompleteCalled);
});
testWidgets('Changing controller updates EditableText', (WidgetTester tester) async {
final TextEditingController controller1 =
TextEditingController(text: 'Wibble');
final TextEditingController controller2 =
TextEditingController(text: 'Wobble');
TextEditingController currentController = controller1;
StateSetter setState;
final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Focus Node');
Widget builder() {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return MaterialApp(
home: MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Material(
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: currentController,
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android)
.black
.subtitle1,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) { },
),
),
),
),
),
);
},
);
}
await tester.pumpWidget(builder());
await tester.pump(); // An extra pump to allow focus request to go through.
final List<MethodCall> log = <MethodCall>[];
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
await tester.showKeyboard(find.byType(EditableText));
// Verify TextInput.setEditingState and TextInput.setEditableSizeAndTransform are
// both fired with updated text when controller is replaced.
setState(() {
currentController = controller2;
});
await tester.pump();
expect(
log.lastWhere((MethodCall m) => m.method == 'TextInput.setEditingState'),
isMethodCall(
'TextInput.setEditingState',
arguments: const <String, dynamic>{
'text': 'Wobble',
'selectionBase': -1,
'selectionExtent': -1,
'selectionAffinity': 'TextAffinity.downstream',
'selectionIsDirectional': false,
'composingBase': -1,
'composingExtent': -1,
},
),
);
expect(
log.lastWhere((MethodCall m) => m.method == 'TextInput.setEditableSizeAndTransform'),
isMethodCall(
'TextInput.setEditableSizeAndTransform',
arguments: <String, dynamic>{
'width': 800,
'height': 14,
'transform': Matrix4.translationValues(0.0, 293.0, 0.0).storage.toList(),
},
),
);
});
testWidgets('EditableText identifies as text field (w/ focus) in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
),
);
expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField]));
await tester.tap(find.byType(EditableText));
await tester.idle();
await tester.pump();
expect(
semantics,
includesNodeWith(flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
]),
);
semantics.dispose();
});
testWidgets('EditableText sets multi-line flag in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
maxLines: 1,
),
),
),
),
);
expect(
semantics,
includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField]),
);
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
maxLines: 3,
),
),
),
),
);
expect(
semantics,
includesNodeWith(flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isMultiline,
]),
);
semantics.dispose();
});
testWidgets('EditableText includes text as value in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const String value1 = 'EditableText content';
controller.text = value1;
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
),
);
expect(
semantics,
includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
value: value1,
),
);
const String value2 = 'Changed the EditableText content';
controller.text = value2;
await tester.idle();
await tester.pump();
expect(
semantics,
includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
value: value2,
),
);
semantics.dispose();
});
testWidgets('exposes correct cursor movement semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
controller.text = 'test';
await tester.pumpWidget(MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
focusNode.requestFocus();
await tester.pump();
expect(
semantics,
includesNodeWith(
value: 'test',
),
);
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
await tester.pumpAndSettle();
// At end, can only go backwards.
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
],
),
);
controller.selection =
TextSelection.collapsed(offset: controller.text.length - 2);
await tester.pumpAndSettle();
// Somewhere in the middle, can go in both directions.
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
),
);
controller.selection = const TextSelection.collapsed(offset: 0);
await tester.pumpAndSettle();
// At beginning, can only go forward.
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
),
);
semantics.dispose();
});
testWidgets('can move cursor with a11y means - character', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const bool doNotExtendSelection = false;
controller.text = 'test';
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
await tester.pumpWidget(MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
],
),
);
final RenderEditable render = tester.allRenderObjects.whereType<RenderEditable>().first;
final int semanticsId = render.debugSemantics.id;
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 4);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 3);
expect(controller.selection.extentOffset, 3);
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 0);
await tester.pumpAndSettle();
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1);
expect(controller.selection.extentOffset, 1);
semantics.dispose();
});
testWidgets('can move cursor with a11y means - word', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const bool doNotExtendSelection = false;
controller.text = 'test for words';
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
await tester.pumpWidget(MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
expect(
semantics,
includesNodeWith(
value: 'test for words',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
],
),
);
final RenderEditable render = tester.allRenderObjects.whereType<RenderEditable>().first;
final int semanticsId = render.debugSemantics.id;
expect(controller.selection.baseOffset, 14);
expect(controller.selection.extentOffset, 14);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 9);
expect(controller.selection.extentOffset, 9);
expect(
semantics,
includesNodeWith(
value: 'test for words',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 0);
await tester.pumpAndSettle();
expect(
semantics,
includesNodeWith(
value: 'test for words',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByWord, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByWord, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 9);
expect(controller.selection.extentOffset, 9);
semantics.dispose();
}, skip: isBrowser);
testWidgets('can extend selection with a11y means - character', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const bool extendSelection = true;
const bool doNotExtendSelection = false;
controller.text = 'test';
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
await tester.pumpWidget(MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
],
),
);
final RenderEditable render = tester.allRenderObjects.whereType<RenderEditable>().first;
final int semanticsId = render.debugSemantics.id;
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 4);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 3);
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 0);
await tester.pumpAndSettle();
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1);
expect(controller.selection.extentOffset, 1);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1);
expect(controller.selection.extentOffset, 2);
semantics.dispose();
}, skip: isBrowser);
testWidgets('can extend selection with a11y means - word', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const bool extendSelection = true;
const bool doNotExtendSelection = false;
controller.text = 'test for words';
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
await tester.pumpWidget(MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
expect(
semantics,
includesNodeWith(
value: 'test for words',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
],
),
);
final RenderEditable render = tester.allRenderObjects.whereType<RenderEditable>().first;
final int semanticsId = render.debugSemantics.id;
expect(controller.selection.baseOffset, 14);
expect(controller.selection.extentOffset, 14);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 14);
expect(controller.selection.extentOffset, 9);
expect(
semantics,
includesNodeWith(
value: 'test for words',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 14);
expect(controller.selection.extentOffset, 5);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 14);
expect(controller.selection.extentOffset, 0);
await tester.pumpAndSettle();
expect(
semantics,
includesNodeWith(
value: 'test for words',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByWord, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByWord, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 9);
semantics.dispose();
}, skip: isBrowser);
testWidgets('password fields have correct semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
controller.text = 'super-secret-password!!1';
await tester.pumpWidget(MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
obscureText: true,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
final String expectedValue = '•' * controller.text.length;
expect(
semantics,
hasSemantics(
TestSemantics(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isObscured,
],
value: expectedValue,
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
semantics.dispose();
});
testWidgets('password fields become obscured with the right semantics when set', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const String originalText = 'super-secret-password!!1';
controller.text = originalText;
await tester.pumpWidget(MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
const String expectedValue = '••••••••••••••••••••••••';
expect(
semantics,
hasSemantics(
TestSemantics(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
],
value: originalText,
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
focusNode.requestFocus();
// Now change it to make it obscure text.
await tester.pumpWidget(MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
obscureText: true,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
expect(findRenderEditable(tester).text.text, expectedValue);
expect(
semantics,
hasSemantics(
TestSemantics(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isObscured,
SemanticsFlag.isFocused,
],
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.moveCursorBackwardByWord,
],
value: expectedValue,
textDirection: TextDirection.ltr,
textSelection: const TextSelection.collapsed(offset: 24),
),
],
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
semantics.dispose();
});
group('a11y copy/cut/paste', () {
Future<void> _buildApp(MockTextSelectionControls controls, WidgetTester tester) {
return tester.pumpWidget(MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: controls,
),
));
}
MockTextSelectionControls controls;
setUp(() {
controller.text = 'test';
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
controls = MockTextSelectionControls();
when(controls.buildHandle(any, any, any)).thenReturn(Container());
when(controls.buildToolbar(any, any, any, any, any, any))
.thenReturn(Container());
});
testWidgets('are exposed', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
when(controls.canCopy(any)).thenReturn(false);
when(controls.canCut(any)).thenReturn(false);
when(controls.canPaste(any)).thenReturn(false);
await _buildApp(controls, tester);
await tester.tap(find.byType(EditableText));
await tester.pump();
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
],
),
);
when(controls.canCopy(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.copy,
],
),
);
when(controls.canCopy(any)).thenReturn(false);
when(controls.canPaste(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.paste,
],
),
);
when(controls.canPaste(any)).thenReturn(false);
when(controls.canCut(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.cut,
],
),
);
when(controls.canCopy(any)).thenReturn(true);
when(controls.canCut(any)).thenReturn(true);
when(controls.canPaste(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.cut,
SemanticsAction.copy,
SemanticsAction.paste,
],
),
);
semantics.dispose();
});
testWidgets('can copy/cut/paste with a11y', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
when(controls.canCopy(any)).thenReturn(true);
when(controls.canCut(any)).thenReturn(true);
when(controls.canPaste(any)).thenReturn(true);
await _buildApp(controls, tester);
await tester.tap(find.byType(EditableText));
await tester.pump();
final SemanticsOwner owner = tester.binding.pipelineOwner.semanticsOwner;
const int expectedNodeId = 4;
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
children: <TestSemantics>[
TestSemantics(
id: 2,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics.rootChild(
id: expectedNodeId,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.copy,
SemanticsAction.cut,
SemanticsAction.paste,
],
value: 'test',
textSelection: TextSelection.collapsed(
offset: controller.text.length),
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
owner.performAction(expectedNodeId, SemanticsAction.copy);
verify(controls.handleCopy(any)).called(1);
owner.performAction(expectedNodeId, SemanticsAction.cut);
verify(controls.handleCut(any)).called(1);
owner.performAction(expectedNodeId, SemanticsAction.paste);
verify(controls.handlePaste(any)).called(1);
semantics.dispose();
});
});
testWidgets('allows customizing text style in subclasses', (WidgetTester tester) async {
controller.text = 'Hello World';
await tester.pumpWidget(MaterialApp(
home: CustomStyleEditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
// Simulate selection change via tap to show handles.
final RenderEditable render = tester.allRenderObjects.whereType<RenderEditable>().first;
expect(render.text.style.fontStyle, FontStyle.italic);
});
testWidgets('Formatters are skipped if text has not changed', (WidgetTester tester) async {
int called = 0;
final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) {
called += 1;
return newValue;
});
final TextEditingController controller = TextEditingController();
final MediaQuery mediaQuery = MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: EditableText(
controller: controller,
backgroundCursorColor: Colors.red,
cursorColor: Colors.red,
focusNode: FocusNode(),
style: textStyle,
inputFormatters: <TextInputFormatter>[
formatter,
],
textDirection: TextDirection.ltr,
),
);
await tester.pumpWidget(mediaQuery);
final EditableTextState state = tester.firstState(find.byType(EditableText));
state.updateEditingValue(const TextEditingValue(
text: 'a',
));
expect(called, 1);
// same value.
state.updateEditingValue(const TextEditingValue(
text: 'a',
));
expect(called, 1);
// same value with different selection.
state.updateEditingValue(const TextEditingValue(
text: 'a',
selection: TextSelection.collapsed(offset: 1),
));
// different value.
state.updateEditingValue(const TextEditingValue(
text: 'b',
));
expect(called, 2);
});
testWidgets('default keyboardAppearance is respected', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/22212.
final List<MethodCall> log = <MethodCall>[];
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(
devicePixelRatio: 1.0
),
child: Directionality(
textDirection: TextDirection.ltr,
child: EditableText(
controller: controller,
focusNode: FocusNode(),
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
),
),
),
);
await tester.showKeyboard(find.byType(EditableText));
final MethodCall setClient = log.first;
expect(setClient.method, 'TextInput.setClient');
expect(setClient.arguments.last['keyboardAppearance'], 'Brightness.light');
});
testWidgets('location of widget is sent on show keyboard', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(
devicePixelRatio: 1.0
),
child: Directionality(
textDirection: TextDirection.ltr,
child: EditableText(
controller: controller,
focusNode: FocusNode(),
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
),
),
),
);
await tester.showKeyboard(find.byType(EditableText));
final MethodCall methodCall = log.firstWhere((MethodCall m) => m.method == 'TextInput.setEditableSizeAndTransform');
expect(
methodCall,
isMethodCall('TextInput.setEditableSizeAndTransform', arguments: <String, dynamic>{
'width': 800,
'height': 600,
'transform': Matrix4.identity().storage.toList(),
}),
);
});
testWidgets('transform and size is reset when text connection opens', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
final TextEditingController controller1 = TextEditingController();
final TextEditingController controller2 = TextEditingController();
controller1.text = 'Text1';
controller2.text = 'Text2';
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(
devicePixelRatio: 1.0
),
child: Directionality(
textDirection: TextDirection.ltr,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
EditableText(
key: ValueKey<String>(controller1.text),
controller: controller1,
focusNode: FocusNode(),
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
),
const SizedBox(height: 200.0),
EditableText(
key: ValueKey<String>(controller2.text),
controller: controller2,
focusNode: FocusNode(),
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
minLines: 10,
maxLines: 20,
),
const SizedBox(height: 100.0),
],
),
),
),
);
await tester.showKeyboard(find.byKey(ValueKey<String>(controller1.text)));
final MethodCall methodCall = log.firstWhere((MethodCall m) => m.method == 'TextInput.setEditableSizeAndTransform');
expect(
methodCall,
isMethodCall('TextInput.setEditableSizeAndTransform', arguments: <String, dynamic>{
'width': 800,
'height': 14,
'transform': Matrix4.identity().storage.toList(),
}),
);
log.clear();
// Move to the next editable text.
await tester.showKeyboard(find.byKey(ValueKey<String>(controller2.text)));
final MethodCall methodCall2 = log.firstWhere((MethodCall m) => m.method == 'TextInput.setEditableSizeAndTransform');
expect(
methodCall2,
isMethodCall('TextInput.setEditableSizeAndTransform', arguments: <String, dynamic>{
'width': 800,
'height': 140.0,
'transform': <double>[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 214.0, 0.0, 1.0],
}),
);
log.clear();
// Move back to the first editable text.
await tester.showKeyboard(find.byKey(ValueKey<String>(controller1.text)));
final MethodCall methodCall3 = log.firstWhere((MethodCall m) => m.method == 'TextInput.setEditableSizeAndTransform');
expect(
methodCall3,
isMethodCall('TextInput.setEditableSizeAndTransform', arguments: <String, dynamic>{
'width': 800,
'height': 14,
'transform': Matrix4.identity().storage.toList(),
}),
);
});
testWidgets('size and transform are sent when they change', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
const Offset offset = Offset(10.0, 20.0);
const Key transformButtonKey = Key('transformButton');
await tester.pumpWidget(
const TransformedEditableText(
offset: offset,
transformButtonKey: transformButtonKey,
),
);
await tester.showKeyboard(find.byType(EditableText));
MethodCall methodCall = log.firstWhere((MethodCall m) => m.method == 'TextInput.setEditableSizeAndTransform');
expect(
methodCall,
isMethodCall('TextInput.setEditableSizeAndTransform', arguments: <String, dynamic>{
'width': 800,
'height': 14,
'transform': Matrix4.identity().storage.toList(),
}),
);
log.clear();
await tester.tap(find.byKey(transformButtonKey));
await tester.pumpAndSettle();
// There should be a new platform message updating the transform.
methodCall = log.firstWhere((MethodCall m) => m.method == 'TextInput.setEditableSizeAndTransform');
expect(
methodCall,
isMethodCall('TextInput.setEditableSizeAndTransform', arguments: <String, dynamic>{
'width': 800,
'height': 14,
'transform': Matrix4.translationValues(offset.dx, offset.dy, 0.0).storage.toList(),
}),
);
});
testWidgets('text styling info is sent on show keyboard', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: EditableText(
textDirection: TextDirection.rtl,
controller: controller,
focusNode: FocusNode(),
style: const TextStyle(
fontSize: 20.0,
fontFamily: 'Roboto',
fontWeight: FontWeight.w600,
),
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
),
),
);
await tester.showKeyboard(find.byType(EditableText));
final MethodCall setStyle = log.firstWhere((MethodCall m) => m.method == 'TextInput.setStyle');
expect(
setStyle,
isMethodCall('TextInput.setStyle', arguments: <String, dynamic>{
'fontSize': 20.0,
'fontFamily': 'Roboto',
'fontWeightIndex': 5,
'textAlignIndex': 4,
'textDirectionIndex': 0,
}),
);
});
testWidgets('text styling info is sent on style update', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
StateSetter setState;
const TextStyle textStyle1 = TextStyle(
fontSize: 20.0,
fontFamily: 'RobotoMono',
fontWeight: FontWeight.w600,
);
const TextStyle textStyle2 = TextStyle(
fontSize: 20.0,
fontFamily: 'Raleway',
fontWeight: FontWeight.w700,
);
TextStyle currentTextStyle = textStyle1;
Widget builder() {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return MaterialApp(
home: MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Material(
child: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: controller,
focusNode: FocusNode(),
style: currentTextStyle,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {},
),
),
),
),
),
);
},
);
}
await tester.pumpWidget(builder());
await tester.showKeyboard(find.byType(EditableText));
final List<MethodCall> log = <MethodCall>[];
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
setState(() {
currentTextStyle = textStyle2;
});
await tester.pump();
// Updated styling information should be sent via TextInput.setStyle method.
final MethodCall setStyle = log.firstWhere((MethodCall m) => m.method == 'TextInput.setStyle');
expect(
setStyle,
isMethodCall('TextInput.setStyle', arguments: <String, dynamic>{
'fontSize': 20.0,
'fontFamily': 'Raleway',
'fontWeightIndex': 6,
'textAlignIndex': 4,
'textDirectionIndex': 1,
}),
);
});
testWidgets('custom keyboardAppearance is respected', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/22212.
final List<MethodCall> log = <MethodCall>[];
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(
devicePixelRatio: 1.0
),
child: Directionality(
textDirection: TextDirection.ltr,
child: EditableText(
controller: controller,
focusNode: FocusNode(),
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
keyboardAppearance: Brightness.dark,
),
),
),
);
await tester.showKeyboard(find.byType(EditableText));
final MethodCall setClient = log.first;
expect(setClient.method, 'TextInput.setClient');
expect(setClient.arguments.last['keyboardAppearance'], 'Brightness.dark');
});
testWidgets('Composing text is underlined and underline is cleared when losing focus', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController.fromValue(
const TextEditingValue(
text: 'text composing text',
selection: TextSelection.collapsed(offset: 14),
composing: TextRange(start: 5, end: 14),
),
);
final FocusNode focusNode = FocusNode(debugLabel: 'Test Focus Node');
await tester.pumpWidget(MaterialApp( // So we can show overlays.
home: EditableText(
autofocus: true,
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onEditingComplete: () {
// This prevents the default focus change behavior on submission.
},
),
));
assert(focusNode.hasFocus);
final RenderEditable renderEditable = findRenderEditable(tester);
// The actual text span is split into 3 parts with the middle part underlined.
expect(renderEditable.text.children.length, 3);
final TextSpan textSpan = renderEditable.text.children[1] as TextSpan;
expect(textSpan.text, 'composing');
expect(textSpan.style.decoration, TextDecoration.underline);
focusNode.unfocus();
await tester.pump();
expect(renderEditable.text.children, isNull);
// Everything's just formated the same way now.
expect(renderEditable.text.text, 'text composing text');
expect(renderEditable.text.style.decoration, isNull);
});
testWidgets('text selection handle visibility', (WidgetTester tester) async {
// Text with two separate words to select.
const String testText = 'XXXXX XXXXX';
final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 100,
child: EditableText(
showSelectionHandles: true,
controller: controller,
focusNode: FocusNode(),
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
),
),
),
));
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
final RenderEditable renderEditable = state.renderEditable;
final Scrollable scrollable = tester.widget<Scrollable>(find.byType(Scrollable));
bool expectedLeftVisibleBefore = false;
bool expectedRightVisibleBefore = false;
Future<void> verifyVisibility(
HandlePositionInViewport leftPosition,
bool expectedLeftVisible,
HandlePositionInViewport rightPosition,
bool expectedRightVisible,
) async {
await tester.pump();
// Check the signal from RenderEditable about whether they're within the
// viewport.
expect(renderEditable.selectionStartInViewport.value, equals(expectedLeftVisible));
expect(renderEditable.selectionEndInViewport.value, equals(expectedRightVisible));
// Check that the animations are functional and going in the right
// direction.
final List<FadeTransition> transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionHandleOverlay'),
matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, 2);
final FadeTransition left = transitions[0];
final FadeTransition right = transitions[1];
if (expectedLeftVisibleBefore)
expect(left.opacity.value, equals(1.0));
if (expectedRightVisibleBefore)
expect(right.opacity.value, equals(1.0));
await tester.pump(TextSelectionOverlay.fadeDuration ~/ 2);
if (expectedLeftVisible != expectedLeftVisibleBefore)
expect(left.opacity.value, equals(0.5));
if (expectedRightVisible != expectedRightVisibleBefore)
expect(right.opacity.value, equals(0.5));
await tester.pump(TextSelectionOverlay.fadeDuration ~/ 2);
if (expectedLeftVisible)
expect(left.opacity.value, equals(1.0));
if (expectedRightVisible)
expect(right.opacity.value, equals(1.0));
expectedLeftVisibleBefore = expectedLeftVisible;
expectedRightVisibleBefore = expectedRightVisible;
// Check that the handles' positions are correct.
final List<RenderBox> handles = List<RenderBox>.from(
tester.renderObjectList<RenderBox>(
find.descendant(
of: find.byType(CompositedTransformFollower),
matching: find.byType(GestureDetector),
),
),
);
final Size viewport = renderEditable.size;
void testPosition(double pos, HandlePositionInViewport expected) {
switch (expected) {
case HandlePositionInViewport.leftEdge:
expect(
pos,
inExclusiveRange(
0 - kMinInteractiveDimension,
0 + kMinInteractiveDimension,
),
);
break;
case HandlePositionInViewport.rightEdge:
expect(
pos,
inExclusiveRange(
viewport.width - kMinInteractiveDimension,
viewport.width + kMinInteractiveDimension,
),
);
break;
case HandlePositionInViewport.within:
expect(
pos,
inExclusiveRange(
0 - kMinInteractiveDimension,
viewport.width + kMinInteractiveDimension,
),
);
break;
default:
throw TestFailure("HandlePositionInViewport can't be null.");
}
}
expect(state.selectionOverlay.handlesAreVisible, isTrue);
testPosition(handles[0].localToGlobal(Offset.zero).dx, leftPosition);
testPosition(handles[1].localToGlobal(Offset.zero).dx, rightPosition);
}
// Select the first word. Both handles should be visible.
await tester.tapAt(const Offset(20, 10));
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
await tester.pump();
await verifyVisibility(HandlePositionInViewport.leftEdge, true, HandlePositionInViewport.within, true);
// Drag the text slightly so the first word is partially visible. Only the
// right handle should be visible.
scrollable.controller.jumpTo(20.0);
await verifyVisibility(HandlePositionInViewport.leftEdge, false, HandlePositionInViewport.within, true);
// Drag the text all the way to the left so the first word is not visible at
// all (and the second word is fully visible). Both handles should be
// invisible now.
scrollable.controller.ju