| // 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. |
| |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter/src/foundation/constants.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| class _TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { |
| _TestSliverPersistentHeaderDelegate({ |
| required this.minExtent, |
| required this.maxExtent, |
| required this.child, |
| this.vsync = const TestVSync(), |
| }); |
| |
| final Widget child; |
| |
| @override |
| final double maxExtent; |
| |
| @override |
| final double minExtent; |
| |
| @override |
| final TickerProvider? vsync; |
| |
| @override |
| final PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(); |
| |
| @override |
| Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => child; |
| |
| @override |
| bool shouldRebuild(_TestSliverPersistentHeaderDelegate oldDelegate) => true; |
| } |
| |
| void main() { |
| const TextStyle textStyle = TextStyle(); |
| const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00); |
| final FocusNode focusNode = FocusNode(); |
| |
| testWidgets('tapping on a partly visible editable brings it fully on screen', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| final TextEditingController controller = TextEditingController(); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Center( |
| child: SizedBox( |
| height: 300.0, |
| child: ListView( |
| controller: scrollController, |
| children: <Widget>[ |
| EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| const SizedBox( |
| height: 350.0, |
| ), |
| ], |
| ), |
| ), |
| ), |
| )); |
| |
| // Scroll the EditableText half off screen. |
| final RenderBox render = tester.renderObject(find.byType(EditableText)); |
| scrollController.jumpTo(render.size.height / 2); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, render.size.height / 2); |
| |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, 0.0); |
| }); |
| |
| testWidgets('tapping on a partly visible editable brings it fully on screen with scrollInsets', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| final TextEditingController controller = TextEditingController(); |
| final FocusNode focusNode = FocusNode(); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Center( |
| child: SizedBox( |
| height: 300.0, |
| child: ListView( |
| controller: scrollController, |
| children: <Widget>[ |
| const SizedBox( |
| height: 200.0, |
| ), |
| EditableText( |
| backgroundCursorColor: Colors.grey, |
| scrollPadding: const EdgeInsets.all(50.0), |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| const SizedBox( |
| height: 850.0, |
| ), |
| ], |
| ), |
| ), |
| ), |
| )); |
| |
| // Scroll the EditableText half off screen. |
| final RenderBox render = tester.renderObject(find.byType(EditableText)); |
| scrollController.jumpTo(200 + render.size.height / 2); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, 200 + render.size.height / 2); |
| |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.pumpAndSettle(); |
| // Container above the text is 200 in height, the scrollInsets are 50 |
| // Tolerance of 5 units (The actual value was 152.0 in the current tests instead of 150.0) |
| expect(scrollController.offset, lessThan(200.0 - 50.0 + 5.0)); |
| expect(scrollController.offset, greaterThan(200.0 - 50.0 - 5.0)); |
| }); |
| |
| testWidgets('editable comes back on screen when entering text while it is off-screen', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(initialScrollOffset: 100.0); |
| final TextEditingController controller = TextEditingController(); |
| final FocusNode focusNode = FocusNode(); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Center( |
| child: SizedBox( |
| height: 300.0, |
| child: ListView( |
| controller: scrollController, |
| children: <Widget>[ |
| const SizedBox( |
| height: 350.0, |
| ), |
| EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| const SizedBox( |
| height: 350.0, |
| ), |
| ], |
| ), |
| ), |
| ), |
| )); |
| |
| // Focus the EditableText and scroll it off screen. |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.pumpAndSettle(); |
| expect(focusNode.hasFocus, isTrue); |
| scrollController.jumpTo(0.0); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, 0.0); |
| expect(find.byType(EditableText), findsNothing); |
| |
| // Entering text brings it back on screen. |
| tester.testTextInput.enterText('Hello'); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, greaterThan(0.0)); |
| expect(find.byType(EditableText), findsOneWidget); |
| }); |
| |
| testWidgets('entering text does not scroll when scrollPhysics.allowImplicitScrolling = false', (WidgetTester tester) async { |
| // regression test for https://github.com/flutter/flutter/issues/19523 |
| |
| final ScrollController scrollController = ScrollController(initialScrollOffset: 100.0); |
| final TextEditingController controller = TextEditingController(); |
| final FocusNode focusNode = FocusNode(); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Center( |
| child: SizedBox( |
| height: 300.0, |
| child: ListView( |
| physics: const NoImplicitScrollPhysics(), |
| controller: scrollController, |
| children: <Widget>[ |
| const SizedBox( |
| height: 350.0, |
| ), |
| EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| const SizedBox( |
| height: 350.0, |
| ), |
| ], |
| ), |
| ), |
| ), |
| )); |
| |
| // Focus the EditableText and scroll it off screen. |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.pumpAndSettle(); |
| expect(focusNode.hasFocus, isTrue); |
| scrollController.jumpTo(0.0); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, 0.0); |
| expect(find.byType(EditableText), findsNothing); |
| |
| // Entering text brings it not back on screen. |
| tester.testTextInput.enterText('Hello'); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, 0.0); |
| expect(find.byType(EditableText), findsNothing); |
| }); |
| |
| testWidgets('entering text does not scroll a surrounding PageView', (WidgetTester tester) async { |
| // regression test for https://github.com/flutter/flutter/issues/19523 |
| |
| final TextEditingController textController = TextEditingController(); |
| final PageController pageController = PageController(initialPage: 1); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: MediaQuery( |
| data: const MediaQueryData(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Material( |
| child: PageView( |
| controller: pageController, |
| children: <Widget>[ |
| Container( |
| color: Colors.red, |
| ), |
| ColoredBox( |
| color: Colors.green, |
| child: TextField( |
| controller: textController, |
| ), |
| ), |
| Container( |
| color: Colors.red, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.pumpAndSettle(); |
| expect(textController.text, ''); |
| tester.testTextInput.enterText('H'); |
| final int frames = await tester.pumpAndSettle(); |
| |
| // The text input should not trigger any animations, which would indicate |
| // that the surrounding PageView is incorrectly scrolling back-and-forth. |
| expect(frames, 1); |
| |
| expect(textController.text, 'H'); |
| }); |
| |
| testWidgets('focused multi-line editable scrolls caret back into view when typing', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| final TextEditingController controller = TextEditingController(); |
| final FocusNode focusNode = FocusNode(); |
| controller.text = "Start${'\n' * 39}End"; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Center( |
| child: SizedBox( |
| height: 300.0, |
| child: ListView( |
| controller: scrollController, |
| children: <Widget>[ |
| EditableText( |
| backgroundCursorColor: Colors.grey, |
| maxLines: null, // multiline |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ], |
| ), |
| ), |
| ), |
| )); |
| |
| // Bring keyboard up and verify that end of EditableText is not on screen. |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.pumpAndSettle(); |
| scrollController.jumpTo(0.0); |
| await tester.pumpAndSettle(); |
| final RenderBox render = tester.renderObject(find.byType(EditableText)); |
| expect(render.size.height, greaterThan(500.0)); |
| expect(scrollController.offset, 0.0); |
| |
| // Enter text at end, which is off-screen. |
| final String textToEnter = '${controller.text} HELLO'; |
| tester.testTextInput.updateEditingValue(TextEditingValue( |
| text: textToEnter, |
| selection: TextSelection.collapsed(offset: textToEnter.length), |
| )); |
| await tester.pumpAndSettle(); |
| |
| // Caret scrolls into view. |
| expect(find.byType(EditableText), findsOneWidget); |
| expect(render.size.height, greaterThan(500.0)); |
| expect(scrollController.offset, greaterThan(0.0)); |
| }); |
| |
| testWidgets('focused multi-line editable does not scroll to old position when non-collapsed selection set', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| final TextEditingController controller = TextEditingController(); |
| final FocusNode focusNode = FocusNode(); |
| final String text = "Start${'\n' * 39}End"; |
| controller.value = TextEditingValue(text: text, selection: TextSelection.collapsed(offset: text.length - 3)); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Center( |
| child: SizedBox( |
| height: 300.0, |
| child: ListView( |
| controller: scrollController, |
| children: <Widget>[ |
| EditableText( |
| backgroundCursorColor: Colors.grey, |
| maxLines: null, // multiline |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ], |
| ), |
| ), |
| ), |
| )); |
| |
| // Bring keyboard up and verify that end of EditableText is not on screen. |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.pumpAndSettle(); |
| |
| scrollController.jumpTo(0.0); |
| await tester.pumpAndSettle(); |
| final RenderBox render = tester.renderObject(find.byType(EditableText)); |
| expect(render.size.height, greaterThan(500.0)); |
| expect(scrollController.offset, 0.0); |
| |
| // Change selection to non-collapased so that cursor isn't shown |
| // and the location requires a bit of scroll. |
| tester.testTextInput.updateEditingValue(TextEditingValue( |
| text: text, |
| selection: const TextSelection(baseOffset: 26, extentOffset: 27), |
| )); |
| await tester.pumpAndSettle(); |
| |
| // Selection extent scrolls into view. |
| expect(find.byType(EditableText), findsOneWidget); |
| expect(render.size.height, greaterThan(500.0)); |
| expect(scrollController.offset, 28.0); |
| }); |
| |
| testWidgets('scrolls into view with scrollInserts after the keyboard pops up', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| final TextEditingController controller = TextEditingController(); |
| final FocusNode focusNode = FocusNode(); |
| |
| const Key container = Key('container'); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Align( |
| alignment: Alignment.bottomCenter, |
| child: SizedBox( |
| height: 300.0, |
| child: ListView( |
| controller: scrollController, |
| children: <Widget>[ |
| const SizedBox( |
| key: container, |
| height: 200.0, |
| ), |
| EditableText( |
| backgroundCursorColor: Colors.grey, |
| scrollPadding: const EdgeInsets.only(bottom: 300.0), |
| controller: controller, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| const SizedBox( |
| height: 400.0, |
| ), |
| ], |
| ), |
| ), |
| ), |
| )); |
| |
| expect(scrollController.offset, 0.0); |
| |
| await tester.showKeyboard(find.byType(EditableText)); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, greaterThan(0.0)); |
| expect(find.byKey(container), findsNothing); |
| }); |
| |
| testWidgets( |
| 'A pinned persistent header should not scroll when its descendant EditableText gains focus', |
| (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/25507. |
| ScrollController controller; |
| final TextEditingController textEditingController = TextEditingController(); |
| final FocusNode focusNode = FocusNode(); |
| |
| const Key headerKey = Key('header'); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Center( |
| child: SizedBox( |
| height: 600.0, |
| width: 600.0, |
| child: CustomScrollView( |
| controller: controller = ScrollController(), |
| slivers: List<Widget>.generate(50, (int i) { |
| return i == 10 |
| ? SliverPersistentHeader( |
| pinned: true, |
| delegate: _TestSliverPersistentHeaderDelegate( |
| minExtent: 50, |
| maxExtent: 50, |
| child: Container( |
| alignment: Alignment.topCenter, |
| child: EditableText( |
| key: headerKey, |
| backgroundCursorColor: Colors.grey, |
| controller: textEditingController, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ) |
| : SliverToBoxAdapter( |
| child: SizedBox( |
| height: 100.0, |
| child: Text('Tile $i'), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // The persistent header should now be pinned at the top. |
| controller.jumpTo(100.0 * 15); |
| await tester.pumpAndSettle(); |
| expect(controller.offset, 100.0 * 15); |
| |
| focusNode.requestFocus(); |
| await tester.pumpAndSettle(); |
| // The scroll offset should remain the same. |
| expect(controller.offset, 100.0 * 15); |
| }, |
| ); |
| |
| testWidgets( |
| 'A pinned persistent header should not scroll when its descendant EditableText gains focus (no animation)', |
| (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/25507. |
| ScrollController controller; |
| final TextEditingController textEditingController = TextEditingController(); |
| final FocusNode focusNode = FocusNode(); |
| |
| const Key headerKey = Key('header'); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Center( |
| child: SizedBox( |
| height: 600.0, |
| width: 600.0, |
| child: CustomScrollView( |
| controller: controller = ScrollController(), |
| slivers: List<Widget>.generate(50, (int i) { |
| return i == 10 |
| ? SliverPersistentHeader( |
| pinned: true, |
| delegate: _TestSliverPersistentHeaderDelegate( |
| minExtent: 50, |
| maxExtent: 50, |
| vsync: null, |
| child: Container( |
| alignment: Alignment.topCenter, |
| child: EditableText( |
| key: headerKey, |
| backgroundCursorColor: Colors.grey, |
| controller: textEditingController, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ) |
| : SliverToBoxAdapter( |
| child: SizedBox( |
| height: 100.0, |
| child: Text('Tile $i'), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // The persistent header should now be pinned at the top. |
| controller.jumpTo(100.0 * 15); |
| await tester.pumpAndSettle(); |
| expect(controller.offset, 100.0 * 15); |
| |
| focusNode.requestFocus(); |
| await tester.pumpAndSettle(); |
| // The scroll offset should remain the same. |
| expect(controller.offset, 100.0 * 15); |
| }, |
| ); |
| |
| void testShowCaretOnScreen({ required bool readOnly }) { |
| group('EditableText._showCaretOnScreen, readOnly=$readOnly', () { |
| final TextEditingController textEditingController = TextEditingController(); |
| final TextInputFormatter rejectEverythingFormatter = TextInputFormatter.withFunction((TextEditingValue old, TextEditingValue value) => old); |
| |
| bool isCaretOnScreen(WidgetTester tester) { |
| final EditableTextState state = tester.state<EditableTextState>( |
| find.byType(EditableText, skipOffstage: false), |
| ); |
| final RenderEditable renderEditable = state.renderEditable; |
| final Rect localRect = renderEditable.getLocalRectForCaret(state.textEditingValue.selection.base); |
| final Offset caretOrigin = renderEditable.localToGlobal(localRect.topLeft); |
| final Rect caretRect = caretOrigin & localRect.size; |
| return const Rect.fromLTWH(0, 0, 800, 600).intersect(caretRect) == caretRect; |
| } |
| |
| Widget buildEditableText({ |
| required bool rejectUserInputs, |
| ScrollController? scrollController, |
| ScrollController? editableScrollController, |
| }) { |
| return MaterialApp( |
| home: Scaffold( |
| body: ListView( |
| controller: scrollController, |
| cacheExtent: 1000, |
| children: <Widget>[ |
| // The text field is not fully visible. |
| const SizedBox(height: 599), |
| EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: textEditingController, |
| scrollController: editableScrollController, |
| inputFormatters: <TextInputFormatter>[if (rejectUserInputs) rejectEverythingFormatter], |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| readOnly: readOnly, |
| ), |
| ], |
| ), |
| ), |
| ); |
| } |
| |
| testWidgets('focus-triggered showCaretOnScreen', (WidgetTester tester) async { |
| textEditingController.text = 'a' * 100; |
| textEditingController.selection = const TextSelection.collapsed(offset: 100); |
| final ScrollController scrollController = ScrollController(); |
| final ScrollController editableScrollController = ScrollController(); |
| |
| await tester.pumpWidget( |
| buildEditableText( |
| rejectUserInputs: false, |
| scrollController: scrollController, |
| editableScrollController: editableScrollController, |
| ), |
| ); |
| |
| focusNode.requestFocus(); |
| await tester.pumpAndSettle(); |
| |
| if (kIsWeb) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pump(); |
| } |
| |
| // On web, the entire field is selected, and only part of that selection |
| // is visible on the screen. |
| expect(isCaretOnScreen(tester), !readOnly && !kIsWeb); |
| expect(scrollController.offset, readOnly ? 0.0 : greaterThan(0.0)); |
| expect(editableScrollController.offset, readOnly ? 0.0 : greaterThan(0.0)); |
| }); |
| |
| testWidgets('selection-triggered showCaretOnScreen: virtual keyboard', (WidgetTester tester) async { |
| textEditingController.text = 'a' * 100; |
| textEditingController.selection = const TextSelection.collapsed(offset: 80); |
| final ScrollController scrollController = ScrollController(); |
| final ScrollController editableScrollController = ScrollController(); |
| |
| await tester.pumpWidget( |
| buildEditableText( |
| rejectUserInputs: false, |
| scrollController: scrollController, |
| editableScrollController: editableScrollController, |
| ), |
| ); |
| |
| focusNode.requestFocus(); |
| await tester.pumpAndSettle(); |
| |
| // Ensure the caret is not fully visible and the text field is focused. |
| scrollController.jumpTo(0); |
| editableScrollController.jumpTo(0); |
| await tester.pumpAndSettle(); |
| expect(isCaretOnScreen(tester), isFalse); |
| |
| final EditableTextState state = tester.state<EditableTextState>( |
| find.byType(EditableText, skipOffstage: false), |
| ); |
| |
| // Change the selection. Show caret on screen when readyOnly is true, |
| // as a read-only text field rejects everything from the software |
| // keyboard (except for web). |
| state.updateEditingValue(state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 90))); |
| await tester.pumpAndSettle(); |
| expect(isCaretOnScreen(tester), !readOnly || kIsWeb); |
| expect(scrollController.offset, readOnly && !kIsWeb ? 0.0 : greaterThan(0.0)); |
| expect(editableScrollController.offset, readOnly && !kIsWeb ? 0.0 : greaterThan(0.0)); |
| |
| // Reject user input. |
| await tester.pumpWidget( |
| buildEditableText( |
| rejectUserInputs: true, |
| scrollController: scrollController, |
| editableScrollController: editableScrollController, |
| ), |
| ); |
| |
| // Ensure the caret is not fully visible and the text field is focused. |
| scrollController.jumpTo(0); |
| editableScrollController.jumpTo(0); |
| await tester.pumpAndSettle(); |
| expect(isCaretOnScreen(tester), isFalse); |
| |
| state.updateEditingValue(state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 100))); |
| await tester.pumpAndSettle(); |
| expect(isCaretOnScreen(tester), !readOnly || kIsWeb); |
| expect(scrollController.offset, readOnly && !kIsWeb ? 0.0 : greaterThan(0.0)); |
| expect(editableScrollController.offset, readOnly && !kIsWeb ? 0.0 : greaterThan(0.0)); |
| }); |
| |
| testWidgets('selection-triggered showCaretOnScreen: text selection delegate', (WidgetTester tester) async { |
| textEditingController.text = 'a' * 100; |
| textEditingController.selection = const TextSelection.collapsed(offset: 80); |
| final ScrollController scrollController = ScrollController(); |
| final ScrollController editableScrollController = ScrollController(); |
| |
| await tester.pumpWidget( |
| buildEditableText( |
| rejectUserInputs: false, |
| scrollController: scrollController, |
| editableScrollController: editableScrollController, |
| ), |
| ); |
| |
| focusNode.requestFocus(); |
| await tester.pumpAndSettle(); |
| |
| // Ensure the caret is not fully visible and the text field is focused. |
| scrollController.jumpTo(0); |
| editableScrollController.jumpTo(0); |
| await tester.pumpAndSettle(); |
| expect(isCaretOnScreen(tester), isFalse); |
| |
| final EditableTextState state = tester.state<EditableTextState>( |
| find.byType(EditableText, skipOffstage: false), |
| ); |
| |
| // Change the selection. Show caret on screen even when readyOnly is |
| // false. |
| state.userUpdateTextEditingValue( |
| state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 90)), |
| null, |
| ); |
| await tester.pumpAndSettle(); |
| expect(isCaretOnScreen(tester), isTrue); |
| expect(scrollController.offset, greaterThan(0.0)); |
| expect(editableScrollController.offset, greaterThan(0.0)); |
| |
| // Rejects user input. |
| await tester.pumpWidget( |
| buildEditableText( |
| rejectUserInputs: true, |
| scrollController: scrollController, |
| editableScrollController: editableScrollController, |
| ), |
| ); |
| |
| // Ensure the caret is not fully visible and the text field is focused. |
| scrollController.jumpTo(0); |
| editableScrollController.jumpTo(0); |
| await tester.pumpAndSettle(); |
| expect(isCaretOnScreen(tester), isFalse); |
| |
| state.userUpdateTextEditingValue( |
| state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 100)), |
| null, |
| ); |
| await tester.pumpAndSettle(); |
| expect(isCaretOnScreen(tester), isTrue); |
| expect(scrollController.offset, greaterThan(0.0)); |
| expect(editableScrollController.offset, greaterThan(0.0)); |
| }); |
| |
| // Regression text for https://github.com/flutter/flutter/pull/74722. |
| testWidgets('does NOT randomly trigger when cursor blinks', (WidgetTester tester) async { |
| textEditingController.text = 'a' * 100; |
| textEditingController.selection = const TextSelection.collapsed(offset: 0); |
| final ScrollController editableScrollController = ScrollController(); |
| final bool deterministicCursor = EditableText.debugDeterministicCursor; |
| EditableText.debugDeterministicCursor = false; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: EditableText( |
| backgroundCursorColor: Colors.grey, |
| controller: textEditingController, |
| scrollController: editableScrollController, |
| focusNode: focusNode, |
| style: textStyle, |
| cursorColor: cursorColor, |
| ), |
| ), |
| ), |
| ); |
| |
| final EditableTextState state = tester.state<EditableTextState>( |
| find.byType(EditableText, skipOffstage: false), |
| ); |
| |
| // Ensure the text was initially visible. |
| expect(isCaretOnScreen(tester), true); |
| expect(editableScrollController.offset, 0.0); |
| |
| // Change the text but keep the cursor location. |
| state.updateEditingValue(textEditingController.value.copyWith( |
| text: 'a' * 101, |
| )); |
| |
| await tester.pumpAndSettle(); |
| |
| // The caret should stay where it was, since the selection didn't change. |
| expect(isCaretOnScreen(tester), true); |
| expect(editableScrollController.offset, 0.0); |
| |
| // Now move to hide the cursor. |
| editableScrollController.jumpTo(100.0); |
| |
| // Does not trigger showCaretOnScreen. |
| await tester.pump(); |
| await tester.pumpAndSettle(); |
| expect(editableScrollController.offset, 100.0); |
| expect(isCaretOnScreen(tester), isFalse); |
| |
| EditableText.debugDeterministicCursor = deterministicCursor; |
| }); |
| }); |
| } |
| |
| testShowCaretOnScreen(readOnly: true); |
| testShowCaretOnScreen(readOnly: false); |
| } |
| |
| class NoImplicitScrollPhysics extends AlwaysScrollableScrollPhysics { |
| const NoImplicitScrollPhysics({ super.parent }); |
| |
| @override |
| bool get allowImplicitScrolling => false; |
| |
| @override |
| NoImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) { |
| return NoImplicitScrollPhysics(parent: buildParent(ancestor)); |
| } |
| } |