Add more RenderEditable test coverage (#27003)
This commit is contained in:
parent
37108c4cc3
commit
32b9c2f07e
@ -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 {
|
|||||||
/// 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
|
|||||||
|
|
||||||
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
|
|||||||
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
|
|||||||
//
|
//
|
||||||
// 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
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
|||||||
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
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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';
|
|||||||
|
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
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;
|
final RenderEditable editable = RenderEditable(
|
||||||
void recursiveFinder(RenderObject child) {
|
backgroundCursorColor: Colors.grey,
|
||||||
if (child is RenderEditable) {
|
textDirection: TextDirection.ltr,
|
||||||
renderEditable = child;
|
cursorColor: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00),
|
||||||
return;
|
offset: ViewportOffset.zero(),
|
||||||
}
|
textSelectionDelegate: delegate,
|
||||||
child.visitChildren(recursiveFinder);
|
text: const TextSpan(
|
||||||
}
|
text: 'test',
|
||||||
root.visitChildren(recursiveFinder);
|
style: TextStyle(
|
||||||
expect(renderEditable, isNotNull);
|
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||||||
return renderEditable;
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
editable.layout(BoxConstraints.loose(const Size(100, 100)));
|
||||||
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start));
|
expect(
|
||||||
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
|
editable,
|
||||||
offset: const Offset(20, 20)));
|
// Draw no cursor by default.
|
||||||
await tester.pump();
|
paintsExactlyCountTimes(#drawRect, 0),
|
||||||
|
|
||||||
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.showCursor = showCursor;
|
||||||
editableTextState.updateFloatingCursor(
|
pumpFrame();
|
||||||
RawFloatingCursorPoint(
|
|
||||||
state: FloatingCursorDragState.Update,
|
|
||||||
offset: const Offset(-250, 20)));
|
|
||||||
|
|
||||||
expect(find.byType(EditableText), paints
|
expect(editable, paints..rect(
|
||||||
..rrect(rrect: RRect.fromRectAndRadius(
|
color: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00),
|
||||||
Rect.fromLTRB(192.6666717529297, -1.5833333730697632, 194.6666717529297, 16.41666603088379),
|
rect: Rect.fromLTWH(40, 2, 1, 6),
|
||||||
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));
|
// 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();
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
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),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
debugDefaultTargetPlatformOverride = null;
|
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),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Can turn off caret.
|
||||||
|
showCursor.value = false;
|
||||||
|
pumpFrame();
|
||||||
|
|
||||||
|
expect(editable, paintsExactlyCountTimes(#drawRRect, 0));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
584
packages/flutter/test/widgets/editable_text_cursor_test.dart
Normal file
584
packages/flutter/test/widgets/editable_text_cursor_test.dart
Normal file
@ -0,0 +1,584 @@
|
|||||||
|
// 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();
|
||||||
|
});
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user