| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // This file is run as part of a reduced test set in CI on Mac and Windows |
| // machines. |
| @Tags(<String>['reduced-test-set']) |
| @TestOn('!chrome') |
| library; |
| |
| import 'dart:math' as math; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import 'editable_text_utils.dart'; |
| |
| const TextStyle textStyle = TextStyle(); |
| const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00); |
| |
| void main() { |
| late TextEditingController controller; |
| late FocusNode focusNode; |
| late FocusScopeNode focusScopeNode; |
| |
| setUp(() async { |
| // Fill the clipboard so that the Paste option is available in the text |
| // selection menu. |
| await Clipboard.setData(const ClipboardData(text: 'Clipboard data')); |
| controller = TextEditingController(); |
| focusNode = FocusNode(); |
| focusScopeNode = FocusScopeNode(); |
| }); |
| |
| tearDown(() { |
| controller.dispose(); |
| focusNode.dispose(); |
| focusScopeNode.dispose(); |
| }); |
| |
| testWidgets('cursor has expected width, height, and radius', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| cursorWidth: 10.0, |
| cursorHeight: 10.0, |
| cursorRadius: const Radius.circular(2.0), |
| ), |
| ), |
| ), |
| ); |
| |
| final EditableText editableText = tester.firstWidget(find.byType(EditableText)); |
| expect(editableText.cursorWidth, 10.0); |
| expect(editableText.cursorHeight, 10.0); |
| expect(editableText.cursorRadius!.x, 2.0); |
| }); |
| |
| testWidgets('cursor layout has correct width', (WidgetTester tester) async { |
| EditableText.debugDeterministicCursor = true; |
| final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); |
| |
| late String changedValue; |
| final Widget widget = MaterialApp( |
| home: RepaintBoundary( |
| key: const ValueKey<int>(1), |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| key: editableTextKey, |
| controller: controller, |
| focusNode: focusNode, |
| style: Typography.material2018().black.titleMedium!, |
| 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 = ' '; |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { |
| if (methodCall.method == 'Clipboard.getData') { |
| return const <String, dynamic>{'text': clipboardContent}; |
| } |
| if (methodCall.method == 'Clipboard.hasStrings') { |
| return <String, dynamic>{'value': clipboardContent.isNotEmpty}; |
| } |
| return null; |
| }); |
| |
| // Long-press to bring up the text editing controls. |
| final Finder textFinder = find.byKey(editableTextKey); |
| await tester.longPress(textFinder); |
| tester.state<EditableTextState>(textFinder).showToolbar(); |
| await tester.pumpAndSettle(); |
| |
| await tester.tap(find.text('Paste')); |
| await tester.pump(); |
| |
| expect(changedValue, clipboardContent); |
| |
| await expectLater( |
| find.byKey(const ValueKey<int>(1)), |
| matchesGoldenFile('editable_text_test.0.png'), |
| ); |
| EditableText.debugDeterministicCursor = false; |
| }); |
| |
| testWidgets('cursor layout has correct radius', (WidgetTester tester) async { |
| final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); |
| |
| late String changedValue; |
| final Widget widget = MaterialApp( |
| home: RepaintBoundary( |
| key: const ValueKey<int>(1), |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| key: editableTextKey, |
| controller: controller, |
| focusNode: focusNode, |
| style: Typography.material2018().black.titleMedium!, |
| 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 = ' '; |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { |
| if (methodCall.method == 'Clipboard.getData') { |
| return const <String, dynamic>{'text': clipboardContent}; |
| } |
| if (methodCall.method == 'Clipboard.hasStrings') { |
| return <String, dynamic>{'value': clipboardContent.isNotEmpty}; |
| } |
| return null; |
| }); |
| |
| // Long-press to bring up the text editing controls. |
| final Finder textFinder = find.byKey(editableTextKey); |
| await tester.longPress(textFinder); |
| tester.state<EditableTextState>(textFinder).showToolbar(); |
| await tester.pumpAndSettle(); |
| |
| 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.png'), |
| ); |
| }); |
| |
| testWidgets('Cursor animates on iOS', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: TextField(), |
| ), |
| ), |
| ); |
| |
| final Finder textFinder = find.byType(TextField); |
| await tester.tap(textFinder); |
| await tester.pump(); |
| |
| final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); |
| final RenderEditable renderEditable = editableTextState.renderEditable; |
| |
| expect(renderEditable.cursorColor!.opacity, 1.0); |
| |
| int walltimeMicrosecond = 0; |
| double lastVerifiedOpacity = 1.0; |
| |
| Future<void> verifyKeyFrame({ required double opacity, required int at }) async { |
| const int delta = 1; |
| assert(at - delta > walltimeMicrosecond); |
| await tester.pump(Duration(microseconds: at - delta - walltimeMicrosecond)); |
| |
| // Instead of verifying the opacity at each key frame, this function |
| // verifies the opacity immediately *before* each key frame to avoid |
| // fp precision issues. |
| expect( |
| renderEditable.cursorColor!.opacity, |
| closeTo(lastVerifiedOpacity, 0.01), |
| reason: 'opacity at ${at-delta} microseconds', |
| ); |
| |
| walltimeMicrosecond = at - delta; |
| lastVerifiedOpacity = opacity; |
| } |
| |
| await verifyKeyFrame(opacity: 1.0, at: 500000); |
| await verifyKeyFrame(opacity: 0.75, at: 537500); |
| await verifyKeyFrame(opacity: 0.5, at: 575000); |
| await verifyKeyFrame(opacity: 0.25, at: 612500); |
| await verifyKeyFrame(opacity: 0.0, at: 650000); |
| await verifyKeyFrame(opacity: 0.0, at: 850000); |
| await verifyKeyFrame(opacity: 0.25, at: 887500); |
| await verifyKeyFrame(opacity: 0.5, at: 925000); |
| await verifyKeyFrame(opacity: 0.75, at: 962500); |
| await verifyKeyFrame(opacity: 1.0, at: 1000000); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); |
| |
| testWidgets('Cursor does not animate on non-iOS platforms', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material(child: TextField(maxLines: 3)), |
| ), |
| ); |
| |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| // Wait for the current animation to finish. If the cursor never stops its |
| // blinking animation the test will timeout. |
| await tester.pumpAndSettle(); |
| |
| for (int i = 0; i < 40; i += 1) { |
| await tester.pump(const Duration(milliseconds: 100)); |
| expect(tester.hasRunningAnimations, false); |
| } |
| }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); |
| |
| testWidgets('Cursor does not animate on Android', (WidgetTester tester) async { |
| final Color defaultCursorColor = Color(ThemeData.fallback().colorScheme.primary.value); |
| 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: defaultCursorColor)); |
| |
| // 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: defaultCursorColor)); |
| |
| 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: defaultCursorColor)); |
| |
| await tester.pump(const Duration(milliseconds: 500)); |
| expect(renderEditable.cursorColor!.alpha, 0); |
| expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0)); |
| }); |
| |
| testWidgets('Cursor does not animates when debugDeterministicCursor is set', (WidgetTester tester) async { |
| EditableText.debugDeterministicCursor = true; |
| final Color defaultCursorColor = Color(ThemeData.fallback().colorScheme.primary.value); |
| 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(); |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(renderEditable.cursorColor!.alpha, 255); |
| expect(renderEditable, paints..rrect(color: defaultCursorColor)); |
| |
| // Cursor draw never changes. |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(renderEditable.cursorColor!.alpha, 255); |
| expect(renderEditable, paints..rrect(color: defaultCursorColor)); |
| |
| // No more transient calls. |
| await tester.pumpAndSettle(); |
| expect(renderEditable.cursorColor!.alpha, 255); |
| expect(renderEditable, paints..rrect(color: defaultCursorColor)); |
| |
| EditableText.debugDeterministicCursor = false; |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets('Cursor does not animate on Android when debugDeterministicCursor is set', (WidgetTester tester) async { |
| final Color defaultCursorColor = Color(ThemeData.fallback().colorScheme.primary.value); |
| 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: defaultCursorColor)); |
| |
| await tester.pump(const Duration(milliseconds: 500)); |
| expect(renderEditable.cursorColor!.alpha, 255); |
| expect(renderEditable, paints..rect(color: defaultCursorColor)); |
| |
| // Cursor draw never changes. |
| await tester.pump(const Duration(milliseconds: 500)); |
| expect(renderEditable.cursorColor!.alpha, 255); |
| expect(renderEditable, paints..rect(color: defaultCursorColor)); |
| |
| // No more transient calls. |
| await tester.pumpAndSettle(); |
| expect(renderEditable.cursorColor!.alpha, 255); |
| expect(renderEditable, paints..rect(color: defaultCursorColor)); |
| |
| EditableText.debugDeterministicCursor = false; |
| }); |
| |
| testWidgets('Cursor animation restarts when it is moved using keys on desktop', (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.macOS; |
| |
| const String testText = 'Some text long enough to move the cursor around'; |
| controller.text = testText; |
| |
| final Widget widget = MaterialApp( |
| home: EditableText( |
| controller: controller, |
| focusNode: focusNode, |
| style: const TextStyle(fontSize: 20.0), |
| cursorColor: Colors.blue, |
| backgroundCursorColor: Colors.grey, |
| selectionControls: materialTextSelectionControls, |
| keyboardType: TextInputType.text, |
| textAlign: TextAlign.left, |
| ), |
| ); |
| await tester.pumpWidget(widget); |
| |
| await tester.tap(find.byType(EditableText)); |
| 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(0xff2196f3))); |
| |
| // 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(0xff2196f3))); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pump(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowLeft); |
| |
| await tester.pump(); |
| expect(renderEditable.cursorColor!.alpha, 255); |
| expect(renderEditable, paints..rect(color: const Color(0xff2196f3))); |
| |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(renderEditable.cursorColor!.alpha, 255); |
| expect(renderEditable, paints..rect(color: const Color(0xff2196f3))); |
| |
| await tester.pump(const Duration(milliseconds: 299)); |
| expect(renderEditable.cursorColor!.alpha, 255); |
| expect(renderEditable, paints..rect(color: const Color(0xff2196f3))); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pump(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pump(); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pump(); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowRight); |
| |
| await tester.pump(); |
| expect(renderEditable.cursorColor!.alpha, 255); |
| expect(renderEditable, paints..rect(color: const Color(0xff2196f3))); |
| |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(renderEditable.cursorColor!.alpha, 255); |
| expect(renderEditable, paints..rect(color: const Color(0xff2196f3))); |
| |
| await tester.pump(const Duration(milliseconds: 299)); |
| expect(renderEditable.cursorColor!.alpha, 255); |
| expect(renderEditable, paints..rect(color: const Color(0xff2196f3))); |
| |
| await tester.pump(const Duration(milliseconds: 1)); |
| expect(renderEditable.cursorColor!.alpha, 0); |
| expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0)); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }, |
| variant: KeySimulatorTransitModeVariant.all(), |
| ); |
| |
| testWidgets('Cursor does not show when showCursor set to false', (WidgetTester tester) async { |
| const Widget widget = MaterialApp( |
| home: Material( |
| child: TextField( |
| showCursor: false, |
| 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; |
| |
| // Make sure it does not paint for a period of time. |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0)); |
| |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0)); |
| |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0)); |
| }); |
| |
| testWidgets('Cursor does not show when not focused', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/106512 . |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField(focusNode: focusNode, autofocus: true), |
| ), |
| ), |
| ); |
| assert(focusNode.hasFocus); |
| final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); |
| final RenderEditable renderEditable = editableTextState.renderEditable; |
| |
| focusNode.unfocus(); |
| await tester.pump(); |
| |
| for (int i = 0; i < 10; i += 10) { |
| // Make sure it does not paint for a period of time. |
| expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0)); |
| expect(tester.hasRunningAnimations, isFalse); |
| await tester.pump(const Duration(milliseconds: 29)); |
| } |
| |
| // Refocus and it should paint the caret. |
| focusNode.requestFocus(); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 100)); |
| expect(renderEditable, isNot(paintsExactlyCountTimes(#drawRect, 0))); |
| }); |
| |
| testWidgets('Cursor radius is 2.0', (WidgetTester tester) async { |
| 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)); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| 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; |
| |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| 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)); |
| |
| // Sets the origin. |
| editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start, 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. |
| |
| // Sets the origin. |
| editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start, 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; |
| |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| 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)); |
| |
| // Sets the origin. |
| editableTextState.updateFloatingCursor(RawFloatingCursorPoint( |
| state: FloatingCursorDragState.Start, |
| 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('Updating the floating cursor can end without update', (WidgetTester tester) async { |
| const String text = 'hello world this is fun and cool and awesome!'; |
| controller.text = text; |
| |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| 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); |
| |
| editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End)); |
| |
| await tester.pumpAndSettle(); |
| // The cursor did not change. |
| expect(controller.selection.baseOffset, 29); |
| expect(tester.takeException(), null); |
| }); |
| |
| testWidgets("Drag the floating cursor, it won't blink.", (WidgetTester tester) async { |
| const String text = 'hello world this is fun and cool and awesome!'; |
| controller.text = text; |
| |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| node: focusScopeNode, |
| autofocus: true, |
| child: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| |
| // Check that the cursor visibility toggles after each blink interval. |
| // Or if it's not blinking at all, it stays on. |
| Future<void> checkCursorBlinking({ bool isBlinking = true }) async { |
| bool initialShowCursor = true; |
| if (isBlinking) { |
| initialShowCursor = editableText.cursorCurrentlyVisible; |
| } |
| await tester.pump(editableText.cursorBlinkInterval); |
| expect(editableText.cursorCurrentlyVisible, equals(isBlinking ? !initialShowCursor : initialShowCursor)); |
| await tester.pump(editableText.cursorBlinkInterval); |
| expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor)); |
| await tester.pump(editableText.cursorBlinkInterval ~/ 10); |
| expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor)); |
| await tester.pump(editableText.cursorBlinkInterval); |
| expect(editableText.cursorCurrentlyVisible, equals(isBlinking ? !initialShowCursor : initialShowCursor)); |
| await tester.pump(editableText.cursorBlinkInterval); |
| expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor)); |
| } |
| |
| final Offset textfieldStart = tester.getTopLeft(find.byType(EditableText)); |
| |
| await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); |
| await tester.pumpAndSettle(); |
| |
| // Before dragging, the cursor should blink. |
| await checkCursorBlinking(); |
| |
| final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); |
| editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start)); |
| |
| // When drag cursor, the cursor shouldn't blink. |
| await checkCursorBlinking(isBlinking: false); |
| |
| editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End)); |
| await tester.pumpAndSettle(); |
| |
| // After dragging, the cursor should blink. |
| await checkCursorBlinking(); |
| }); |
| |
| testWidgets('Turning showCursor off stops the cursor', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/108187. |
| final bool debugDeterministicCursor = EditableText.debugDeterministicCursor; |
| // This doesn't really matter. |
| EditableText.debugDeterministicCursor = false; |
| addTearDown(() { EditableText.debugDeterministicCursor = debugDeterministicCursor; }); |
| const Key key = Key('EditableText'); |
| |
| Widget buildEditableText({ required bool showCursor }) { |
| return MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: EditableText( |
| key: key, |
| backgroundCursorColor: Colors.grey, |
| // Use animation controller to animate cursor blink for testing. |
| cursorOpacityAnimates: true, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| showCursor: showCursor, |
| ), |
| ), |
| ); |
| } |
| late final EditableTextState editableTextState = tester.state(find.byKey(key)); |
| await tester.pumpWidget(buildEditableText(showCursor: false)); |
| await tester.tap(find.byKey(key)); |
| await tester.pump(); |
| |
| // No cursor even when focused. |
| expect(editableTextState.cursorCurrentlyVisible, false); |
| |
| // The EditableText still has focus, so the cursor should starts blinking. |
| await tester.pumpWidget(buildEditableText(showCursor: true)); |
| expect(editableTextState.cursorCurrentlyVisible, true); |
| await tester.pump(); |
| expect(editableTextState.cursorCurrentlyVisible, true); |
| |
| // readOnly disables blinking cursor. |
| await tester.pumpWidget(buildEditableText(showCursor: false)); |
| expect(editableTextState.cursorCurrentlyVisible, false); |
| await tester.pump(); |
| expect(editableTextState.cursorCurrentlyVisible, false); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/pull/30475. |
| testWidgets('Trying to select with the floating cursor does not crash', (WidgetTester tester) async { |
| const String text = 'hello world this is fun and cool and awesome!'; |
| controller.text = text; |
| |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| 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)); |
| |
| // Sets the origin. |
| editableTextState.updateFloatingCursor(RawFloatingCursorPoint( |
| state: FloatingCursorDragState.Start, |
| 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)); |
| // Immediately start a new floating cursor, in the same way as happens when |
| // the user tries to select text in trackpad mode. |
| editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start, offset: const Offset(20, 20))); |
| await tester.pumpAndSettle(); |
| |
| // Set and move the second cursor like a selection. Previously, the second |
| // Update here caused a crash. |
| editableTextState.updateFloatingCursor(RawFloatingCursorPoint( |
| state: FloatingCursorDragState.Update, |
| offset: const Offset(-250, 20), |
| )); |
| editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End)); |
| await tester.pumpAndSettle(); |
| }); |
| |
| testWidgets('autofocus sets cursor to the end of text', (WidgetTester tester) async { |
| const String text = 'hello world'; |
| controller.text = text; |
| |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(), |
| 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 TextStyle textStyle = TextStyle(); |
| const String text = 'hello world this is fun and cool and awesome!'; |
| controller.text = text; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| 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, offset: const Offset(20, 20)), |
| ); |
| await tester.pump(); |
| |
| expect(editable, paints |
| ..rrect( |
| rrect: RRect.fromRectAndRadius( |
| const Rect.fromLTRB(463.8333435058594, -0.916666666666668, 466.8333435058594, 19.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( |
| const Rect.fromLTWH(193.83334350585938, -0.916666666666668, 3.0, 20.0), |
| const Radius.circular(1.0), |
| ), |
| color: const Color(0xbf2196f3), |
| ), |
| ); |
| |
| // Move the cursor away from characters, this will show the regular cursor. |
| editableTextState.updateFloatingCursor( |
| RawFloatingCursorPoint( |
| state: FloatingCursorDragState.Update, |
| offset: const Offset(800, 0), |
| ), |
| ); |
| |
| expect(find.byType(EditableText), paints |
| ..rrect( |
| rrect: RRect.fromRectAndRadius( |
| const Rect.fromLTWH(719.3333333333333, -0.9166666666666679, 2.0, 18.0), |
| const Radius.circular(2.0), |
| ), |
| color: const Color(0xff999999), |
| ) |
| ..rrect( |
| rrect: RRect.fromRectAndRadius( |
| const Rect.fromLTRB(800.5, -5.0, 803.5, 15.0), |
| const Radius.circular(1.0), |
| ), |
| color: const Color(0xbf2196f3), |
| ), |
| ); |
| |
| editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End)); |
| await tester.pumpAndSettle(); |
| debugDefaultTargetPlatformOverride = null; |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets('cursor layout', (WidgetTester tester) async { |
| EditableText.debugDeterministicCursor = true; |
| final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); |
| |
| late String changedValue; |
| final Widget widget = MaterialApp( |
| home: RepaintBoundary( |
| key: const ValueKey<int>(1), |
| child: Column( |
| children: <Widget>[ |
| const SizedBox(width: 10, height: 10), |
| EditableText( |
| backgroundCursorColor: Colors.grey, |
| key: editableTextKey, |
| controller: controller, |
| focusNode: focusNode, |
| style: Typography.material2018(platform: TargetPlatform.iOS).black.titleMedium!, |
| 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 = 'Hello world!'; |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { |
| if (methodCall.method == 'Clipboard.getData') { |
| return const <String, dynamic>{'text': clipboardContent}; |
| } |
| if (methodCall.method == 'Clipboard.hasStrings') { |
| return <String, dynamic>{'value': clipboardContent.isNotEmpty}; |
| } |
| return null; |
| }); |
| |
| // Long-press to bring up the text editing controls. |
| final Finder textFinder = find.byKey(editableTextKey); |
| await tester.longPress(textFinder); |
| tester.state<EditableTextState>(textFinder).showToolbar(); |
| await tester.pumpAndSettle(); |
| |
| await tester.tap(find.text('Paste')); |
| await tester.pump(); |
| |
| expect(changedValue, clipboardContent); |
| |
| await expectLater( |
| find.byKey(const ValueKey<int>(1)), |
| matchesGoldenFile('editable_text_test.2.png'), |
| ); |
| EditableText.debugDeterministicCursor = false; |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('cursor layout has correct height', (WidgetTester tester) async { |
| EditableText.debugDeterministicCursor = true; |
| final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); |
| |
| late String changedValue; |
| final Widget widget = MaterialApp( |
| home: RepaintBoundary( |
| key: const ValueKey<int>(1), |
| child: Column( |
| children: <Widget>[ |
| const SizedBox(width: 10, height: 10), |
| EditableText( |
| backgroundCursorColor: Colors.grey, |
| key: editableTextKey, |
| controller: controller, |
| focusNode: focusNode, |
| style: Typography.material2018(platform: TargetPlatform.iOS).black.titleMedium!, |
| cursorColor: Colors.blue, |
| selectionControls: materialTextSelectionControls, |
| keyboardType: TextInputType.text, |
| onChanged: (String value) { |
| changedValue = value; |
| }, |
| cursorWidth: 15.0, |
| cursorHeight: 30.0, |
| ), |
| ], |
| ), |
| ), |
| ); |
| await tester.pumpWidget(widget); |
| |
| // Populate a fake clipboard. |
| const String clipboardContent = 'Hello world!'; |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { |
| if (methodCall.method == 'Clipboard.getData') { |
| return const <String, dynamic>{'text': clipboardContent}; |
| } |
| if (methodCall.method == 'Clipboard.hasStrings') { |
| return <String, dynamic>{'value': clipboardContent.isNotEmpty}; |
| } |
| return null; |
| }); |
| |
| // Long-press to bring up the text editing controls. |
| final Finder textFinder = find.byKey(editableTextKey); |
| await tester.longPress(textFinder); |
| tester.state<EditableTextState>(textFinder).showToolbar(); |
| await tester.pumpAndSettle(); |
| |
| await tester.tap(find.text('Paste')); |
| await tester.pump(); |
| |
| expect(changedValue, clipboardContent); |
| |
| await expectLater( |
| find.byKey(const ValueKey<int>(1)), |
| matchesGoldenFile('editable_text_test.3.png'), |
| ); |
| EditableText.debugDeterministicCursor = false; |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('password briefly does not show last character when disabled by system', (WidgetTester tester) async { |
| final bool debugDeterministicCursor = EditableText.debugDeterministicCursor; |
| EditableText.debugDeterministicCursor = false; |
| addTearDown(() { |
| EditableText.debugDeterministicCursor = debugDeterministicCursor; |
| }); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| obscureText: true, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| )); |
| |
| await tester.enterText(find.byType(EditableText), 'AA'); |
| await tester.pump(); |
| await tester.enterText(find.byType(EditableText), 'AAA'); |
| await tester.pump(); |
| |
| tester.binding.platformDispatcher.brieflyShowPasswordTestValue = false; |
| addTearDown(() { |
| tester.binding.platformDispatcher.brieflyShowPasswordTestValue = true; |
| }); |
| expect((findRenderEditable(tester).text! as TextSpan).text, '••A'); |
| await tester.pump(const Duration(milliseconds: 500)); |
| expect((findRenderEditable(tester).text! as TextSpan).text, '•••'); |
| await tester.pump(const Duration(milliseconds: 500)); |
| await tester.pump(const Duration(milliseconds: 500)); |
| await tester.pump(const Duration(milliseconds: 500)); |
| expect((findRenderEditable(tester).text! as TextSpan).text, '•••'); |
| }); |
| |
| testWidgets('getLocalRectForCaret with empty text', (WidgetTester tester) async { |
| EditableText.debugDeterministicCursor = true; |
| addTearDown(() { EditableText.debugDeterministicCursor = false; }); |
| const String text = '12'; |
| final TextEditingController controller = TextEditingController.fromValue( |
| const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: text.length), |
| ), |
| ); |
| addTearDown(controller.dispose); |
| |
| final Widget widget = EditableText( |
| autofocus: true, |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: const TextStyle(fontSize: 20), |
| textAlign: TextAlign.center, |
| keyboardType: TextInputType.text, |
| cursorColor: cursorColor, |
| maxLines: null, |
| ); |
| await tester.pumpWidget(MaterialApp(home: widget)); |
| |
| final EditableTextState editableTextState = tester.firstState(find.byWidget(widget)); |
| final RenderEditable renderEditable = editableTextState.renderEditable; |
| final Rect initialLocalCaretRect = renderEditable.getLocalRectForCaret(const TextPosition(offset: text.length)); |
| |
| for (int i = 0; i < 3; i++) { |
| Actions.invoke(primaryFocus!.context!, const DeleteCharacterIntent(forward: false)); |
| await tester.pump(); |
| expect(controller.text.length, math.max(0, text.length - 1 - i)); |
| final Rect localRect = renderEditable.getLocalRectForCaret( |
| TextPosition(offset: controller.text.length), |
| ); |
| |
| expect(localRect.size, initialLocalCaretRect.size); |
| expect(localRect.top, initialLocalCaretRect.top); |
| expect(localRect.left, lessThan(initialLocalCaretRect.left)); |
| } |
| |
| expect(controller.text, isEmpty); |
| }); |
| |
| testWidgets('Caret center space test', (WidgetTester tester) async { |
| EditableText.debugDeterministicCursor = true; |
| addTearDown(() { EditableText.debugDeterministicCursor = false; }); |
| final String text = 'test${' ' * 1000}'; |
| final TextEditingController controller = TextEditingController.fromValue( |
| TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: text.length, affinity: TextAffinity.upstream), |
| ), |
| ); |
| addTearDown(controller.dispose); |
| |
| final Widget widget = EditableText( |
| autofocus: true, |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: const TextStyle(fontSize: 17), |
| textAlign: TextAlign.center, |
| keyboardType: TextInputType.text, |
| cursorColor: cursorColor, |
| cursorWidth: 13.0, |
| cursorHeight: 17.0, |
| maxLines: null, |
| ); |
| await tester.pumpWidget(MaterialApp(home: widget)); |
| |
| final EditableTextState editableTextState = tester.firstState(find.byWidget(widget)); |
| final Rect editableTextRect = tester.getRect(find.byWidget(widget)); |
| final RenderEditable renderEditable = editableTextState.renderEditable; |
| // The trailing whitespaces are not line break opportunities. |
| expect(renderEditable.getLineAtOffset(TextPosition(offset: text.length)).start, 0); |
| |
| // The caretRect shouldn't be outside of the RenderEditable. |
| final Rect caretRect = Rect.fromLTWH( |
| editableTextRect.right - 13.0 - 1.0, |
| editableTextRect.top, |
| 13.0, |
| 17.0, |
| ); |
| expect( |
| renderEditable, |
| paints..rect(color: cursorColor, rect: caretRect), |
| ); |
| }, |
| skip: isBrowser && !isSkiaWeb, // https://github.com/flutter/flutter/issues/56308 |
| ); |
| |
| testWidgets( |
| 'Caret with a cursorHeight smaller than font size is vertically centered on non-Apple platforms', |
| (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/143480. |
| final TextEditingController controller = TextEditingController.fromValue( |
| const TextEditingValue(selection: TextSelection.collapsed(offset: 0)), |
| ); |
| addTearDown(controller.dispose); |
| |
| const double cursorHeight = 12.0; |
| const double cursorWidth = 4.0; |
| const double fontSize = 16.0; |
| |
| final Widget widget = EditableText( |
| autofocus: true, |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: const TextStyle(fontSize: fontSize), |
| keyboardType: TextInputType.text, |
| cursorColor: cursorColor, |
| cursorHeight: cursorHeight, |
| cursorWidth: cursorWidth, |
| ); |
| await tester.pumpWidget(MaterialApp(home: widget)); |
| |
| final EditableTextState editableTextState = tester.firstState(find.byWidget(widget)); |
| final RenderEditable renderEditable = editableTextState.renderEditable; |
| |
| // The caretRect is vertically centered. |
| const Rect caretRect = Rect.fromLTWH( |
| 0.0, |
| (fontSize - cursorHeight) / 2, |
| cursorWidth, |
| cursorHeight, |
| ); |
| expect( |
| renderEditable, |
| paints..rect(color: cursorColor, rect: caretRect), |
| ); |
| }, |
| variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{TargetPlatform.macOS, TargetPlatform.iOS}), |
| skip: isBrowser && !isCanvasKit, // https://github.com/flutter/flutter/issues/56308 |
| ); |
| |
| testWidgets( |
| 'Caret with a cursorHeight bigger than font size is vertically centered on non-Apple platforms', |
| (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/143480. |
| final TextEditingController controller = TextEditingController.fromValue( |
| const TextEditingValue(selection: TextSelection.collapsed(offset: 0)), |
| ); |
| addTearDown(controller.dispose); |
| |
| const double cursorHeight = 24.0; |
| const double cursorWidth = 4.0; |
| const double fontSize = 16.0; |
| |
| final Widget widget = EditableText( |
| autofocus: true, |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: const TextStyle(fontSize: fontSize), |
| keyboardType: TextInputType.text, |
| cursorColor: cursorColor, |
| cursorHeight: cursorHeight, |
| cursorWidth: cursorWidth, |
| ); |
| await tester.pumpWidget(MaterialApp(home: widget)); |
| |
| final EditableTextState editableTextState = tester.firstState(find.byWidget(widget)); |
| final RenderEditable renderEditable = editableTextState.renderEditable; |
| |
| // The caretRect is vertically centered. |
| const Rect caretRect = Rect.fromLTWH( |
| 0.0, |
| (fontSize - cursorHeight) / 2, |
| cursorWidth, |
| cursorHeight, |
| ); |
| expect( |
| renderEditable, |
| paints..rect(color: cursorColor, rect: caretRect), |
| ); |
| }, |
| variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{TargetPlatform.macOS, TargetPlatform.iOS}), |
| skip: isBrowser && !isCanvasKit, // https://github.com/flutter/flutter/issues/56308 |
| ); |
| |
| testWidgets('getLocalRectForCaret reports the real caret Rect', (WidgetTester tester) async { |
| EditableText.debugDeterministicCursor = true; |
| addTearDown(() { EditableText.debugDeterministicCursor = false; }); |
| final String text = 'test${' ' * 50}\n' |
| '2nd line\n' |
| '\n'; |
| final TextEditingController controller = TextEditingController.fromValue(TextEditingValue( |
| text: text, |
| selection: const TextSelection.collapsed(offset: 0), |
| )); |
| addTearDown(controller.dispose); |
| |
| final Widget widget = EditableText( |
| autofocus: true, |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: const TextStyle(fontSize: 20), |
| textAlign: TextAlign.center, |
| keyboardType: TextInputType.text, |
| cursorColor: cursorColor, |
| maxLines: null, |
| ); |
| await tester.pumpWidget(MaterialApp(home: widget)); |
| |
| final EditableTextState editableTextState = tester.firstState(find.byWidget(widget)); |
| final Rect editableTextRect = tester.getRect(find.byWidget(widget)); |
| final RenderEditable renderEditable = editableTextState.renderEditable; |
| |
| final Iterable<TextPosition> positions = List<int> |
| .generate(text.length + 1, (int index) => index) |
| .expand((int i) => <TextPosition>[TextPosition(offset: i, affinity: TextAffinity.upstream), TextPosition(offset: i)]); |
| for (final TextPosition position in positions) { |
| controller.selection = TextSelection.fromPosition(position); |
| await tester.pump(); |
| |
| final Rect localRect = renderEditable.getLocalRectForCaret(position); |
| expect( |
| renderEditable, |
| paints..rect(color: cursorColor, rect: localRect.shift(editableTextRect.topLeft)), |
| ); |
| } |
| }, |
| variant: TargetPlatformVariant.all(), |
| ); |
| |
| testWidgets('Floating cursor showing with local position', (WidgetTester tester) async { |
| EditableText.debugDeterministicCursor = true; |
| final GlobalKey key = GlobalKey(); |
| controller.text = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ\n1234567890'; |
| controller.selection = const TextSelection.collapsed(offset: 0); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: EditableText( |
| key: key, |
| autofocus: true, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: Colors.blue, |
| backgroundCursorColor: Colors.grey, |
| cursorOpacityAnimates: true, |
| maxLines: 2, |
| ), |
| ), |
| ); |
| final EditableTextState state = tester.state(find.byType(EditableText)); |
| |
| state.updateFloatingCursor( |
| RawFloatingCursorPoint( |
| state: FloatingCursorDragState.Start, |
| offset: Offset.zero, |
| startLocation: (Offset.zero, TextPosition(offset: controller.selection.baseOffset, affinity: controller.selection.affinity)) |
| ) |
| ); |
| await tester.pump(); |
| |
| expect(key.currentContext!.findRenderObject(), paints..rrect( |
| rrect: RRect.fromRectAndRadius( |
| const Rect.fromLTWH(-0.5, -3.0, 3, 12), |
| const Radius.circular(1) |
| ) |
| )); |
| |
| state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update, offset: const Offset(51, 0))); |
| await tester.pump(); |
| |
| expect(key.currentContext!.findRenderObject(), paints..rrect( |
| rrect: RRect.fromRectAndRadius( |
| const Rect.fromLTWH(50.5, -3.0, 3, 12), |
| const Radius.circular(1) |
| ) |
| )); |
| |
| state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End)); |
| await tester.pumpAndSettle(); |
| |
| state.updateFloatingCursor( |
| RawFloatingCursorPoint( |
| state: FloatingCursorDragState.Start, |
| offset: Offset.zero, |
| startLocation: (const Offset(800, 10), TextPosition(offset: controller.selection.baseOffset, affinity: controller.selection.affinity)) |
| ) |
| ); |
| await tester.pump(); |
| |
| expect(key.currentContext!.findRenderObject(), paints..rrect( |
| rrect: RRect.fromRectAndRadius( |
| const Rect.fromLTWH(799.5, 4.0, 3, 12), |
| const Radius.circular(1) |
| ) |
| )); |
| |
| state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update, offset: const Offset(100, 10))); |
| await tester.pump(); |
| |
| expect(key.currentContext!.findRenderObject(), paints..rrect( |
| rrect: RRect.fromRectAndRadius( |
| const Rect.fromLTWH(800.5, 14.0, 3, 12), |
| const Radius.circular(1) |
| ) |
| )); |
| |
| state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End)); |
| await tester.pumpAndSettle(); |
| |
| EditableText.debugDeterministicCursor = false; |
| }); |
| } |