diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index f7a3345836..00f082eecf 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -1516,7 +1516,7 @@ class RenderEditable extends RenderBox { _textPainter.paint(context.canvas, effectiveOffset); if (_selection != null && !_floatingCursorOn) { - if (_selection.isCollapsed && cursorColor != null && _hasFocus) { + if (_selection.isCollapsed && _showCursor.value && cursorColor != null) { _paintCaret(context.canvas, effectiveOffset, _selection.extent); } else if (!_selection.isCollapsed && _selectionColor != null) { _selectionRects ??= _textPainter.getBoxesForSelection(_selection); diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index e63affede8..01f17027c4 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -565,7 +565,8 @@ class EditableText extends StatefulWidget { /// State for a [EditableText]. class EditableTextState extends State with AutomaticKeepAliveClientMixin, WidgetsBindingObserver, TickerProviderStateMixin implements TextInputClient, TextSelectionDelegate { Timer _cursorTimer; - final ValueNotifier _showCursor = ValueNotifier(false); + bool _targetCursorVisibility = false; + final ValueNotifier _cursorVisibilityNotifier = ValueNotifier(true); final GlobalKey _editableKey = GlobalKey(); TextInputConnection _textInputConnection; @@ -1006,12 +1007,13 @@ class EditableTextState extends State with AutomaticKeepAliveClien void _onCursorColorTick() { renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value); + _cursorVisibilityNotifier.value = _cursorBlinkOpacityController.value > 0; } /// Whether the blinking cursor is actually visible at this precise moment /// (it's hidden half the time, since it blinks). @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" /// state or the "off" state). A complete cursor blink period is twice this @@ -1027,7 +1029,8 @@ class EditableTextState extends State with AutomaticKeepAliveClien int _obscureLatestCharIndex; void _cursorTick(Timer timer) { - _showCursor.value = !_showCursor.value; + _targetCursorVisibility = !_targetCursorVisibility; + final double targetOpacity = _targetCursorVisibility ? 1.0 : 0.0; if (widget.cursorOpacityAnimates) { // 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 @@ -1036,10 +1039,9 @@ class EditableTextState extends State with AutomaticKeepAliveClien // // These values and curves have been obtained through eyeballing, so are // likely not exactly the same as the values for native iOS. - final double toValue = _showCursor.value ? 1.0 : 0.0; - _cursorBlinkOpacityController.animateTo(toValue, curve: Curves.easeOut); + _cursorBlinkOpacityController.animateTo(targetOpacity, curve: Curves.easeOut); } else { - _cursorBlinkOpacityController.value = _showCursor.value ? 1.0 : 0.0; + _cursorBlinkOpacityController.value = targetOpacity; } if (_obscureShowCharTicksPending > 0) { @@ -1056,7 +1058,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien } void _startCursorTimer() { - _showCursor.value = true; + _targetCursorVisibility = true; _cursorBlinkOpacityController.value = 1.0; if (EditableText.debugDeterministicCursor) return; @@ -1070,7 +1072,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien void _stopCursorTimer({ bool resetCharTicks = true }) { _cursorTimer?.cancel(); _cursorTimer = null; - _showCursor.value = false; + _targetCursorVisibility = false; _cursorBlinkOpacityController.value = 0.0; if (EditableText.debugDeterministicCursor) return; @@ -1196,7 +1198,9 @@ class EditableTextState extends State with AutomaticKeepAliveClien value: _value, cursorColor: _cursorColor, backgroundCursorColor: widget.backgroundCursorColor, - showCursor: EditableText.debugDeterministicCursor ? ValueNotifier(true) : _showCursor, + showCursor: EditableText.debugDeterministicCursor + ? ValueNotifier(true) + : _cursorVisibilityNotifier, hasFocus: _hasFocus, maxLines: widget.maxLines, selectionColor: widget.selectionColor, diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 7d865f0dfb..c762974020 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -125,12 +125,12 @@ void main() { SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall); const String kThreeLines = - 'First line of text is ' - 'Second line goes until ' - 'Third line of stuff '; + 'First line of text is\n' + 'Second line goes until\n' + 'Third line of stuff'; const String kMoreThanFourLines = kThreeLines + - 'Fourth line won\'t display and ends at'; + '\nFourth line won\'t display and ends at'; // Returns the first RenderEditable. RenderEditable findRenderEditable(WidgetTester tester) { @@ -903,7 +903,7 @@ void main() { ); 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 skipPastScrollingAnimation(tester); @@ -973,7 +973,9 @@ void main() { testWidgets('Can scroll multiline input', (WidgetTester tester) async { final Key textFieldKey = UniqueKey(); - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = TextEditingController( + text: kMoreThanFourLines, + ); await tester.pumpWidget( 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)); final RenderBox inputBox = findInputBox(); @@ -1016,6 +1012,7 @@ void main() { await tester.pump(const Duration(seconds: 1)); await gesture.up(); await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // Now the first line is scrolled up, and the fourth line is visible. Offset newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); @@ -1026,15 +1023,21 @@ void main() { expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isTrue); // Now try scrolling by dragging the selection handle. - // Long press the 'i' in 'Fourth line' to select the word. - await tester.pump(const Duration(seconds: 1)); - final Offset untilPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth line')+8); - gesture = await tester.startGesture(untilPos, pointer: 7); + final Offset selectedWordPos = textOffsetToPosition( + tester, + kMoreThanFourLines.indexOf('Fourth line') + 8, + ); + + gesture = await tester.startGesture(selectedWordPos, pointer: 7); await tester.pump(const Duration(seconds: 1)); await gesture.up(); + await tester.pump(); 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 List endpoints = globalize( renderEditable.getEndpointsForSelection(controller.selection), @@ -1043,7 +1046,7 @@ void main() { expect(endpoints.length, 2); // 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); gesture = await tester.startGesture(handlePos, pointer: 7); await tester.pump(const Duration(seconds: 1)); @@ -1059,9 +1062,7 @@ void main() { expect(newFirstPos.dy, firstPos.dy); expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue); 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 { String textFieldValue; diff --git a/packages/flutter/test/rendering/editable_test.dart b/packages/flutter/test/rendering/editable_test.dart index a3b05b9bf0..ebb433a99b 100644 --- a/packages/flutter/test/rendering/editable_test.dart +++ b/packages/flutter/test/rendering/editable_test.dart @@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart'; import '../rendering/mock_canvas.dart'; import '../rendering/recording_canvas.dart'; +import 'rendering_tester.dart'; class FakeEditableTextState extends TextSelectionDelegate { @override @@ -26,10 +27,6 @@ class FakeEditableTextState extends TextSelectionDelegate { } void main() { - - final TextEditingController controller = TextEditingController(); - const TextStyle textStyle = TextStyle(); - test('editable intrinsics', () { final TextSelectionDelegate delegate = FakeEditableTextState(); final RenderEditable editable = RenderEditable( @@ -99,86 +96,76 @@ void main() { ); }); - RenderEditable findRenderEditable(WidgetTester tester) { - final RenderObject root = tester.renderObject(find.byType(EditableText)); - expect(root, isNotNull); + test('Can change cursor color, radius, visibility', () { + final TextSelectionDelegate delegate = FakeEditableTextState(); + final ValueNotifier showCursor = ValueNotifier(true); + EditableText.debugDeterministicCursor = true; - 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('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, - ), - ), + final RenderEditable editable = RenderEditable( + backgroundCursorColor: Colors.grey, + textDirection: TextDirection.ltr, + cursorColor: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00), + offset: ViewportOffset.zero(), + textSelectionDelegate: delegate, + text: const TextSpan( + text: 'test', + style: TextStyle( + height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', ), ), + selection: const TextSelection.collapsed( + offset: 4, + affinity: TextAffinity.upstream, + ), ); - await tester.tap(find.byType(EditableText)); - final RenderEditable editable = findRenderEditable(tester); - editable.selection = const TextSelection(baseOffset: 29, extentOffset: 29); + layout(editable); - 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)) + editable.layout(BoxConstraints.loose(const Size(100, 100))); + expect( + editable, + // Draw no cursor by default. + paintsExactlyCountTimes(#drawRect, 0), ); - // Moves the cursor right a few characters. - editableTextState.updateFloatingCursor( - RawFloatingCursorPoint( - state: FloatingCursorDragState.Update, - offset: const Offset(-250, 20))); + editable.showCursor = showCursor; + pumpFrame(); - 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)) - ); + expect(editable, paints..rect( + color: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00), + rect: Rect.fromLTWH(40, 2, 1, 6), + )); - 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)); }); } diff --git a/packages/flutter/test/widgets/editable_text_cursor_test.dart b/packages/flutter/test/widgets/editable_text_cursor_test.dart new file mode 100644 index 0000000000..dea8cb150b --- /dev/null +++ b/packages/flutter/test/widgets/editable_text_cursor_test.dart @@ -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 editableTextKey = GlobalKey(); + + String changedValue; + final Widget widget = MaterialApp( + home: RepaintBoundary( + key: const ValueKey(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 {'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(1)), + matchesGoldenFile('editable_text_test.0.0.png'), + ); + }, skip: !Platform.isLinux); + + testWidgets('cursor layout has correct radius', (WidgetTester tester) async { + final GlobalKey editableTextKey = GlobalKey(); + + String changedValue; + final Widget widget = MaterialApp( + home: RepaintBoundary( + key: const ValueKey(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 {'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(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(); + }); +} diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 19e14b0c08..88f630aaf0 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -15,13 +14,30 @@ import 'package:flutter/foundation.dart'; import 'semantics_tester.dart'; -void main() { - 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); +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; +} + +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(() { debugResetSemanticsIdCounter(); }); @@ -88,31 +104,10 @@ void main() { expect(editableText.maxLines, equals(1)); expect(editableText.obscureText, isFalse); expect(editableText.autocorrect, isTrue); + expect(editableText.textAlign, TextAlign.start); 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', (WidgetTester tester) async { await tester.pumpWidget( @@ -526,113 +521,6 @@ void main() { expect(changedValue, clipboardContent); }); - testWidgets('cursor layout has correct width', (WidgetTester tester) async { - final GlobalKey editableTextKey = - GlobalKey(); - - String changedValue; - final Widget widget = MaterialApp( - home: RepaintBoundary( - key: const ValueKey(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 {'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(1)), - matchesGoldenFile('editable_text_test.0.0.png'), - ); - }, skip: !Platform.isLinux); - - testWidgets('cursor layout has correct radius', (WidgetTester tester) async { - final GlobalKey editableTextKey = - GlobalKey(); - - String changedValue; - final Widget widget = MaterialApp( - home: RepaintBoundary( - key: const ValueKey(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 {'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(1)), - matchesGoldenFile('editable_text_test.1.0.png'), - ); - }, skip: !Platform.isLinux); - testWidgets('Does not lose focus by default when "next" action is pressed', (WidgetTester tester) async { final GlobalKey editableTextKey = @@ -841,119 +729,7 @@ void main() { // and onSubmission callbacks. }); - testWidgets('Cursor animates on iOS', (WidgetTester tester) async { - 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( + testWidgets( 'When "newline" action is called on a Editable text with maxLines != 1, onEditingComplete and onSubmitted callbacks are not invoked.', (WidgetTester tester) async { final GlobalKey editableTextKey = @@ -1055,19 +831,20 @@ testWidgets( expect(log, hasLength(1)); expect( - log.single, - isMethodCall( - 'TextInput.setEditingState', - arguments: const { - 'text': 'Wobble', - 'selectionBase': -1, - 'selectionExtent': -1, - 'selectionAffinity': 'TextAffinity.downstream', - 'selectionIsDirectional': false, - 'composingBase': -1, - 'composingExtent': -1, - }, - )); + log.single, + isMethodCall( + 'TextInput.setEditingState', + arguments: const { + 'text': 'Wobble', + 'selectionBase': -1, + 'selectionExtent': -1, + 'selectionAffinity': 'TextAffinity.downstream', + 'selectionIsDirectional': false, + 'composingBase': -1, + 'composingExtent': -1, + }, + ), + ); }); testWidgets('EditableText identifies as text field (w/ focus) in semantics', @@ -1094,19 +871,19 @@ testWidgets( ), ); - expect(semantics, - includesNodeWith(flags: [SemanticsFlag.isTextField])); + expect(semantics, includesNodeWith(flags: [SemanticsFlag.isTextField])); await tester.tap(find.byType(EditableText)); await tester.idle(); await tester.pump(); expect( - semantics, - includesNodeWith(flags: [ - SemanticsFlag.isTextField, - SemanticsFlag.isFocused - ])); + semantics, + includesNodeWith(flags: [ + SemanticsFlag.isTextField, + SemanticsFlag.isFocused + ]), + ); semantics.dispose(); }); @@ -1139,11 +916,12 @@ testWidgets( ); expect( - semantics, - includesNodeWith( - flags: [SemanticsFlag.isTextField], - value: value1, - )); + semantics, + includesNodeWith( + flags: [SemanticsFlag.isTextField], + value: value1, + ), + ); const String value2 = 'Changed the EditableText content'; controller.text = value2; @@ -1151,17 +929,17 @@ testWidgets( await tester.pump(); expect( - semantics, - includesNodeWith( - flags: [SemanticsFlag.isTextField], - value: value2, - )); + semantics, + includesNodeWith( + flags: [SemanticsFlag.isTextField], + value: value2, + ), + ); semantics.dispose(); }); - testWidgets('changing selection with keyboard does not show handles', - (WidgetTester tester) async { + testWidgets('changing selection with keyboard does not show handles', (WidgetTester tester) async { const String value1 = 'Hello World'; controller.text = value1; @@ -1190,8 +968,9 @@ testWidgets( expect(textState.selectionOverlay.handlesAreVisible, isTrue); expect( - textState.selectionOverlay.selectionDelegate.textEditingValue.selection, - const TextSelection.collapsed(offset: 4)); + textState.selectionOverlay.selectionDelegate.textEditingValue.selection, + const TextSelection.collapsed(offset: 4), + ); // Simulate selection change via keyboard and expect handles to disappear. render.onSelectionChanged(const TextSelection.collapsed(offset: 10), render, @@ -1200,8 +979,9 @@ testWidgets( expect(textState.selectionOverlay.handlesAreVisible, isFalse); expect( - textState.selectionOverlay.selectionDelegate.textEditingValue.selection, - const TextSelection.collapsed(offset: 10)); + textState.selectionOverlay.selectionDelegate.textEditingValue.selection, + const TextSelection.collapsed(offset: 10), + ); }); testWidgets('exposes correct cursor movement semantics', @@ -1221,10 +1001,11 @@ testWidgets( )); expect( - semantics, - includesNodeWith( - value: 'test', - )); + semantics, + includesNodeWith( + value: 'test', + ), + ); controller.selection = TextSelection.collapsed(offset: controller.text.length); @@ -1232,15 +1013,16 @@ testWidgets( // At end, can only go backwards. expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - SemanticsAction.setSelection, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + SemanticsAction.setSelection, + ], + ), + ); controller.selection = TextSelection.collapsed(offset: controller.text.length - 2); @@ -1248,32 +1030,34 @@ testWidgets( // Somewhere in the middle, can go in both directions. expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorForwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - SemanticsAction.moveCursorForwardByWord, - SemanticsAction.setSelection, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorForwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + SemanticsAction.moveCursorForwardByWord, + SemanticsAction.setSelection, + ], + ), + ); controller.selection = const TextSelection.collapsed(offset: 0); await tester.pumpAndSettle(); // At beginning, can only go forward. expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorForwardByCharacter, - SemanticsAction.moveCursorForwardByWord, - SemanticsAction.setSelection, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorForwardByCharacter, + SemanticsAction.moveCursorForwardByWord, + SemanticsAction.setSelection, + ], + ), + ); semantics.dispose(); }); @@ -1297,14 +1081,15 @@ testWidgets( )); expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + ], + ), + ); final RenderEditable render = tester.allRenderObjects .firstWhere((RenderObject o) => o.runtimeType == RenderEditable); @@ -1321,17 +1106,18 @@ testWidgets( expect(controller.selection.extentOffset, 3); expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorForwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - SemanticsAction.moveCursorForwardByWord, - SemanticsAction.setSelection, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorForwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + SemanticsAction.moveCursorForwardByWord, + SemanticsAction.setSelection, + ], + ), + ); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection); @@ -1348,15 +1134,16 @@ testWidgets( await tester.pumpAndSettle(); expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorForwardByCharacter, - SemanticsAction.moveCursorForwardByWord, - SemanticsAction.setSelection, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorForwardByCharacter, + SemanticsAction.moveCursorForwardByWord, + SemanticsAction.setSelection, + ], + ), + ); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection); @@ -1387,14 +1174,15 @@ testWidgets( )); expect( - semantics, - includesNodeWith( - value: 'test for words', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - ], - )); + semantics, + includesNodeWith( + value: 'test for words', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + ], + ), + ); final RenderEditable render = tester.allRenderObjects .firstWhere((RenderObject o) => o.runtimeType == RenderEditable); @@ -1411,17 +1199,18 @@ testWidgets( expect(controller.selection.extentOffset, 9); expect( - semantics, - includesNodeWith( - value: 'test for words', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorForwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - SemanticsAction.moveCursorForwardByWord, - SemanticsAction.setSelection, - ], - )); + semantics, + includesNodeWith( + value: 'test for words', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorForwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + SemanticsAction.moveCursorForwardByWord, + SemanticsAction.setSelection, + ], + ), + ); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByWord, doNotExtendSelection); @@ -1439,15 +1228,16 @@ testWidgets( await tester.pumpAndSettle(); expect( - semantics, - includesNodeWith( - value: 'test for words', - actions: [ - SemanticsAction.moveCursorForwardByCharacter, - SemanticsAction.moveCursorForwardByWord, - SemanticsAction.setSelection, - ], - )); + semantics, + includesNodeWith( + value: 'test for words', + actions: [ + SemanticsAction.moveCursorForwardByCharacter, + SemanticsAction.moveCursorForwardByWord, + SemanticsAction.setSelection, + ], + ), + ); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByWord, doNotExtendSelection); @@ -1487,14 +1277,15 @@ testWidgets( )); expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + ], + ), + ); final RenderEditable render = tester.allRenderObjects .firstWhere((RenderObject o) => o.runtimeType == RenderEditable); @@ -1511,17 +1302,18 @@ testWidgets( expect(controller.selection.extentOffset, 3); expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorForwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - SemanticsAction.moveCursorForwardByWord, - SemanticsAction.setSelection, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorForwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + SemanticsAction.moveCursorForwardByWord, + SemanticsAction.setSelection, + ], + ), + ); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection); @@ -1538,15 +1330,16 @@ testWidgets( await tester.pumpAndSettle(); expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorForwardByCharacter, - SemanticsAction.moveCursorForwardByWord, - SemanticsAction.setSelection, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorForwardByCharacter, + SemanticsAction.moveCursorForwardByWord, + SemanticsAction.setSelection, + ], + ), + ); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection); @@ -1586,14 +1379,15 @@ testWidgets( )); expect( - semantics, - includesNodeWith( - value: 'test for words', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - ], - )); + semantics, + includesNodeWith( + value: 'test for words', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + ], + ), + ); final RenderEditable render = tester.allRenderObjects .firstWhere((RenderObject o) => o.runtimeType == RenderEditable); @@ -1610,17 +1404,18 @@ testWidgets( expect(controller.selection.extentOffset, 9); expect( - semantics, - includesNodeWith( - value: 'test for words', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorForwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - SemanticsAction.moveCursorForwardByWord, - SemanticsAction.setSelection, - ], - )); + semantics, + includesNodeWith( + value: 'test for words', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorForwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + SemanticsAction.moveCursorForwardByWord, + SemanticsAction.setSelection, + ], + ), + ); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByWord, extendSelection); @@ -1638,15 +1433,16 @@ testWidgets( await tester.pumpAndSettle(); expect( - semantics, - includesNodeWith( - value: 'test for words', - actions: [ - SemanticsAction.moveCursorForwardByCharacter, - SemanticsAction.moveCursorForwardByWord, - SemanticsAction.setSelection, - ], - )); + semantics, + includesNodeWith( + value: 'test for words', + actions: [ + SemanticsAction.moveCursorForwardByCharacter, + SemanticsAction.moveCursorForwardByWord, + SemanticsAction.setSelection, + ], + ), + ); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByWord, doNotExtendSelection); @@ -1685,32 +1481,130 @@ testWidgets( final String expectedValue = '•' * controller.text.length; expect( - semantics, - hasSemantics( - TestSemantics( + semantics, + hasSemantics( + TestSemantics( + children: [ + TestSemantics.rootChild( children: [ - TestSemantics.rootChild( + TestSemantics( + flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( - flags: [SemanticsFlag.scopesRoute], - children: [ - TestSemantics( - flags: [ - SemanticsFlag.isTextField, - SemanticsFlag.isObscured - ], - value: expectedValue, - textDirection: TextDirection.ltr, - ), + flags: [ + SemanticsFlag.isTextField, + SemanticsFlag.isObscured ], + value: expectedValue, + textDirection: TextDirection.ltr, ), ], ), ], ), - ignoreTransform: true, - ignoreRect: true, - ignoreId: true)); + ], + ), + ignoreTransform: true, + ignoreRect: true, + ignoreId: true, + ), + ); + + semantics.dispose(); + }); + + testWidgets('password fields become obscured with the right semantics when set', + (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + + const String originalText = 'super-secret-password!!1'; + controller.text = originalText; + + await tester.pumpWidget(MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: controller, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + ), + )); + + const String expectedValue = '••••••••••••••••••••••••'; + + expect( + semantics, + hasSemantics( + TestSemantics( + children: [ + TestSemantics.rootChild( + children: [ + TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + flags: [ + 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.rootChild( + children: [ + TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + flags: [ + SemanticsFlag.isTextField, + SemanticsFlag.isObscured, + SemanticsFlag.isFocused, + ], + value: expectedValue, + textDirection: TextDirection.ltr, + ), + ], + ), + ], + ), + ], + ), + ignoreTransform: true, + ignoreRect: true, + ignoreId: true, + ), + ); semantics.dispose(); }); @@ -1755,77 +1649,82 @@ testWidgets( await tester.pump(); expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - SemanticsAction.setSelection, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + SemanticsAction.setSelection, + ], + ), + ); when(controls.canCopy(any)).thenReturn(true); await _buildApp(controls, tester); expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - SemanticsAction.setSelection, - SemanticsAction.copy, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + SemanticsAction.setSelection, + SemanticsAction.copy, + ], + ), + ); when(controls.canCopy(any)).thenReturn(false); when(controls.canPaste(any)).thenReturn(true); await _buildApp(controls, tester); expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - SemanticsAction.setSelection, - SemanticsAction.paste, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + SemanticsAction.setSelection, + SemanticsAction.paste, + ], + ), + ); when(controls.canPaste(any)).thenReturn(false); when(controls.canCut(any)).thenReturn(true); await _buildApp(controls, tester); expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - SemanticsAction.setSelection, - SemanticsAction.cut, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + SemanticsAction.setSelection, + SemanticsAction.cut, + ], + ), + ); when(controls.canCopy(any)).thenReturn(true); when(controls.canCut(any)).thenReturn(true); when(controls.canPaste(any)).thenReturn(true); await _buildApp(controls, tester); expect( - semantics, - includesNodeWith( - value: 'test', - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - SemanticsAction.setSelection, - SemanticsAction.cut, - SemanticsAction.copy, - SemanticsAction.paste, - ], - )); + semantics, + includesNodeWith( + value: 'test', + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + SemanticsAction.setSelection, + SemanticsAction.cut, + SemanticsAction.copy, + SemanticsAction.paste, + ], + ), + ); semantics.dispose(); }); @@ -1844,44 +1743,46 @@ testWidgets( const int expectedNodeId = 4; expect( - semantics, - hasSemantics( - TestSemantics.root( + semantics, + hasSemantics( + TestSemantics.root( + children: [ + TestSemantics.rootChild( + id: 1, children: [ - TestSemantics.rootChild( - id: 1, + TestSemantics( + id: 2, + flags: [SemanticsFlag.scopesRoute], children: [ - TestSemantics( - id: 2, - flags: [SemanticsFlag.scopesRoute], - children: [ - TestSemantics.rootChild( - id: expectedNodeId, - flags: [ - SemanticsFlag.isTextField, - SemanticsFlag.isFocused - ], - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.moveCursorBackwardByWord, - SemanticsAction.setSelection, - SemanticsAction.copy, - SemanticsAction.cut, - SemanticsAction.paste - ], - value: 'test', - textSelection: TextSelection.collapsed( - offset: controller.text.length), - textDirection: TextDirection.ltr, - ), + TestSemantics.rootChild( + id: expectedNodeId, + flags: [ + SemanticsFlag.isTextField, + SemanticsFlag.isFocused ], + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.moveCursorBackwardByWord, + SemanticsAction.setSelection, + SemanticsAction.copy, + SemanticsAction.cut, + SemanticsAction.paste + ], + value: 'test', + textSelection: TextSelection.collapsed( + offset: controller.text.length), + textDirection: TextDirection.ltr, ), ], ), ], ), - ignoreRect: true, - ignoreTransform: true)); + ], + ), + ignoreRect: true, + ignoreTransform: true, + ), + ); owner.performAction(expectedNodeId, SemanticsAction.copy); verify(controls.handleCopy(any)).called(1); @@ -1915,196 +1816,6 @@ testWidgets( 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 { int called = 0; final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) {