Unverified Commit 32b9c2f0 authored by xster's avatar xster Committed by GitHub

Add more RenderEditable test coverage (#27003)

parent 37108c4c
...@@ -1516,7 +1516,7 @@ class RenderEditable extends RenderBox { ...@@ -1516,7 +1516,7 @@ class RenderEditable extends RenderBox {
_textPainter.paint(context.canvas, effectiveOffset); _textPainter.paint(context.canvas, effectiveOffset);
if (_selection != null && !_floatingCursorOn) { if (_selection != null && !_floatingCursorOn) {
if (_selection.isCollapsed && cursorColor != null && _hasFocus) { if (_selection.isCollapsed && _showCursor.value && cursorColor != null) {
_paintCaret(context.canvas, effectiveOffset, _selection.extent); _paintCaret(context.canvas, effectiveOffset, _selection.extent);
} else if (!_selection.isCollapsed && _selectionColor != null) { } else if (!_selection.isCollapsed && _selectionColor != null) {
_selectionRects ??= _textPainter.getBoxesForSelection(_selection); _selectionRects ??= _textPainter.getBoxesForSelection(_selection);
......
...@@ -565,7 +565,8 @@ class EditableText extends StatefulWidget { ...@@ -565,7 +565,8 @@ class EditableText extends StatefulWidget {
/// State for a [EditableText]. /// State for a [EditableText].
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText> implements TextInputClient, TextSelectionDelegate { class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText> implements TextInputClient, TextSelectionDelegate {
Timer _cursorTimer; Timer _cursorTimer;
final ValueNotifier<bool> _showCursor = ValueNotifier<bool>(false); bool _targetCursorVisibility = false;
final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
final GlobalKey _editableKey = GlobalKey(); final GlobalKey _editableKey = GlobalKey();
TextInputConnection _textInputConnection; TextInputConnection _textInputConnection;
...@@ -1006,12 +1007,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1006,12 +1007,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void _onCursorColorTick() { void _onCursorColorTick() {
renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value); renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
_cursorVisibilityNotifier.value = _cursorBlinkOpacityController.value > 0;
} }
/// Whether the blinking cursor is actually visible at this precise moment /// Whether the blinking cursor is actually visible at this precise moment
/// (it's hidden half the time, since it blinks). /// (it's hidden half the time, since it blinks).
@visibleForTesting @visibleForTesting
bool get cursorCurrentlyVisible => _showCursor.value; bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0;
/// The cursor blink interval (the amount of time the cursor is in the "on" /// The cursor blink interval (the amount of time the cursor is in the "on"
/// state or the "off" state). A complete cursor blink period is twice this /// state or the "off" state). A complete cursor blink period is twice this
...@@ -1027,7 +1029,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1027,7 +1029,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
int _obscureLatestCharIndex; int _obscureLatestCharIndex;
void _cursorTick(Timer timer) { void _cursorTick(Timer timer) {
_showCursor.value = !_showCursor.value; _targetCursorVisibility = !_targetCursorVisibility;
final double targetOpacity = _targetCursorVisibility ? 1.0 : 0.0;
if (widget.cursorOpacityAnimates) { if (widget.cursorOpacityAnimates) {
// If we want to show the cursor, we will animate the opacity to the value // If we want to show the cursor, we will animate the opacity to the value
// of 1.0, and likewise if we want to make it disappear, to 0.0. An easing // of 1.0, and likewise if we want to make it disappear, to 0.0. An easing
...@@ -1036,10 +1039,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1036,10 +1039,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// //
// These values and curves have been obtained through eyeballing, so are // These values and curves have been obtained through eyeballing, so are
// likely not exactly the same as the values for native iOS. // likely not exactly the same as the values for native iOS.
final double toValue = _showCursor.value ? 1.0 : 0.0; _cursorBlinkOpacityController.animateTo(targetOpacity, curve: Curves.easeOut);
_cursorBlinkOpacityController.animateTo(toValue, curve: Curves.easeOut);
} else { } else {
_cursorBlinkOpacityController.value = _showCursor.value ? 1.0 : 0.0; _cursorBlinkOpacityController.value = targetOpacity;
} }
if (_obscureShowCharTicksPending > 0) { if (_obscureShowCharTicksPending > 0) {
...@@ -1056,7 +1058,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1056,7 +1058,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
void _startCursorTimer() { void _startCursorTimer() {
_showCursor.value = true; _targetCursorVisibility = true;
_cursorBlinkOpacityController.value = 1.0; _cursorBlinkOpacityController.value = 1.0;
if (EditableText.debugDeterministicCursor) if (EditableText.debugDeterministicCursor)
return; return;
...@@ -1070,7 +1072,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1070,7 +1072,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void _stopCursorTimer({ bool resetCharTicks = true }) { void _stopCursorTimer({ bool resetCharTicks = true }) {
_cursorTimer?.cancel(); _cursorTimer?.cancel();
_cursorTimer = null; _cursorTimer = null;
_showCursor.value = false; _targetCursorVisibility = false;
_cursorBlinkOpacityController.value = 0.0; _cursorBlinkOpacityController.value = 0.0;
if (EditableText.debugDeterministicCursor) if (EditableText.debugDeterministicCursor)
return; return;
...@@ -1196,7 +1198,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1196,7 +1198,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
value: _value, value: _value,
cursorColor: _cursorColor, cursorColor: _cursorColor,
backgroundCursorColor: widget.backgroundCursorColor, backgroundCursorColor: widget.backgroundCursorColor,
showCursor: EditableText.debugDeterministicCursor ? ValueNotifier<bool>(true) : _showCursor, showCursor: EditableText.debugDeterministicCursor
? ValueNotifier<bool>(true)
: _cursorVisibilityNotifier,
hasFocus: _hasFocus, hasFocus: _hasFocus,
maxLines: widget.maxLines, maxLines: widget.maxLines,
selectionColor: widget.selectionColor, selectionColor: widget.selectionColor,
......
...@@ -125,12 +125,12 @@ void main() { ...@@ -125,12 +125,12 @@ void main() {
SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall); SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall);
const String kThreeLines = const String kThreeLines =
'First line of text is ' 'First line of text is\n'
'Second line goes until ' 'Second line goes until\n'
'Third line of stuff '; 'Third line of stuff';
const String kMoreThanFourLines = const String kMoreThanFourLines =
kThreeLines + kThreeLines +
'Fourth line won\'t display and ends at'; '\nFourth line won\'t display and ends at';
// Returns the first RenderEditable. // Returns the first RenderEditable.
RenderEditable findRenderEditable(WidgetTester tester) { RenderEditable findRenderEditable(WidgetTester tester) {
...@@ -903,7 +903,7 @@ void main() { ...@@ -903,7 +903,7 @@ void main() {
); );
const String testValue = kThreeLines; const String testValue = kThreeLines;
const String cutValue = 'First line of stuff '; const String cutValue = 'First line of stuff';
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester); await skipPastScrollingAnimation(tester);
...@@ -973,7 +973,9 @@ void main() { ...@@ -973,7 +973,9 @@ void main() {
testWidgets('Can scroll multiline input', (WidgetTester tester) async { testWidgets('Can scroll multiline input', (WidgetTester tester) async {
final Key textFieldKey = UniqueKey(); final Key textFieldKey = UniqueKey();
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController(
text: kMoreThanFourLines,
);
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
...@@ -986,12 +988,6 @@ void main() { ...@@ -986,12 +988,6 @@ void main() {
), ),
), ),
); );
await tester.pump(const Duration(seconds: 1));
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
final RenderBox inputBox = findInputBox(); final RenderBox inputBox = findInputBox();
...@@ -1016,6 +1012,7 @@ void main() { ...@@ -1016,6 +1012,7 @@ void main() {
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Now the first line is scrolled up, and the fourth line is visible. // Now the first line is scrolled up, and the fourth line is visible.
Offset newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); Offset newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
...@@ -1026,15 +1023,21 @@ void main() { ...@@ -1026,15 +1023,21 @@ void main() {
expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isTrue); expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isTrue);
// Now try scrolling by dragging the selection handle. // Now try scrolling by dragging the selection handle.
// Long press the 'i' in 'Fourth line' to select the word. // Long press the 'i' in 'Fourth line' to select the word.
await tester.pump(const Duration(seconds: 1)); final Offset selectedWordPos = textOffsetToPosition(
final Offset untilPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth line')+8); tester,
gesture = await tester.startGesture(untilPos, pointer: 7); kMoreThanFourLines.indexOf('Fourth line') + 8,
);
gesture = await tester.startGesture(selectedWordPos, pointer: 7);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
await gesture.up(); await gesture.up();
await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(controller.selection.base.offset, 91);
expect(controller.selection.extent.offset, 94);
final RenderEditable renderEditable = findRenderEditable(tester); final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize( final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection), renderEditable.getEndpointsForSelection(controller.selection),
...@@ -1043,7 +1046,7 @@ void main() { ...@@ -1043,7 +1046,7 @@ void main() {
expect(endpoints.length, 2); expect(endpoints.length, 2);
// Drag the left handle to the first line, just after 'First'. // Drag the left handle to the first line, just after 'First'.
final Offset handlePos = endpoints[0].point + const Offset(-1.0, 1.0); final Offset handlePos = endpoints[0].point + const Offset(-1, 1);
final Offset newHandlePos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First') + 5); final Offset newHandlePos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First') + 5);
gesture = await tester.startGesture(handlePos, pointer: 7); gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
...@@ -1059,9 +1062,7 @@ void main() { ...@@ -1059,9 +1062,7 @@ void main() {
expect(newFirstPos.dy, firstPos.dy); expect(newFirstPos.dy, firstPos.dy);
expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue); expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue);
expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse); expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse);
}, });
// This test fails on some Mac environments when libtxt is enabled.
skip: Platform.isMacOS);
testWidgets('TextField smoke test', (WidgetTester tester) async { testWidgets('TextField smoke test', (WidgetTester tester) async {
String textFieldValue; String textFieldValue;
......
...@@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart'; ...@@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
import '../rendering/recording_canvas.dart'; import '../rendering/recording_canvas.dart';
import 'rendering_tester.dart';
class FakeEditableTextState extends TextSelectionDelegate { class FakeEditableTextState extends TextSelectionDelegate {
@override @override
...@@ -26,10 +27,6 @@ class FakeEditableTextState extends TextSelectionDelegate { ...@@ -26,10 +27,6 @@ class FakeEditableTextState extends TextSelectionDelegate {
} }
void main() { void main() {
final TextEditingController controller = TextEditingController();
const TextStyle textStyle = TextStyle();
test('editable intrinsics', () { test('editable intrinsics', () {
final TextSelectionDelegate delegate = FakeEditableTextState(); final TextSelectionDelegate delegate = FakeEditableTextState();
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
...@@ -99,86 +96,76 @@ void main() { ...@@ -99,86 +96,76 @@ void main() {
); );
}); });
RenderEditable findRenderEditable(WidgetTester tester) { test('Can change cursor color, radius, visibility', () {
final RenderObject root = tester.renderObject(find.byType(EditableText)); final TextSelectionDelegate delegate = FakeEditableTextState();
expect(root, isNotNull); final ValueNotifier<bool> showCursor = ValueNotifier<bool>(true);
EditableText.debugDeterministicCursor = true;
RenderEditable renderEditable;
void recursiveFinder(RenderObject child) { final RenderEditable editable = RenderEditable(
if (child is RenderEditable) { backgroundCursorColor: Colors.grey,
renderEditable = child; textDirection: TextDirection.ltr,
return; cursorColor: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00),
} offset: ViewportOffset.zero(),
child.visitChildren(recursiveFinder); textSelectionDelegate: delegate,
} text: const TextSpan(
root.visitChildren(recursiveFinder); text: 'test',
expect(renderEditable, isNotNull); style: TextStyle(
return renderEditable; height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
}
testWidgets('Floating cursor is painted', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const String text = 'hello world this is fun and cool and awesome!';
controller.text = text;
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Padding(
padding: const EdgeInsets.only(top: 0.25),
child: Material(
child: TextField(
controller: controller,
focusNode: focusNode,
style: textStyle,
),
), ),
), ),
selection: const TextSelection.collapsed(
offset: 4,
affinity: TextAffinity.upstream,
), ),
); );
await tester.tap(find.byType(EditableText)); layout(editable);
final RenderEditable editable = findRenderEditable(tester);
editable.selection = const TextSelection(baseOffset: 29, extentOffset: 29);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(20, 20)));
await tester.pump();
expect(editable, paints
..rrect(rrect: RRect.fromRectAndRadius(
Rect.fromLTRB(464.6666564941406, -1.5833333730697632, 466.6666564941406, 16.41666603088379),
const Radius.circular(2.0)),
color: const Color(0xff8e8e93))
..rrect(rrect: RRect.fromRectAndRadius(
Rect.fromLTRB(465.1666564941406, -2.416666269302368, 468.1666564941406, 17.58333396911621),
const Radius.circular(1.0)),
color: const Color(0xbf2196f3))
);
// Moves the cursor right a few characters. editable.layout(BoxConstraints.loose(const Size(100, 100)));
editableTextState.updateFloatingCursor( expect(
RawFloatingCursorPoint( editable,
state: FloatingCursorDragState.Update, // Draw no cursor by default.
offset: const Offset(-250, 20))); paintsExactlyCountTimes(#drawRect, 0),
expect(find.byType(EditableText), paints
..rrect(rrect: RRect.fromRectAndRadius(
Rect.fromLTRB(192.6666717529297, -1.5833333730697632, 194.6666717529297, 16.41666603088379),
const Radius.circular(2.0)),
color: const Color(0xff8e8e93))
..rrect(rrect: RRect.fromRectAndRadius(
Rect.fromLTRB(195.16665649414062, -2.416666269302368, 198.16665649414062, 17.58333396911621),
const Radius.circular(1.0)),
color: const Color(0xbf2196f3))
); );
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End)); editable.showCursor = showCursor;
pumpFrame();
expect(editable, paints..rect(
color: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00),
rect: Rect.fromLTWH(40, 2, 1, 6),
));
// Now change to a rounded caret.
editable.cursorColor = const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF);
editable.cursorWidth = 4;
editable.cursorRadius = const Radius.circular(3);
pumpFrame();
expect(editable, paints..rrect(
color: const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF),
rrect: RRect.fromRectAndRadius(
Rect.fromLTWH(40, 2, 4, 6),
const Radius.circular(3),
),
));
editable.textScaleFactor = 2;
pumpFrame();
// Now the caret height is much bigger due to the bigger font scale.
expect(editable, paints..rrect(
color: const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF),
rrect: RRect.fromRectAndRadius(
Rect.fromLTWH(80, 2, 4, 16),
const Radius.circular(3),
),
));
await tester.pumpAndSettle(); // Can turn off caret.
showCursor.value = false;
pumpFrame();
debugDefaultTargetPlatformOverride = null; expect(editable, paintsExactlyCountTimes(#drawRRect, 0));
}); });
} }
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';
import '../rendering/mock_canvas.dart';
import 'editable_text_test.dart';
void main() {
testWidgets('cursor has expected width and radius', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: EditableText(
backgroundCursorColor: Colors.grey,
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('cursor layout has correct width', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
String changedValue;
final Widget widget = MaterialApp(
home: RepaintBoundary(
key: const ValueKey<int>(1),
child: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: TextEditingController(),
focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {
changedValue = value;
},
cursorWidth: 15.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(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.tap(find.text('PASTE'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
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 = GlobalKey<EditableTextState>();
String changedValue;
final Widget widget = MaterialApp(
home: RepaintBoundary(
key: const ValueKey<int>(1),
child: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: TextEditingController(),
focusNode: FocusNode(),
style: 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.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('Cursor animates on iOS', (WidgetTester tester) async {
final Widget widget = MaterialApp(
theme: ThemeData(platform: TargetPlatform.iOS),
home: const Material(
child: TextField(
maxLines: 3,
),
),
);
await tester.pumpWidget(widget);
await tester.tap(find.byType(TextField));
await tester.pump();
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorColor.alpha, 255);
// Trigger initial timer. When focusing the first time, the cursor shows
// for slightly longer than the average on time.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
// Start timing standard cursor show period.
expect(renderEditable.cursorColor.alpha, 255);
expect(renderEditable, paints..rrect(color: const Color(0xff2196f3)));
await tester.pump(const Duration(milliseconds: 500));
// Start to animate the cursor away.
expect(renderEditable.cursorColor.alpha, 255);
expect(renderEditable, paints..rrect(color: const Color(0xff2196f3)));
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 110);
expect(renderEditable, paints..rrect(color: const Color(0x6e2196f3)));
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 16);
expect(renderEditable, paints..rrect(color: const Color(0x102196f3)));
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 0);
// Don't try to draw the cursor.
expect(renderEditable, paintsExactlyCountTimes(#drawRRect, 0));
// Wait some more while the cursor is gone. It'll trigger the cursor to
// start animating in again.
await tester.pump(const Duration(milliseconds: 300));
expect(renderEditable.cursorColor.alpha, 0);
expect(renderEditable, paintsExactlyCountTimes(#drawRRect, 0));
await tester.pump(const Duration(milliseconds: 50));
// Cursor starts coming back.
expect(renderEditable.cursorColor.alpha, 79);
expect(renderEditable, paints..rrect(color: const Color(0x4f2196f3)));
});
testWidgets('Cursor does not animate on Android', (WidgetTester tester) async {
const Widget widget = MaterialApp(
home: Material(
child: TextField(
maxLines: 3,
),
),
);
await tester.pumpWidget(widget);
await tester.tap(find.byType(TextField));
await tester.pump();
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
await tester.pump();
expect(renderEditable.cursorColor.alpha, 255);
expect(renderEditable, paints..rect(color: const Color(0xff4285f4)));
// Android cursor goes from exactly on to exactly off on the 500ms dot.
await tester.pump(const Duration(milliseconds: 499));
expect(renderEditable.cursorColor.alpha, 255);
expect(renderEditable, paints..rect(color: const Color(0xff4285f4)));
await tester.pump(const Duration(milliseconds: 1));
expect(renderEditable.cursorColor.alpha, 0);
// Don't try to draw the cursor.
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
await tester.pump(const Duration(milliseconds: 500));
expect(renderEditable.cursorColor.alpha, 255);
expect(renderEditable, paints..rect(color: const Color(0xff4285f4)));
await tester.pump(const Duration(milliseconds: 500));
expect(renderEditable.cursorColor.alpha, 0);
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
});
testWidgets(
'Cursor does not animates on iOS when debugDeterministicCursor is set',
(WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
final Widget widget = MaterialApp(
theme: ThemeData(platform: TargetPlatform.iOS),
home: const Material(
child: TextField(
maxLines: 3,
),
),
);
await tester.pumpWidget(widget);
await tester.tap(find.byType(TextField));
await tester.pump();
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorColor.alpha, 255);
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(renderEditable.cursorColor.alpha, 255);
expect(renderEditable, paints..rrect(color: const Color(0xff2196f3)));
// Cursor draw never changes.
await tester.pump(const Duration(milliseconds: 200));
expect(renderEditable.cursorColor.alpha, 255);
expect(renderEditable, paints..rrect(color: const Color(0xff2196f3)));
// No more transient calls.
await tester.pumpAndSettle();
expect(renderEditable.cursorColor.alpha, 255);
expect(renderEditable, paints..rrect(color: const Color(0xff2196f3)));
EditableText.debugDeterministicCursor = false;
});
testWidgets(
'Cursor does not animate on Android when debugDeterministicCursor is set',
(WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
const Widget widget = MaterialApp(
home: Material(
child: TextField(
maxLines: 3,
),
),
);
await tester.pumpWidget(widget);
await tester.tap(find.byType(TextField));
await tester.pump();
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
await tester.pump();
expect(renderEditable.cursorColor.alpha, 255);
expect(renderEditable, paints..rect(color: const Color(0xff4285f4)));
await tester.pump(const Duration(milliseconds: 500));
expect(renderEditable.cursorColor.alpha, 255);
expect(renderEditable, paints..rect(color: const Color(0xff4285f4)));
// Cursor draw never changes.
await tester.pump(const Duration(milliseconds: 500));
expect(renderEditable.cursorColor.alpha, 255);
expect(renderEditable, paints..rect(color: const Color(0xff4285f4)));
// No more transient calls.
await tester.pumpAndSettle();
expect(renderEditable.cursorColor.alpha, 255);
expect(renderEditable, paints..rect(color: const Color(0xff4285f4)));
EditableText.debugDeterministicCursor = false;
});
testWidgets('Cursor radius is 2.0 on iOS', (WidgetTester tester) async {
final Widget widget = MaterialApp(
theme: ThemeData(platform: TargetPlatform.iOS),
home: const Material(
child: TextField(
maxLines: 3,
),
),
);
await tester.pumpWidget(widget);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorRadius, const Radius.circular(2.0));
});
testWidgets('Cursor gets placed correctly after going out of bounds', (WidgetTester tester) async {
const String text = 'hello world this is fun and cool and awesome!';
controller.text = text;
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
),
);
await tester.tap(find.byType(EditableText));
final RenderEditable renderEditable = findRenderEditable(tester);
renderEditable.selection = const TextSelection(baseOffset: 29, extentOffset: 29);
expect(controller.selection.baseOffset, 29);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start));
expect(controller.selection.baseOffset, 29);
// Sets the origin.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(20, 20)));
expect(controller.selection.baseOffset, 29);
// Moves the cursor super far right
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(2090, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(2100, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(2090, 20)));
// After peaking the cursor, we move in the opposite direction.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(1400, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
// The cursor has been set.
expect(controller.selection.baseOffset, 8);
// Go in the other direction.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start));
// Sets the origin.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(20, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-5000, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-5010, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-5000, 20)));
// Move back in the opposite direction only a few hundred.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-4850, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 10);
});
testWidgets('Updating the floating cursor correctly moves the cursor', (WidgetTester tester) async {
const String text = 'hello world this is fun and cool and awesome!';
controller.text = text;
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
),
);
await tester.tap(find.byType(EditableText));
final RenderEditable renderEditable = findRenderEditable(tester);
renderEditable.selection = const TextSelection(baseOffset: 29, extentOffset: 29);
expect(controller.selection.baseOffset, 29);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start));
expect(controller.selection.baseOffset, 29);
// Sets the origin.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(20, 20)));
expect(controller.selection.baseOffset, 29);
// Moves the cursor right a few characters.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-250, 20)));
// But we have not yet set the offset because the user is not done placing the cursor.
expect(controller.selection.baseOffset, 29);
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
// The cursor has been set.
expect(controller.selection.baseOffset, 10);
});
testWidgets('autofocus sets cursor to the end of text', (WidgetTester tester) async {
const String text = 'hello world';
final FocusScopeNode focusScopeNode = FocusScopeNode();
final FocusNode focusNode = FocusNode();
controller.text = text;
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
autofocus: true,
style: textStyle,
cursorColor: cursorColor,
),
),
),
),
);
expect(focusNode.hasFocus, true);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, text.length);
});
testWidgets('Floating cursor is painted', (WidgetTester tester) async {
const String text = 'hello world this is fun and cool and awesome!';
controller.text = text;
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.iOS),
home: Padding(
padding: const EdgeInsets.only(top: 0.25),
child: Material(
child: TextField(
controller: controller,
focusNode: focusNode,
style: textStyle,
),
),
),
),
);
await tester.tap(find.byType(EditableText));
final RenderEditable editable = findRenderEditable(tester);
editable.selection = const TextSelection(baseOffset: 29, extentOffset: 29);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
editableTextState.updateFloatingCursor(
RawFloatingCursorPoint(state: FloatingCursorDragState.Start),
);
editableTextState.updateFloatingCursor(
RawFloatingCursorPoint(
state: FloatingCursorDragState.Update,
offset: const Offset(20, 20),
),
);
await tester.pump();
expect(editable, paints
..rrect(
rrect: RRect.fromRectAndRadius(
Rect.fromLTRB(464.6666564941406, 2.0833332538604736, 466.6666564941406, 14.083333015441895),
const Radius.circular(2.0),
),
color: const Color(0xff8e8e93))
..rrect(
rrect: RRect.fromRectAndRadius(
Rect.fromLTRB(465.1666564941406, 1.0833336114883423, 468.1666564941406, 15.083333969116211),
const Radius.circular(1.0),
),
color: const Color(0xbf2196f3))
);
// Moves the cursor right a few characters.
editableTextState.updateFloatingCursor(
RawFloatingCursorPoint(
state: FloatingCursorDragState.Update,
offset: const Offset(-250, 20)));
expect(find.byType(EditableText), paints
..rrect(
rrect: RRect.fromRectAndRadius(
Rect.fromLTRB(192.6666717529297, 2.0833332538604736, 194.6666717529297, 14.083333015441895),
const Radius.circular(2.0),
),
color: const Color(0xff8e8e93))
..rrect(
rrect: RRect.fromRectAndRadius(
Rect.fromLTRB(195.16665649414062, 1.0833336114883423, 198.16665649414062, 15.083333969116211),
const Radius.circular(1.0),
),
color: const Color(0xbf2196f3))
);
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
});
}
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// 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';
...@@ -15,13 +14,30 @@ import 'package:flutter/foundation.dart'; ...@@ -15,13 +14,30 @@ import 'package:flutter/foundation.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
void main() { RenderEditable findRenderEditable(WidgetTester tester) {
final TextEditingController controller = TextEditingController(); final RenderObject root = tester.renderObject(find.byType(EditableText));
final FocusNode focusNode = FocusNode(); expect(root, isNotNull);
final FocusScopeNode focusScopeNode = FocusScopeNode();
const TextStyle textStyle = TextStyle();
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
RenderEditable renderEditable;
void recursiveFinder(RenderObject child) {
if (child is RenderEditable) {
renderEditable = child;
return;
}
child.visitChildren(recursiveFinder);
}
root.visitChildren(recursiveFinder);
expect(renderEditable, isNotNull);
return renderEditable;
}
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
final FocusScopeNode focusScopeNode = FocusScopeNode();
const TextStyle textStyle = TextStyle();
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
void main() {
setUp(() { setUp(() {
debugResetSemanticsIdCounter(); debugResetSemanticsIdCounter();
}); });
...@@ -88,31 +104,10 @@ void main() { ...@@ -88,31 +104,10 @@ 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.textAlign, TextAlign.start);
expect(editableText.cursorWidth, 2.0); expect(editableText.cursorWidth, 2.0);
}); });
testWidgets('cursor has expected width and radius',
(WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: EditableText(
backgroundCursorColor: Colors.grey,
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',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -526,113 +521,6 @@ void main() { ...@@ -526,113 +521,6 @@ void main() {
expect(changedValue, clipboardContent); expect(changedValue, clipboardContent);
}); });
testWidgets('cursor layout has correct width', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
String changedValue;
final Widget widget = MaterialApp(
home: RepaintBoundary(
key: const ValueKey<int>(1),
child: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: TextEditingController(),
focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {
changedValue = value;
},
cursorWidth: 15.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.tap(find.text('PASTE'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 600));
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 =
GlobalKey<EditableTextState>();
String changedValue;
final Widget widget = MaterialApp(
home: RepaintBoundary(
key: const ValueKey<int>(1),
child: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: TextEditingController(),
focusNode: FocusNode(),
style: 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.tap(find.text('PASTE'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 600));
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', testWidgets('Does not lose focus by default when "next" action is pressed',
(WidgetTester tester) async { (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = final GlobalKey<EditableTextState> editableTextKey =
...@@ -841,119 +729,7 @@ void main() { ...@@ -841,119 +729,7 @@ void main() {
// and onSubmission callbacks. // and onSubmission callbacks.
}); });
testWidgets('Cursor animates on iOS', (WidgetTester tester) async { testWidgets(
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const Widget widget =
MaterialApp(
home: Material(
child: TextField(
maxLines: 3,
)
),
);
await tester.pumpWidget(widget);
await tester.tap(find.byType(TextField));
await tester.pump();
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorColor.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 110);
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 16);
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 0);
debugDefaultTargetPlatformOverride = null;
});
testWidgets('Cursor does not animate on Android', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
const Widget widget =
MaterialApp(
home: Material(
child: TextField(
maxLines: 3,
)
),
);
await tester.pumpWidget(widget);
await tester.tap(find.byType(TextField));
await tester.pump();
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorColor.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 0);
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 0);
debugDefaultTargetPlatformOverride = null;
});
testWidgets('Cursor radius is 2.0 on iOS', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const Widget widget =
MaterialApp(
home: Material(
child: TextField(
maxLines: 3,
)
),
);
await tester.pumpWidget(widget);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorRadius, const Radius.circular(2.0));
debugDefaultTargetPlatformOverride = null;
});
testWidgets(
'When "newline" action is called on a Editable text with maxLines != 1, onEditingComplete and onSubmitted callbacks are not invoked.', 'When "newline" action is called on a Editable text with maxLines != 1, onEditingComplete and onSubmitted callbacks are not invoked.',
(WidgetTester tester) async { (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = final GlobalKey<EditableTextState> editableTextKey =
...@@ -1067,7 +843,8 @@ testWidgets( ...@@ -1067,7 +843,8 @@ testWidgets(
'composingBase': -1, 'composingBase': -1,
'composingExtent': -1, 'composingExtent': -1,
}, },
)); ),
);
}); });
testWidgets('EditableText identifies as text field (w/ focus) in semantics', testWidgets('EditableText identifies as text field (w/ focus) in semantics',
...@@ -1094,8 +871,7 @@ testWidgets( ...@@ -1094,8 +871,7 @@ testWidgets(
), ),
); );
expect(semantics, expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField]));
includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField]));
await tester.tap(find.byType(EditableText)); await tester.tap(find.byType(EditableText));
await tester.idle(); await tester.idle();
...@@ -1106,7 +882,8 @@ testWidgets( ...@@ -1106,7 +882,8 @@ testWidgets(
includesNodeWith(flags: <SemanticsFlag>[ includesNodeWith(flags: <SemanticsFlag>[
SemanticsFlag.isTextField, SemanticsFlag.isTextField,
SemanticsFlag.isFocused SemanticsFlag.isFocused
])); ]),
);
semantics.dispose(); semantics.dispose();
}); });
...@@ -1143,7 +920,8 @@ testWidgets( ...@@ -1143,7 +920,8 @@ testWidgets(
includesNodeWith( includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.isTextField], flags: <SemanticsFlag>[SemanticsFlag.isTextField],
value: value1, value: value1,
)); ),
);
const String value2 = 'Changed the EditableText content'; const String value2 = 'Changed the EditableText content';
controller.text = value2; controller.text = value2;
...@@ -1155,13 +933,13 @@ testWidgets( ...@@ -1155,13 +933,13 @@ testWidgets(
includesNodeWith( includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.isTextField], flags: <SemanticsFlag>[SemanticsFlag.isTextField],
value: value2, value: value2,
)); ),
);
semantics.dispose(); semantics.dispose();
}); });
testWidgets('changing selection with keyboard does not show handles', testWidgets('changing selection with keyboard does not show handles', (WidgetTester tester) async {
(WidgetTester tester) async {
const String value1 = 'Hello World'; const String value1 = 'Hello World';
controller.text = value1; controller.text = value1;
...@@ -1191,7 +969,8 @@ testWidgets( ...@@ -1191,7 +969,8 @@ testWidgets(
expect(textState.selectionOverlay.handlesAreVisible, isTrue); expect(textState.selectionOverlay.handlesAreVisible, isTrue);
expect( expect(
textState.selectionOverlay.selectionDelegate.textEditingValue.selection, textState.selectionOverlay.selectionDelegate.textEditingValue.selection,
const TextSelection.collapsed(offset: 4)); 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, render.onSelectionChanged(const TextSelection.collapsed(offset: 10), render,
...@@ -1201,7 +980,8 @@ testWidgets( ...@@ -1201,7 +980,8 @@ testWidgets(
expect(textState.selectionOverlay.handlesAreVisible, isFalse); expect(textState.selectionOverlay.handlesAreVisible, isFalse);
expect( expect(
textState.selectionOverlay.selectionDelegate.textEditingValue.selection, textState.selectionOverlay.selectionDelegate.textEditingValue.selection,
const TextSelection.collapsed(offset: 10)); const TextSelection.collapsed(offset: 10),
);
}); });
testWidgets('exposes correct cursor movement semantics', testWidgets('exposes correct cursor movement semantics',
...@@ -1224,7 +1004,8 @@ testWidgets( ...@@ -1224,7 +1004,8 @@ testWidgets(
semantics, semantics,
includesNodeWith( includesNodeWith(
value: 'test', value: 'test',
)); ),
);
controller.selection = controller.selection =
TextSelection.collapsed(offset: controller.text.length); TextSelection.collapsed(offset: controller.text.length);
...@@ -1240,7 +1021,8 @@ testWidgets( ...@@ -1240,7 +1021,8 @@ testWidgets(
SemanticsAction.moveCursorBackwardByWord, SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection, SemanticsAction.setSelection,
], ],
)); ),
);
controller.selection = controller.selection =
TextSelection.collapsed(offset: controller.text.length - 2); TextSelection.collapsed(offset: controller.text.length - 2);
...@@ -1258,7 +1040,8 @@ testWidgets( ...@@ -1258,7 +1040,8 @@ testWidgets(
SemanticsAction.moveCursorForwardByWord, SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection, SemanticsAction.setSelection,
], ],
)); ),
);
controller.selection = const TextSelection.collapsed(offset: 0); controller.selection = const TextSelection.collapsed(offset: 0);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -1273,7 +1056,8 @@ testWidgets( ...@@ -1273,7 +1056,8 @@ testWidgets(
SemanticsAction.moveCursorForwardByWord, SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection, SemanticsAction.setSelection,
], ],
)); ),
);
semantics.dispose(); semantics.dispose();
}); });
...@@ -1304,7 +1088,8 @@ testWidgets( ...@@ -1304,7 +1088,8 @@ testWidgets(
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord, SemanticsAction.moveCursorBackwardByWord,
], ],
)); ),
);
final RenderEditable render = tester.allRenderObjects final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable); .firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
...@@ -1331,7 +1116,8 @@ testWidgets( ...@@ -1331,7 +1116,8 @@ testWidgets(
SemanticsAction.moveCursorForwardByWord, SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection, SemanticsAction.setSelection,
], ],
)); ),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection); SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
...@@ -1356,7 +1142,8 @@ testWidgets( ...@@ -1356,7 +1142,8 @@ testWidgets(
SemanticsAction.moveCursorForwardByWord, SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection, SemanticsAction.setSelection,
], ],
)); ),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection); SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
...@@ -1394,7 +1181,8 @@ testWidgets( ...@@ -1394,7 +1181,8 @@ testWidgets(
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord, SemanticsAction.moveCursorBackwardByWord,
], ],
)); ),
);
final RenderEditable render = tester.allRenderObjects final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable); .firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
...@@ -1421,7 +1209,8 @@ testWidgets( ...@@ -1421,7 +1209,8 @@ testWidgets(
SemanticsAction.moveCursorForwardByWord, SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection, SemanticsAction.setSelection,
], ],
)); ),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, doNotExtendSelection); SemanticsAction.moveCursorBackwardByWord, doNotExtendSelection);
...@@ -1447,7 +1236,8 @@ testWidgets( ...@@ -1447,7 +1236,8 @@ testWidgets(
SemanticsAction.moveCursorForwardByWord, SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection, SemanticsAction.setSelection,
], ],
)); ),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByWord, doNotExtendSelection); SemanticsAction.moveCursorForwardByWord, doNotExtendSelection);
...@@ -1494,7 +1284,8 @@ testWidgets( ...@@ -1494,7 +1284,8 @@ testWidgets(
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord, SemanticsAction.moveCursorBackwardByWord,
], ],
)); ),
);
final RenderEditable render = tester.allRenderObjects final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable); .firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
...@@ -1521,7 +1312,8 @@ testWidgets( ...@@ -1521,7 +1312,8 @@ testWidgets(
SemanticsAction.moveCursorForwardByWord, SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection, SemanticsAction.setSelection,
], ],
)); ),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection); SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
...@@ -1546,7 +1338,8 @@ testWidgets( ...@@ -1546,7 +1338,8 @@ testWidgets(
SemanticsAction.moveCursorForwardByWord, SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection, SemanticsAction.setSelection,
], ],
)); ),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection); SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
...@@ -1593,7 +1386,8 @@ testWidgets( ...@@ -1593,7 +1386,8 @@ testWidgets(
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord, SemanticsAction.moveCursorBackwardByWord,
], ],
)); ),
);
final RenderEditable render = tester.allRenderObjects final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable); .firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
...@@ -1620,7 +1414,8 @@ testWidgets( ...@@ -1620,7 +1414,8 @@ testWidgets(
SemanticsAction.moveCursorForwardByWord, SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection, SemanticsAction.setSelection,
], ],
)); ),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, extendSelection); SemanticsAction.moveCursorBackwardByWord, extendSelection);
...@@ -1646,7 +1441,8 @@ testWidgets( ...@@ -1646,7 +1441,8 @@ testWidgets(
SemanticsAction.moveCursorForwardByWord, SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection, SemanticsAction.setSelection,
], ],
)); ),
);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByWord, doNotExtendSelection); SemanticsAction.moveCursorForwardByWord, doNotExtendSelection);
...@@ -1710,7 +1506,105 @@ testWidgets( ...@@ -1710,7 +1506,105 @@ testWidgets(
), ),
ignoreTransform: true, ignoreTransform: true,
ignoreRect: true, ignoreRect: true,
ignoreId: true)); ignoreId: true,
),
);
semantics.dispose();
});
testWidgets('password fields become obscured with the right semantics when set',
(WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const String originalText = 'super-secret-password!!1';
controller.text = originalText;
await tester.pumpWidget(MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
const String expectedValue = '••••••••••••••••••••••••';
expect(
semantics,
hasSemantics(
TestSemantics(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
],
value: originalText,
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
// Now change it to make it obscure text.
await tester.pumpWidget(MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
obscureText: true,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
expect(findRenderEditable(tester).text.text, expectedValue);
expect(
semantics,
hasSemantics(
TestSemantics(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isObscured,
SemanticsFlag.isFocused,
],
value: expectedValue,
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
semantics.dispose(); semantics.dispose();
}); });
...@@ -1763,7 +1657,8 @@ testWidgets( ...@@ -1763,7 +1657,8 @@ testWidgets(
SemanticsAction.moveCursorBackwardByWord, SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection, SemanticsAction.setSelection,
], ],
)); ),
);
when(controls.canCopy(any)).thenReturn(true); when(controls.canCopy(any)).thenReturn(true);
await _buildApp(controls, tester); await _buildApp(controls, tester);
...@@ -1777,7 +1672,8 @@ testWidgets( ...@@ -1777,7 +1672,8 @@ testWidgets(
SemanticsAction.setSelection, SemanticsAction.setSelection,
SemanticsAction.copy, 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);
...@@ -1792,7 +1688,8 @@ testWidgets( ...@@ -1792,7 +1688,8 @@ testWidgets(
SemanticsAction.setSelection, SemanticsAction.setSelection,
SemanticsAction.paste, 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);
...@@ -1807,7 +1704,8 @@ testWidgets( ...@@ -1807,7 +1704,8 @@ testWidgets(
SemanticsAction.setSelection, SemanticsAction.setSelection,
SemanticsAction.cut, 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);
...@@ -1825,7 +1723,8 @@ testWidgets( ...@@ -1825,7 +1723,8 @@ testWidgets(
SemanticsAction.copy, SemanticsAction.copy,
SemanticsAction.paste, SemanticsAction.paste,
], ],
)); ),
);
semantics.dispose(); semantics.dispose();
}); });
...@@ -1881,7 +1780,9 @@ testWidgets( ...@@ -1881,7 +1780,9 @@ testWidgets(
], ],
), ),
ignoreRect: true, ignoreRect: true,
ignoreTransform: 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);
...@@ -1915,196 +1816,6 @@ testWidgets( ...@@ -1915,196 +1816,6 @@ testWidgets(
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 {
const String text = 'hello world';
final FocusScopeNode focusScopeNode = FocusScopeNode();
final FocusNode focusNode = FocusNode();
controller.text = text;
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
autofocus: true,
style: textStyle,
cursorColor: cursorColor,
),
),
),
),
);
expect(focusNode.hasFocus, true);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, text.length);
});
RenderEditable findRenderEditable(WidgetTester tester) {
final RenderObject root = tester.renderObject(find.byType(EditableText));
expect(root, isNotNull);
RenderEditable renderEditable;
void recursiveFinder(RenderObject child) {
if (child is RenderEditable) {
renderEditable = child;
return;
}
child.visitChildren(recursiveFinder);
}
root.visitChildren(recursiveFinder);
expect(renderEditable, isNotNull);
return renderEditable;
}
testWidgets('Updating the floating cursor correctly moves the cursor', (WidgetTester tester) async {
const String text = 'hello world this is fun and cool and awesome!';
controller.text = text;
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
),
);
await tester.tap(find.byType(EditableText));
final RenderEditable renderEditable = findRenderEditable(tester);
renderEditable.selection = const TextSelection(baseOffset: 29, extentOffset: 29);
expect(controller.selection.baseOffset, 29);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start));
expect(controller.selection.baseOffset, 29);
// Sets the origin.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(20, 20)));
expect(controller.selection.baseOffset, 29);
// Moves the cursor right a few characters.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-250, 20)));
// But we have not yet set the offset because the user is not done placing the cursor.
expect(controller.selection.baseOffset, 29);
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
// The cursor has been set.
expect(controller.selection.baseOffset, 10);
});
testWidgets('Cursor gets placed correctly after going out of bounds', (WidgetTester tester) async {
const String text = 'hello world this is fun and cool and awesome!';
controller.text = text;
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
),
);
await tester.tap(find.byType(EditableText));
final RenderEditable renderEditable = findRenderEditable(tester);
renderEditable.selection = const TextSelection(baseOffset: 29, extentOffset: 29);
expect(controller.selection.baseOffset, 29);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start));
expect(controller.selection.baseOffset, 29);
// Sets the origin.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(20, 20)));
expect(controller.selection.baseOffset, 29);
// Moves the cursor super far right
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(2090, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(2100, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(2090, 20)));
// After peaking the cursor, we move in the opposite direction.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(1400, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
// The cursor has been set.
expect(controller.selection.baseOffset, 8);
// Go in the other direction.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start));
// Sets the origin.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(20, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-5000, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-5010, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-5000, 20)));
// Move back in the opposite direction only a few hundred.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-4850, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 10);
});
testWidgets('Formatters are skipped if text has not changed', (WidgetTester tester) async { testWidgets('Formatters are skipped if text has not changed', (WidgetTester tester) async {
int called = 0; int called = 0;
final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) { final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) {
......
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