Unverified Commit 581df52a authored by sandrasandeep's avatar sandrasandeep Committed by GitHub

Make EditableText cursor configurable (#18888)

* fixed segmented control golden test

* fixed segmented control golden test

* added cursorWidth, cursorRadius

* added default value for cursorWidth based on Apple specs

* test default cursorWidth

* removed cursorHeight stuff

* added functionality to keep cursor from blinking

* cursor width and radius is configurable + tests

* changed goldens repo version in goldens.version

* working version of configurable cursor (erased debugKeepCursorOn)

* minor changes

* docs

* changed textfield test that was failing due to new default cursorwidth

* added default value of cursorwidth in RenderEditable

* only run golden file tests on Mac

* cursor tests

* the tests are actually there now

* weak warning fixed

* switching to Linux

* changed default cursorWidth: 2.0 -> 1.0

* assorted changes, including changing text field test

* re-paint -> re-layout when changing cursorWidth
parent 989f5741
...@@ -16,7 +16,6 @@ import 'viewport_offset.dart'; ...@@ -16,7 +16,6 @@ import 'viewport_offset.dart';
const double _kCaretGap = 1.0; // pixels const double _kCaretGap = 1.0; // pixels
const double _kCaretHeightOffset = 2.0; // pixels const double _kCaretHeightOffset = 2.0; // pixels
const double _kCaretWidth = 1.0; // pixels
/// Signature for the callback that reports when the user changes the selection /// Signature for the callback that reports when the user changes the selection
/// (including the cursor location). /// (including the cursor location).
...@@ -134,6 +133,8 @@ class RenderEditable extends RenderBox { ...@@ -134,6 +133,8 @@ class RenderEditable extends RenderBox {
this.ignorePointer = false, this.ignorePointer = false,
bool obscureText = false, bool obscureText = false,
Locale locale, Locale locale,
double cursorWidth = 1.0,
Radius cursorRadius,
}) : assert(textAlign != null), }) : assert(textAlign != null),
assert(textDirection != null, 'RenderEditable created without a textDirection.'), assert(textDirection != null, 'RenderEditable created without a textDirection.'),
assert(maxLines == null || maxLines > 0), assert(maxLines == null || maxLines > 0),
...@@ -155,6 +156,8 @@ class RenderEditable extends RenderBox { ...@@ -155,6 +156,8 @@ class RenderEditable extends RenderBox {
_selectionColor = selectionColor, _selectionColor = selectionColor,
_selection = selection, _selection = selection,
_offset = offset, _offset = offset,
_cursorWidth = cursorWidth,
_cursorRadius = cursorRadius,
_obscureText = obscureText { _obscureText = obscureText {
assert(_showCursor != null); assert(_showCursor != null);
assert(!_showCursor.value || cursorColor != null); assert(!_showCursor.value || cursorColor != null);
...@@ -382,6 +385,26 @@ class RenderEditable extends RenderBox { ...@@ -382,6 +385,26 @@ class RenderEditable extends RenderBox {
markNeedsLayout(); markNeedsLayout();
} }
/// How thick the cursor will be.
double get cursorWidth => _cursorWidth;
double _cursorWidth = 1.0;
set cursorWidth(double value) {
if (_cursorWidth == value)
return;
_cursorWidth = value;
markNeedsLayout();
}
/// How rounded the corners of the cursor should be.
Radius get cursorRadius => _cursorRadius;
Radius _cursorRadius;
set cursorRadius(Radius value) {
if (_cursorRadius == value)
return;
_cursorRadius = value;
markNeedsPaint();
}
@override @override
void describeSemanticsConfiguration(SemanticsConfiguration config) { void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config); super.describeSemanticsConfiguration(config);
...@@ -546,7 +569,7 @@ class RenderEditable extends RenderBox { ...@@ -546,7 +569,7 @@ class RenderEditable extends RenderBox {
_layoutText(constraints.maxWidth); _layoutText(constraints.maxWidth);
final Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype); final Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype);
// This rect is the same as _caretPrototype but without the vertical padding. // This rect is the same as _caretPrototype but without the vertical padding.
return new Rect.fromLTWH(0.0, 0.0, _kCaretWidth, preferredLineHeight).shift(caretOffset + _paintOffset); return new Rect.fromLTWH(0.0, 0.0, cursorWidth, preferredLineHeight).shift(caretOffset + _paintOffset);
} }
@override @override
...@@ -683,7 +706,7 @@ class RenderEditable extends RenderBox { ...@@ -683,7 +706,7 @@ class RenderEditable extends RenderBox {
assert(constraintWidth != null); assert(constraintWidth != null);
if (_textLayoutLastWidth == constraintWidth) if (_textLayoutLastWidth == constraintWidth)
return; return;
const double caretMargin = _kCaretGap + _kCaretWidth; final double caretMargin = _kCaretGap + cursorWidth;
final double availableWidth = math.max(0.0, constraintWidth - caretMargin); final double availableWidth = math.max(0.0, constraintWidth - caretMargin);
final double maxWidth = _isMultiline ? availableWidth : double.infinity; final double maxWidth = _isMultiline ? availableWidth : double.infinity;
_textPainter.layout(minWidth: availableWidth, maxWidth: maxWidth); _textPainter.layout(minWidth: availableWidth, maxWidth: maxWidth);
...@@ -693,7 +716,7 @@ class RenderEditable extends RenderBox { ...@@ -693,7 +716,7 @@ class RenderEditable extends RenderBox {
@override @override
void performLayout() { void performLayout() {
_layoutText(constraints.maxWidth); _layoutText(constraints.maxWidth);
_caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, preferredLineHeight - 2.0 * _kCaretHeightOffset); _caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, cursorWidth, preferredLineHeight - 2.0 * _kCaretHeightOffset);
_selectionRects = null; _selectionRects = null;
// We grab _textPainter.size here because assigning to `size` on the next // We grab _textPainter.size here because assigning to `size` on the next
// line will trigger us to validate our intrinsic sizes, which will change // line will trigger us to validate our intrinsic sizes, which will change
...@@ -705,7 +728,7 @@ class RenderEditable extends RenderBox { ...@@ -705,7 +728,7 @@ class RenderEditable extends RenderBox {
// See also RenderParagraph which has a similar issue. // See also RenderParagraph which has a similar issue.
final Size textPainterSize = _textPainter.size; final Size textPainterSize = _textPainter.size;
size = new Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight(constraints.maxWidth))); size = new Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight(constraints.maxWidth)));
final Size contentSize = new Size(textPainterSize.width + _kCaretGap + _kCaretWidth, textPainterSize.height); final Size contentSize = new Size(textPainterSize.width + _kCaretGap + cursorWidth, textPainterSize.height);
final double _maxScrollExtent = _getMaxScrollExtent(contentSize); final double _maxScrollExtent = _getMaxScrollExtent(contentSize);
_hasVisualOverflow = _maxScrollExtent > 0.0; _hasVisualOverflow = _maxScrollExtent > 0.0;
offset.applyViewportDimension(_viewportExtent); offset.applyViewportDimension(_viewportExtent);
...@@ -715,9 +738,18 @@ class RenderEditable extends RenderBox { ...@@ -715,9 +738,18 @@ class RenderEditable extends RenderBox {
void _paintCaret(Canvas canvas, Offset effectiveOffset) { void _paintCaret(Canvas canvas, Offset effectiveOffset) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth);
final Offset caretOffset = _textPainter.getOffsetForCaret(_selection.extent, _caretPrototype); final Offset caretOffset = _textPainter.getOffsetForCaret(_selection.extent, _caretPrototype);
final Paint paint = new Paint()..color = _cursorColor; final Paint paint = new Paint()
..color = _cursorColor;
final Rect caretRect = _caretPrototype.shift(caretOffset + effectiveOffset); final Rect caretRect = _caretPrototype.shift(caretOffset + effectiveOffset);
if (cursorRadius == null) {
canvas.drawRect(caretRect, paint); canvas.drawRect(caretRect, paint);
} else {
final RRect caretRRect = RRect.fromRectAndRadius(caretRect, cursorRadius);
canvas.drawRRect(caretRRect, paint);
}
if (caretRect != _lastCaretRect) { if (caretRect != _lastCaretRect) {
_lastCaretRect = caretRect; _lastCaretRect = caretRect;
if (onCaretChanged != null) if (onCaretChanged != null)
......
...@@ -208,6 +208,8 @@ class EditableText extends StatefulWidget { ...@@ -208,6 +208,8 @@ class EditableText extends StatefulWidget {
this.onSelectionChanged, this.onSelectionChanged,
List<TextInputFormatter> inputFormatters, List<TextInputFormatter> inputFormatters,
this.rendererIgnoresPointer = false, this.rendererIgnoresPointer = false,
this.cursorWidth = 1.0,
this.cursorRadius,
}) : assert(controller != null), }) : assert(controller != null),
assert(focusNode != null), assert(focusNode != null),
assert(obscureText != null), assert(obscureText != null),
...@@ -353,6 +355,16 @@ class EditableText extends StatefulWidget { ...@@ -353,6 +355,16 @@ class EditableText extends StatefulWidget {
/// This property is false by default. /// This property is false by default.
final bool rendererIgnoresPointer; final bool rendererIgnoresPointer;
/// How thick the cursor will be.
///
/// Defaults to 1.0
final double cursorWidth;
/// How rounded the corners of the cursor should be.
///
/// By default, the cursor has a Radius of zero.
final Radius cursorRadius;
@override @override
EditableTextState createState() => new EditableTextState(); EditableTextState createState() => new EditableTextState();
...@@ -810,6 +822,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -810,6 +822,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
FocusScope.of(context).reparentIfNeeded(widget.focusNode); FocusScope.of(context).reparentIfNeeded(widget.focusNode);
super.build(context); // See AutomaticKeepAliveClientMixin. super.build(context); // See AutomaticKeepAliveClientMixin.
final TextSelectionControls controls = widget.selectionControls; final TextSelectionControls controls = widget.selectionControls;
return new Scrollable( return new Scrollable(
excludeFromSemantics: true, excludeFromSemantics: true,
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right, axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
...@@ -841,6 +854,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -841,6 +854,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
onSelectionChanged: _handleSelectionChanged, onSelectionChanged: _handleSelectionChanged,
onCaretChanged: _handleCaretChanged, onCaretChanged: _handleCaretChanged,
rendererIgnoresPointer: widget.rendererIgnoresPointer, rendererIgnoresPointer: widget.rendererIgnoresPointer,
cursorWidth: widget.cursorWidth,
cursorRadius: widget.cursorRadius,
), ),
), ),
); );
...@@ -902,6 +917,8 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -902,6 +917,8 @@ class _Editable extends LeafRenderObjectWidget {
this.onSelectionChanged, this.onSelectionChanged,
this.onCaretChanged, this.onCaretChanged,
this.rendererIgnoresPointer = false, this.rendererIgnoresPointer = false,
this.cursorWidth,
this.cursorRadius,
}) : assert(textDirection != null), }) : assert(textDirection != null),
assert(rendererIgnoresPointer != null), assert(rendererIgnoresPointer != null),
super(key: key); super(key: key);
...@@ -923,6 +940,8 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -923,6 +940,8 @@ class _Editable extends LeafRenderObjectWidget {
final SelectionChangedHandler onSelectionChanged; final SelectionChangedHandler onSelectionChanged;
final CaretChangedHandler onCaretChanged; final CaretChangedHandler onCaretChanged;
final bool rendererIgnoresPointer; final bool rendererIgnoresPointer;
final double cursorWidth;
final Radius cursorRadius;
@override @override
RenderEditable createRenderObject(BuildContext context) { RenderEditable createRenderObject(BuildContext context) {
...@@ -943,6 +962,8 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -943,6 +962,8 @@ class _Editable extends LeafRenderObjectWidget {
onCaretChanged: onCaretChanged, onCaretChanged: onCaretChanged,
ignorePointer: rendererIgnoresPointer, ignorePointer: rendererIgnoresPointer,
obscureText: obscureText, obscureText: obscureText,
cursorWidth: cursorWidth,
cursorRadius: cursorRadius,
); );
} }
...@@ -964,6 +985,8 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -964,6 +985,8 @@ class _Editable extends LeafRenderObjectWidget {
..onSelectionChanged = onSelectionChanged ..onSelectionChanged = onSelectionChanged
..onCaretChanged = onCaretChanged ..onCaretChanged = onCaretChanged
..ignorePointer = rendererIgnoresPointer ..ignorePointer = rendererIgnoresPointer
..obscureText = obscureText; ..obscureText = obscureText
..cursorWidth = cursorWidth
..cursorRadius = cursorRadius;
} }
} }
...@@ -967,6 +967,7 @@ void main() { ...@@ -967,6 +967,7 @@ void main() {
final Offset center = tester.getCenter(find.text('B')); final Offset center = tester.getCenter(find.text('B'));
await tester.startGesture(center); await tester.startGesture(center);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await expectLater( await expectLater(
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile('segmented_control_test.1.0.png'), matchesGoldenFile('segmented_control_test.1.0.png'),
......
...@@ -1208,7 +1208,7 @@ void main() { ...@@ -1208,7 +1208,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft, editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft,
); );
expect(topLeft.dx, equals(399.0)); expect(topLeft.dx, equals(399));
await tester.enterText(find.byType(TextField), 'abcd'); await tester.enterText(find.byType(TextField), 'abcd');
await tester.pump(); await tester.pump();
...@@ -1217,7 +1217,7 @@ void main() { ...@@ -1217,7 +1217,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft, editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
); );
expect(topLeft.dx, equals(399.0)); expect(topLeft.dx, equals(399));
}); });
testWidgets('Can align to center within center', (WidgetTester tester) async { testWidgets('Can align to center within center', (WidgetTester tester) async {
...@@ -1240,7 +1240,7 @@ void main() { ...@@ -1240,7 +1240,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft, editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft,
); );
expect(topLeft.dx, equals(399.0)); expect(topLeft.dx, equals(399));
await tester.enterText(find.byType(TextField), 'abcd'); await tester.enterText(find.byType(TextField), 'abcd');
await tester.pump(); await tester.pump();
...@@ -1249,7 +1249,7 @@ void main() { ...@@ -1249,7 +1249,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft, editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
); );
expect(topLeft.dx, equals(399.0)); expect(topLeft.dx, equals(399));
}); });
testWidgets('Controller can update server', (WidgetTester tester) async { testWidgets('Controller can update server', (WidgetTester tester) async {
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -56,7 +57,8 @@ void main() { ...@@ -56,7 +57,8 @@ void main() {
controller.text = 'test'; controller.text = 'test';
await tester.idle(); await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test')); expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals(serializedActionName)); expect(tester.testTextInput.setClientArgs['inputAction'],
equals(serializedActionName));
} }
testWidgets('has expected defaults', (WidgetTester tester) async { testWidgets('has expected defaults', (WidgetTester tester) async {
...@@ -77,6 +79,26 @@ void main() { ...@@ -77,6 +79,26 @@ void main() {
expect(editableText.maxLines, equals(1)); expect(editableText.maxLines, equals(1));
expect(editableText.obscureText, isFalse); expect(editableText.obscureText, isFalse);
expect(editableText.autocorrect, isTrue); expect(editableText.autocorrect, isTrue);
expect(editableText.cursorWidth, 1.0);
});
testWidgets('cursor has expected width and radius',
(WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
cursorWidth: 10.0,
cursorRadius: const Radius.circular(2.0),
)));
final EditableText editableText =
tester.firstWidget(find.byType(EditableText));
expect(editableText.cursorWidth, 10.0);
expect(editableText.cursorRadius.x, 2.0);
}); });
testWidgets('text keyboard is requested when maxLines is default', testWidgets('text keyboard is requested when maxLines is default',
...@@ -104,12 +126,14 @@ void main() { ...@@ -104,12 +126,14 @@ void main() {
tester.firstWidget(find.byType(EditableText)); tester.firstWidget(find.byType(EditableText));
expect(editableText.maxLines, equals(1)); expect(editableText.maxLines, equals(1));
expect(tester.testTextInput.editingState['text'], equals('test')); expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.text')); expect(tester.testTextInput.setClientArgs['inputType']['name'],
equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'], expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done')); equals('TextInputAction.done'));
}); });
testWidgets('Keyboard is configured for "unspecified" action when explicitly requested', testWidgets(
'Keyboard is configured for "unspecified" action when explicitly requested',
(WidgetTester tester) async { (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
...@@ -118,7 +142,8 @@ void main() { ...@@ -118,7 +142,8 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "none" action when explicitly requested', testWidgets(
'Keyboard is configured for "none" action when explicitly requested',
(WidgetTester tester) async { (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
...@@ -127,7 +152,8 @@ void main() { ...@@ -127,7 +152,8 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "done" action when explicitly requested', testWidgets(
'Keyboard is configured for "done" action when explicitly requested',
(WidgetTester tester) async { (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
...@@ -136,7 +162,8 @@ void main() { ...@@ -136,7 +162,8 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "send" action when explicitly requested', testWidgets(
'Keyboard is configured for "send" action when explicitly requested',
(WidgetTester tester) async { (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
...@@ -145,7 +172,8 @@ void main() { ...@@ -145,7 +172,8 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "go" action when explicitly requested', testWidgets(
'Keyboard is configured for "go" action when explicitly requested',
(WidgetTester tester) async { (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
...@@ -154,7 +182,8 @@ void main() { ...@@ -154,7 +182,8 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "search" action when explicitly requested', testWidgets(
'Keyboard is configured for "search" action when explicitly requested',
(WidgetTester tester) async { (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
...@@ -163,7 +192,8 @@ void main() { ...@@ -163,7 +192,8 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "send" action when explicitly requested', testWidgets(
'Keyboard is configured for "send" action when explicitly requested',
(WidgetTester tester) async { (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
...@@ -172,7 +202,8 @@ void main() { ...@@ -172,7 +202,8 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "next" action when explicitly requested', testWidgets(
'Keyboard is configured for "next" action when explicitly requested',
(WidgetTester tester) async { (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
...@@ -181,7 +212,8 @@ void main() { ...@@ -181,7 +212,8 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "previous" action when explicitly requested', testWidgets(
'Keyboard is configured for "previous" action when explicitly requested',
(WidgetTester tester) async { (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
...@@ -190,7 +222,8 @@ void main() { ...@@ -190,7 +222,8 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "continue" action when explicitly requested', testWidgets(
'Keyboard is configured for "continue" action when explicitly requested',
(WidgetTester tester) async { (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
...@@ -199,7 +232,8 @@ void main() { ...@@ -199,7 +232,8 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "join" action when explicitly requested', testWidgets(
'Keyboard is configured for "join" action when explicitly requested',
(WidgetTester tester) async { (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
...@@ -208,7 +242,8 @@ void main() { ...@@ -208,7 +242,8 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "route" action when explicitly requested', testWidgets(
'Keyboard is configured for "route" action when explicitly requested',
(WidgetTester tester) async { (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
...@@ -217,7 +252,8 @@ void main() { ...@@ -217,7 +252,8 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "emergencyCall" action when explicitly requested', testWidgets(
'Keyboard is configured for "emergencyCall" action when explicitly requested',
(WidgetTester tester) async { (WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
...@@ -250,11 +286,14 @@ void main() { ...@@ -250,11 +286,14 @@ void main() {
controller.text = 'test'; controller.text = 'test';
await tester.idle(); await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test')); expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.multiline')); expect(tester.testTextInput.setClientArgs['inputType']['name'],
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.newline')); equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.newline'));
}); });
testWidgets('Correct keyboard is requested when set explicitly and maxLines > 1', testWidgets(
'Correct keyboard is requested when set explicitly and maxLines > 1',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new Directionality( new Directionality(
...@@ -279,8 +318,10 @@ void main() { ...@@ -279,8 +318,10 @@ void main() {
controller.text = 'test'; controller.text = 'test';
await tester.idle(); await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test')); expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.phone')); expect(tester.testTextInput.setClientArgs['inputType']['name'],
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.done')); equals('TextInputType.phone'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done'));
}); });
testWidgets('multiline keyboard is requested when set implicitly', testWidgets('multiline keyboard is requested when set implicitly',
...@@ -307,8 +348,10 @@ void main() { ...@@ -307,8 +348,10 @@ void main() {
controller.text = 'test'; controller.text = 'test';
await tester.idle(); await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test')); expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.multiline')); expect(tester.testTextInput.setClientArgs['inputType']['name'],
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.newline')); equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.newline'));
}); });
testWidgets('single line inputs have correct default keyboard', testWidgets('single line inputs have correct default keyboard',
...@@ -335,12 +378,16 @@ void main() { ...@@ -335,12 +378,16 @@ void main() {
controller.text = 'test'; controller.text = 'test';
await tester.idle(); await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test')); expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.text')); expect(tester.testTextInput.setClientArgs['inputType']['name'],
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.done')); equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done'));
}); });
testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async { testWidgets('Fires onChanged when text changes via TextSelectionOverlay',
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>(); (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
String changedValue; String changedValue;
final Widget widget = new MaterialApp( final Widget widget = new MaterialApp(
...@@ -361,9 +408,10 @@ void main() { ...@@ -361,9 +408,10 @@ void main() {
// Populate a fake clipboard. // Populate a fake clipboard.
const String clipboardContent = 'Dobunezumi mitai ni utsukushiku naritai'; const String clipboardContent = 'Dobunezumi mitai ni utsukushiku naritai';
SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async { SystemChannels.platform
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.getData') if (methodCall.method == 'Clipboard.getData')
return const <String, dynamic>{ 'text': clipboardContent }; return const <String, dynamic>{'text': clipboardContent};
return null; return null;
}); });
...@@ -378,39 +426,111 @@ void main() { ...@@ -378,39 +426,111 @@ void main() {
expect(changedValue, clipboardContent); expect(changedValue, clipboardContent);
}); });
testWidgets('Loses focus by default when "done" action is pressed', (WidgetTester tester) async { testWidgets('cursor layout has correct width', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>(); final GlobalKey<EditableTextState> editableTextKey =
final FocusNode focusNode = new FocusNode(); new GlobalKey<EditableTextState>();
String changedValue;
final Widget widget = new MaterialApp( final Widget widget = new MaterialApp(
home: new EditableText( home: new RepaintBoundary(
key: const ValueKey<int>(1),
child: new EditableText(
key: editableTextKey, key: editableTextKey,
controller: new TextEditingController(), controller: new TextEditingController(),
focusNode: focusNode, focusNode: new FocusNode(),
style: new Typography(platform: TargetPlatform.android).black.subhead, style: new Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue, cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls, selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
onChanged: (String value) {
changedValue = value;
},
cursorWidth: 15.0,
),
), ),
); );
await tester.pumpWidget(widget); await tester.pumpWidget(widget);
// Select EditableText to give it focus. // Populate a fake clipboard.
const String clipboardContent = ' ';
SystemChannels.platform
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.getData')
return const <String, dynamic>{'text': clipboardContent};
return null;
});
// Long-press to bring up the text editing controls.
final Finder textFinder = find.byKey(editableTextKey); final Finder textFinder = find.byKey(editableTextKey);
await tester.tap(textFinder); await tester.longPress(textFinder);
await tester.pump(); await tester.pump();
assert(focusNode.hasFocus); await tester.tap(find.text('PASTE'));
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump(); await tester.pump();
// Lost focus because "done" was pressed. expect(changedValue, clipboardContent);
expect(focusNode.hasFocus, false);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('editable_text_test.0.0.png'),
);
}, skip: !Platform.isLinux);
testWidgets('cursor layout has correct radius', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
String changedValue;
final Widget widget = new MaterialApp(
home: new RepaintBoundary(
key: const ValueKey<int>(1),
child: new EditableText(
key: editableTextKey,
controller: new TextEditingController(),
focusNode: new FocusNode(),
style: new Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {
changedValue = value;
},
cursorWidth: 15.0,
cursorRadius: const Radius.circular(3.0),
),
),
);
await tester.pumpWidget(widget);
// Populate a fake clipboard.
const String clipboardContent = ' ';
SystemChannels.platform
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.getData')
return const <String, dynamic>{'text': clipboardContent};
return null;
}); });
testWidgets('Does not lose focus by default when "next" action is pressed', (WidgetTester tester) async { // Long-press to bring up the text editing controls.
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>(); final Finder textFinder = find.byKey(editableTextKey);
await tester.longPress(textFinder);
await tester.pump();
await tester.tap(find.text('PASTE'));
await tester.pump();
expect(changedValue, clipboardContent);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('editable_text_test.1.0.png'),
);
}, skip: !Platform.isLinux);
testWidgets('Does not lose focus by default when "next" action is pressed',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode(); final FocusNode focusNode = new FocusNode();
final Widget widget = new MaterialApp( final Widget widget = new MaterialApp(
...@@ -440,9 +560,11 @@ void main() { ...@@ -440,9 +560,11 @@ void main() {
expect(focusNode.hasFocus, true); expect(focusNode.hasFocus, true);
}); });
testWidgets('Does not lose focus by default when "done" action is pressed and onEditingComplete is provided', testWidgets(
'Does not lose focus by default when "done" action is pressed and onEditingComplete is provided',
(WidgetTester tester) async { (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>(); final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode(); final FocusNode focusNode = new FocusNode();
final Widget widget = new MaterialApp( final Widget widget = new MaterialApp(
...@@ -476,9 +598,11 @@ void main() { ...@@ -476,9 +598,11 @@ void main() {
expect(focusNode.hasFocus, true); expect(focusNode.hasFocus, true);
}); });
testWidgets('When "done" is pressed callbacks are invoked: onEditingComplete > onSubmitted', testWidgets(
'When "done" is pressed callbacks are invoked: onEditingComplete > onSubmitted',
(WidgetTester tester) async { (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>(); final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode(); final FocusNode focusNode = new FocusNode();
bool onEditingCompleteCalled = false; bool onEditingCompleteCalled = false;
...@@ -518,9 +642,11 @@ void main() { ...@@ -518,9 +642,11 @@ void main() {
// and onSubmission callbacks. // and onSubmission callbacks.
}); });
testWidgets('When "next" is pressed callbacks are invoked: onEditingComplete > onSubmitted', testWidgets(
'When "next" is pressed callbacks are invoked: onEditingComplete > onSubmitted',
(WidgetTester tester) async { (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>(); final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode(); final FocusNode focusNode = new FocusNode();
bool onEditingCompleteCalled = false; bool onEditingCompleteCalled = false;
...@@ -560,10 +686,14 @@ void main() { ...@@ -560,10 +686,14 @@ void main() {
// and onSubmission callbacks. // and onSubmission callbacks.
}); });
testWidgets('Changing controller updates EditableText', (WidgetTester tester) async { testWidgets('Changing controller updates EditableText',
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>(); (WidgetTester tester) async {
final TextEditingController controller1 = new TextEditingController(text: 'Wibble'); final GlobalKey<EditableTextState> editableTextKey =
final TextEditingController controller2 = new TextEditingController(text: 'Wobble'); new GlobalKey<EditableTextState>();
final TextEditingController controller1 =
new TextEditingController(text: 'Wibble');
final TextEditingController controller2 =
new TextEditingController(text: 'Wobble');
TextEditingController currentController = controller1; TextEditingController currentController = controller1;
StateSetter setState; StateSetter setState;
...@@ -579,11 +709,13 @@ void main() { ...@@ -579,11 +709,13 @@ void main() {
key: editableTextKey, key: editableTextKey,
controller: currentController, controller: currentController,
focusNode: new FocusNode(), focusNode: new FocusNode(),
style: new Typography(platform: TargetPlatform.android).black.subhead, style: new Typography(platform: TargetPlatform.android)
.black
.subhead,
cursorColor: Colors.blue, cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls, selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
onChanged: (String value) { }, onChanged: (String value) {},
), ),
), ),
), ),
...@@ -591,6 +723,7 @@ void main() { ...@@ -591,6 +723,7 @@ void main() {
}, },
); );
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await tester.showKeyboard(find.byType(EditableText)); await tester.showKeyboard(find.byType(EditableText));
...@@ -605,7 +738,9 @@ void main() { ...@@ -605,7 +738,9 @@ void main() {
await tester.pump(); await tester.pump();
expect(log, hasLength(1)); expect(log, hasLength(1));
expect(log.single, isMethodCall( expect(
log.single,
isMethodCall(
'TextInput.setEditingState', 'TextInput.setEditingState',
arguments: const <String, dynamic>{ arguments: const <String, dynamic>{
'text': 'Wobble', 'text': 'Wobble',
...@@ -619,7 +754,8 @@ void main() { ...@@ -619,7 +754,8 @@ void main() {
)); ));
}); });
testWidgets('EditableText identifies as text field (w/ focus) in semantics', (WidgetTester tester) async { testWidgets('EditableText identifies as text field (w/ focus) in semantics',
(WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
...@@ -638,18 +774,25 @@ void main() { ...@@ -638,18 +774,25 @@ void main() {
), ),
); );
expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField])); expect(semantics,
includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField]));
await tester.tap(find.byType(EditableText)); await tester.tap(find.byType(EditableText));
await tester.idle(); await tester.idle();
await tester.pump(); await tester.pump();
expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isFocused])); expect(
semantics,
includesNodeWith(flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused
]));
semantics.dispose(); semantics.dispose();
}); });
testWidgets('EditableText includes text as value in semantics', (WidgetTester tester) async { testWidgets('EditableText includes text as value in semantics',
(WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
const String value1 = 'EditableText content'; const String value1 = 'EditableText content';
...@@ -671,7 +814,9 @@ void main() { ...@@ -671,7 +814,9 @@ void main() {
), ),
); );
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.isTextField], flags: <SemanticsFlag>[SemanticsFlag.isTextField],
value: value1, value: value1,
)); ));
...@@ -681,7 +826,9 @@ void main() { ...@@ -681,7 +826,9 @@ void main() {
await tester.idle(); await tester.idle();
await tester.pump(); await tester.pump();
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.isTextField], flags: <SemanticsFlag>[SemanticsFlag.isTextField],
value: value2, value: value2,
)); ));
...@@ -689,12 +836,14 @@ void main() { ...@@ -689,12 +836,14 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('changing selection with keyboard does not show handles', (WidgetTester tester) async { testWidgets('changing selection with keyboard does not show handles',
(WidgetTester tester) async {
const String value1 = 'Hello World'; const String value1 = 'Hello World';
controller.text = value1; controller.text = value1;
await tester.pumpWidget(new MaterialApp( await tester.pumpWidget(
new MaterialApp(
home: new EditableText( home: new EditableText(
controller: controller, controller: controller,
selectionControls: materialTextSelectionControls, selectionControls: materialTextSelectionControls,
...@@ -702,27 +851,36 @@ void main() { ...@@ -702,27 +851,36 @@ void main() {
style: textStyle, style: textStyle,
cursorColor: cursorColor, cursorColor: cursorColor,
), ),
)); ),
);
// Simulate selection change via tap to show handles. // Simulate selection change via tap to show handles.
final RenderEditable render = tester.allRenderObjects.firstWhere((RenderObject o) => o.runtimeType == RenderEditable); final RenderEditable render = tester.allRenderObjects
render.onSelectionChanged(const TextSelection.collapsed(offset: 4), render, SelectionChangedCause.tap); .firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
render.onSelectionChanged(const TextSelection.collapsed(offset: 4), render,
SelectionChangedCause.tap);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final EditableTextState textState = tester.state(find.byType(EditableText)); final EditableTextState textState = tester.state(find.byType(EditableText));
expect(textState.selectionOverlay.handlesAreVisible, isTrue); expect(textState.selectionOverlay.handlesAreVisible, isTrue);
expect(textState.selectionOverlay.selectionDelegate.textEditingValue.selection, const TextSelection.collapsed(offset: 4)); expect(
textState.selectionOverlay.selectionDelegate.textEditingValue.selection,
const TextSelection.collapsed(offset: 4));
// Simulate selection change via keyboard and expect handles to disappear. // Simulate selection change via keyboard and expect handles to disappear.
render.onSelectionChanged(const TextSelection.collapsed(offset: 10), render, SelectionChangedCause.keyboard); render.onSelectionChanged(const TextSelection.collapsed(offset: 10), render,
SelectionChangedCause.keyboard);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(textState.selectionOverlay.handlesAreVisible, isFalse); expect(textState.selectionOverlay.handlesAreVisible, isFalse);
expect(textState.selectionOverlay.selectionDelegate.textEditingValue.selection, const TextSelection.collapsed(offset: 10)); expect(
textState.selectionOverlay.selectionDelegate.textEditingValue.selection,
const TextSelection.collapsed(offset: 10));
}); });
testWidgets('exposes correct cursor movement semantics', (WidgetTester tester) async { testWidgets('exposes correct cursor movement semantics',
(WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
controller.text = 'test'; controller.text = 'test';
...@@ -736,15 +894,20 @@ void main() { ...@@ -736,15 +894,20 @@ void main() {
), ),
)); ));
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
)); ));
controller.selection = new TextSelection.collapsed(offset: controller.text.length); controller.selection =
new TextSelection.collapsed(offset: controller.text.length);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// At end, can only go backwards. // At end, can only go backwards.
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
...@@ -752,11 +915,14 @@ void main() { ...@@ -752,11 +915,14 @@ void main() {
], ],
)); ));
controller.selection = new TextSelection.collapsed(offset: controller.text.length - 2); controller.selection =
new TextSelection.collapsed(offset: controller.text.length - 2);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Somewhere in the middle, can go in both directions. // Somewhere in the middle, can go in both directions.
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
...@@ -769,7 +935,9 @@ void main() { ...@@ -769,7 +935,9 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// At beginning, can only go forward. // At beginning, can only go forward.
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter, SemanticsAction.moveCursorForwardByCharacter,
...@@ -785,7 +953,8 @@ void main() { ...@@ -785,7 +953,8 @@ void main() {
const bool doNotExtendSelection = false; const bool doNotExtendSelection = false;
controller.text = 'test'; controller.text = 'test';
controller.selection = new TextSelection.collapsed(offset: controller.text.length); controller.selection =
new TextSelection.collapsed(offset: controller.text.length);
await tester.pumpWidget(new MaterialApp( await tester.pumpWidget(new MaterialApp(
home: new EditableText( home: new EditableText(
...@@ -796,26 +965,32 @@ void main() { ...@@ -796,26 +965,32 @@ void main() {
), ),
)); ));
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
], ],
)); ));
final RenderEditable render = tester.allRenderObjects.firstWhere((RenderObject o) => o.runtimeType == RenderEditable); final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
final int semanticsId = render.debugSemantics.id; final int semanticsId = render.debugSemantics.id;
expect(controller.selection.baseOffset, 4); expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 4); expect(controller.selection.extentOffset, 4);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 3); expect(controller.selection.baseOffset, 3);
expect(controller.selection.extentOffset, 3); expect(controller.selection.extentOffset, 3);
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
...@@ -824,18 +999,23 @@ void main() { ...@@ -824,18 +999,23 @@ void main() {
], ],
)); ));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0); expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 0); expect(controller.selection.extentOffset, 0);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter, SemanticsAction.moveCursorForwardByCharacter,
...@@ -843,7 +1023,8 @@ void main() { ...@@ -843,7 +1023,8 @@ void main() {
], ],
)); ));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1); expect(controller.selection.baseOffset, 1);
...@@ -852,13 +1033,15 @@ void main() { ...@@ -852,13 +1033,15 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('can extend selection with a11y means', (WidgetTester tester) async { testWidgets('can extend selection with a11y means',
(WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
const bool extendSelection = true; const bool extendSelection = true;
const bool doNotExtendSelection = false; const bool doNotExtendSelection = false;
controller.text = 'test'; controller.text = 'test';
controller.selection = new TextSelection.collapsed(offset: controller.text.length); controller.selection =
new TextSelection.collapsed(offset: controller.text.length);
await tester.pumpWidget(new MaterialApp( await tester.pumpWidget(new MaterialApp(
home: new EditableText( home: new EditableText(
...@@ -869,26 +1052,32 @@ void main() { ...@@ -869,26 +1052,32 @@ void main() {
), ),
)); ));
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
], ],
)); ));
final RenderEditable render = tester.allRenderObjects.firstWhere((RenderObject o) => o.runtimeType == RenderEditable); final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
final int semanticsId = render.debugSemantics.id; final int semanticsId = render.debugSemantics.id;
expect(controller.selection.baseOffset, 4); expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 4); expect(controller.selection.extentOffset, 4);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4); expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 3); expect(controller.selection.extentOffset, 3);
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
...@@ -897,18 +1086,23 @@ void main() { ...@@ -897,18 +1086,23 @@ void main() {
], ],
)); ));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4); expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 0); expect(controller.selection.extentOffset, 0);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter, SemanticsAction.moveCursorForwardByCharacter,
...@@ -916,13 +1110,15 @@ void main() { ...@@ -916,13 +1110,15 @@ void main() {
], ],
)); ));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1); expect(controller.selection.baseOffset, 1);
expect(controller.selection.extentOffset, 1); expect(controller.selection.extentOffset, 1);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, extendSelection); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, extendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1); expect(controller.selection.baseOffset, 1);
...@@ -931,7 +1127,8 @@ void main() { ...@@ -931,7 +1127,8 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('password fields have correct semantics', (WidgetTester tester) async { testWidgets('password fields have correct semantics',
(WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
controller.text = 'super-secret-password!!1'; controller.text = 'super-secret-password!!1';
...@@ -948,7 +1145,10 @@ void main() { ...@@ -948,7 +1145,10 @@ void main() {
final String expectedValue = '•' * controller.text.length; final String expectedValue = '•' * controller.text.length;
expect(semantics, hasSemantics(new TestSemantics( expect(
semantics,
hasSemantics(
new TestSemantics(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
children: <TestSemantics>[ children: <TestSemantics>[
...@@ -956,7 +1156,10 @@ void main() { ...@@ -956,7 +1156,10 @@ void main() {
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isObscured], flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isObscured
],
value: expectedValue, value: expectedValue,
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
...@@ -965,13 +1168,17 @@ void main() { ...@@ -965,13 +1168,17 @@ void main() {
], ],
), ),
], ],
), ignoreTransform: true, ignoreRect: true, ignoreId: true)); ),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true));
semantics.dispose(); semantics.dispose();
}); });
group('a11y copy/cut/paste', () { group('a11y copy/cut/paste', () {
Future<Null> _buildApp(MockTextSelectionControls controls, WidgetTester tester) { Future<Null> _buildApp(
MockTextSelectionControls controls, WidgetTester tester) {
return tester.pumpWidget(new MaterialApp( return tester.pumpWidget(new MaterialApp(
home: new EditableText( home: new EditableText(
controller: controller, controller: controller,
...@@ -987,11 +1194,13 @@ void main() { ...@@ -987,11 +1194,13 @@ void main() {
setUp(() { setUp(() {
controller.text = 'test'; controller.text = 'test';
controller.selection = new TextSelection.collapsed(offset: controller.text.length); controller.selection =
new TextSelection.collapsed(offset: controller.text.length);
controls = new MockTextSelectionControls(); controls = new MockTextSelectionControls();
when(controls.buildHandle(any, any, any)).thenReturn(new Container()); when(controls.buildHandle(any, any, any)).thenReturn(new Container());
when(controls.buildToolbar(any, any, any, any)).thenReturn(new Container()); when(controls.buildToolbar(any, any, any, any))
.thenReturn(new Container());
}); });
testWidgets('are exposed', (WidgetTester tester) async { testWidgets('are exposed', (WidgetTester tester) async {
...@@ -1005,7 +1214,9 @@ void main() { ...@@ -1005,7 +1214,9 @@ void main() {
await tester.tap(find.byType(EditableText)); await tester.tap(find.byType(EditableText));
await tester.pump(); await tester.pump();
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
...@@ -1015,7 +1226,9 @@ void main() { ...@@ -1015,7 +1226,9 @@ void main() {
when(controls.canCopy(any)).thenReturn(true); when(controls.canCopy(any)).thenReturn(true);
await _buildApp(controls, tester); await _buildApp(controls, tester);
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
...@@ -1027,7 +1240,9 @@ void main() { ...@@ -1027,7 +1240,9 @@ void main() {
when(controls.canCopy(any)).thenReturn(false); when(controls.canCopy(any)).thenReturn(false);
when(controls.canPaste(any)).thenReturn(true); when(controls.canPaste(any)).thenReturn(true);
await _buildApp(controls, tester); await _buildApp(controls, tester);
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
...@@ -1039,7 +1254,9 @@ void main() { ...@@ -1039,7 +1254,9 @@ void main() {
when(controls.canPaste(any)).thenReturn(false); when(controls.canPaste(any)).thenReturn(false);
when(controls.canCut(any)).thenReturn(true); when(controls.canCut(any)).thenReturn(true);
await _buildApp(controls, tester); await _buildApp(controls, tester);
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
...@@ -1052,7 +1269,9 @@ void main() { ...@@ -1052,7 +1269,9 @@ void main() {
when(controls.canCut(any)).thenReturn(true); when(controls.canCut(any)).thenReturn(true);
when(controls.canPaste(any)).thenReturn(true); when(controls.canPaste(any)).thenReturn(true);
await _buildApp(controls, tester); await _buildApp(controls, tester);
expect(semantics, includesNodeWith( expect(
semantics,
includesNodeWith(
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
...@@ -1079,7 +1298,10 @@ void main() { ...@@ -1079,7 +1298,10 @@ void main() {
final SemanticsOwner owner = tester.binding.pipelineOwner.semanticsOwner; final SemanticsOwner owner = tester.binding.pipelineOwner.semanticsOwner;
const int expectedNodeId = 4; const int expectedNodeId = 4;
expect(semantics, hasSemantics(new TestSemantics.root( expect(
semantics,
hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 1,
...@@ -1102,7 +1324,8 @@ void main() { ...@@ -1102,7 +1324,8 @@ void main() {
SemanticsAction.paste SemanticsAction.paste
], ],
value: 'test', value: 'test',
textSelection: new TextSelection.collapsed(offset: controller.text.length), textSelection: new TextSelection.collapsed(
offset: controller.text.length),
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
], ],
...@@ -1110,7 +1333,9 @@ void main() { ...@@ -1110,7 +1333,9 @@ void main() {
], ],
), ),
], ],
), ignoreRect: true, ignoreTransform: true)); ),
ignoreRect: true,
ignoreTransform: true));
owner.performAction(expectedNodeId, SemanticsAction.copy); owner.performAction(expectedNodeId, SemanticsAction.copy);
verify(controls.handleCopy(any)).called(1); verify(controls.handleCopy(any)).called(1);
...@@ -1125,7 +1350,8 @@ void main() { ...@@ -1125,7 +1350,8 @@ void main() {
}); });
}); });
testWidgets('allows customizing text style in subclasses', (WidgetTester tester) async { testWidgets('allows customizing text style in subclasses',
(WidgetTester tester) async {
controller.text = 'Hello World'; controller.text = 'Hello World';
await tester.pumpWidget(new MaterialApp( await tester.pumpWidget(new MaterialApp(
...@@ -1138,11 +1364,13 @@ void main() { ...@@ -1138,11 +1364,13 @@ void main() {
)); ));
// Simulate selection change via tap to show handles. // Simulate selection change via tap to show handles.
final RenderEditable render = tester.allRenderObjects.firstWhere((RenderObject o) => o.runtimeType == RenderEditable); final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
expect(render.text.style.fontStyle, FontStyle.italic); expect(render.text.style.fontStyle, FontStyle.italic);
}); });
testWidgets('autofocus sets cursor to the end of text', (WidgetTester tester) async { testWidgets('autofocus sets cursor to the end of text',
(WidgetTester tester) async {
const String text = 'hello world'; const String text = 'hello world';
final FocusScopeNode focusScopeNode = new FocusScopeNode(); final FocusScopeNode focusScopeNode = new FocusScopeNode();
final FocusNode focusNode = new FocusNode(); final FocusNode focusNode = new FocusNode();
...@@ -1177,8 +1405,8 @@ class CustomStyleEditableText extends EditableText { ...@@ -1177,8 +1405,8 @@ class CustomStyleEditableText extends EditableText {
Color cursorColor, Color cursorColor,
FocusNode focusNode, FocusNode focusNode,
TextStyle style, TextStyle style,
}): super( }) : super(
controller:controller, controller: controller,
cursorColor: cursorColor, cursorColor: cursorColor,
focusNode: focusNode, focusNode: focusNode,
style: style, style: style,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment