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);
canvas.drawRect(caretRect, paint);
if (cursorRadius == null) {
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,13 +126,15 @@ void main() { ...@@ -104,13 +126,15 @@ 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(
(WidgetTester tester) async { 'Keyboard is configured for "unspecified" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
action: TextInputAction.unspecified, action: TextInputAction.unspecified,
...@@ -118,8 +142,9 @@ void main() { ...@@ -118,8 +142,9 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "none" action when explicitly requested', testWidgets(
(WidgetTester tester) async { 'Keyboard is configured for "none" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
action: TextInputAction.none, action: TextInputAction.none,
...@@ -127,8 +152,9 @@ void main() { ...@@ -127,8 +152,9 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "done" action when explicitly requested', testWidgets(
(WidgetTester tester) async { 'Keyboard is configured for "done" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
action: TextInputAction.done, action: TextInputAction.done,
...@@ -136,8 +162,9 @@ void main() { ...@@ -136,8 +162,9 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "send" action when explicitly requested', testWidgets(
(WidgetTester tester) async { 'Keyboard is configured for "send" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
action: TextInputAction.send, action: TextInputAction.send,
...@@ -145,8 +172,9 @@ void main() { ...@@ -145,8 +172,9 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "go" action when explicitly requested', testWidgets(
(WidgetTester tester) async { 'Keyboard is configured for "go" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
action: TextInputAction.go, action: TextInputAction.go,
...@@ -154,8 +182,9 @@ void main() { ...@@ -154,8 +182,9 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "search" action when explicitly requested', testWidgets(
(WidgetTester tester) async { 'Keyboard is configured for "search" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
action: TextInputAction.search, action: TextInputAction.search,
...@@ -163,8 +192,9 @@ void main() { ...@@ -163,8 +192,9 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "send" action when explicitly requested', testWidgets(
(WidgetTester tester) async { 'Keyboard is configured for "send" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
action: TextInputAction.send, action: TextInputAction.send,
...@@ -172,8 +202,9 @@ void main() { ...@@ -172,8 +202,9 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "next" action when explicitly requested', testWidgets(
(WidgetTester tester) async { 'Keyboard is configured for "next" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
action: TextInputAction.next, action: TextInputAction.next,
...@@ -181,8 +212,9 @@ void main() { ...@@ -181,8 +212,9 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "previous" action when explicitly requested', testWidgets(
(WidgetTester tester) async { 'Keyboard is configured for "previous" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
action: TextInputAction.previous, action: TextInputAction.previous,
...@@ -190,8 +222,9 @@ void main() { ...@@ -190,8 +222,9 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "continue" action when explicitly requested', testWidgets(
(WidgetTester tester) async { 'Keyboard is configured for "continue" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
action: TextInputAction.continueAction, action: TextInputAction.continueAction,
...@@ -199,8 +232,9 @@ void main() { ...@@ -199,8 +232,9 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "join" action when explicitly requested', testWidgets(
(WidgetTester tester) async { 'Keyboard is configured for "join" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
action: TextInputAction.join, action: TextInputAction.join,
...@@ -208,8 +242,9 @@ void main() { ...@@ -208,8 +242,9 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "route" action when explicitly requested', testWidgets(
(WidgetTester tester) async { 'Keyboard is configured for "route" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
action: TextInputAction.route, action: TextInputAction.route,
...@@ -217,8 +252,9 @@ void main() { ...@@ -217,8 +252,9 @@ void main() {
); );
}); });
testWidgets('Keyboard is configured for "emergencyCall" action when explicitly requested', testWidgets(
(WidgetTester tester) async { 'Keyboard is configured for "emergencyCall" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested( await _desiredKeyboardActionIsRequested(
tester: tester, tester: tester,
action: TextInputAction.emergencyCall, action: TextInputAction.emergencyCall,
...@@ -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: editableTextKey, key: const ValueKey<int>(1),
controller: new TextEditingController(), child: new EditableText(
focusNode: focusNode, key: editableTextKey,
style: new Typography(platform: TargetPlatform.android).black.subhead, controller: new TextEditingController(),
cursorColor: Colors.blue, focusNode: new FocusNode(),
selectionControls: materialTextSelectionControls, style: new Typography(platform: TargetPlatform.android).black.subhead,
keyboardType: TextInputType.text, cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
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.pump();
await tester.testTextInput.receiveAction(TextInputAction.done); expect(changedValue, clipboardContent);
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;
});
// Long-press to bring up the text editing controls.
final Finder textFinder = find.byKey(editableTextKey);
await tester.longPress(textFinder);
await tester.pump(); await tester.pump();
// Lost focus because "done" was pressed. await tester.tap(find.text('PASTE'));
expect(focusNode.hasFocus, false); await tester.pump();
});
testWidgets('Does not lose focus by default when "next" action is pressed', (WidgetTester tester) async { expect(changedValue, clipboardContent);
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
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(
(WidgetTester tester) async { 'Does not lose focus by default when "done" action is pressed and onEditingComplete is provided',
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>(); (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(
...@@ -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(
(WidgetTester tester) async { 'When "done" is pressed callbacks are invoked: onEditingComplete > onSubmitted',
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>(); (WidgetTester tester) async {
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(
(WidgetTester tester) async { 'When "next" is pressed callbacks are invoked: onEditingComplete > onSubmitted',
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>(); (WidgetTester tester) async {
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',
...@@ -616,10 +751,11 @@ void main() { ...@@ -616,10 +751,11 @@ void main() {
'composingBase': -1, 'composingBase': -1,
'composingExtent': -1, 'composingExtent': -1,
}, },
)); ));
}); });
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,58 +814,73 @@ void main() { ...@@ -671,58 +814,73 @@ void main() {
), ),
); );
expect(semantics, includesNodeWith( expect(
flags: <SemanticsFlag>[SemanticsFlag.isTextField], semantics,
value: value1, includesNodeWith(
)); flags: <SemanticsFlag>[SemanticsFlag.isTextField],
value: value1,
));
const String value2 = 'Changed the EditableText content'; const String value2 = 'Changed the EditableText content';
controller.text = value2; controller.text = value2;
await tester.idle(); await tester.idle();
await tester.pump(); await tester.pump();
expect(semantics, includesNodeWith( expect(
flags: <SemanticsFlag>[SemanticsFlag.isTextField], semantics,
value: value2, includesNodeWith(
)); flags: <SemanticsFlag>[SemanticsFlag.isTextField],
value: value2,
));
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(
home: new EditableText( new MaterialApp(
controller: controller, home: new EditableText(
selectionControls: materialTextSelectionControls, controller: controller,
focusNode: focusNode, selectionControls: materialTextSelectionControls,
style: textStyle, focusNode: focusNode,
cursorColor: cursorColor, style: textStyle,
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,46 +894,56 @@ void main() { ...@@ -736,46 +894,56 @@ void main() {
), ),
)); ));
expect(semantics, includesNodeWith( expect(
value: 'test', semantics,
)); includesNodeWith(
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(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorBackwardByCharacter, value: 'test',
SemanticsAction.setSelection, actions: <SemanticsAction>[
], SemanticsAction.moveCursorBackwardByCharacter,
)); SemanticsAction.setSelection,
],
));
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(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorBackwardByCharacter, value: 'test',
SemanticsAction.moveCursorForwardByCharacter, actions: <SemanticsAction>[
SemanticsAction.setSelection, SemanticsAction.moveCursorBackwardByCharacter,
], SemanticsAction.moveCursorForwardByCharacter,
)); SemanticsAction.setSelection,
],
));
controller.selection = const TextSelection.collapsed(offset: 0); controller.selection = const TextSelection.collapsed(offset: 0);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// At beginning, can only go forward. // At beginning, can only go forward.
expect(semantics, includesNodeWith( expect(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorForwardByCharacter, value: 'test',
SemanticsAction.setSelection, actions: <SemanticsAction>[
], SemanticsAction.moveCursorForwardByCharacter,
)); SemanticsAction.setSelection,
],
));
semantics.dispose(); semantics.dispose();
}); });
...@@ -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,54 +965,66 @@ void main() { ...@@ -796,54 +965,66 @@ void main() {
), ),
)); ));
expect(semantics, includesNodeWith( expect(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorBackwardByCharacter, value: 'test',
], actions: <SemanticsAction>[
)); 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(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorBackwardByCharacter, value: 'test',
SemanticsAction.moveCursorForwardByCharacter, actions: <SemanticsAction>[
SemanticsAction.setSelection, SemanticsAction.moveCursorBackwardByCharacter,
], SemanticsAction.moveCursorForwardByCharacter,
)); SemanticsAction.setSelection,
],
));
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(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorForwardByCharacter, value: 'test',
SemanticsAction.setSelection, actions: <SemanticsAction>[
], SemanticsAction.moveCursorForwardByCharacter,
)); SemanticsAction.setSelection,
],
));
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,60 +1052,73 @@ void main() { ...@@ -869,60 +1052,73 @@ void main() {
), ),
)); ));
expect(semantics, includesNodeWith( expect(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorBackwardByCharacter, value: 'test',
], actions: <SemanticsAction>[
)); 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(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorBackwardByCharacter, value: 'test',
SemanticsAction.moveCursorForwardByCharacter, actions: <SemanticsAction>[
SemanticsAction.setSelection, SemanticsAction.moveCursorBackwardByCharacter,
], SemanticsAction.moveCursorForwardByCharacter,
)); SemanticsAction.setSelection,
],
));
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(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorForwardByCharacter, value: 'test',
SemanticsAction.setSelection, actions: <SemanticsAction>[
], SemanticsAction.moveCursorForwardByCharacter,
)); SemanticsAction.setSelection,
],
));
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,30 +1145,40 @@ void main() { ...@@ -948,30 +1145,40 @@ void main() {
final String expectedValue = '•' * controller.text.length; final String expectedValue = '•' * controller.text.length;
expect(semantics, hasSemantics(new TestSemantics( expect(
children: <TestSemantics>[ semantics,
new TestSemantics.rootChild( hasSemantics(
children: <TestSemantics>[
new TestSemantics( new TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], children: <TestSemantics>[
children: <TestSemantics>[ new TestSemantics.rootChild(
new TestSemantics( children: <TestSemantics>[
flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isObscured], new TestSemantics(
value: expectedValue, flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
textDirection: TextDirection.ltr, children: <TestSemantics>[
new TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isObscured
],
value: expectedValue,
textDirection: TextDirection.ltr,
),
],
),
],
), ),
], ],
), ),
], 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,63 +1214,73 @@ void main() { ...@@ -1005,63 +1214,73 @@ void main() {
await tester.tap(find.byType(EditableText)); await tester.tap(find.byType(EditableText));
await tester.pump(); await tester.pump();
expect(semantics, includesNodeWith( expect(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorBackwardByCharacter, value: 'test',
SemanticsAction.setSelection, actions: <SemanticsAction>[
], SemanticsAction.moveCursorBackwardByCharacter,
)); SemanticsAction.setSelection,
],
));
when(controls.canCopy(any)).thenReturn(true); when(controls.canCopy(any)).thenReturn(true);
await _buildApp(controls, tester); await _buildApp(controls, tester);
expect(semantics, includesNodeWith( expect(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorBackwardByCharacter, value: 'test',
SemanticsAction.setSelection, actions: <SemanticsAction>[
SemanticsAction.copy, SemanticsAction.moveCursorBackwardByCharacter,
], SemanticsAction.setSelection,
)); SemanticsAction.copy,
],
));
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(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorBackwardByCharacter, value: 'test',
SemanticsAction.setSelection, actions: <SemanticsAction>[
SemanticsAction.paste, SemanticsAction.moveCursorBackwardByCharacter,
], SemanticsAction.setSelection,
)); SemanticsAction.paste,
],
));
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(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorBackwardByCharacter, value: 'test',
SemanticsAction.setSelection, actions: <SemanticsAction>[
SemanticsAction.cut, SemanticsAction.moveCursorBackwardByCharacter,
], SemanticsAction.setSelection,
)); SemanticsAction.cut,
],
));
when(controls.canCopy(any)).thenReturn(true); when(controls.canCopy(any)).thenReturn(true);
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(
value: 'test', semantics,
actions: <SemanticsAction>[ includesNodeWith(
SemanticsAction.moveCursorBackwardByCharacter, value: 'test',
SemanticsAction.setSelection, actions: <SemanticsAction>[
SemanticsAction.cut, SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.copy, SemanticsAction.setSelection,
SemanticsAction.paste, SemanticsAction.cut,
], SemanticsAction.copy,
)); SemanticsAction.paste,
],
));
semantics.dispose(); semantics.dispose();
}); });
...@@ -1079,38 +1298,44 @@ void main() { ...@@ -1079,38 +1298,44 @@ 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(
children: <TestSemantics>[ semantics,
new TestSemantics.rootChild( hasSemantics(
id: 1, new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics(
id: 2,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: expectedNodeId, id: 1,
flags: <SemanticsFlag>[ children: <TestSemantics>[
SemanticsFlag.isTextField, new TestSemantics(
SemanticsFlag.isFocused id: 2,
], flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
actions: <SemanticsAction>[ children: <TestSemantics>[
SemanticsAction.moveCursorBackwardByCharacter, new TestSemantics.rootChild(
SemanticsAction.setSelection, id: expectedNodeId,
SemanticsAction.copy, flags: <SemanticsFlag>[
SemanticsAction.cut, SemanticsFlag.isTextField,
SemanticsAction.paste SemanticsFlag.isFocused
],
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.copy,
SemanticsAction.cut,
SemanticsAction.paste
],
value: 'test',
textSelection: new TextSelection.collapsed(
offset: controller.text.length),
textDirection: TextDirection.ltr,
),
],
),
], ],
value: 'test',
textSelection: new TextSelection.collapsed(offset: controller.text.length),
textDirection: TextDirection.ltr,
), ),
], ],
), ),
], 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,12 +1405,12 @@ class CustomStyleEditableText extends EditableText { ...@@ -1177,12 +1405,12 @@ 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,
); );
@override @override
CustomStyleEditableTextState createState() => CustomStyleEditableTextState createState() =>
new CustomStyleEditableTextState(); new CustomStyleEditableTextState();
...@@ -1192,8 +1420,8 @@ class CustomStyleEditableTextState extends EditableTextState { ...@@ -1192,8 +1420,8 @@ class CustomStyleEditableTextState extends EditableTextState {
@override @override
TextSpan buildTextSpan() { TextSpan buildTextSpan() {
return new TextSpan( return new TextSpan(
style: const TextStyle(fontStyle: FontStyle.italic), style: const TextStyle(fontStyle: FontStyle.italic),
text: widget.controller.value.text, text: widget.controller.value.text,
); );
} }
} }
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