19234 lines
688 KiB
Dart
19234 lines
688 KiB
Dart
// 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.
|
||
|
||
// reduced-test-set:
|
||
// This file is run as part of a reduced test set in CI on Mac and Windows
|
||
// machines.
|
||
// no-shuffle:
|
||
// TODO(122950): Remove this tag once this test's state leaks/test
|
||
// dependencies have been fixed.
|
||
// https://github.com/flutter/flutter/issues/122950
|
||
// Fails with "flutter test --test-randomize-ordering-seed=20230318"
|
||
@Tags(<String>['reduced-test-set', 'no-shuffle'])
|
||
library;
|
||
|
||
import 'dart:math' as math;
|
||
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, SemanticsInputType;
|
||
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/gestures.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/rendering.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
|
||
import '../widgets/clipboard_utils.dart';
|
||
import '../widgets/editable_text_utils.dart';
|
||
import '../widgets/feedback_tester.dart';
|
||
import '../widgets/live_text_utils.dart';
|
||
import '../widgets/process_text_utils.dart';
|
||
import '../widgets/semantics_tester.dart';
|
||
import '../widgets/text_selection_toolbar_utils.dart';
|
||
|
||
void main() {
|
||
TestWidgetsFlutterBinding.ensureInitialized();
|
||
final mockClipboard = MockClipboard();
|
||
|
||
const kThreeLines =
|
||
'First line of text is\n'
|
||
'Second line goes until\n'
|
||
'Third line of stuff';
|
||
const kMoreThanFourLines =
|
||
'$kThreeLines\n'
|
||
"Fourth line won't display and ends at";
|
||
// Gap between caret and edge of input, defined in editable.dart.
|
||
const kCaretGap = 1;
|
||
|
||
setUp(() async {
|
||
debugResetSemanticsIdCounter();
|
||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.platform,
|
||
mockClipboard.handleMethodCall,
|
||
);
|
||
// Fill the clipboard so that the Paste option is available in the text
|
||
// selection menu.
|
||
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||
});
|
||
|
||
tearDown(() {
|
||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.platform,
|
||
null,
|
||
);
|
||
});
|
||
|
||
final Key textFieldKey = UniqueKey();
|
||
Widget textFieldBuilder({int? maxLines = 1, int? minLines}) {
|
||
return boilerplate(
|
||
child: TextField(
|
||
key: textFieldKey,
|
||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||
maxLines: maxLines,
|
||
minLines: minLines,
|
||
decoration: const InputDecoration(hintText: 'Placeholder'),
|
||
),
|
||
);
|
||
}
|
||
|
||
testWidgets('Live Text button shows and hides correctly when LiveTextStatus changes', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final liveTextInputTester = LiveTextInputTester();
|
||
addTearDown(liveTextInputTester.dispose);
|
||
final TextEditingController controller = _textEditingController();
|
||
const Key key = ValueKey<String>('TextField');
|
||
final FocusNode focusNode = _focusNode();
|
||
final Widget app = MaterialApp(
|
||
theme: ThemeData(platform: TargetPlatform.iOS),
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: TextField(key: key, controller: controller, focusNode: focusNode),
|
||
),
|
||
),
|
||
);
|
||
|
||
liveTextInputTester.mockLiveTextInputEnabled = true;
|
||
await tester.pumpWidget(app);
|
||
focusNode.requestFocus();
|
||
await tester.pumpAndSettle();
|
||
|
||
final Finder textFinder = find.byType(EditableText);
|
||
await tester.longPress(textFinder);
|
||
await tester.pumpAndSettle();
|
||
expect(findLiveTextButton(), kIsWeb ? findsNothing : findsOneWidget);
|
||
|
||
liveTextInputTester.mockLiveTextInputEnabled = false;
|
||
await tester.longPress(textFinder);
|
||
await tester.pumpAndSettle();
|
||
expect(findLiveTextButton(), findsNothing);
|
||
});
|
||
|
||
testWidgets(
|
||
'text field selection toolbar should hide when the user starts typing',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: SizedBox(
|
||
width: 100,
|
||
height: 100,
|
||
child: TextField(decoration: InputDecoration(hintText: 'Placeholder')),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
|
||
const testValue = 'A B C';
|
||
tester.testTextInput.updateEditingValue(const TextEditingValue(text: testValue));
|
||
await tester.pump();
|
||
|
||
// The selectWordsInRange with SelectionChangedCause.tap seems to be needed to show the toolbar.
|
||
// (This is true even if we provide selection parameter to the TextEditingValue above.)
|
||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
state.renderEditable.selectWordsInRange(from: Offset.zero, cause: SelectionChangedCause.tap);
|
||
|
||
expect(state.showToolbar(), true);
|
||
|
||
// This is needed for the AnimatedOpacity to turn from 0 to 1 so the toolbar is visible.
|
||
await tester.pumpAndSettle();
|
||
|
||
// Sanity check that the toolbar widget exists.
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
|
||
const newValue = 'A B C D';
|
||
tester.testTextInput.updateEditingValue(const TextEditingValue(text: newValue));
|
||
await tester.pump();
|
||
|
||
expect(state.selectionOverlay!.toolbarIsVisible, isFalse);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets('Composing change does not hide selection handle caret', (WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/108673
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller)));
|
||
|
||
const testValue = 'I Love Flutter!';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
expect(controller.value.text, testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Handle not shown.
|
||
expect(controller.selection.isCollapsed, true);
|
||
final Finder fadeFinder = find.byType(FadeTransition);
|
||
FadeTransition handle = tester.widget(fadeFinder.at(0));
|
||
expect(handle.opacity.value, equals(0.0));
|
||
|
||
// Tap on the text field to show the handle.
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(fadeFinder, findsNWidgets(1));
|
||
handle = tester.widget(fadeFinder.at(0));
|
||
expect(handle.opacity.value, equals(1.0));
|
||
final RenderObject handleRenderObjectBegin = tester.renderObject(fadeFinder.at(0));
|
||
|
||
expect(
|
||
controller.value,
|
||
const TextEditingValue(
|
||
text: 'I Love Flutter!',
|
||
selection: TextSelection.collapsed(offset: 15, affinity: TextAffinity.upstream),
|
||
),
|
||
);
|
||
|
||
// Simulate text composing change.
|
||
tester.testTextInput.updateEditingValue(
|
||
controller.value.copyWith(composing: const TextRange(start: 7, end: 15)),
|
||
);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
expect(
|
||
controller.value,
|
||
const TextEditingValue(
|
||
text: 'I Love Flutter!',
|
||
selection: TextSelection.collapsed(offset: 15, affinity: TextAffinity.upstream),
|
||
composing: TextRange(start: 7, end: 15),
|
||
),
|
||
);
|
||
|
||
// Handle still shown.
|
||
expect(controller.selection.isCollapsed, true);
|
||
handle = tester.widget(fadeFinder.at(0));
|
||
expect(handle.opacity.value, equals(1.0));
|
||
|
||
// Simulate text composing and affinity change.
|
||
tester.testTextInput.updateEditingValue(
|
||
controller.value.copyWith(
|
||
selection: controller.value.selection.copyWith(affinity: TextAffinity.downstream),
|
||
composing: const TextRange(start: 8, end: 15),
|
||
),
|
||
);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
expect(
|
||
controller.value,
|
||
const TextEditingValue(
|
||
text: 'I Love Flutter!',
|
||
selection: TextSelection.collapsed(offset: 15, affinity: TextAffinity.upstream),
|
||
composing: TextRange(start: 8, end: 15),
|
||
),
|
||
);
|
||
|
||
// Handle still shown.
|
||
expect(controller.selection.isCollapsed, true);
|
||
handle = tester.widget(fadeFinder.at(0));
|
||
expect(handle.opacity.value, equals(1.0));
|
||
|
||
final RenderObject handleRenderObjectEnd = tester.renderObject(fadeFinder.at(0));
|
||
// The RenderObject sub-tree should not be unmounted.
|
||
expect(identical(handleRenderObjectBegin, handleRenderObjectEnd), true);
|
||
});
|
||
|
||
testWidgets(
|
||
'can use the desktop cut/copy/paste buttons on Mac',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'blah1 blah2');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Initially, the menu is not shown and there is no selection.
|
||
expectNoCupertinoToolbar();
|
||
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
|
||
|
||
final Offset midBlah1 = textOffsetToPosition(tester, 2);
|
||
|
||
// Right clicking shows the menu.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
midBlah1,
|
||
kind: PointerDeviceKind.mouse,
|
||
buttons: kSecondaryMouseButton,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
|
||
expect(find.text('Cut'), findsOneWidget);
|
||
expect(find.text('Copy'), findsOneWidget);
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
|
||
// Copy the first word.
|
||
await tester.tap(find.text('Copy'));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.text, 'blah1 blah2');
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
|
||
expectNoCupertinoToolbar();
|
||
|
||
// Paste it at the end.
|
||
await gesture.down(textOffsetToPosition(tester, controller.text.length));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream),
|
||
);
|
||
expect(find.text('Cut'), findsNothing);
|
||
expect(find.text('Copy'), findsNothing);
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
await tester.tap(find.text('Paste'));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.text, 'blah1 blah2blah1');
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 16));
|
||
|
||
// Cut the first word.
|
||
await gesture.down(midBlah1);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('Cut'), findsOneWidget);
|
||
expect(find.text('Copy'), findsOneWidget);
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
|
||
await tester.tap(find.text('Cut'));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.text, ' blah2blah1');
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0));
|
||
expectNoCupertinoToolbar();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.macOS}),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'can use the desktop cut/copy/paste buttons on Windows and Linux',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'blah1 blah2');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Initially, the menu is not shown and there is no selection.
|
||
expectNoCupertinoToolbar();
|
||
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
|
||
|
||
final Offset midBlah1 = textOffsetToPosition(tester, 2);
|
||
|
||
// Right clicking shows the menu.
|
||
TestGesture gesture = await tester.startGesture(
|
||
midBlah1,
|
||
kind: PointerDeviceKind.mouse,
|
||
buttons: kSecondaryMouseButton,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await gesture.removePointer();
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 2));
|
||
expect(find.text('Cut'), findsNothing);
|
||
expect(find.text('Copy'), findsNothing);
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
expect(find.text('Select all'), findsOneWidget);
|
||
|
||
// Double tap to select the first word, then right click to show the menu.
|
||
final Offset startBlah1 = textOffsetToPosition(tester, 0);
|
||
gesture = await tester.startGesture(startBlah1, kind: PointerDeviceKind.mouse);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 100));
|
||
await gesture.down(startBlah1);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await gesture.removePointer();
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
|
||
expect(find.text('Cut'), findsNothing);
|
||
expect(find.text('Copy'), findsNothing);
|
||
expect(find.text('Paste'), findsNothing);
|
||
expect(find.text('Select all'), findsNothing);
|
||
gesture = await tester.startGesture(
|
||
midBlah1,
|
||
kind: PointerDeviceKind.mouse,
|
||
buttons: kSecondaryMouseButton,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await gesture.removePointer();
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
|
||
expect(find.text('Cut'), findsOneWidget);
|
||
expect(find.text('Copy'), findsOneWidget);
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
|
||
// Copy the first word.
|
||
await tester.tap(find.text('Copy'));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.text, 'blah1 blah2');
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
|
||
expectNoCupertinoToolbar();
|
||
|
||
// Paste it at the end.
|
||
gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, controller.text.length),
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await gesture.removePointer();
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream),
|
||
);
|
||
gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, controller.text.length),
|
||
kind: PointerDeviceKind.mouse,
|
||
buttons: kSecondaryMouseButton,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await gesture.removePointer();
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream),
|
||
);
|
||
expect(find.text('Cut'), findsNothing);
|
||
expect(find.text('Copy'), findsNothing);
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
await tester.tap(find.text('Paste'));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.text, 'blah1 blah2blah1');
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 16));
|
||
|
||
// Cut the first word.
|
||
gesture = await tester.startGesture(midBlah1, kind: PointerDeviceKind.mouse);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 100));
|
||
await gesture.down(startBlah1);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await gesture.removePointer();
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
|
||
expect(find.text('Cut'), findsNothing);
|
||
expect(find.text('Copy'), findsNothing);
|
||
expect(find.text('Paste'), findsNothing);
|
||
expect(find.text('Select all'), findsNothing);
|
||
gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, controller.text.length),
|
||
kind: PointerDeviceKind.mouse,
|
||
buttons: kSecondaryMouseButton,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await gesture.removePointer();
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
|
||
expect(find.text('Cut'), findsOneWidget);
|
||
expect(find.text('Copy'), findsOneWidget);
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
await tester.tap(find.text('Cut'));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.text, ' blah2blah1');
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0));
|
||
expectNoCupertinoToolbar();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.linux,
|
||
TargetPlatform.windows,
|
||
}),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Look Up shows up on iOS only',
|
||
(WidgetTester tester) async {
|
||
String? lastLookUp;
|
||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.platform,
|
||
(MethodCall methodCall) async {
|
||
if (methodCall.method == 'LookUp.invoke') {
|
||
expect(methodCall.arguments, isA<String>());
|
||
lastLookUp = methodCall.arguments as String;
|
||
}
|
||
return null;
|
||
},
|
||
);
|
||
|
||
final TextEditingController controller = _textEditingController(text: 'Test ');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
final isTargetPlatformiOS = defaultTargetPlatform == TargetPlatform.iOS;
|
||
|
||
// Long press to put the cursor after the "s".
|
||
const index = 3;
|
||
await tester.longPressAt(textOffsetToPosition(tester, index));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Double tap on the same location to select the word around the cursor.
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4));
|
||
expect(find.text('Look Up'), isTargetPlatformiOS ? findsOneWidget : findsNothing);
|
||
|
||
if (isTargetPlatformiOS) {
|
||
await tester.tap(find.text('Look Up'));
|
||
expect(lastLookUp, 'Test');
|
||
}
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.android,
|
||
}),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Search Web shows up on iOS only',
|
||
(WidgetTester tester) async {
|
||
String? lastSearch;
|
||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.platform,
|
||
(MethodCall methodCall) async {
|
||
if (methodCall.method == 'SearchWeb.invoke') {
|
||
expect(methodCall.arguments, isA<String>());
|
||
lastSearch = methodCall.arguments as String;
|
||
}
|
||
return null;
|
||
},
|
||
);
|
||
|
||
final TextEditingController controller = _textEditingController(text: 'Test ');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
final isTargetPlatformiOS = defaultTargetPlatform == TargetPlatform.iOS;
|
||
|
||
// Long press to put the cursor after the "s".
|
||
const index = 3;
|
||
await tester.longPressAt(textOffsetToPosition(tester, index));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Double tap on the same location to select the word around the cursor.
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4));
|
||
expect(find.text('Search Web'), isTargetPlatformiOS ? findsOneWidget : findsNothing);
|
||
|
||
if (isTargetPlatformiOS) {
|
||
await tester.tap(find.text('Search Web'));
|
||
expect(lastSearch, 'Test');
|
||
}
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.android,
|
||
}),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Share shows up on iOS and Android',
|
||
(WidgetTester tester) async {
|
||
String? lastShare;
|
||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.platform,
|
||
(MethodCall methodCall) async {
|
||
if (methodCall.method == 'Share.invoke') {
|
||
expect(methodCall.arguments, isA<String>());
|
||
lastShare = methodCall.arguments as String;
|
||
}
|
||
return null;
|
||
},
|
||
);
|
||
|
||
final TextEditingController controller = _textEditingController(text: 'Test ');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
final isTargetPlatformiOS = defaultTargetPlatform == TargetPlatform.iOS;
|
||
|
||
// Long press to put the cursor after the "s".
|
||
const index = 3;
|
||
await tester.longPressAt(textOffsetToPosition(tester, index));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Double tap on the same location to select the word around the cursor.
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4));
|
||
|
||
if (isTargetPlatformiOS) {
|
||
expect(find.text('Share...'), findsOneWidget);
|
||
await tester.tap(find.text('Share...'));
|
||
} else {
|
||
expect(find.text('Share'), findsOneWidget);
|
||
await tester.tap(find.text('Share'));
|
||
}
|
||
expect(lastShare, 'Test');
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.android,
|
||
}),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets('uses DefaultSelectionStyle for selection and cursor colors if provided', (
|
||
WidgetTester tester,
|
||
) async {
|
||
const Color selectionColor = Colors.orange;
|
||
const Color cursorColor = Colors.red;
|
||
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(
|
||
child: DefaultSelectionStyle(
|
||
selectionColor: selectionColor,
|
||
cursorColor: cursorColor,
|
||
child: TextField(autofocus: true),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
expect(state.widget.selectionColor, selectionColor);
|
||
expect(state.widget.cursorColor, cursorColor);
|
||
});
|
||
|
||
testWidgets(
|
||
'Use error cursor color when an InputDecoration with an errorText or error widget is provided',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(
|
||
child: TextField(
|
||
autofocus: true,
|
||
decoration: InputDecoration(
|
||
error: Text('error'),
|
||
errorStyle: TextStyle(color: Colors.teal),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
expect(state.widget.cursorColor, Colors.teal);
|
||
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(
|
||
child: TextField(
|
||
autofocus: true,
|
||
decoration: InputDecoration(
|
||
errorText: 'error',
|
||
errorStyle: TextStyle(color: Colors.teal),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
expect(state.widget.cursorColor, Colors.teal);
|
||
},
|
||
);
|
||
|
||
testWidgets('suffix has correct semantics', (WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/169499.
|
||
final suffix = UniqueKey();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Scaffold(
|
||
body: Center(
|
||
child: TextField(
|
||
autofocus: true,
|
||
decoration: InputDecoration(
|
||
suffix: Semantics(
|
||
key: suffix,
|
||
identifier: 'myId',
|
||
container: true,
|
||
child: const SizedBox(width: 50, height: 50, child: Text('suffix')),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle(); // Wait for autofocus and suffix animation.
|
||
expect(find.text('suffix'), findsOneWidget);
|
||
expect(
|
||
tester.semantics.find(find.byKey(suffix)),
|
||
isSemantics(label: 'suffix', identifier: 'myId', rect: const Rect.fromLTWH(0, 0, 50, 50)),
|
||
);
|
||
});
|
||
|
||
testWidgets('sets cursorOpacityAnimates on EditableText correctly', (WidgetTester tester) async {
|
||
// True
|
||
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(child: TextField(autofocus: true, cursorOpacityAnimates: true)),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
EditableText editableText = tester.widget(find.byType(EditableText));
|
||
expect(editableText.cursorOpacityAnimates, true);
|
||
|
||
// False
|
||
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(child: TextField(autofocus: true, cursorOpacityAnimates: false)),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
editableText = tester.widget(find.byType(EditableText));
|
||
expect(editableText.cursorOpacityAnimates, false);
|
||
});
|
||
|
||
testWidgets(
|
||
'Activates the text field when receives semantics focus on desktops',
|
||
(WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
||
final FocusNode focusNode = _focusNode();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(focusNode: focusNode)),
|
||
),
|
||
);
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: 1,
|
||
textDirection: TextDirection.ltr,
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: 2,
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: 3,
|
||
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: 4,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
SemanticsAction.focus,
|
||
SemanticsAction.didGainAccessibilityFocus,
|
||
SemanticsAction.didLoseAccessibilityFocus,
|
||
],
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 0,
|
||
textDirection: TextDirection.ltr,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreRect: true,
|
||
ignoreTransform: true,
|
||
),
|
||
);
|
||
|
||
expect(focusNode.hasFocus, isFalse);
|
||
semanticsOwner.performAction(4, SemanticsAction.didGainAccessibilityFocus);
|
||
await tester.pumpAndSettle();
|
||
expect(focusNode.hasFocus, isTrue);
|
||
|
||
semanticsOwner.performAction(4, SemanticsAction.didLoseAccessibilityFocus);
|
||
await tester.pumpAndSettle();
|
||
expect(focusNode.hasFocus, isFalse);
|
||
semantics.dispose();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.macOS,
|
||
TargetPlatform.windows,
|
||
TargetPlatform.linux,
|
||
}),
|
||
);
|
||
|
||
testWidgets('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async {
|
||
void onEditingComplete() {}
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(onEditingComplete: onEditingComplete)),
|
||
),
|
||
);
|
||
|
||
final Finder editableTextFinder = find.byType(EditableText);
|
||
expect(editableTextFinder, findsOneWidget);
|
||
|
||
final EditableText editableTextWidget = tester.widget(editableTextFinder);
|
||
expect(editableTextWidget.onEditingComplete, onEditingComplete);
|
||
});
|
||
|
||
// Regression test for https://github.com/flutter/flutter/issues/127597.
|
||
testWidgets(
|
||
'The second TextField is clicked, triggers the onTapOutside callback of the previous TextField',
|
||
(WidgetTester tester) async {
|
||
final GlobalKey keyA = GlobalKey();
|
||
final GlobalKey keyB = GlobalKey();
|
||
final GlobalKey keyC = GlobalKey();
|
||
var outsideClickA = false;
|
||
var outsideClickB = false;
|
||
var outsideClickC = false;
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Align(
|
||
alignment: Alignment.topLeft,
|
||
child: Column(
|
||
children: <Widget>[
|
||
const Text('Outside'),
|
||
Material(
|
||
child: TextField(
|
||
key: keyA,
|
||
groupId: 'Group A',
|
||
onTapOutside: (PointerDownEvent event) {
|
||
outsideClickA = true;
|
||
},
|
||
),
|
||
),
|
||
Material(
|
||
child: TextField(
|
||
key: keyB,
|
||
groupId: 'Group B',
|
||
onTapOutside: (PointerDownEvent event) {
|
||
outsideClickB = true;
|
||
},
|
||
),
|
||
),
|
||
Material(
|
||
child: TextField(
|
||
key: keyC,
|
||
groupId: 'Group C',
|
||
onTapOutside: (PointerDownEvent event) {
|
||
outsideClickC = true;
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.pump();
|
||
|
||
Future<void> click(Finder finder) async {
|
||
await tester.tap(finder);
|
||
await tester.enterText(finder, 'Hello');
|
||
await tester.pump();
|
||
}
|
||
|
||
expect(outsideClickA, false);
|
||
expect(outsideClickB, false);
|
||
expect(outsideClickC, false);
|
||
|
||
await click(find.byKey(keyA));
|
||
await tester.showKeyboard(find.byKey(keyA));
|
||
await tester.idle();
|
||
expect(outsideClickA, false);
|
||
expect(outsideClickB, false);
|
||
expect(outsideClickC, false);
|
||
|
||
await click(find.byKey(keyB));
|
||
expect(outsideClickA, true);
|
||
expect(outsideClickB, false);
|
||
expect(outsideClickC, false);
|
||
|
||
await click(find.byKey(keyC));
|
||
expect(outsideClickA, true);
|
||
expect(outsideClickB, true);
|
||
expect(outsideClickC, false);
|
||
|
||
await tester.tap(find.text('Outside'));
|
||
expect(outsideClickA, true);
|
||
expect(outsideClickB, true);
|
||
expect(outsideClickC, true);
|
||
},
|
||
);
|
||
|
||
testWidgets('TextField has consistent size', (WidgetTester tester) async {
|
||
final Key textFieldKey = UniqueKey();
|
||
String? textFieldValue;
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
key: textFieldKey,
|
||
decoration: const InputDecoration(hintText: 'Placeholder'),
|
||
onChanged: (String value) {
|
||
textFieldValue = value;
|
||
},
|
||
),
|
||
),
|
||
);
|
||
|
||
RenderBox findTextFieldBox() => tester.renderObject(find.byKey(textFieldKey));
|
||
|
||
final RenderBox inputBox = findTextFieldBox();
|
||
final Size emptyInputSize = inputBox.size;
|
||
|
||
Future<void> checkText(String testValue) async {
|
||
return TestAsyncUtils.guard(() async {
|
||
expect(textFieldValue, isNull);
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
// Check that the onChanged event handler fired.
|
||
expect(textFieldValue, equals(testValue));
|
||
textFieldValue = null;
|
||
await skipPastScrollingAnimation(tester);
|
||
});
|
||
}
|
||
|
||
await checkText(' ');
|
||
expect(findTextFieldBox(), equals(inputBox));
|
||
expect(inputBox.size, equals(emptyInputSize));
|
||
|
||
await checkText('Test');
|
||
expect(findTextFieldBox(), equals(inputBox));
|
||
expect(inputBox.size, equals(emptyInputSize));
|
||
});
|
||
|
||
testWidgets('Cursor blinks', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: const TextField(decoration: InputDecoration(hintText: 'Placeholder')),
|
||
),
|
||
);
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
|
||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||
|
||
// Check that the cursor visibility toggles after each blink interval.
|
||
Future<void> checkCursorToggle() async {
|
||
final bool initialShowCursor = editableText.cursorCurrentlyVisible;
|
||
await tester.pump(editableText.cursorBlinkInterval);
|
||
expect(editableText.cursorCurrentlyVisible, equals(!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(!initialShowCursor));
|
||
await tester.pump(editableText.cursorBlinkInterval);
|
||
expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
|
||
}
|
||
|
||
await checkCursorToggle();
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
|
||
// Try the test again with a nonempty EditableText.
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(text: 'X', selection: TextSelection.collapsed(offset: 1)),
|
||
);
|
||
await tester.idle();
|
||
expect(tester.state(find.byType(EditableText)), editableText);
|
||
await checkCursorToggle();
|
||
});
|
||
|
||
// Regression test for https://github.com/flutter/flutter/issues/78918.
|
||
testWidgets('RenderEditable sets correct text editing value', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'how are you');
|
||
final icon = UniqueKey();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(
|
||
controller: controller,
|
||
decoration: InputDecoration(
|
||
suffixIcon: IconButton(
|
||
key: icon,
|
||
icon: const Icon(Icons.cancel),
|
||
onPressed: () => controller.clear(),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byKey(icon));
|
||
await tester.pump();
|
||
expect(controller.text, '');
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 0));
|
||
});
|
||
|
||
testWidgets(
|
||
'Cursor radius is 2.0',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(const MaterialApp(home: Material(child: TextField())));
|
||
|
||
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 has expected defaults', (WidgetTester tester) async {
|
||
await tester.pumpWidget(overlay(child: const TextField()));
|
||
|
||
final TextField textField = tester.firstWidget(find.byType(TextField));
|
||
expect(textField.cursorWidth, 2.0);
|
||
expect(textField.cursorHeight, null);
|
||
expect(textField.cursorRadius, null);
|
||
});
|
||
|
||
testWidgets('cursor has expected radius value', (WidgetTester tester) async {
|
||
await tester.pumpWidget(overlay(child: const TextField(cursorRadius: Radius.circular(3.0))));
|
||
|
||
final TextField textField = tester.firstWidget(find.byType(TextField));
|
||
expect(textField.cursorWidth, 2.0);
|
||
expect(textField.cursorRadius, const Radius.circular(3.0));
|
||
});
|
||
|
||
testWidgets('clipBehavior has expected defaults', (WidgetTester tester) async {
|
||
await tester.pumpWidget(overlay(child: const TextField()));
|
||
|
||
final TextField textField = tester.firstWidget(find.byType(TextField));
|
||
expect(textField.clipBehavior, Clip.hardEdge);
|
||
});
|
||
|
||
testWidgets('Overflow clipBehavior none golden', (WidgetTester tester) async {
|
||
final controller = OverflowWidgetTextEditingController();
|
||
addTearDown(controller.dispose);
|
||
final Widget widget = Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: RepaintBoundary(
|
||
key: const ValueKey<int>(1),
|
||
child: SizedBox(
|
||
height: 200,
|
||
width: 200,
|
||
child: Center(
|
||
child: SizedBox(
|
||
// Make sure the input field is not high enough for the WidgetSpan.
|
||
height: 50,
|
||
child: TextField(controller: controller, clipBehavior: Clip.none),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpWidget(widget);
|
||
|
||
final TextField textField = tester.firstWidget(find.byType(TextField));
|
||
expect(textField.clipBehavior, Clip.none);
|
||
|
||
final EditableText editableText = tester.firstWidget(find.byType(EditableText));
|
||
expect(editableText.clipBehavior, Clip.none);
|
||
|
||
await expectLater(
|
||
find.byKey(const ValueKey<int>(1)),
|
||
matchesGoldenFile('overflow_clipbehavior_none.material.0.png'),
|
||
);
|
||
});
|
||
|
||
testWidgets('Material cursor android golden', (WidgetTester tester) async {
|
||
final Widget widget = Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: const RepaintBoundary(
|
||
key: ValueKey<int>(1),
|
||
child: TextField(
|
||
cursorColor: Colors.blue,
|
||
cursorWidth: 15,
|
||
cursorRadius: Radius.circular(3.0),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpWidget(widget);
|
||
|
||
const testValue = 'A short phrase';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
|
||
await tester.pump();
|
||
|
||
await expectLater(
|
||
find.byKey(const ValueKey<int>(1)),
|
||
matchesGoldenFile('text_field_cursor_test.material.0.png'),
|
||
);
|
||
});
|
||
|
||
testWidgets(
|
||
'Material cursor golden',
|
||
(WidgetTester tester) async {
|
||
final Widget widget = Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: const RepaintBoundary(
|
||
key: ValueKey<int>(1),
|
||
child: TextField(
|
||
cursorColor: Colors.blue,
|
||
cursorWidth: 15,
|
||
cursorRadius: Radius.circular(3.0),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpWidget(widget);
|
||
|
||
const testValue = 'A short phrase';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
|
||
await tester.pump();
|
||
|
||
await expectLater(
|
||
find.byKey(const ValueKey<int>(1)),
|
||
matchesGoldenFile(
|
||
'text_field_cursor_test_${debugDefaultTargetPlatformOverride!.name.toLowerCase()}.material.1.png',
|
||
),
|
||
);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'TextInputFormatter gets correct selection value',
|
||
(WidgetTester tester) async {
|
||
late TextEditingValue actualOldValue;
|
||
late TextEditingValue actualNewValue;
|
||
void callBack(TextEditingValue oldValue, TextEditingValue newValue) {
|
||
actualOldValue = oldValue;
|
||
actualNewValue = newValue;
|
||
}
|
||
|
||
final FocusNode focusNode = _focusNode();
|
||
final TextEditingController controller = _textEditingController(text: '123');
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: TextField(
|
||
controller: controller,
|
||
focusNode: focusNode,
|
||
inputFormatters: <TextInputFormatter>[TestFormatter(callBack)],
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(
|
||
actualOldValue,
|
||
const TextEditingValue(
|
||
text: '123',
|
||
selection: TextSelection.collapsed(offset: 3, affinity: TextAffinity.upstream),
|
||
),
|
||
);
|
||
expect(
|
||
actualNewValue,
|
||
const TextEditingValue(text: '12', selection: TextSelection.collapsed(offset: 2)),
|
||
);
|
||
},
|
||
// [intended] only applies to platforms where we handle key events.
|
||
skip: areKeyEventsHandledByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'text field selection toolbar renders correctly inside opacity',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: const Scaffold(
|
||
body: Center(
|
||
child: SizedBox(
|
||
width: 100,
|
||
height: 100,
|
||
child: Opacity(
|
||
opacity: 0.5,
|
||
child: TextField(decoration: InputDecoration(hintText: 'Placeholder')),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
|
||
const testValue = 'A B C';
|
||
tester.testTextInput.updateEditingValue(const TextEditingValue(text: testValue));
|
||
await tester.pump();
|
||
|
||
// The selectWordsInRange with SelectionChangedCause.tap seems to be needed to show the toolbar.
|
||
// (This is true even if we provide selection parameter to the TextEditingValue above.)
|
||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
state.renderEditable.selectWordsInRange(from: Offset.zero, cause: SelectionChangedCause.tap);
|
||
|
||
expect(state.showToolbar(), true);
|
||
|
||
// This is needed for the AnimatedOpacity to turn from 0 to 1 so the toolbar is visible.
|
||
await tester.pumpAndSettle();
|
||
await tester.pump(const Duration(seconds: 1));
|
||
|
||
// Sanity check that the toolbar widget exists.
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
|
||
await expectLater(
|
||
// The toolbar exists in the Overlay above the MaterialApp.
|
||
find.byType(Overlay),
|
||
matchesGoldenFile('text_field_opacity_test.0.png'),
|
||
);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'text field toolbar options correctly changes options',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
|
||
// On macOS, we select the precise position of the tap.
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
controller: controller,
|
||
toolbarOptions: const ToolbarOptions(copy: true),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// This tap just puts the cursor somewhere different than where the double
|
||
// tap will occur to test that the double tap moves the existing cursor first.
|
||
await tester.tapAt(textOffsetToPosition(tester, 3));
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
await tester.tapAt(textOffsetToPosition(tester, 8));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap moved the cursor.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 8));
|
||
await tester.tapAt(textOffsetToPosition(tester, 8));
|
||
await tester.pump();
|
||
|
||
// Second tap selects the word around the cursor.
|
||
expect(controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12));
|
||
|
||
// Selected text shows 'Copy', and not 'Paste', 'Cut', 'Select All'.
|
||
expect(find.text('Paste'), findsNothing);
|
||
expect(find.text('Copy'), findsOneWidget);
|
||
expect(find.text('Cut'), findsNothing);
|
||
expect(find.text('Select All'), findsNothing);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets('text selection style 1', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwasssup!',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(
|
||
child: RepaintBoundary(
|
||
child: Container(
|
||
width: 650.0,
|
||
height: 600.0,
|
||
decoration: const BoxDecoration(color: Color(0xff00ff00)),
|
||
child: Column(
|
||
children: <Widget>[
|
||
TextField(
|
||
key: const Key('field0'),
|
||
controller: controller,
|
||
style: const TextStyle(height: 4, color: Colors.black45),
|
||
toolbarOptions: const ToolbarOptions(copy: true, selectAll: true),
|
||
selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingTop,
|
||
maxLines: 3,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
|
||
|
||
await tester.longPressAt(textfieldStart + const Offset(50.0, 2.0));
|
||
await tester.pumpAndSettle(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textfieldStart + const Offset(100.0, 107.0));
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
|
||
await expectLater(
|
||
find.byType(MaterialApp),
|
||
matchesGoldenFile('text_field_golden.TextSelectionStyle.1.png'),
|
||
);
|
||
});
|
||
|
||
testWidgets('text selection style 2', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwasssup!',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(
|
||
child: RepaintBoundary(
|
||
child: Container(
|
||
width: 650.0,
|
||
height: 600.0,
|
||
decoration: const BoxDecoration(color: Color(0xff00ff00)),
|
||
child: Column(
|
||
children: <Widget>[
|
||
TextField(
|
||
key: const Key('field0'),
|
||
controller: controller,
|
||
style: const TextStyle(height: 4, color: Colors.black45),
|
||
toolbarOptions: const ToolbarOptions(copy: true, selectAll: true),
|
||
selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingBottom,
|
||
selectionWidthStyle: ui.BoxWidthStyle.tight,
|
||
maxLines: 3,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
final EditableTextState editableTextState = tester.state(find.byType(EditableText));
|
||
|
||
// Double tap to select the first word.
|
||
const index = 4;
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pumpAndSettle();
|
||
expect(editableTextState.selectionOverlay!.handlesAreVisible, isTrue);
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 7);
|
||
|
||
// Select all text. Use the toolbar if possible. iOS only shows the toolbar
|
||
// when the selection is collapsed.
|
||
if (isContextMenuProvidedByPlatform || defaultTargetPlatform == TargetPlatform.iOS) {
|
||
controller.selection = TextSelection(baseOffset: 0, extentOffset: controller.text.length);
|
||
expect(controller.selection.extentOffset, controller.text.length);
|
||
} else {
|
||
await tester.tap(find.text('Select all'));
|
||
await tester.pump();
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, controller.text.length);
|
||
}
|
||
|
||
await expectLater(
|
||
find.byType(MaterialApp),
|
||
matchesGoldenFile('text_field_golden.TextSelectionStyle.2.png'),
|
||
);
|
||
// Text selection styles are not fully supported on web.
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/93723
|
||
|
||
testWidgets(
|
||
'text field toolbar options correctly changes options',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
controller: controller,
|
||
toolbarOptions: const ToolbarOptions(copy: true),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset pos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
|
||
|
||
await tester.tapAt(pos);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
|
||
await tester.tapAt(pos);
|
||
await tester.pump();
|
||
|
||
// Selected text shows 'Copy', and not 'Paste', 'Cut', 'Select all'.
|
||
expect(find.text('Paste'), findsNothing);
|
||
expect(find.text('Copy'), findsOneWidget);
|
||
expect(find.text('Cut'), findsNothing);
|
||
expect(find.text('Select all'), findsNothing);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
TargetPlatform.linux,
|
||
TargetPlatform.windows,
|
||
}),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets('cursor layout has correct width', (WidgetTester tester) async {
|
||
final controller = TextEditingController.fromValue(
|
||
const TextEditingValue(selection: TextSelection.collapsed(offset: 0)),
|
||
);
|
||
addTearDown(controller.dispose);
|
||
final FocusNode focusNode = _focusNode();
|
||
EditableText.debugDeterministicCursor = true;
|
||
await tester.pumpWidget(
|
||
Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: RepaintBoundary(
|
||
child: TextField(cursorWidth: 15.0, controller: controller, focusNode: focusNode),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
focusNode.requestFocus();
|
||
await tester.pump();
|
||
|
||
await expectLater(
|
||
find.byType(TextField),
|
||
matchesGoldenFile('text_field_cursor_width_test.0.png'),
|
||
);
|
||
EditableText.debugDeterministicCursor = false;
|
||
});
|
||
|
||
testWidgets('cursor layout has correct radius', (WidgetTester tester) async {
|
||
final controller = TextEditingController.fromValue(
|
||
const TextEditingValue(selection: TextSelection.collapsed(offset: 0)),
|
||
);
|
||
addTearDown(controller.dispose);
|
||
final FocusNode focusNode = _focusNode();
|
||
EditableText.debugDeterministicCursor = true;
|
||
await tester.pumpWidget(
|
||
Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: RepaintBoundary(
|
||
child: TextField(
|
||
cursorWidth: 15.0,
|
||
cursorRadius: const Radius.circular(3.0),
|
||
controller: controller,
|
||
focusNode: focusNode,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
focusNode.requestFocus();
|
||
await tester.pump();
|
||
|
||
await expectLater(
|
||
find.byType(TextField),
|
||
matchesGoldenFile('text_field_cursor_width_test.1.png'),
|
||
);
|
||
EditableText.debugDeterministicCursor = false;
|
||
});
|
||
|
||
testWidgets('cursor layout has correct height', (WidgetTester tester) async {
|
||
final controller = TextEditingController.fromValue(
|
||
const TextEditingValue(selection: TextSelection.collapsed(offset: 0)),
|
||
);
|
||
addTearDown(controller.dispose);
|
||
final FocusNode focusNode = _focusNode();
|
||
|
||
EditableText.debugDeterministicCursor = true;
|
||
await tester.pumpWidget(
|
||
Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: RepaintBoundary(
|
||
child: TextField(
|
||
cursorWidth: 15.0,
|
||
cursorHeight: 30.0,
|
||
controller: controller,
|
||
focusNode: focusNode,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
focusNode.requestFocus();
|
||
await tester.pump();
|
||
|
||
await expectLater(
|
||
find.byType(TextField),
|
||
matchesGoldenFile('text_field_cursor_width_test.2.png'),
|
||
);
|
||
EditableText.debugDeterministicCursor = false;
|
||
});
|
||
|
||
testWidgets('Overflowing a line with spaces stops the cursor at the end', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: TextField(key: textFieldKey, controller: controller, maxLines: null),
|
||
),
|
||
),
|
||
);
|
||
expect(controller.selection.baseOffset, -1);
|
||
expect(controller.selection.extentOffset, -1);
|
||
|
||
const testValueOneLine = 'enough text to be exactly at the end of the line.';
|
||
await tester.enterText(find.byType(TextField), testValueOneLine);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
|
||
|
||
RenderBox inputBox = findInputBox();
|
||
final Size oneLineInputSize = inputBox.size;
|
||
|
||
await tester.tapAt(textOffsetToPosition(tester, testValueOneLine.length));
|
||
await tester.pump();
|
||
|
||
const testValueTwoLines = 'enough text to overflow the first line and go to the second';
|
||
await tester.enterText(find.byType(TextField), testValueTwoLines);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
expect(inputBox, findInputBox());
|
||
inputBox = findInputBox();
|
||
expect(inputBox.size.height, greaterThan(oneLineInputSize.height));
|
||
final Size twoLineInputSize = inputBox.size;
|
||
|
||
// Enter a string with the same number of characters as testValueTwoLines,
|
||
// but where the overflowing part is all spaces. Assert that it only renders
|
||
// on one line.
|
||
const testValueSpaces = '$testValueOneLine ';
|
||
expect(testValueSpaces.length, testValueTwoLines.length);
|
||
await tester.enterText(find.byType(TextField), testValueSpaces);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
expect(inputBox, findInputBox());
|
||
inputBox = findInputBox();
|
||
expect(inputBox.size.height, oneLineInputSize.height);
|
||
|
||
// Swapping the final space for a letter causes it to wrap to 2 lines.
|
||
const testValueSpacesOverflow = '$testValueOneLine a';
|
||
expect(testValueSpacesOverflow.length, testValueTwoLines.length);
|
||
await tester.enterText(find.byType(TextField), testValueSpacesOverflow);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
expect(inputBox, findInputBox());
|
||
inputBox = findInputBox();
|
||
expect(inputBox.size.height, twoLineInputSize.height);
|
||
|
||
// Positioning the cursor at the end of a line overflowing with spaces puts
|
||
// it inside the input still.
|
||
await tester.enterText(find.byType(TextField), testValueSpaces);
|
||
await skipPastScrollingAnimation(tester);
|
||
await tester.tapAt(textOffsetToPosition(tester, testValueSpaces.length));
|
||
await tester.pump();
|
||
|
||
final double inputWidth = findRenderEditable(tester).size.width;
|
||
final Offset cursorOffsetSpaces = findRenderEditable(
|
||
tester,
|
||
).getLocalRectForCaret(const TextPosition(offset: testValueSpaces.length)).bottomRight;
|
||
|
||
expect(cursorOffsetSpaces.dx, inputWidth - kCaretGap);
|
||
});
|
||
|
||
testWidgets('Overflowing a line with spaces stops the cursor at the end (rtl direction)', (
|
||
WidgetTester tester,
|
||
) async {
|
||
await tester.pumpWidget(
|
||
overlay(child: const TextField(textDirection: TextDirection.rtl, maxLines: null)),
|
||
);
|
||
|
||
const testValueOneLine = 'enough text to be exactly at the end of the line.';
|
||
const testValueSpaces = '$testValueOneLine ';
|
||
|
||
// Positioning the cursor at the end of a line overflowing with spaces puts
|
||
// it inside the input still.
|
||
await tester.enterText(find.byType(TextField), testValueSpaces);
|
||
await skipPastScrollingAnimation(tester);
|
||
await tester.tapAt(textOffsetToPosition(tester, testValueSpaces.length));
|
||
await tester.pump();
|
||
|
||
final Offset cursorOffsetSpaces = findRenderEditable(
|
||
tester,
|
||
).getLocalRectForCaret(const TextPosition(offset: testValueSpaces.length)).topLeft;
|
||
|
||
expect(cursorOffsetSpaces.dx >= 0, isTrue);
|
||
});
|
||
|
||
testWidgets(
|
||
'mobile obscureText control test',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: const TextField(
|
||
obscureText: true,
|
||
decoration: InputDecoration(hintText: 'Placeholder'),
|
||
),
|
||
),
|
||
);
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
|
||
const testValue = 'ABC';
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(
|
||
text: testValue,
|
||
selection: TextSelection.collapsed(offset: testValue.length),
|
||
),
|
||
);
|
||
|
||
await tester.pump();
|
||
|
||
// Enter a character into the obscured field and verify that the character
|
||
// is temporarily shown to the user and then changed to a bullet.
|
||
const newChar = 'X';
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(
|
||
text: testValue + newChar,
|
||
selection: TextSelection.collapsed(offset: testValue.length + 1),
|
||
),
|
||
);
|
||
|
||
await tester.pump();
|
||
|
||
String editText = (findRenderEditable(tester).text! as TextSpan).text!;
|
||
expect(editText.substring(editText.length - 1), newChar);
|
||
|
||
await tester.pump(const Duration(seconds: 2));
|
||
|
||
editText = (findRenderEditable(tester).text! as TextSpan).text!;
|
||
expect(editText.substring(editText.length - 1), '\u2022');
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.android}),
|
||
);
|
||
|
||
testWidgets(
|
||
'desktop obscureText control test',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: const TextField(
|
||
obscureText: true,
|
||
decoration: InputDecoration(hintText: 'Placeholder'),
|
||
),
|
||
),
|
||
);
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
|
||
const testValue = 'ABC';
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(
|
||
text: testValue,
|
||
selection: TextSelection.collapsed(offset: testValue.length),
|
||
),
|
||
);
|
||
|
||
await tester.pump();
|
||
|
||
// Enter a character into the obscured field and verify that the character
|
||
// isn't shown to the user.
|
||
const newChar = 'X';
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(
|
||
text: testValue + newChar,
|
||
selection: TextSelection.collapsed(offset: testValue.length + 1),
|
||
),
|
||
);
|
||
|
||
await tester.pump();
|
||
|
||
final String editText = (findRenderEditable(tester).text! as TextSpan).text!;
|
||
expect(editText.substring(editText.length - 1), '\u2022');
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.macOS,
|
||
TargetPlatform.linux,
|
||
TargetPlatform.windows,
|
||
}),
|
||
);
|
||
|
||
testWidgets('Caret position is updated on tap', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller)));
|
||
expect(controller.selection.baseOffset, -1);
|
||
expect(controller.selection.extentOffset, -1);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Tap to reposition the caret.
|
||
final int tapIndex = testValue.indexOf('e');
|
||
final Offset ePos = textOffsetToPosition(tester, tapIndex);
|
||
await tester.tapAt(ePos);
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, tapIndex);
|
||
expect(controller.selection.extentOffset, tapIndex);
|
||
});
|
||
|
||
testWidgets('enableInteractiveSelection = false, tap', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(child: TextField(controller: controller, enableInteractiveSelection: false)),
|
||
);
|
||
expect(controller.selection.baseOffset, -1);
|
||
expect(controller.selection.extentOffset, -1);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Tap would ordinarily reposition the caret.
|
||
final int tapIndex = testValue.indexOf('e');
|
||
final Offset ePos = textOffsetToPosition(tester, tapIndex);
|
||
await tester.tapAt(ePos);
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, testValue.length);
|
||
expect(controller.selection.isCollapsed, isTrue);
|
||
});
|
||
|
||
testWidgets('Can long press to select', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller)));
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
expect(controller.value.text, testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
|
||
// Long press the 'e' to select 'def'.
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
await tester.longPressAt(ePos, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
|
||
// 'def' is selected.
|
||
expect(controller.selection.baseOffset, testValue.indexOf('d'));
|
||
expect(controller.selection.extentOffset, testValue.indexOf('f') + 1);
|
||
|
||
// Tapping elsewhere immediately collapses and moves the cursor.
|
||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('h')));
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('h'));
|
||
});
|
||
|
||
testWidgets("Slight movements in longpress don't hide/show handles", (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller)));
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
expect(controller.value.text, testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
|
||
// Long press the 'e' to select 'def', but don't release the gesture.
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
|
||
await tester.pump(const Duration(seconds: 2));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Handles are shown
|
||
final Finder fadeFinder = find.byType(FadeTransition);
|
||
expect(fadeFinder, findsNWidgets(2)); // 2 handles, 1 toolbar
|
||
FadeTransition handle = tester.widget(fadeFinder.at(0));
|
||
expect(handle.opacity.value, equals(1.0));
|
||
|
||
// Move the gesture very slightly
|
||
await gesture.moveBy(const Offset(1.0, 1.0));
|
||
await tester.pump(SelectionOverlay.fadeDuration * 0.5);
|
||
handle = tester.widget(fadeFinder.at(0));
|
||
|
||
// The handle should still be fully opaque.
|
||
expect(handle.opacity.value, equals(1.0));
|
||
});
|
||
|
||
testWidgets(
|
||
'Long pressing a field with selection 0,0 shows the selection menu',
|
||
(WidgetTester tester) async {
|
||
late final TextEditingController controller;
|
||
addTearDown(() => controller.dispose());
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
controller: controller = TextEditingController.fromValue(
|
||
const TextEditingValue(selection: TextSelection(baseOffset: 0, extentOffset: 0)),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Paste'), findsNothing);
|
||
final Offset emptyPos = textOffsetToPosition(tester, 0);
|
||
await tester.longPressAt(emptyPos, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets('infinite multi-line text hint text is not ellipsized by default', (
|
||
WidgetTester tester,
|
||
) async {
|
||
const kLongString =
|
||
'Enter your email Enter your email Enter your '
|
||
'email Enter your email Enter your email Enter '
|
||
'your email Enter your email';
|
||
const double defaultLineHeight = 24;
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: const TextField(
|
||
maxLines: null,
|
||
decoration: InputDecoration(labelText: 'Email', hintText: kLongString),
|
||
),
|
||
),
|
||
);
|
||
final Text hintText = tester.widget<Text>(find.text(kLongString));
|
||
expect(hintText.overflow, isNull);
|
||
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.text(kLongString));
|
||
expect(paragraph.size.height > defaultLineHeight * 2, isTrue);
|
||
});
|
||
|
||
testWidgets('non-infinite multi-line hint text is ellipsized by default', (
|
||
WidgetTester tester,
|
||
) async {
|
||
const kLongString =
|
||
'Enter your email Enter your email Enter your '
|
||
'email Enter your email Enter your email Enter '
|
||
'your email Enter your email';
|
||
const double defaultLineHeight = 24;
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: const TextField(
|
||
maxLines: 2,
|
||
decoration: InputDecoration(labelText: 'Email', hintText: kLongString),
|
||
),
|
||
),
|
||
);
|
||
final Text hintText = tester.widget<Text>(find.text(kLongString));
|
||
expect(hintText.overflow, TextOverflow.ellipsis);
|
||
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.text(kLongString));
|
||
expect(paragraph.size.height < defaultLineHeight * 2 + precisionErrorTolerance, isTrue);
|
||
});
|
||
|
||
testWidgets('Entering text hides selection handle caret', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller)));
|
||
|
||
const testValue = 'abcdefghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
expect(controller.value.text, testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Handle not shown.
|
||
expect(controller.selection.isCollapsed, true);
|
||
final Finder fadeFinder = find.byType(FadeTransition);
|
||
FadeTransition handle = tester.widget(fadeFinder.at(0));
|
||
expect(handle.opacity.value, equals(0.0));
|
||
|
||
// Tap on the text field to show the handle.
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(fadeFinder, findsNWidgets(1));
|
||
handle = tester.widget(fadeFinder.at(0));
|
||
expect(handle.opacity.value, equals(1.0));
|
||
|
||
// Enter more text.
|
||
const testValueAddition = 'jklmni';
|
||
await tester.enterText(find.byType(TextField), testValueAddition);
|
||
expect(controller.value.text, testValueAddition);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Handle not shown.
|
||
expect(controller.selection.isCollapsed, true);
|
||
handle = tester.widget(fadeFinder.at(0));
|
||
expect(handle.opacity.value, equals(0.0));
|
||
});
|
||
|
||
testWidgets('multiple text fields with prefix and suffix have correct semantics order.', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller1 = _textEditingController(text: 'abc');
|
||
final TextEditingController controller2 = _textEditingController(text: 'def');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Column(
|
||
children: <Widget>[
|
||
TextField(
|
||
decoration: const InputDecoration(prefixText: 'prefix1', suffixText: 'suffix1'),
|
||
enabled: false,
|
||
controller: controller1,
|
||
),
|
||
TextField(
|
||
decoration: const InputDecoration(prefixText: 'prefix2', suffixText: 'suffix2'),
|
||
enabled: false,
|
||
controller: controller2,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
final List<String> orders = tester.semantics
|
||
.simulatedAccessibilityTraversal(startNode: find.semantics.byLabel('prefix1'))
|
||
.map((SemanticsNode node) => node.label + node.value)
|
||
.toList();
|
||
expect(orders, <String>['prefix1', 'abc', 'suffix1', 'prefix2', 'def', 'suffix2']);
|
||
});
|
||
|
||
testWidgets('selection handles are excluded from the semantics', (WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller)));
|
||
|
||
const testValue = 'abcdefghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
expect(controller.value.text, testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
// Tap on the text field to show the handle.
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
// The semantics should only have the text field.
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isFocused,
|
||
],
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
SemanticsAction.focus,
|
||
SemanticsAction.moveCursorBackwardByCharacter,
|
||
SemanticsAction.setSelection,
|
||
SemanticsAction.paste,
|
||
SemanticsAction.setText,
|
||
SemanticsAction.moveCursorBackwardByWord,
|
||
],
|
||
value: 'abcdefghi',
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 9,
|
||
textDirection: TextDirection.ltr,
|
||
textSelection: const TextSelection.collapsed(offset: 9),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreRect: true,
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
),
|
||
);
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('Mouse long press is just like a tap', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller)));
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
expect(controller.value.text, testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
|
||
// Long press the 'e' using a mouse device.
|
||
final int eIndex = testValue.indexOf('e');
|
||
final Offset ePos = textOffsetToPosition(tester, eIndex);
|
||
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
|
||
await tester.pump(const Duration(seconds: 2));
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
// The cursor is placed just like a regular tap.
|
||
expect(controller.selection.baseOffset, eIndex);
|
||
expect(controller.selection.extentOffset, eIndex);
|
||
});
|
||
|
||
testWidgets('Read only text field basic', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'readonly');
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller, readOnly: true)));
|
||
// Read only text field cannot open keyboard.
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
// On web, we always create a client connection to the engine.
|
||
expect(tester.testTextInput.hasAnyClients, isBrowser ? isTrue : isFalse);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump();
|
||
// On web, we always create a client connection to the engine.
|
||
expect(tester.testTextInput.hasAnyClients, isBrowser ? isTrue : isFalse);
|
||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||
// Collapse selection should not paint.
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
|
||
// Long press on the 'd' character of text 'readOnly' to show context menu.
|
||
const dIndex = 3;
|
||
final Offset dPos = textOffsetToPosition(tester, dIndex);
|
||
await tester.longPressAt(dPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
// Context menu should not have paste and cut.
|
||
expect(find.text('Copy'), isContextMenuProvidedByPlatform ? findsNothing : findsOneWidget);
|
||
expect(find.text('Paste'), findsNothing);
|
||
expect(find.text('Cut'), findsNothing);
|
||
});
|
||
|
||
testWidgets(
|
||
'does not paint toolbar when no options available',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(const MaterialApp(home: Material(child: TextField(readOnly: true))));
|
||
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
|
||
await tester.tap(find.byType(TextField));
|
||
// Wait for context menu to be built.
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byType(CupertinoTextSelectionToolbar), findsNothing);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'text field build empty toolbar when no options available',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(const MaterialApp(home: Material(child: TextField(readOnly: true))));
|
||
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
|
||
await tester.tap(find.byType(TextField));
|
||
// Wait for context menu to be built.
|
||
await tester.pumpAndSettle();
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||
final SizedBox sizedBox = tester.widget(
|
||
find.descendant(
|
||
of: find.byType(AdaptiveTextSelectionToolbar),
|
||
matching: find.byType(SizedBox),
|
||
),
|
||
);
|
||
expect(sizedBox.width, 0.0);
|
||
expect(sizedBox.height, 0.0);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
TargetPlatform.linux,
|
||
TargetPlatform.windows,
|
||
}),
|
||
);
|
||
|
||
testWidgets('Swapping controllers should update selection', (WidgetTester tester) async {
|
||
TextEditingController controller = _textEditingController(text: 'readonly');
|
||
final entry = OverlayEntry(
|
||
builder: (BuildContext context) {
|
||
return Center(
|
||
child: Material(child: TextField(controller: controller, readOnly: true)),
|
||
);
|
||
},
|
||
);
|
||
addTearDown(
|
||
() => entry
|
||
..remove()
|
||
..dispose(),
|
||
);
|
||
await tester.pumpWidget(overlayWithEntry(entry));
|
||
const dIndex = 3;
|
||
final Offset dPos = textOffsetToPosition(tester, dIndex);
|
||
await tester.longPressAt(dPos);
|
||
await tester.pumpAndSettle();
|
||
final EditableTextState state = tester.state(find.byType(EditableText));
|
||
TextSelection currentOverlaySelection = state.selectionOverlay!.value.selection;
|
||
expect(currentOverlaySelection.baseOffset, 0);
|
||
expect(currentOverlaySelection.extentOffset, 8);
|
||
|
||
// Update selection from [0 to 8] to [1 to 7].
|
||
controller = TextEditingController.fromValue(
|
||
controller.value.copyWith(selection: const TextSelection(baseOffset: 1, extentOffset: 7)),
|
||
);
|
||
addTearDown(controller.dispose);
|
||
|
||
// Mark entry to be dirty in order to trigger overlay update.
|
||
entry.markNeedsBuild();
|
||
|
||
await tester.pump();
|
||
currentOverlaySelection = state.selectionOverlay!.value.selection;
|
||
expect(currentOverlaySelection.baseOffset, 1);
|
||
expect(currentOverlaySelection.extentOffset, 7);
|
||
});
|
||
|
||
testWidgets('Read only text should not compose', (WidgetTester tester) async {
|
||
final controller = TextEditingController.fromValue(
|
||
const TextEditingValue(
|
||
text: 'readonly',
|
||
composing: TextRange(start: 0, end: 8), // Simulate text composing.
|
||
),
|
||
);
|
||
addTearDown(controller.dispose);
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller, readOnly: true)));
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
// There should be no composing.
|
||
expect(renderEditable.text, TextSpan(text: 'readonly', style: renderEditable.text!.style));
|
||
});
|
||
|
||
testWidgets(
|
||
'Dynamically switching between read only and not read only should hide or show collapse cursor',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'readonly');
|
||
var readOnly = true;
|
||
final entry = OverlayEntry(
|
||
builder: (BuildContext context) {
|
||
return Center(
|
||
child: Material(
|
||
child: TextField(controller: controller, readOnly: readOnly),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
addTearDown(
|
||
() => entry
|
||
..remove()
|
||
..dispose(),
|
||
);
|
||
await tester.pumpWidget(overlayWithEntry(entry));
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump();
|
||
|
||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||
// Collapse selection should not paint.
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
|
||
|
||
readOnly = false;
|
||
// Mark entry to be dirty in order to trigger overlay update.
|
||
entry.markNeedsBuild();
|
||
await tester.pumpAndSettle();
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
|
||
|
||
readOnly = true;
|
||
entry.markNeedsBuild();
|
||
await tester.pumpAndSettle();
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
|
||
},
|
||
);
|
||
|
||
testWidgets('Dynamically switching to read only should close input connection', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller = _textEditingController(text: 'readonly');
|
||
var readOnly = false;
|
||
final entry = OverlayEntry(
|
||
builder: (BuildContext context) {
|
||
return Center(
|
||
child: Material(
|
||
child: TextField(controller: controller, readOnly: readOnly),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
addTearDown(
|
||
() => entry
|
||
..remove()
|
||
..dispose(),
|
||
);
|
||
await tester.pumpWidget(overlayWithEntry(entry));
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump();
|
||
expect(tester.testTextInput.hasAnyClients, true);
|
||
|
||
readOnly = true;
|
||
// Mark entry to be dirty in order to trigger overlay update.
|
||
entry.markNeedsBuild();
|
||
await tester.pump();
|
||
// On web, we always have a client connection to the engine.
|
||
expect(tester.testTextInput.hasAnyClients, isBrowser ? isTrue : isFalse);
|
||
});
|
||
|
||
testWidgets('Dynamically switching to non read only should open input connection', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller = _textEditingController(text: 'readonly');
|
||
var readOnly = true;
|
||
final entry = OverlayEntry(
|
||
builder: (BuildContext context) {
|
||
return Center(
|
||
child: Material(
|
||
child: TextField(controller: controller, readOnly: readOnly),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
addTearDown(
|
||
() => entry
|
||
..remove()
|
||
..dispose(),
|
||
);
|
||
await tester.pumpWidget(overlayWithEntry(entry));
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump();
|
||
// On web, we always have a client connection to the engine.
|
||
expect(tester.testTextInput.hasAnyClients, isBrowser ? isTrue : isFalse);
|
||
|
||
readOnly = false;
|
||
// Mark entry to be dirty in order to trigger overlay update.
|
||
entry.markNeedsBuild();
|
||
await tester.pump();
|
||
expect(tester.testTextInput.hasAnyClients, true);
|
||
});
|
||
|
||
testWidgets('enableInteractiveSelection = false, long-press', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(child: TextField(controller: controller, enableInteractiveSelection: false)),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
expect(controller.value.text, testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
|
||
// Long press the 'e' to select 'def'.
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
await tester.longPressAt(ePos, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.length);
|
||
});
|
||
|
||
testWidgets('Selection updates on tap down (Desktop platforms)', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset ePos = textOffsetToPosition(tester, 5);
|
||
final Offset gPos = textOffsetToPosition(tester, 8);
|
||
|
||
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 5);
|
||
expect(controller.selection.extentOffset, 5);
|
||
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
|
||
await gesture.down(gPos);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 8);
|
||
|
||
// This should do nothing. The selection is set on tap down on desktop platforms.
|
||
await gesture.up();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 8);
|
||
}, variant: TargetPlatformVariant.desktop());
|
||
|
||
testWidgets('Selection updates on tap up (Mobile platforms)', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
final isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset ePos = textOffsetToPosition(tester, 5);
|
||
final Offset gPos = textOffsetToPosition(tester, 8);
|
||
|
||
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
|
||
await gesture.down(gPos);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 5);
|
||
expect(controller.selection.extentOffset, 5);
|
||
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 8);
|
||
|
||
final TestGesture touchGesture = await tester.startGesture(ePos);
|
||
await touchGesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
// On iOS a tap to select, selects the word edge instead of the exact tap position.
|
||
expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5);
|
||
expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5);
|
||
|
||
// Selection should stay the same since it is set on tap up for mobile platforms.
|
||
await touchGesture.down(gPos);
|
||
await tester.pump();
|
||
expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5);
|
||
expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5);
|
||
|
||
await touchGesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 8);
|
||
}, variant: TargetPlatformVariant.mobile());
|
||
|
||
testWidgets(
|
||
'Can select text with a mouse when wrapped in a GestureDetector with tap/double tap callbacks',
|
||
(WidgetTester tester) async {
|
||
// This is a regression test for https://github.com/flutter/flutter/issues/129161.
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: GestureDetector(
|
||
onTap: () {},
|
||
onDoubleTap: () {},
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
|
||
|
||
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
// This is to allow the GestureArena to decide a winner between TapGestureRecognizer,
|
||
// DoubleTapGestureRecognizer, and BaseTapAndDragGestureRecognizer.
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('e'));
|
||
|
||
await gesture.down(ePos);
|
||
await tester.pump();
|
||
await gesture.moveTo(gPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, testValue.indexOf('e'));
|
||
expect(controller.selection.extentOffset, testValue.indexOf('g'));
|
||
},
|
||
variant: TargetPlatformVariant.desktop(),
|
||
);
|
||
|
||
testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
|
||
|
||
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
|
||
await tester.pump();
|
||
await gesture.moveTo(gPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, testValue.indexOf('e'));
|
||
expect(controller.selection.extentOffset, testValue.indexOf('g'));
|
||
});
|
||
|
||
testWidgets(
|
||
'Can move cursor when dragging, when tap is on collapsed selection (iOS)',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
|
||
|
||
// Tap on text field to gain focus, and set selection to '|g'. On iOS
|
||
// the selection is set to the word edge closest to the tap position.
|
||
// We await for kDoubleTapTimeout after the up event, so our next down event
|
||
// does not register as a double tap.
|
||
final TestGesture gesture = await tester.startGesture(ePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 7);
|
||
|
||
// If the position we tap during a drag start is on the collapsed selection, then
|
||
// we can move the cursor with a drag.
|
||
// Here we tap on '|g', where our selection was previously, and move to '|i'.
|
||
await gesture.down(textOffsetToPosition(tester, 7));
|
||
await tester.pump();
|
||
await gesture.moveTo(iPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('i'));
|
||
|
||
// End gesture and skip the magnifier hide animation, so it can release
|
||
// resources.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Cursor should not move on a quick touch drag when touch does not begin on previous selection (iOS)',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
|
||
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
|
||
|
||
// Tap on text field to gain focus, and set selection to '|a'. On iOS
|
||
// the selection is set to the word edge closest to the tap position.
|
||
// We await for kDoubleTapTimeout after the up event, so our next down event
|
||
// does not register as a double tap.
|
||
final TestGesture gesture = await tester.startGesture(aPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 0);
|
||
|
||
// The position we tap during a drag start is not on the collapsed selection,
|
||
// so the cursor should not move.
|
||
await gesture.down(textOffsetToPosition(tester, 7));
|
||
await gesture.moveTo(iPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 0);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can move cursor when dragging, when tap is on collapsed selection (iOS) - multiline',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: null,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc\ndef\nghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
|
||
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
|
||
|
||
// Tap on text field to gain focus, and set selection to '|a'. On iOS
|
||
// the selection is set to the word edge closest to the tap position.
|
||
// We await for kDoubleTapTimeout after the up event, so our next down event
|
||
// does not register as a double tap.
|
||
final TestGesture gesture = await tester.startGesture(aPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 0);
|
||
|
||
// If the position we tap during a drag start is on the collapsed selection, then
|
||
// we can move the cursor with a drag.
|
||
// Here we tap on '|a', where our selection was previously, and move to '|i'.
|
||
await gesture.down(aPos);
|
||
await tester.pump();
|
||
await gesture.moveTo(iPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('i'));
|
||
|
||
// End gesture and skip the magnifier hide animation, so it can release
|
||
// resources.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can move cursor when dragging, when tap is on collapsed selection (iOS) - PageView',
|
||
(WidgetTester tester) async {
|
||
// This is a regression test for
|
||
// https://github.com/flutter/flutter/issues/142624.
|
||
final TextEditingController controller = _textEditingController();
|
||
final pageController = PageController();
|
||
addTearDown(pageController.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: PageView(
|
||
controller: pageController,
|
||
children: <Widget>[
|
||
Center(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
),
|
||
),
|
||
const SizedBox(height: 200.0, child: Center(child: Text('Page 2'))),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi jkl mno pqr stu vwx yz';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
|
||
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
|
||
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
|
||
|
||
// Tap on text field to gain focus, and set selection to '|a'. On iOS
|
||
// the selection is set to the word edge closest to the tap position.
|
||
// We await for kDoubleTapTimeout after the up event, so our next down event
|
||
// does not register as a double tap.
|
||
final TestGesture gesture = await tester.startGesture(aPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 0);
|
||
|
||
// If the position we tap during a drag start is on the collapsed selection, then
|
||
// we can move the cursor with a drag.
|
||
// Here we tap on '|a', where our selection was previously, and attempt move
|
||
// to '|g'.
|
||
await gesture.down(aPos);
|
||
await tester.pump();
|
||
await gesture.moveTo(gPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('g'));
|
||
|
||
// Release the pointer.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// If the position we tap during a drag start is on the collapsed selection, then
|
||
// we can move the cursor with a drag.
|
||
// Here we tap on '|g', where our selection was previously, and move to '|i'.
|
||
await gesture.down(gPos);
|
||
await tester.pump();
|
||
await gesture.moveTo(iPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('i'));
|
||
|
||
// End gesture and skip the magnifier hide animation, so it can release
|
||
// resources.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(pageController.page, isNotNull);
|
||
expect(pageController.page, 0.0);
|
||
// A horizontal drag directly on the TextField, but not on the current
|
||
// collapsed selection should move the page view to the next page.
|
||
final Rect textFieldRect = tester.getRect(find.byType(TextField));
|
||
await tester.dragFrom(
|
||
textFieldRect.centerRight - const Offset(0.1, 0.0),
|
||
const Offset(-500.0, 0.0),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, testValue.indexOf('i'));
|
||
expect(pageController.page, isNotNull);
|
||
expect(pageController.page, 1.0);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can move cursor when dragging, when tap is on collapsed selection (iOS) - TextField in Dismissible',
|
||
(WidgetTester tester) async {
|
||
// This is a regression test for
|
||
// https://github.com/flutter/flutter/issues/124421.
|
||
final TextEditingController controller = _textEditingController();
|
||
var dismissed = false;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: ListView(
|
||
children: <Widget>[
|
||
Dismissible(
|
||
key: UniqueKey(),
|
||
onDismissed: (DismissDirection? direction) {
|
||
dismissed = true;
|
||
},
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi jkl mno pqr stu vwx yz';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
|
||
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
|
||
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
|
||
|
||
// Tap on text field to gain focus, and set selection to '|a'. On iOS
|
||
// the selection is set to the word edge closest to the tap position.
|
||
// We await for kDoubleTapTimeout after the up event, so our next down event
|
||
// does not register as a double tap.
|
||
final TestGesture gesture = await tester.startGesture(aPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 0);
|
||
|
||
// If the position we tap during a drag start is on the collapsed selection, then
|
||
// we can move the cursor with a drag.
|
||
// Here we tap on '|a', where our selection was previously, and attempt move
|
||
// to '|g'.
|
||
await gesture.down(aPos);
|
||
await tester.pump();
|
||
await gesture.moveTo(gPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('g'));
|
||
|
||
// Release the pointer.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// If the position we tap during a drag start is on the collapsed selection, then
|
||
// we can move the cursor with a drag.
|
||
// Here we tap on '|g', where our selection was previously, and move to '|i'.
|
||
await gesture.down(gPos);
|
||
await tester.pump();
|
||
await gesture.moveTo(iPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('i'));
|
||
|
||
// End gesture and skip the magnifier hide animation, so it can release
|
||
// resources.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(dismissed, false);
|
||
// A horizontal drag directly on the TextField, but not on the current
|
||
// collapsed selection should allow for the Dismissible to be dismissed.
|
||
await tester.dragFrom(
|
||
tester.getRect(find.byType(TextField)).centerRight - const Offset(0.1, 0.0),
|
||
const Offset(-400.0, 0.0),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
expect(dismissed, true);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can move cursor when dragging, when tap is on collapsed selection (iOS) - ListView',
|
||
(WidgetTester tester) async {
|
||
// This is a regression test for
|
||
// https://github.com/flutter/flutter/issues/122519
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: ListView(
|
||
children: <Widget>[
|
||
TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: null,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc\ndef\nghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
|
||
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
|
||
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
|
||
|
||
// Tap on text field to gain focus, and set selection to '|a'. On iOS
|
||
// the selection is set to the word edge closest to the tap position.
|
||
// We await for kDoubleTapTimeout after the up event, so our next down event
|
||
// does not register as a double tap.
|
||
final TestGesture gesture = await tester.startGesture(aPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 0);
|
||
|
||
// If the position we tap during a drag start is on the collapsed selection, then
|
||
// we can move the cursor with a drag.
|
||
// Here we tap on '|a', where our selection was previously, and attempt move
|
||
// to '|g'.
|
||
await gesture.down(aPos);
|
||
await tester.pump();
|
||
await gesture.moveTo(gPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('g'));
|
||
|
||
// Release the pointer.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// If the position we tap during a drag start is on the collapsed selection, then
|
||
// we can move the cursor with a drag.
|
||
// Here we tap on '|g', where our selection was previously, and move to '|i'.
|
||
await gesture.down(gPos);
|
||
await tester.pump();
|
||
await gesture.moveTo(iPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('i'));
|
||
|
||
// End gesture and skip the magnifier hide animation, so it can release
|
||
// resources.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can move cursor when dragging (Android)',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
|
||
|
||
// Tap on text field to gain focus, and set selection to '|e'.
|
||
// We await for kDoubleTapTimeout after the up event, so our next down event
|
||
// does not register as a double tap.
|
||
final TestGesture gesture = await tester.startGesture(ePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('e'));
|
||
|
||
// Here we tap on '|d', and move to '|g'.
|
||
await gesture.down(textOffsetToPosition(tester, testValue.indexOf('d')));
|
||
await tester.pump();
|
||
await gesture.moveTo(gPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('g'));
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can move cursor when dragging (Android) - multiline',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: null,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc\ndef\nghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
|
||
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
|
||
|
||
// Tap on text field to gain focus, and set selection to '|a'.
|
||
// We await for kDoubleTapTimeout after the up event, so our next down event
|
||
// does not register as a double tap.
|
||
final TestGesture gesture = await tester.startGesture(aPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('a'));
|
||
|
||
// Here we tap on '|c', and move down to '|g'.
|
||
await gesture.down(textOffsetToPosition(tester, testValue.indexOf('c')));
|
||
await tester.pump();
|
||
await gesture.moveTo(gPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('g'));
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can move cursor when dragging (Android) - ListView',
|
||
(WidgetTester tester) async {
|
||
// This is a regression test for
|
||
// https://github.com/flutter/flutter/issues/122519
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: ListView(
|
||
children: <Widget>[
|
||
TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: null,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc\ndef\nghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
|
||
final Offset cPos = textOffsetToPosition(tester, testValue.indexOf('c'));
|
||
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
|
||
|
||
// Tap on text field to gain focus, and set selection to '|c'.
|
||
// We await for kDoubleTapTimeout after the up event, so our next down event
|
||
// does not register as a double tap.
|
||
final TestGesture gesture = await tester.startGesture(cPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('c'));
|
||
|
||
// Here we tap on '|a', and attempt move to '|g'. The cursor will not move
|
||
// because the `VerticalDragGestureRecognizer` in the scrollable will beat
|
||
// the `TapAndHorizontalDragGestureRecognizer` in the TextField. This is
|
||
// because moving from `|a` to `|g` is a completely vertical movement.
|
||
await gesture.down(aPos);
|
||
await tester.pump();
|
||
await gesture.moveTo(gPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('c'));
|
||
|
||
// Release the pointer.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// Here we tap on '|c', and move to '|g'. Unlike our previous attempt to
|
||
// drag to `|g`, this works because moving from `|c` to `|g` includes a
|
||
// horizontal movement so the `TapAndHorizontalDragGestureRecognizer`
|
||
// in TextField can beat the `VerticalDragGestureRecognizer` in the scrollable.
|
||
await gesture.down(cPos);
|
||
await tester.pump();
|
||
await gesture.moveTo(gPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('g'));
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
}),
|
||
);
|
||
|
||
testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async {
|
||
var selectionChangedCount = 0;
|
||
const testValue = 'abc def ghi';
|
||
final TextEditingController controller = _textEditingController(text: testValue);
|
||
|
||
controller.addListener(() {
|
||
selectionChangedCount++;
|
||
});
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
style: const TextStyle(fontSize: 10.0),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset cPos = textOffsetToPosition(tester, 2); // Index of 'c'.
|
||
final Offset gPos = textOffsetToPosition(tester, 8); // Index of 'g'.
|
||
final Offset hPos = textOffsetToPosition(tester, 9); // Index of 'h'.
|
||
|
||
// Drag from 'c' to 'g'.
|
||
final TestGesture gesture = await tester.startGesture(cPos, kind: PointerDeviceKind.mouse);
|
||
await tester.pump();
|
||
await gesture.moveTo(gPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(selectionChangedCount, isNonZero);
|
||
selectionChangedCount = 0;
|
||
expect(controller.selection.baseOffset, 2);
|
||
expect(controller.selection.extentOffset, 8);
|
||
|
||
// Tiny movement shouldn't cause text selection to change.
|
||
await gesture.moveTo(gPos + const Offset(2.0, 0.0));
|
||
await tester.pumpAndSettle();
|
||
expect(selectionChangedCount, 0);
|
||
|
||
// Now a text selection change will occur after a significant movement.
|
||
await gesture.moveTo(hPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(selectionChangedCount, 1);
|
||
expect(controller.selection.baseOffset, 2);
|
||
expect(controller.selection.extentOffset, 9);
|
||
});
|
||
|
||
testWidgets('Dragging in opposite direction also works', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
|
||
|
||
final TestGesture gesture = await tester.startGesture(gPos, kind: PointerDeviceKind.mouse);
|
||
await tester.pump();
|
||
await gesture.moveTo(ePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, testValue.indexOf('g'));
|
||
expect(controller.selection.extentOffset, testValue.indexOf('e'));
|
||
});
|
||
|
||
testWidgets('Slow mouse dragging also selects text', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
|
||
|
||
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
|
||
await tester.pump(const Duration(seconds: 2));
|
||
await gesture.moveTo(gPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
|
||
expect(controller.selection.baseOffset, testValue.indexOf('e'));
|
||
expect(controller.selection.extentOffset, testValue.indexOf('g'));
|
||
});
|
||
|
||
testWidgets(
|
||
'Can drag handles to change selection on Apple platforms',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Double tap the 'e' to select 'def'.
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
// The first tap.
|
||
TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
|
||
// The second tap.
|
||
await gesture.down(ePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
final TextSelection selection = controller.selection;
|
||
expect(selection.baseOffset, 4);
|
||
expect(selection.extentOffset, 7);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Drag the right handle 2 letters to the right.
|
||
// We use a small offset because the endpoint is on the very corner
|
||
// of the handle.
|
||
Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
|
||
Offset newHandlePos = textOffsetToPosition(tester, testValue.length);
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 11);
|
||
|
||
// Drag the left handle 2 letters to the left.
|
||
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
|
||
newHandlePos = textOffsetToPosition(tester, 2);
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
switch (defaultTargetPlatform) {
|
||
// On Apple platforms, dragging the base handle makes it the extent.
|
||
case TargetPlatform.iOS:
|
||
case TargetPlatform.macOS:
|
||
expect(controller.selection.baseOffset, 11);
|
||
expect(controller.selection.extentOffset, 2);
|
||
case TargetPlatform.android:
|
||
case TargetPlatform.fuchsia:
|
||
case TargetPlatform.linux:
|
||
case TargetPlatform.windows:
|
||
expect(controller.selection.baseOffset, 2);
|
||
expect(controller.selection.extentOffset, 11);
|
||
}
|
||
|
||
// Drag the left handle 2 letters to the left again.
|
||
endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
|
||
newHandlePos = textOffsetToPosition(tester, 0);
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.iOS:
|
||
case TargetPlatform.macOS:
|
||
// The left handle was already the extent, and it remains so.
|
||
expect(controller.selection.baseOffset, 11);
|
||
expect(controller.selection.extentOffset, 0);
|
||
case TargetPlatform.android:
|
||
case TargetPlatform.fuchsia:
|
||
case TargetPlatform.linux:
|
||
case TargetPlatform.windows:
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 11);
|
||
}
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can drag handles to change selection on non-Apple platforms',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Long press the 'e' to select 'def'.
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
|
||
await tester.pump(const Duration(seconds: 2));
|
||
await gesture.up();
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
|
||
final TextSelection selection = controller.selection;
|
||
expect(selection.baseOffset, 4);
|
||
expect(selection.extentOffset, 7);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Drag the right handle 2 letters to the right.
|
||
// We use a small offset because the endpoint is on the very corner
|
||
// of the handle.
|
||
Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
|
||
Offset newHandlePos = textOffsetToPosition(tester, testValue.length);
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 11);
|
||
|
||
// Drag the left handle 2 letters to the left.
|
||
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
|
||
newHandlePos = textOffsetToPosition(tester, 2);
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
switch (defaultTargetPlatform) {
|
||
// On Apple platforms, dragging the base handle makes it the extent.
|
||
case TargetPlatform.iOS:
|
||
case TargetPlatform.macOS:
|
||
expect(controller.selection.baseOffset, 11);
|
||
expect(controller.selection.extentOffset, 2);
|
||
case TargetPlatform.android:
|
||
case TargetPlatform.fuchsia:
|
||
case TargetPlatform.linux:
|
||
case TargetPlatform.windows:
|
||
expect(controller.selection.baseOffset, 2);
|
||
expect(controller.selection.extentOffset, 11);
|
||
}
|
||
|
||
// Drag the left handle 2 letters to the left again.
|
||
endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
|
||
newHandlePos = textOffsetToPosition(tester, 0);
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.iOS:
|
||
case TargetPlatform.macOS:
|
||
// The left handle was already the extent, and it remains so.
|
||
expect(controller.selection.baseOffset, 11);
|
||
expect(controller.selection.extentOffset, 0);
|
||
case TargetPlatform.android:
|
||
case TargetPlatform.fuchsia:
|
||
case TargetPlatform.linux:
|
||
case TargetPlatform.windows:
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 11);
|
||
}
|
||
},
|
||
variant: TargetPlatformVariant.all(
|
||
excluding: <TargetPlatform>{TargetPlatform.iOS, TargetPlatform.macOS},
|
||
),
|
||
);
|
||
|
||
testWidgets(
|
||
'assertion error is not thrown when attempting to drag both selection handles',
|
||
(WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/168578.
|
||
final controller = TextEditingController(text: 'abc def ghi');
|
||
addTearDown(controller.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: Center(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
style: const TextStyle(fontSize: 10.0),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Double tap on 'e' to select 'def'.
|
||
final Offset ePos = textOffsetToPosition(tester, 5);
|
||
await tester.tapAt(ePos, pointer: 7);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection.isCollapsed, isTrue);
|
||
expect(controller.selection.baseOffset, 5);
|
||
await tester.tapAt(ePos, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 7);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Drag the end handle to 'g'.
|
||
final Offset endHandlePos = endpoints[1].point + const Offset(1.0, 1.0);
|
||
Offset newHandlePos = textOffsetToPosition(tester, 9); // Position of 'g'.
|
||
final TestGesture endHandleGesture = await tester.startGesture(endHandlePos, pointer: 7);
|
||
await tester.pump();
|
||
await endHandleGesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 9);
|
||
|
||
// Attempt to drag the start handle to the start of the text.
|
||
final Offset startHandlePos = endpoints[0].point + const Offset(1.0, 1.0);
|
||
newHandlePos = textOffsetToPosition(tester, 0);
|
||
final TestGesture startHandleGesture = await tester.startGesture(startHandlePos, pointer: 8);
|
||
await tester.pump();
|
||
await startHandleGesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await startHandleGesture.up();
|
||
await tester.pump();
|
||
|
||
// Drag the end handle to the end of the text after releasing the start handle.
|
||
newHandlePos = textOffsetToPosition(tester, 11); // Position of 'i'.
|
||
await tester.pump();
|
||
await endHandleGesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await endHandleGesture.up();
|
||
await tester.pump();
|
||
|
||
expect(tester.takeException(), isNull);
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 11);
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can only drag one selection handle at a time on iOS',
|
||
(WidgetTester tester) async {
|
||
final controller = TextEditingController(text: 'abc def ghi');
|
||
addTearDown(controller.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: Center(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
style: const TextStyle(fontSize: 10.0),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Double tap on 'e' to select 'def'.
|
||
final Offset ePos = textOffsetToPosition(tester, 5);
|
||
await tester.tapAt(ePos, pointer: 7);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection.isCollapsed, isTrue);
|
||
expect(controller.selection.baseOffset, 7);
|
||
await tester.tapAt(ePos, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 7);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Drag the end handle to the end of the text.
|
||
final Offset endHandlePos = endpoints[1].point;
|
||
Offset newHandlePos = textOffsetToPosition(tester, 11); // Position of 'i'.
|
||
final TestGesture endHandleGesture = await tester.startGesture(endHandlePos, pointer: 7);
|
||
await tester.pump();
|
||
await endHandleGesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 11);
|
||
|
||
// Attempt to drag the start handle to the start of the text.
|
||
final Offset startHandlePos = endpoints[0].point;
|
||
newHandlePos = textOffsetToPosition(tester, 0);
|
||
final TestGesture startHandleGesture = await tester.startGesture(startHandlePos, pointer: 8);
|
||
await tester.pump();
|
||
await startHandleGesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await startHandleGesture.up();
|
||
await endHandleGesture.up();
|
||
await tester.pump();
|
||
|
||
// The start handle does not cause the selection to change.
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 11);
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can only drag one selection handle at a time on Android web',
|
||
(WidgetTester tester) async {
|
||
final controller = TextEditingController(text: 'abc def ghi');
|
||
addTearDown(controller.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: Center(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
style: const TextStyle(fontSize: 10.0),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Double tap on 'e' to select 'def'.
|
||
final Offset ePos = textOffsetToPosition(tester, 5);
|
||
await tester.tapAt(ePos, pointer: 7);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection.isCollapsed, isTrue);
|
||
expect(controller.selection.baseOffset, 5);
|
||
await tester.tapAt(ePos, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 7);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Drag the end handle to the end of the text.
|
||
final Offset endHandlePos = endpoints[1].point + const Offset(1.0, 1.0);
|
||
Offset newHandlePos = textOffsetToPosition(tester, 11); // Position of 'i'.
|
||
final TestGesture endHandleGesture = await tester.startGesture(endHandlePos, pointer: 7);
|
||
await tester.pump();
|
||
await endHandleGesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 11);
|
||
|
||
// Attempt to drag the start handle to the start of the text.
|
||
final Offset startHandlePos = endpoints[0].point + const Offset(1.0, 1.0);
|
||
newHandlePos = textOffsetToPosition(tester, 0);
|
||
final TestGesture startHandleGesture = await tester.startGesture(startHandlePos, pointer: 8);
|
||
await tester.pump();
|
||
await startHandleGesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await startHandleGesture.up();
|
||
await endHandleGesture.up();
|
||
await tester.pump();
|
||
|
||
// Moving the start handle does not change the selection.
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 11);
|
||
},
|
||
skip: !kIsWeb, // [intended] on web only one selection handle can be dragged at a time.
|
||
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can drag both selection handles at a time on Android',
|
||
(WidgetTester tester) async {
|
||
final controller = TextEditingController(text: 'abc def ghi');
|
||
addTearDown(controller.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: Center(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
style: const TextStyle(fontSize: 10.0),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Double tap on 'e' to select 'def'.
|
||
final Offset ePos = textOffsetToPosition(tester, 5);
|
||
await tester.tapAt(ePos, pointer: 7);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection.isCollapsed, isTrue);
|
||
expect(controller.selection.baseOffset, 5);
|
||
await tester.tapAt(ePos, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 7);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Drag the end handle to the end of the text.
|
||
final Offset endHandlePos = endpoints[1].point + const Offset(1.0, 1.0);
|
||
Offset newHandlePos = textOffsetToPosition(tester, 11); // Position of 'i'.
|
||
final TestGesture endHandleGesture = await tester.startGesture(endHandlePos, pointer: 7);
|
||
await tester.pump();
|
||
await endHandleGesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 11);
|
||
|
||
// Attempt to drag the start handle to the start of the text.
|
||
final Offset startHandlePos = endpoints[0].point + const Offset(1.0, 1.0);
|
||
newHandlePos = textOffsetToPosition(tester, 0);
|
||
final TestGesture startHandleGesture = await tester.startGesture(startHandlePos, pointer: 8);
|
||
await tester.pump();
|
||
await startHandleGesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await startHandleGesture.up();
|
||
await endHandleGesture.up();
|
||
await tester.pump();
|
||
|
||
// Moving the start handle changes the selection.
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 11);
|
||
},
|
||
skip: kIsWeb, // [intended] on web only one selection handle can be dragged at a time.
|
||
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
||
);
|
||
|
||
testWidgets('Can drag the left handle while the right handle remains off-screen', (
|
||
WidgetTester tester,
|
||
) async {
|
||
// Text is longer than textfield width.
|
||
const testValue = 'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
||
final TextEditingController controller = _textEditingController(text: testValue);
|
||
final scrollController = ScrollController();
|
||
addTearDown(scrollController.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: MediaQuery(
|
||
data: const MediaQueryData(size: Size(800.0, 600.0)),
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
scrollController: scrollController,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Double tap 'b' to show handles.
|
||
final Offset bPos = textOffsetToPosition(tester, testValue.indexOf('b'));
|
||
await tester.tapAt(bPos);
|
||
await tester.pump(kDoubleTapTimeout ~/ 2);
|
||
await tester.tapAt(bPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
final TextSelection selection = controller.selection;
|
||
expect(selection.baseOffset, 28);
|
||
expect(selection.extentOffset, testValue.length);
|
||
|
||
// Move to the left edge.
|
||
scrollController.jumpTo(0);
|
||
await tester.pumpAndSettle();
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Left handle should appear between textfield's left and right position.
|
||
final Offset textFieldLeftPosition = tester.getTopLeft(find.byType(TextField));
|
||
expect(endpoints[0].point.dx - textFieldLeftPosition.dx, isPositive);
|
||
final Offset textFieldRightPosition = tester.getTopRight(find.byType(TextField));
|
||
expect(textFieldRightPosition.dx - endpoints[0].point.dx, isPositive);
|
||
// Right handle should remain off-screen.
|
||
expect(endpoints[1].point.dx - textFieldRightPosition.dx, isPositive);
|
||
|
||
// Drag the left handle to the right by 25 offset.
|
||
const toOffset = 25;
|
||
final double beforeScrollOffset = scrollController.offset;
|
||
final Offset handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
|
||
final Offset newHandlePos = textOffsetToPosition(tester, toOffset);
|
||
final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.iOS:
|
||
case TargetPlatform.macOS:
|
||
// On Apple platforms, dragging the base handle makes it the extent.
|
||
expect(controller.selection.baseOffset, testValue.length);
|
||
expect(controller.selection.extentOffset, toOffset);
|
||
case TargetPlatform.android:
|
||
case TargetPlatform.fuchsia:
|
||
case TargetPlatform.linux:
|
||
case TargetPlatform.windows:
|
||
expect(controller.selection.baseOffset, toOffset);
|
||
expect(controller.selection.extentOffset, testValue.length);
|
||
}
|
||
|
||
// The scroll area of text field should not move.
|
||
expect(scrollController.offset, beforeScrollOffset);
|
||
});
|
||
|
||
testWidgets('Can drag the right handle while the left handle remains off-screen', (
|
||
WidgetTester tester,
|
||
) async {
|
||
// Text is longer than textfield width.
|
||
const testValue = 'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
||
final TextEditingController controller = _textEditingController(text: testValue);
|
||
final scrollController = ScrollController();
|
||
addTearDown(scrollController.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: MediaQuery(
|
||
data: const MediaQueryData(size: Size(800.0, 600.0)),
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
scrollController: scrollController,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Double tap 'a' to show handles.
|
||
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
|
||
await tester.tapAt(aPos);
|
||
await tester.pump(kDoubleTapTimeout ~/ 2);
|
||
await tester.tapAt(aPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
final TextSelection selection = controller.selection;
|
||
expect(selection.baseOffset, 0);
|
||
expect(selection.extentOffset, 27);
|
||
|
||
// Move to the right edge.
|
||
scrollController.jumpTo(800);
|
||
await tester.pumpAndSettle();
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Right handle should appear between textfield's left and right position.
|
||
final Offset textFieldLeftPosition = tester.getTopLeft(find.byType(TextField));
|
||
expect(endpoints[1].point.dx - textFieldLeftPosition.dx, isPositive);
|
||
final Offset textFieldRightPosition = tester.getTopRight(find.byType(TextField));
|
||
expect(textFieldRightPosition.dx - endpoints[1].point.dx, isPositive);
|
||
// Left handle should remain off-screen.
|
||
expect(endpoints[0].point.dx, isNegative);
|
||
|
||
// Drag the right handle to the left by 50 offset.
|
||
const toOffset = 50;
|
||
final double beforeScrollOffset = scrollController.offset;
|
||
final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
|
||
final Offset newHandlePos = textOffsetToPosition(tester, toOffset);
|
||
final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, toOffset);
|
||
|
||
// The scroll area of text field should not move.
|
||
expect(scrollController.offset, beforeScrollOffset);
|
||
});
|
||
|
||
testWidgets('Drag handles trigger feedback', (WidgetTester tester) async {
|
||
final feedback = FeedbackTester();
|
||
addTearDown(feedback.dispose);
|
||
final TextEditingController controller = _textEditingController();
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
expect(feedback.hapticCount, 0);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Long press the 'e' to select 'def'.
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
|
||
await tester.pump(const Duration(seconds: 2));
|
||
await gesture.up();
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
|
||
final TextSelection selection = controller.selection;
|
||
expect(selection.baseOffset, 4);
|
||
expect(selection.extentOffset, 7);
|
||
expect(feedback.hapticCount, 1);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Drag the right handle 2 letters to the right.
|
||
// Use a small offset because the endpoint is on the very corner
|
||
// of the handle.
|
||
final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
|
||
final Offset newHandlePos = textOffsetToPosition(tester, testValue.length);
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 11);
|
||
expect(feedback.hapticCount, 2);
|
||
});
|
||
|
||
testWidgets('Dragging a collapsed handle should trigger feedback.', (WidgetTester tester) async {
|
||
final feedback = FeedbackTester();
|
||
addTearDown(feedback.dispose);
|
||
final TextEditingController controller = _textEditingController();
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
expect(feedback.hapticCount, 0);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Tap the 'e' to bring up a collapsed handle.
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
|
||
final TextSelection selection = controller.selection;
|
||
expect(selection.baseOffset, 5);
|
||
expect(selection.extentOffset, 5);
|
||
expect(feedback.hapticCount, 0);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 1);
|
||
|
||
// Drag the right handle 3 letters to the right.
|
||
// Use a small offset because the endpoint is on the very corner
|
||
// of the handle.
|
||
final Offset handlePos = endpoints[0].point + const Offset(1.0, 1.0);
|
||
final Offset newHandlePos = textOffsetToPosition(tester, testValue.indexOf('g'));
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 8);
|
||
expect(feedback.hapticCount, 1);
|
||
});
|
||
|
||
testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Long press the 'e' to select 'def'.
|
||
final Offset ePos = textOffsetToPosition(tester, 5); // Position before 'e'.
|
||
TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
|
||
await tester.pump(const Duration(seconds: 2));
|
||
await gesture.up();
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
|
||
final TextSelection selection = controller.selection;
|
||
expect(selection.baseOffset, 4);
|
||
expect(selection.extentOffset, 7);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Drag the right handle until there's only 1 char selected.
|
||
// We use a small offset because the endpoint is on the very corner
|
||
// of the handle.
|
||
final Offset handlePos = endpoints[1].point + const Offset(4.0, 0.0);
|
||
Offset newHandlePos = textOffsetToPosition(tester, 5); // Position before 'e'.
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 5);
|
||
|
||
newHandlePos = textOffsetToPosition(tester, 2); // Position before 'c'.
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 4);
|
||
// The selection doesn't move beyond the left handle. There's always at
|
||
// least 1 char selected.
|
||
expect(controller.selection.extentOffset, 5);
|
||
});
|
||
|
||
testWidgets(
|
||
'Dragging between multiple lines keeps the contact point at the same place on the handle on Android',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
// 11 first line, 19 second line, 17 third line = length 49
|
||
text: 'a big house\njumped over a mouse\nOne more line yay',
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: 3,
|
||
minLines: 3,
|
||
),
|
||
),
|
||
);
|
||
|
||
// Double tap to select 'over'.
|
||
final Offset pos = textOffsetToPosition(tester, controller.text.indexOf('v'));
|
||
// The first tap.
|
||
TestGesture gesture = await tester.startGesture(pos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
|
||
// The second tap.
|
||
await gesture.down(pos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
final TextSelection selection = controller.selection;
|
||
expect(controller.selection, const TextSelection(baseOffset: 19, extentOffset: 23));
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Drag the right handle 4 letters to the right.
|
||
// The adjustment moves the tap from the text position to the handle.
|
||
const endHandleAdjustment = Offset(1.0, 6.0);
|
||
Offset handlePos = endpoints[1].point + endHandleAdjustment;
|
||
Offset newHandlePos = textOffsetToPosition(tester, 27) + endHandleAdjustment;
|
||
await tester.pump();
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 19, extentOffset: 27));
|
||
|
||
// Drag the right handle 1 line down.
|
||
endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
handlePos = endpoints[1].point + endHandleAdjustment;
|
||
final toNextLine = Offset(0.0, findRenderEditable(tester).preferredLineHeight + 3.0);
|
||
newHandlePos = handlePos + toNextLine;
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 19, extentOffset: 47));
|
||
|
||
// Drag the right handle back up 1 line.
|
||
endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
handlePos = endpoints[1].point + endHandleAdjustment;
|
||
newHandlePos = handlePos - toNextLine;
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 19, extentOffset: 27));
|
||
|
||
// Drag the left handle 4 letters to the left.
|
||
// The adjustment moves the tap from the text position to the handle.
|
||
const startHandleAdjustment = Offset(-1.0, 6.0);
|
||
endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
handlePos = endpoints[0].point + startHandleAdjustment;
|
||
newHandlePos = textOffsetToPosition(tester, 15) + startHandleAdjustment;
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 15, extentOffset: 27));
|
||
|
||
// Drag the left handle 1 line up.
|
||
endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
handlePos = endpoints[0].point + startHandleAdjustment;
|
||
newHandlePos = handlePos - toNextLine;
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 3, extentOffset: 27));
|
||
|
||
// Drag the left handle 1 line back down.
|
||
endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
handlePos = endpoints[0].point + startHandleAdjustment;
|
||
newHandlePos = handlePos + toNextLine;
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 15, extentOffset: 27));
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.android}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Dragging between multiple lines keeps the contact point at the same place on the handle on iOS',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
// 11 first line, 19 second line, 17 third line = length 49
|
||
text: 'a big house\njumped over a mouse\nOne more line yay',
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: 3,
|
||
minLines: 3,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Double tap to select 'over'.
|
||
final Offset pos = textOffsetToPosition(tester, controller.text.indexOf('v'));
|
||
// The first tap.
|
||
TestGesture gesture = await tester.startGesture(pos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
|
||
// The second tap.
|
||
await gesture.down(pos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
final TextSelection selection = controller.selection;
|
||
expect(controller.selection, const TextSelection(baseOffset: 19, extentOffset: 23));
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Drag the right handle 4 letters to the right.
|
||
// The adjustment moves the tap from the text position to the handle.
|
||
const endHandleAdjustment = Offset(1.0, 6.0);
|
||
Offset handlePos = endpoints[1].point + endHandleAdjustment;
|
||
Offset newHandlePos = textOffsetToPosition(tester, 27) + endHandleAdjustment;
|
||
await tester.pump();
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 19, extentOffset: 27));
|
||
|
||
// Drag the right handle 1 line down.
|
||
endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
handlePos = endpoints[1].point + endHandleAdjustment;
|
||
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
|
||
final toNextLine = Offset(0.0, lineHeight + 3.0);
|
||
newHandlePos = handlePos + toNextLine;
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 19, extentOffset: 47));
|
||
|
||
// Drag the right handle back up 1 line.
|
||
endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
handlePos = endpoints[1].point + endHandleAdjustment;
|
||
newHandlePos = handlePos - toNextLine;
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 19, extentOffset: 27));
|
||
|
||
// Drag the left handle 4 letters to the left.
|
||
// The adjustment moves the tap from the text position to the handle.
|
||
final startHandleAdjustment = Offset(-1.0, -lineHeight + 6.0);
|
||
endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
handlePos = endpoints[0].point + startHandleAdjustment;
|
||
newHandlePos = textOffsetToPosition(tester, 15) + startHandleAdjustment;
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
// On Apple platforms, dragging the base handle makes it the extent.
|
||
expect(controller.selection, const TextSelection(baseOffset: 27, extentOffset: 15));
|
||
|
||
// Drag the left handle 1 line up.
|
||
endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
handlePos = endpoints[0].point + startHandleAdjustment;
|
||
// Move handle a sufficient global distance so it can be considered a drag
|
||
// by the selection handle's [PanGestureRecognizer].
|
||
newHandlePos = handlePos - (toNextLine * 2);
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 27, extentOffset: 3));
|
||
|
||
// Drag the left handle 1 line back down.
|
||
endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
handlePos = endpoints[0].point + startHandleAdjustment;
|
||
newHandlePos = handlePos + toNextLine;
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
// Move handle up a small amount before dragging it down so the total global
|
||
// distance travelled can be accepted by the selection handle's [PanGestureRecognizer] as a drag.
|
||
// This way it can declare itself the winner before the [TapAndDragGestureRecognizer] that
|
||
// is on the selection overlay.
|
||
await gesture.moveTo(handlePos - toNextLine);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 27, extentOffset: 15));
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets(
|
||
"dragging caret within a word doesn't affect composing region",
|
||
(WidgetTester tester) async {
|
||
const testValue = 'abc def ghi';
|
||
final controller = TextEditingController.fromValue(
|
||
const TextEditingValue(
|
||
text: testValue,
|
||
selection: TextSelection(baseOffset: 4, extentOffset: 4, affinity: TextAffinity.upstream),
|
||
composing: TextRange(start: 4, end: 7),
|
||
),
|
||
);
|
||
addTearDown(controller.dispose);
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
);
|
||
|
||
await tester.pump();
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.value.composing.start, 4);
|
||
expect(controller.value.composing.end, 7);
|
||
|
||
// Tap the caret to show the handle.
|
||
final Offset ePos = textOffsetToPosition(tester, 4);
|
||
await tester.tapAt(ePos);
|
||
await tester.pumpAndSettle();
|
||
|
||
final TextSelection selection = controller.selection;
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(selection.baseOffset, 4);
|
||
expect(controller.value.composing.start, 4);
|
||
expect(controller.value.composing.end, 7);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 1);
|
||
|
||
// Drag the right handle 2 letters to the right.
|
||
// We use a small offset because the endpoint is on the very corner
|
||
// of the handle.
|
||
final Offset handlePos = endpoints[0].point + const Offset(1.0, 1.0);
|
||
final Offset newHandlePos = textOffsetToPosition(tester, 7);
|
||
final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 7);
|
||
expect(controller.value.composing.start, 4);
|
||
expect(controller.value.composing.end, 7);
|
||
},
|
||
skip: kIsWeb, // [intended] text selection is handled by the browser
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.iOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can use selection toolbar',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller)));
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Tap the selection handle to bring up the "paste / select all" menu.
|
||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
RenderEditable renderEditable = findRenderEditable(tester);
|
||
List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
// Tapping on the part of the handle's GestureDetector where it overlaps
|
||
// with the text itself does not show the menu, so add a small vertical
|
||
// offset to tap below the text.
|
||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 13.0));
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
|
||
// Select all should select all the text.
|
||
await tester.tap(find.text('Select all'));
|
||
await tester.pump();
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, testValue.length);
|
||
|
||
// Copy should reset the selection.
|
||
await tester.tap(find.text('Copy'));
|
||
await skipPastScrollingAnimation(tester);
|
||
expect(controller.selection.isCollapsed, true);
|
||
|
||
// Tap again to bring back the menu.
|
||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
||
await tester.pump();
|
||
// Allow time for handle to appear and double tap to time out.
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('e'));
|
||
expect(controller.selection.extentOffset, testValue.indexOf('e'));
|
||
renderEditable = findRenderEditable(tester);
|
||
endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('e'));
|
||
expect(controller.selection.extentOffset, testValue.indexOf('e'));
|
||
|
||
// Paste right before the 'e'.
|
||
await tester.tap(find.text('Paste'));
|
||
await tester.pump();
|
||
expect(controller.text, 'abc d${testValue}ef ghi');
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
// Show the selection menu at the given index into the text by tapping to
|
||
// place the cursor and then tapping on the handle.
|
||
Future<void> showSelectionMenuAt(
|
||
WidgetTester tester,
|
||
TextEditingController controller,
|
||
int index,
|
||
) async {
|
||
await tester.tapAt(tester.getCenter(find.byType(EditableText)));
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
expect(find.text('Select all'), findsNothing);
|
||
|
||
// Tap the selection handle to bring up the "paste / select all" menu for
|
||
// the last line of text.
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
// Tapping on the part of the handle's GestureDetector where it overlaps
|
||
// with the text itself does not show the menu, so add a small vertical
|
||
// offset to tap below the text.
|
||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 13.0));
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
}
|
||
|
||
testWidgets(
|
||
'Check the toolbar appears below the TextField when there is not enough space above the TextField to show it',
|
||
(WidgetTester tester) async {
|
||
// This is a regression test for
|
||
// https://github.com/flutter/flutter/issues/29808
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Padding(
|
||
padding: const EdgeInsets.all(30.0),
|
||
child: TextField(controller: controller),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
await showSelectionMenuAt(tester, controller, testValue.indexOf('e'));
|
||
|
||
// Verify the selection toolbar position is below the text.
|
||
Offset toolbarTopLeft = tester.getTopLeft(find.text('Select all'));
|
||
Offset textFieldTopLeft = tester.getTopLeft(find.byType(TextField));
|
||
expect(textFieldTopLeft.dy, lessThan(toolbarTopLeft.dy));
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Padding(
|
||
padding: const EdgeInsets.all(150.0),
|
||
child: TextField(controller: controller),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
await showSelectionMenuAt(tester, controller, testValue.indexOf('e'));
|
||
|
||
// Verify the selection toolbar position
|
||
toolbarTopLeft = tester.getTopLeft(find.text('Select all'));
|
||
textFieldTopLeft = tester.getTopLeft(find.byType(TextField));
|
||
expect(toolbarTopLeft.dy, lessThan(textFieldTopLeft.dy));
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'the toolbar adjusts its position above/below when bottom inset changes',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 48.0),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: <Widget>[
|
||
IntrinsicHeight(
|
||
child: TextField(controller: controller, expands: true, maxLines: null),
|
||
),
|
||
const SizedBox(height: 325.0),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
await showSelectionMenuAt(tester, controller, testValue.indexOf('e'));
|
||
|
||
// Verify the selection toolbar position is above the text.
|
||
expect(find.text('Select all'), findsOneWidget);
|
||
Offset toolbarTopLeft = tester.getTopLeft(find.text('Select all'));
|
||
Offset textFieldTopLeft = tester.getTopLeft(find.byType(TextField));
|
||
expect(toolbarTopLeft.dy, lessThan(textFieldTopLeft.dy));
|
||
|
||
// Add a viewInset tall enough to push the field to the top, where there
|
||
// is no room to display the toolbar above. This is similar to when the
|
||
// keyboard is shown.
|
||
tester.view.viewInsets = const FakeViewPadding(bottom: 500.0);
|
||
addTearDown(tester.view.reset);
|
||
await tester.pumpAndSettle();
|
||
|
||
// Verify the selection toolbar position is below the text.
|
||
toolbarTopLeft = tester.getTopLeft(find.text('Select all'));
|
||
textFieldTopLeft = tester.getTopLeft(find.byType(TextField));
|
||
expect(toolbarTopLeft.dy, greaterThan(textFieldTopLeft.dy));
|
||
|
||
// Remove the viewInset, as if the keyboard were hidden.
|
||
tester.view.resetViewInsets();
|
||
await tester.pumpAndSettle();
|
||
|
||
// Verify the selection toolbar position is below the text.
|
||
toolbarTopLeft = tester.getTopLeft(find.text('Select all'));
|
||
textFieldTopLeft = tester.getTopLeft(find.byType(TextField));
|
||
expect(toolbarTopLeft.dy, lessThan(textFieldTopLeft.dy));
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Toolbar appears in the right places in multiline inputs',
|
||
(WidgetTester tester) async {
|
||
// This is a regression test for
|
||
// https://github.com/flutter/flutter/issues/36749
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Scaffold(
|
||
body: Padding(
|
||
padding: const EdgeInsets.all(30.0),
|
||
child: TextField(controller: controller, minLines: 6, maxLines: 6),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Select all'), findsNothing);
|
||
const testValue = 'abc\ndef\nghi\njkl\nmno\npqr';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Show the selection menu on the first line and verify the selection
|
||
// toolbar position is below the first line.
|
||
await showSelectionMenuAt(tester, controller, testValue.indexOf('c'));
|
||
expect(find.text('Select all'), findsOneWidget);
|
||
final Offset firstLineToolbarTopLeft = tester.getTopLeft(find.text('Select all'));
|
||
final Offset firstLineTopLeft = textOffsetToPosition(tester, testValue.indexOf('a'));
|
||
expect(firstLineTopLeft.dy, lessThan(firstLineToolbarTopLeft.dy));
|
||
|
||
// Show the selection menu on the second to last line and verify the
|
||
// selection toolbar position is above that line and above the first
|
||
// line's toolbar.
|
||
await showSelectionMenuAt(tester, controller, testValue.indexOf('o'));
|
||
expect(find.text('Select all'), findsOneWidget);
|
||
final Offset penultimateLineToolbarTopLeft = tester.getTopLeft(find.text('Select all'));
|
||
final Offset penultimateLineTopLeft = textOffsetToPosition(tester, testValue.indexOf('p'));
|
||
expect(penultimateLineToolbarTopLeft.dy, lessThan(penultimateLineTopLeft.dy));
|
||
expect(penultimateLineToolbarTopLeft.dy, lessThan(firstLineToolbarTopLeft.dy));
|
||
|
||
// Show the selection menu on the last line and verify the selection
|
||
// toolbar position is above that line and below the position of the
|
||
// second to last line's toolbar.
|
||
await showSelectionMenuAt(tester, controller, testValue.indexOf('r'));
|
||
expect(find.text('Select all'), findsOneWidget);
|
||
final Offset lastLineToolbarTopLeft = tester.getTopLeft(find.text('Select all'));
|
||
final Offset lastLineTopLeft = textOffsetToPosition(tester, testValue.indexOf('p'));
|
||
expect(lastLineToolbarTopLeft.dy, lessThan(lastLineTopLeft.dy));
|
||
expect(lastLineToolbarTopLeft.dy, greaterThan(penultimateLineToolbarTopLeft.dy));
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Selection toolbar fades in',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller)));
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Tap the selection handle to bring up the "paste / select all" menu.
|
||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
||
await tester.pump();
|
||
// Allow time for the handle to appear and for a double tap to time out.
|
||
await tester.pump(const Duration(milliseconds: 600));
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||
// Pump an extra frame to allow the selection menu to read the clipboard.
|
||
await tester.pump();
|
||
await tester.pump();
|
||
|
||
// Toolbar should fade in. Starting at 0% opacity.
|
||
expect(find.text('Select all'), findsOneWidget);
|
||
final Element target = tester.element(find.text('Select all'));
|
||
final FadeTransition opacity = target.findAncestorWidgetOfExactType<FadeTransition>()!;
|
||
expect(opacity.opacity.value, equals(0.0));
|
||
|
||
// Still fading in.
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
final FadeTransition opacity2 = target.findAncestorWidgetOfExactType<FadeTransition>()!;
|
||
expect(opacity, same(opacity2));
|
||
expect(opacity.opacity.value, greaterThan(0.0));
|
||
expect(opacity.opacity.value, lessThan(1.0));
|
||
|
||
// End the test here to ensure the animation is properly disposed of.
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets('An obscured TextField is selectable by default', (WidgetTester tester) async {
|
||
// This is a regression test for
|
||
// https://github.com/flutter/flutter/issues/32845
|
||
|
||
final TextEditingController controller = _textEditingController();
|
||
Widget buildFrame(bool obscureText) {
|
||
return overlay(
|
||
child: TextField(controller: controller, obscureText: obscureText),
|
||
);
|
||
}
|
||
|
||
// Obscure text and don't enable or disable selection.
|
||
await tester.pumpWidget(buildFrame(true));
|
||
await tester.enterText(find.byType(TextField), 'abcdefghi');
|
||
await skipPastScrollingAnimation(tester);
|
||
expect(controller.selection.isCollapsed, true);
|
||
|
||
// Long press does select text.
|
||
final Offset ePos = textOffsetToPosition(tester, 1);
|
||
await tester.longPressAt(ePos, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.isCollapsed, false);
|
||
});
|
||
|
||
testWidgets('An obscured TextField is not selectable when disabled', (WidgetTester tester) async {
|
||
// This is a regression test for
|
||
// https://github.com/flutter/flutter/issues/32845
|
||
|
||
final TextEditingController controller = _textEditingController();
|
||
Widget buildFrame(bool obscureText, bool enableInteractiveSelection) {
|
||
return overlay(
|
||
child: TextField(
|
||
controller: controller,
|
||
obscureText: obscureText,
|
||
enableInteractiveSelection: enableInteractiveSelection,
|
||
),
|
||
);
|
||
}
|
||
|
||
// Explicitly disabled selection on obscured text.
|
||
await tester.pumpWidget(buildFrame(true, false));
|
||
await tester.enterText(find.byType(TextField), 'abcdefghi');
|
||
await skipPastScrollingAnimation(tester);
|
||
expect(controller.selection.isCollapsed, true);
|
||
|
||
// Long press doesn't select text.
|
||
final Offset ePos2 = textOffsetToPosition(tester, 1);
|
||
await tester.longPressAt(ePos2, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.isCollapsed, true);
|
||
});
|
||
|
||
testWidgets('An obscured TextField is not selectable when read-only', (
|
||
WidgetTester tester,
|
||
) async {
|
||
// This is a regression test for
|
||
// https://github.com/flutter/flutter/issues/32845
|
||
|
||
final TextEditingController controller = _textEditingController();
|
||
Widget buildFrame(bool obscureText, bool readOnly) {
|
||
return overlay(
|
||
child: TextField(controller: controller, obscureText: obscureText, readOnly: readOnly),
|
||
);
|
||
}
|
||
|
||
// Explicitly disabled selection on obscured text that is read-only.
|
||
await tester.pumpWidget(buildFrame(true, true));
|
||
await tester.enterText(find.byType(TextField), 'abcdefghi');
|
||
await skipPastScrollingAnimation(tester);
|
||
expect(controller.selection.isCollapsed, true);
|
||
|
||
// Long press doesn't select text.
|
||
final Offset ePos2 = textOffsetToPosition(tester, 1);
|
||
await tester.longPressAt(ePos2, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.isCollapsed, true);
|
||
});
|
||
|
||
testWidgets('An obscured TextField is selected as one word', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller, obscureText: true)));
|
||
await tester.enterText(find.byType(TextField), 'abcde fghi');
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Long press does select text.
|
||
final Offset bPos = textOffsetToPosition(tester, 1);
|
||
await tester.longPressAt(bPos, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
final TextSelection selection = controller.selection;
|
||
expect(selection.isCollapsed, false);
|
||
expect(selection.baseOffset, 0);
|
||
expect(selection.extentOffset, 10);
|
||
});
|
||
|
||
testWidgets(
|
||
'An obscured TextField has correct default context menu',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller, obscureText: true)));
|
||
await tester.enterText(find.byType(TextField), 'abcde fghi');
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Long press to select text.
|
||
final Offset bPos = textOffsetToPosition(tester, 1);
|
||
await tester.longPressAt(bPos, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
|
||
// Should only have paste option when whole obscure text is selected.
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
expect(find.text('Copy'), findsNothing);
|
||
expect(find.text('Cut'), findsNothing);
|
||
expect(find.text('Select all'), findsNothing);
|
||
|
||
// Long press at the end
|
||
final Offset iPos = textOffsetToPosition(tester, 10);
|
||
final Offset slightRight = iPos + const Offset(30.0, 0.0);
|
||
await tester.longPressAt(slightRight, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
|
||
// Should have paste and select all options when collapse.
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
expect(find.text('Select all'), findsOneWidget);
|
||
expect(find.text('Copy'), findsNothing);
|
||
expect(find.text('Cut'), findsNothing);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'create selection overlay if none exists when toggleToolbar is called',
|
||
(WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/111660
|
||
final Widget testWidget = MaterialApp(
|
||
home: Scaffold(
|
||
appBar: AppBar(
|
||
title: const Text('Test'),
|
||
actions: <Widget>[
|
||
PopupMenuButton<String>(
|
||
itemBuilder: (BuildContext context) {
|
||
return <String>{'About'}.map((String value) {
|
||
return PopupMenuItem<String>(value: value, child: Text(value));
|
||
}).toList();
|
||
},
|
||
),
|
||
],
|
||
),
|
||
body: const TextField(),
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(testWidget);
|
||
|
||
// Tap on TextField.
|
||
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
|
||
final TestGesture gesture = await tester.startGesture(textFieldStart);
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// Tap on 3 dot menu.
|
||
await tester.tap(find.byType(PopupMenuButton<String>));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Tap on TextField.
|
||
await gesture.down(textFieldStart);
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// Tap on TextField again.
|
||
await tester.tapAt(textFieldStart);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(tester.takeException(), isNull);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets('TextField height with minLines unset', (WidgetTester tester) async {
|
||
await tester.pumpWidget(textFieldBuilder());
|
||
|
||
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
|
||
|
||
final RenderBox inputBox = findInputBox();
|
||
final Size emptyInputSize = inputBox.size;
|
||
|
||
await tester.enterText(find.byType(TextField), 'No wrapping here.');
|
||
await tester.pumpWidget(textFieldBuilder());
|
||
expect(findInputBox(), equals(inputBox));
|
||
expect(inputBox.size, equals(emptyInputSize));
|
||
|
||
// Even when entering multiline text, TextField doesn't grow. It's a single
|
||
// line input.
|
||
await tester.enterText(find.byType(TextField), kThreeLines);
|
||
await tester.pumpWidget(textFieldBuilder());
|
||
expect(findInputBox(), equals(inputBox));
|
||
expect(inputBox.size, equals(emptyInputSize));
|
||
|
||
// maxLines: 3 makes the TextField 3 lines tall
|
||
await tester.enterText(find.byType(TextField), '');
|
||
await tester.pumpWidget(textFieldBuilder(maxLines: 3));
|
||
expect(findInputBox(), equals(inputBox));
|
||
expect(inputBox.size.height, greaterThan(emptyInputSize.height));
|
||
expect(inputBox.size.width, emptyInputSize.width);
|
||
|
||
final Size threeLineInputSize = inputBox.size;
|
||
|
||
// Filling with 3 lines of text stays the same size
|
||
await tester.enterText(find.byType(TextField), kThreeLines);
|
||
await tester.pumpWidget(textFieldBuilder(maxLines: 3));
|
||
expect(findInputBox(), equals(inputBox));
|
||
expect(inputBox.size, threeLineInputSize);
|
||
|
||
// An extra line won't increase the size because we max at 3.
|
||
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
|
||
await tester.pumpWidget(textFieldBuilder(maxLines: 3));
|
||
expect(findInputBox(), equals(inputBox));
|
||
expect(inputBox.size, threeLineInputSize);
|
||
|
||
// But now it will... but it will max at four
|
||
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
|
||
await tester.pumpWidget(textFieldBuilder(maxLines: 4));
|
||
expect(findInputBox(), equals(inputBox));
|
||
expect(inputBox.size.height, greaterThan(threeLineInputSize.height));
|
||
expect(inputBox.size.width, threeLineInputSize.width);
|
||
|
||
final Size fourLineInputSize = inputBox.size;
|
||
|
||
// Now it won't max out until the end
|
||
await tester.enterText(find.byType(TextField), '');
|
||
await tester.pumpWidget(textFieldBuilder(maxLines: null));
|
||
expect(findInputBox(), equals(inputBox));
|
||
expect(inputBox.size, equals(emptyInputSize));
|
||
await tester.enterText(find.byType(TextField), kThreeLines);
|
||
await tester.pump();
|
||
expect(inputBox.size, equals(threeLineInputSize));
|
||
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
|
||
await tester.pump();
|
||
expect(inputBox.size.height, greaterThan(fourLineInputSize.height));
|
||
expect(inputBox.size.width, fourLineInputSize.width);
|
||
});
|
||
|
||
testWidgets('TextField height with minLines and maxLines', (WidgetTester tester) async {
|
||
await tester.pumpWidget(textFieldBuilder());
|
||
|
||
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
|
||
|
||
final RenderBox inputBox = findInputBox();
|
||
final Size emptyInputSize = inputBox.size;
|
||
|
||
await tester.enterText(find.byType(TextField), 'No wrapping here.');
|
||
await tester.pumpWidget(textFieldBuilder());
|
||
expect(findInputBox(), equals(inputBox));
|
||
expect(inputBox.size, equals(emptyInputSize));
|
||
|
||
// min and max set to same value locks height to value.
|
||
await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 3));
|
||
expect(findInputBox(), equals(inputBox));
|
||
expect(inputBox.size.height, greaterThan(emptyInputSize.height));
|
||
expect(inputBox.size.width, emptyInputSize.width);
|
||
|
||
final Size threeLineInputSize = inputBox.size;
|
||
|
||
// maxLines: null with minLines set grows beyond minLines
|
||
await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: null));
|
||
expect(findInputBox(), equals(inputBox));
|
||
expect(inputBox.size, threeLineInputSize);
|
||
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
|
||
await tester.pump();
|
||
expect(inputBox.size.height, greaterThan(threeLineInputSize.height));
|
||
expect(inputBox.size.width, threeLineInputSize.width);
|
||
|
||
// With minLines and maxLines set, input will expand through the range
|
||
await tester.enterText(find.byType(TextField), '');
|
||
await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 4));
|
||
expect(findInputBox(), equals(inputBox));
|
||
expect(inputBox.size, equals(threeLineInputSize));
|
||
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
|
||
await tester.pump();
|
||
expect(inputBox.size.height, greaterThan(threeLineInputSize.height));
|
||
expect(inputBox.size.width, threeLineInputSize.width);
|
||
|
||
// minLines can't be greater than maxLines.
|
||
expect(() async {
|
||
await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 2));
|
||
}, throwsAssertionError);
|
||
|
||
// maxLines defaults to 1 and can't be less than minLines
|
||
expect(() async {
|
||
await tester.pumpWidget(textFieldBuilder(minLines: 3));
|
||
}, throwsAssertionError);
|
||
});
|
||
|
||
testWidgets('Multiline text when wrapped in Expanded', (WidgetTester tester) async {
|
||
Widget expandedTextFieldBuilder({int? maxLines = 1, int? minLines, bool expands = false}) {
|
||
return boilerplate(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: <Widget>[
|
||
Expanded(
|
||
child: TextField(
|
||
key: textFieldKey,
|
||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||
maxLines: maxLines,
|
||
minLines: minLines,
|
||
expands: expands,
|
||
decoration: const InputDecoration(hintText: 'Placeholder'),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(expandedTextFieldBuilder());
|
||
|
||
RenderBox findBorder() {
|
||
return tester.renderObject(
|
||
find.descendant(
|
||
of: find.byType(InputDecorator),
|
||
matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_BorderContainer'),
|
||
),
|
||
);
|
||
}
|
||
|
||
final RenderBox border = findBorder();
|
||
|
||
// Without expanded: true and maxLines: null, the TextField does not expand
|
||
// to fill its parent when wrapped in an Expanded widget.
|
||
final Size unexpandedInputSize = border.size;
|
||
|
||
// It does expand to fill its parent when expands: true, maxLines: null, and
|
||
// it's wrapped in an Expanded widget.
|
||
await tester.pumpWidget(expandedTextFieldBuilder(expands: true, maxLines: null));
|
||
expect(border.size.height, greaterThan(unexpandedInputSize.height));
|
||
expect(border.size.width, unexpandedInputSize.width);
|
||
|
||
// min/maxLines that is not null and expands: true contradict each other.
|
||
expect(() async {
|
||
await tester.pumpWidget(expandedTextFieldBuilder(expands: true, maxLines: 4));
|
||
}, throwsAssertionError);
|
||
expect(() async {
|
||
await tester.pumpWidget(expandedTextFieldBuilder(expands: true, minLines: 1, maxLines: null));
|
||
}, throwsAssertionError);
|
||
});
|
||
|
||
// Regression test for https://github.com/flutter/flutter/pull/29093
|
||
testWidgets('Multiline text when wrapped in IntrinsicHeight', (WidgetTester tester) async {
|
||
final Key intrinsicHeightKey = UniqueKey();
|
||
Widget intrinsicTextFieldBuilder(bool wrapInIntrinsic) {
|
||
final textField = TextFormField(
|
||
key: textFieldKey,
|
||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||
maxLines: null,
|
||
decoration: const InputDecoration(counterText: 'I am counter'),
|
||
);
|
||
final Widget widget = wrapInIntrinsic
|
||
? IntrinsicHeight(key: intrinsicHeightKey, child: textField)
|
||
: textField;
|
||
return boilerplate(
|
||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[widget]),
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(intrinsicTextFieldBuilder(false));
|
||
expect(find.byKey(intrinsicHeightKey), findsNothing);
|
||
|
||
RenderBox findEditableText() => tester.renderObject(find.byType(EditableText));
|
||
RenderBox editableText = findEditableText();
|
||
final Size unwrappedEditableTextSize = editableText.size;
|
||
|
||
// Wrapping in IntrinsicHeight should not affect the height of the input
|
||
await tester.pumpWidget(intrinsicTextFieldBuilder(true));
|
||
editableText = findEditableText();
|
||
expect(editableText.size.height, unwrappedEditableTextSize.height);
|
||
expect(editableText.size.width, unwrappedEditableTextSize.width);
|
||
});
|
||
|
||
// Regression test for https://github.com/flutter/flutter/pull/29093
|
||
testWidgets('errorText empty string', (WidgetTester tester) async {
|
||
Widget textFormFieldBuilder(String? errorText) {
|
||
return boilerplate(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: <Widget>[
|
||
TextFormField(
|
||
key: textFieldKey,
|
||
maxLength: 3,
|
||
maxLengthEnforcement: MaxLengthEnforcement.none,
|
||
decoration: InputDecoration(counterText: '', errorText: errorText),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(textFormFieldBuilder(null));
|
||
|
||
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
|
||
final RenderBox inputBox = findInputBox();
|
||
final Size errorNullInputSize = inputBox.size;
|
||
|
||
// Setting errorText causes the input's height to increase to accommodate it
|
||
await tester.pumpWidget(textFormFieldBuilder('im errorText'));
|
||
expect(inputBox, findInputBox());
|
||
expect(inputBox.size.height, greaterThan(errorNullInputSize.height));
|
||
expect(inputBox.size.width, errorNullInputSize.width);
|
||
final Size errorInputSize = inputBox.size;
|
||
|
||
// Setting errorText to an empty string causes the input's height to
|
||
// increase to accommodate it, even though it's not displayed.
|
||
// This may or may not be ideal behavior, but it is legacy behavior and
|
||
// there are visual tests that rely on it (see Github issue referenced at
|
||
// the top of this test). A counterText of empty string does not affect
|
||
// input height, however.
|
||
await tester.pumpWidget(textFormFieldBuilder(''));
|
||
expect(inputBox, findInputBox());
|
||
expect(inputBox.size.height, errorInputSize.height);
|
||
expect(inputBox.size.width, errorNullInputSize.width);
|
||
});
|
||
|
||
testWidgets('Growable TextField when content height exceeds parent', (WidgetTester tester) async {
|
||
const height = 200.0;
|
||
const padding = 24.0;
|
||
|
||
Widget containedTextFieldBuilder({
|
||
Widget? counter,
|
||
String? helperText,
|
||
String? labelText,
|
||
Widget? prefix,
|
||
}) {
|
||
return boilerplate(
|
||
theme: ThemeData(useMaterial3: false),
|
||
child: SizedBox(
|
||
height: height,
|
||
child: TextField(
|
||
key: textFieldKey,
|
||
maxLines: null,
|
||
decoration: InputDecoration(
|
||
counter: counter,
|
||
helperText: helperText,
|
||
labelText: labelText,
|
||
prefix: prefix,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(containedTextFieldBuilder());
|
||
RenderBox findEditableText() => tester.renderObject(find.byType(EditableText));
|
||
|
||
final RenderBox inputBox = findEditableText();
|
||
|
||
// With no decoration and when overflowing with content, the EditableText
|
||
// takes up the full height minus the padding, so the input fits perfectly
|
||
// inside the parent.
|
||
await tester.enterText(find.byType(TextField), 'a\n' * 11);
|
||
await tester.pump();
|
||
expect(findEditableText(), equals(inputBox));
|
||
expect(inputBox.size.height, height - padding);
|
||
|
||
// Adding a counter causes the EditableText to shrink to fit the counter
|
||
// inside the parent as well.
|
||
const counterHeight = 40.0;
|
||
const subtextGap = 8.0;
|
||
const double counterSpace = counterHeight + subtextGap;
|
||
await tester.pumpWidget(containedTextFieldBuilder(counter: Container(height: counterHeight)));
|
||
expect(findEditableText(), equals(inputBox));
|
||
expect(inputBox.size.height, height - padding - counterSpace);
|
||
|
||
// Including helperText causes the EditableText to shrink to fit the text
|
||
// inside the parent as well.
|
||
await tester.pumpWidget(containedTextFieldBuilder(helperText: 'I am helperText'));
|
||
expect(findEditableText(), equals(inputBox));
|
||
const helperTextSpace = 12.0;
|
||
expect(inputBox.size.height, height - padding - helperTextSpace - subtextGap);
|
||
|
||
// When both helperText and counter are present, EditableText shrinks by the
|
||
// height of the taller of the two in order to fit both within the parent.
|
||
await tester.pumpWidget(
|
||
containedTextFieldBuilder(
|
||
counter: Container(height: counterHeight),
|
||
helperText: 'I am helperText',
|
||
),
|
||
);
|
||
expect(findEditableText(), equals(inputBox));
|
||
expect(inputBox.size.height, height - padding - counterSpace);
|
||
|
||
// When a label is present, EditableText shrinks to fit it at the top so
|
||
// that the bottom of the input still lines up perfectly with the parent.
|
||
await tester.pumpWidget(containedTextFieldBuilder(labelText: 'I am labelText'));
|
||
const labelSpace = 16.0;
|
||
expect(findEditableText(), equals(inputBox));
|
||
expect(inputBox.size.height, height - padding - labelSpace);
|
||
|
||
// When decoration is present on the top and bottom, EditableText shrinks to
|
||
// fit both inside the parent independently.
|
||
await tester.pumpWidget(
|
||
containedTextFieldBuilder(
|
||
counter: Container(height: counterHeight),
|
||
labelText: 'I am labelText',
|
||
),
|
||
);
|
||
expect(findEditableText(), equals(inputBox));
|
||
expect(inputBox.size.height, height - padding - counterSpace - labelSpace);
|
||
|
||
// When a prefix or suffix is present in an input that's full of content,
|
||
// it is ignored and allowed to expand beyond the top of the input. Other
|
||
// top and bottom decoration is still respected.
|
||
await tester.pumpWidget(
|
||
containedTextFieldBuilder(
|
||
counter: Container(height: counterHeight),
|
||
labelText: 'I am labelText',
|
||
prefix: const SizedBox(width: 10, height: 60),
|
||
),
|
||
);
|
||
expect(findEditableText(), equals(inputBox));
|
||
expect(inputBox.size.height, height - padding - labelSpace - counterSpace);
|
||
});
|
||
|
||
testWidgets('Multiline hint text will wrap up to maxLines', (WidgetTester tester) async {
|
||
final Key textFieldKey = UniqueKey();
|
||
|
||
Widget builder(int? maxLines, final String hintMsg) {
|
||
return boilerplate(
|
||
child: TextField(
|
||
key: textFieldKey,
|
||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||
maxLines: maxLines,
|
||
decoration: InputDecoration(hintText: hintMsg),
|
||
),
|
||
);
|
||
}
|
||
|
||
const hintPlaceholder = 'Placeholder';
|
||
const multipleLineText =
|
||
"Here's a text, which is more than one line, to demonstrate the multiple line hint text";
|
||
await tester.pumpWidget(builder(null, hintPlaceholder));
|
||
|
||
RenderBox findHintText(String hint) => tester.renderObject(find.text(hint));
|
||
|
||
final RenderBox hintTextBox = findHintText(hintPlaceholder);
|
||
final Size oneLineHintSize = hintTextBox.size;
|
||
|
||
await tester.pumpWidget(builder(null, hintPlaceholder));
|
||
expect(findHintText(hintPlaceholder), equals(hintTextBox));
|
||
expect(hintTextBox.size, equals(oneLineHintSize));
|
||
|
||
const maxLines = 3;
|
||
await tester.pumpWidget(builder(maxLines, multipleLineText));
|
||
final Text hintTextWidget = tester.widget(find.text(multipleLineText));
|
||
expect(hintTextWidget.maxLines, equals(maxLines));
|
||
expect(findHintText(multipleLineText).size.width, greaterThanOrEqualTo(oneLineHintSize.width));
|
||
expect(
|
||
findHintText(multipleLineText).size.height,
|
||
greaterThanOrEqualTo(oneLineHintSize.height),
|
||
);
|
||
});
|
||
|
||
testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||
maxLines: 3,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = kThreeLines;
|
||
const cutValue = 'First line of stuff';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Check that the text spans multiple lines.
|
||
final Offset firstPos = textOffsetToPosition(tester, testValue.indexOf('First'));
|
||
final Offset secondPos = textOffsetToPosition(tester, testValue.indexOf('Second'));
|
||
final Offset thirdPos = textOffsetToPosition(tester, testValue.indexOf('Third'));
|
||
final Offset middleStringPos = textOffsetToPosition(tester, testValue.indexOf('irst'));
|
||
expect(firstPos.dx, lessThan(middleStringPos.dx));
|
||
expect(firstPos.dx, secondPos.dx);
|
||
expect(firstPos.dx, thirdPos.dx);
|
||
expect(firstPos.dy, lessThan(secondPos.dy));
|
||
expect(secondPos.dy, lessThan(thirdPos.dy));
|
||
|
||
// Long press the 'n' in 'until' to select the word.
|
||
final Offset untilPos = textOffsetToPosition(tester, testValue.indexOf('until') + 1);
|
||
TestGesture gesture = await tester.startGesture(untilPos, pointer: 7);
|
||
await tester.pump(const Duration(seconds: 2));
|
||
await gesture.up();
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 39, extentOffset: 44));
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Drag the right handle to the third line, just after 'Third'.
|
||
Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
|
||
// The distance below the y value returned by textOffsetToPosition required
|
||
// to register a full vertical line drag.
|
||
const downLineOffset = Offset(0.0, 3.0);
|
||
Offset newHandlePos =
|
||
textOffsetToPosition(tester, testValue.indexOf('Third') + 5) + downLineOffset;
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 39, extentOffset: 50));
|
||
|
||
// Drag the left handle to the first line, just after 'First'.
|
||
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
|
||
newHandlePos = textOffsetToPosition(tester, testValue.indexOf('First') + 5);
|
||
gesture = await tester.startGesture(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveTo(newHandlePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 5);
|
||
expect(controller.selection.extentOffset, 50);
|
||
|
||
if (!isContextMenuProvidedByPlatform) {
|
||
await tester.tap(find.text('Cut'));
|
||
await tester.pump();
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.text, cutValue);
|
||
}
|
||
});
|
||
|
||
testWidgets('Can scroll multiline input', (WidgetTester tester) async {
|
||
final Key textFieldKey = UniqueKey();
|
||
final TextEditingController controller = _textEditingController(text: kMoreThanFourLines);
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
key: textFieldKey,
|
||
controller: controller,
|
||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||
maxLines: 2,
|
||
),
|
||
),
|
||
);
|
||
|
||
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
|
||
final RenderBox inputBox = findInputBox();
|
||
|
||
// Check that the last line of text is not displayed.
|
||
final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
|
||
final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
|
||
expect(firstPos.dx, fourthPos.dx);
|
||
expect(firstPos.dy, lessThan(fourthPos.dy));
|
||
expect(
|
||
inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(firstPos)),
|
||
isTrue,
|
||
);
|
||
expect(
|
||
inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(fourthPos)),
|
||
isFalse,
|
||
);
|
||
|
||
TestGesture gesture = await tester.startGesture(firstPos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveBy(const Offset(0.0, -1000.0));
|
||
await tester.pump(const Duration(seconds: 1));
|
||
// Wait and drag again to trigger https://github.com/flutter/flutter/issues/6329
|
||
// (No idea why this is necessary, but the bug wouldn't repro without it.)
|
||
await gesture.moveBy(const Offset(0.0, -1000.0));
|
||
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'));
|
||
Offset newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
|
||
|
||
expect(newFirstPos.dy, lessThan(firstPos.dy));
|
||
expect(
|
||
inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFirstPos)),
|
||
isFalse,
|
||
);
|
||
expect(
|
||
inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFourthPos)),
|
||
isTrue,
|
||
);
|
||
|
||
// Now try scrolling by dragging the selection handle.
|
||
// Long press the middle of the word "won't" in the fourth line.
|
||
final Offset selectedWordPos = textOffsetToPosition(
|
||
tester,
|
||
kMoreThanFourLines.indexOf('Fourth line') + 14,
|
||
);
|
||
|
||
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, 77);
|
||
expect(controller.selection.extent.offset, 82);
|
||
// Sanity check for the word selected is the intended one.
|
||
expect(
|
||
controller.text.substring(controller.selection.baseOffset, controller.selection.extentOffset),
|
||
"won't",
|
||
);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 2);
|
||
|
||
// Drag the left handle to the first line, just after 'First'.
|
||
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));
|
||
await gesture.moveTo(newHandlePos + const Offset(0.0, -10.0));
|
||
await tester.pump(const Duration(seconds: 1));
|
||
await gesture.up();
|
||
await tester.pump(const Duration(seconds: 1));
|
||
|
||
// The text should have scrolled up with the handle to keep the active
|
||
// cursor visible, back to its original position.
|
||
newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
|
||
newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
|
||
expect(newFirstPos.dy, firstPos.dy);
|
||
expect(
|
||
inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFirstPos)),
|
||
isTrue,
|
||
);
|
||
expect(
|
||
inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFourthPos)),
|
||
isFalse,
|
||
);
|
||
});
|
||
|
||
testWidgets('TextField smoke test', (WidgetTester tester) async {
|
||
late String textFieldValue;
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
decoration: null,
|
||
onChanged: (String value) {
|
||
textFieldValue = value;
|
||
},
|
||
),
|
||
),
|
||
);
|
||
|
||
Future<void> checkText(String testValue) {
|
||
return TestAsyncUtils.guard(() async {
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
|
||
// Check that the onChanged event handler fired.
|
||
expect(textFieldValue, equals(testValue));
|
||
|
||
await tester.pump();
|
||
});
|
||
}
|
||
|
||
await checkText('Hello World');
|
||
});
|
||
|
||
testWidgets('TextField with global key', (WidgetTester tester) async {
|
||
final GlobalKey textFieldKey = GlobalKey(debugLabel: 'textFieldKey');
|
||
late String textFieldValue;
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
key: textFieldKey,
|
||
decoration: const InputDecoration(hintText: 'Placeholder'),
|
||
onChanged: (String value) {
|
||
textFieldValue = value;
|
||
},
|
||
),
|
||
),
|
||
);
|
||
|
||
Future<void> checkText(String testValue) async {
|
||
return TestAsyncUtils.guard(() async {
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
|
||
// Check that the onChanged event handler fired.
|
||
expect(textFieldValue, equals(testValue));
|
||
|
||
await tester.pump();
|
||
});
|
||
}
|
||
|
||
await checkText('Hello World');
|
||
});
|
||
|
||
testWidgets('TextField errorText trumps helperText', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: const TextField(
|
||
decoration: InputDecoration(errorText: 'error text', helperText: 'helper text'),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
expect(find.text('helper text'), findsNothing);
|
||
expect(find.text('error text'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('TextField with default helperStyle', (WidgetTester tester) async {
|
||
final themeData = ThemeData(hintColor: Colors.blue[500], useMaterial3: false);
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: Theme(
|
||
data: themeData,
|
||
child: const TextField(decoration: InputDecoration(helperText: 'helper text')),
|
||
),
|
||
),
|
||
);
|
||
final Text helperText = tester.widget(find.text('helper text'));
|
||
expect(helperText.style!.color, themeData.hintColor);
|
||
expect(helperText.style!.fontSize, Typography.englishLike2014.bodySmall!.fontSize);
|
||
});
|
||
|
||
testWidgets('TextField with specified helperStyle', (WidgetTester tester) async {
|
||
final style = TextStyle(inherit: false, color: Colors.pink[500], fontSize: 10.0);
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
decoration: InputDecoration(helperText: 'helper text', helperStyle: style),
|
||
),
|
||
),
|
||
);
|
||
final Text helperText = tester.widget(find.text('helper text'));
|
||
expect(helperText.style, style);
|
||
});
|
||
|
||
testWidgets('TextField with default hintStyle', (WidgetTester tester) async {
|
||
final style = TextStyle(color: Colors.pink[500], fontSize: 10.0);
|
||
final themeData = ThemeData();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: Theme(
|
||
data: themeData,
|
||
child: TextField(
|
||
decoration: const InputDecoration(hintText: 'Placeholder'),
|
||
style: style,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Text hintText = tester.widget(find.text('Placeholder'));
|
||
expect(hintText.style!.color, themeData.colorScheme.onSurfaceVariant);
|
||
expect(hintText.style!.fontSize, style.fontSize);
|
||
});
|
||
|
||
testWidgets('Material2 - TextField with default hintStyle', (WidgetTester tester) async {
|
||
final style = TextStyle(color: Colors.pink[500], fontSize: 10.0);
|
||
final themeData = ThemeData(useMaterial3: false, hintColor: Colors.blue[500]);
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: Theme(
|
||
data: themeData,
|
||
child: TextField(
|
||
decoration: const InputDecoration(hintText: 'Placeholder'),
|
||
style: style,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Text hintText = tester.widget(find.text('Placeholder'));
|
||
expect(hintText.style!.color, themeData.hintColor);
|
||
expect(hintText.style!.fontSize, style.fontSize);
|
||
});
|
||
|
||
testWidgets('TextField with specified hintStyle', (WidgetTester tester) async {
|
||
final hintStyle = TextStyle(inherit: false, color: Colors.pink[500], fontSize: 10.0);
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
decoration: InputDecoration(hintText: 'Placeholder', hintStyle: hintStyle),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Text hintText = tester.widget(find.text('Placeholder'));
|
||
expect(hintText.style, hintStyle);
|
||
});
|
||
|
||
testWidgets('TextField with specified prefixStyle', (WidgetTester tester) async {
|
||
final prefixStyle = TextStyle(inherit: false, color: Colors.pink[500], fontSize: 10.0);
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
decoration: InputDecoration(prefixText: 'Prefix:', prefixStyle: prefixStyle),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Text prefixText = tester.widget(find.text('Prefix:'));
|
||
expect(prefixText.style, prefixStyle);
|
||
});
|
||
|
||
testWidgets('TextField prefix and suffix create a sibling node', (WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
controller: _textEditingController(text: 'some text'),
|
||
decoration: const InputDecoration(prefixText: 'Prefix', suffixText: 'Suffix'),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
children: <TestSemantics>[
|
||
TestSemantics(id: 2, textDirection: TextDirection.ltr, label: 'Prefix'),
|
||
TestSemantics(
|
||
textDirection: TextDirection.ltr,
|
||
value: 'some text',
|
||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 9,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
),
|
||
TestSemantics(id: 3, textDirection: TextDirection.ltr, label: 'Suffix'),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('TextField prefix icon and suffix icon create a sibling node', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final semantics = SemanticsTester(tester);
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
controller: _textEditingController(text: 'some text'),
|
||
decoration: const InputDecoration(prefixIcon: Text('Prefix'), suffixIcon: Text('Suffix')),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
textDirection: TextDirection.ltr,
|
||
value: 'some text',
|
||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 9,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
),
|
||
TestSemantics(textDirection: TextDirection.ltr, label: 'Prefix'),
|
||
TestSemantics(textDirection: TextDirection.ltr, label: 'Suffix'),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async {
|
||
final suffixStyle = TextStyle(color: Colors.pink[500], fontSize: 10.0);
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
decoration: InputDecoration(suffixText: '.com', suffixStyle: suffixStyle),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Text suffixText = tester.widget(find.text('.com'));
|
||
expect(suffixText.style, suffixStyle);
|
||
});
|
||
|
||
testWidgets('TextField prefix and suffix appear correctly with no hint or label', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final Key secondKey = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: Column(
|
||
children: <Widget>[
|
||
const TextField(decoration: InputDecoration(labelText: 'First')),
|
||
TextField(
|
||
key: secondKey,
|
||
decoration: const InputDecoration(prefixText: 'Prefix', suffixText: 'Suffix'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Prefix'), findsOneWidget);
|
||
expect(find.text('Suffix'), findsOneWidget);
|
||
|
||
// Focus the Input. The prefix should still display.
|
||
await tester.tap(find.byKey(secondKey));
|
||
await tester.pump();
|
||
|
||
expect(find.text('Prefix'), findsOneWidget);
|
||
expect(find.text('Suffix'), findsOneWidget);
|
||
|
||
// Enter some text, and the prefix should still display.
|
||
await tester.enterText(find.byKey(secondKey), 'Hi');
|
||
await tester.pump();
|
||
await tester.pump(const Duration(seconds: 1));
|
||
|
||
expect(find.text('Prefix'), findsOneWidget);
|
||
expect(find.text('Suffix'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('TextField prefix and suffix appear correctly with hint text', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final hintStyle = TextStyle(inherit: false, color: Colors.pink[500], fontSize: 10.0);
|
||
final Key secondKey = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: Column(
|
||
children: <Widget>[
|
||
const TextField(decoration: InputDecoration(labelText: 'First')),
|
||
TextField(
|
||
key: secondKey,
|
||
decoration: InputDecoration(
|
||
hintText: 'Hint',
|
||
hintStyle: hintStyle,
|
||
prefixText: 'Prefix',
|
||
suffixText: 'Suffix',
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
// Neither the prefix or the suffix should initially be visible, only the hint.
|
||
expect(getOpacity(tester, find.text('Prefix')), 0.0);
|
||
expect(getOpacity(tester, find.text('Suffix')), 0.0);
|
||
expect(getOpacity(tester, find.text('Hint')), 1.0);
|
||
|
||
await tester.tap(find.byKey(secondKey));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Focus the Input. The hint, prefix, and suffix should appear
|
||
expect(getOpacity(tester, find.text('Prefix')), 1.0);
|
||
expect(getOpacity(tester, find.text('Suffix')), 1.0);
|
||
expect(getOpacity(tester, find.text('Hint')), 1.0);
|
||
|
||
// Enter some text, and the hint should disappear and the prefix and suffix
|
||
// should continue to be visible
|
||
await tester.enterText(find.byKey(secondKey), 'Hi');
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(getOpacity(tester, find.text('Prefix')), 1.0);
|
||
expect(getOpacity(tester, find.text('Suffix')), 1.0);
|
||
expect(getOpacity(tester, find.text('Hint')), 0.0);
|
||
|
||
// Check and make sure that the right styles were applied.
|
||
final Text prefixText = tester.widget(find.text('Prefix'));
|
||
expect(prefixText.style, hintStyle);
|
||
final Text suffixText = tester.widget(find.text('Suffix'));
|
||
expect(suffixText.style, hintStyle);
|
||
});
|
||
|
||
testWidgets('TextField prefix and suffix appear correctly with label text', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final prefixStyle = TextStyle(color: Colors.pink[500], fontSize: 10.0);
|
||
final suffixStyle = TextStyle(color: Colors.green[500], fontSize: 12.0);
|
||
final Key secondKey = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: Column(
|
||
children: <Widget>[
|
||
const TextField(decoration: InputDecoration(labelText: 'First')),
|
||
TextField(
|
||
key: secondKey,
|
||
decoration: InputDecoration(
|
||
labelText: 'Label',
|
||
prefixText: 'Prefix',
|
||
prefixStyle: prefixStyle,
|
||
suffixText: 'Suffix',
|
||
suffixStyle: suffixStyle,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
// Not focused. The prefix and suffix should not appear, but the label should.
|
||
expect(getOpacity(tester, find.text('Prefix')), 0.0);
|
||
expect(getOpacity(tester, find.text('Suffix')), 0.0);
|
||
expect(find.text('Label'), findsOneWidget);
|
||
|
||
// Focus the input. The label, prefix, and suffix should appear.
|
||
await tester.tap(find.byKey(secondKey));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(getOpacity(tester, find.text('Prefix')), 1.0);
|
||
expect(getOpacity(tester, find.text('Suffix')), 1.0);
|
||
expect(find.text('Label'), findsOneWidget);
|
||
|
||
// Enter some text. The label, prefix, and suffix should remain visible.
|
||
await tester.enterText(find.byKey(secondKey), 'Hi');
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(getOpacity(tester, find.text('Prefix')), 1.0);
|
||
expect(getOpacity(tester, find.text('Suffix')), 1.0);
|
||
expect(find.text('Label'), findsOneWidget);
|
||
|
||
// Check and make sure that the right styles were applied.
|
||
final Text prefixText = tester.widget(find.text('Prefix'));
|
||
expect(prefixText.style, prefixStyle);
|
||
final Text suffixText = tester.widget(find.text('Suffix'));
|
||
expect(suffixText.style, suffixStyle);
|
||
});
|
||
|
||
testWidgets('TextField label text animates', (WidgetTester tester) async {
|
||
final Key secondKey = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: Column(
|
||
children: <Widget>[
|
||
const TextField(decoration: InputDecoration(labelText: 'First')),
|
||
TextField(
|
||
key: secondKey,
|
||
decoration: const InputDecoration(labelText: 'Second'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
Offset pos = tester.getTopLeft(find.text('Second'));
|
||
|
||
// Focus the Input. The label should start animating upwards.
|
||
await tester.tap(find.byKey(secondKey));
|
||
await tester.idle();
|
||
await tester.pump();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
|
||
Offset newPos = tester.getTopLeft(find.text('Second'));
|
||
expect(newPos.dy, lessThan(pos.dy));
|
||
|
||
// Label should still be sliding upward.
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
pos = newPos;
|
||
newPos = tester.getTopLeft(find.text('Second'));
|
||
expect(newPos.dy, lessThan(pos.dy));
|
||
});
|
||
|
||
testWidgets('Icon is separated from input/label by 16+12', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: const TextField(
|
||
decoration: InputDecoration(icon: Icon(Icons.phone), labelText: 'label', filled: true),
|
||
),
|
||
),
|
||
);
|
||
final double iconRight = tester.getTopRight(find.byType(Icon)).dx;
|
||
// There's a 16 pixels gap between the right edge of the icon and the text field's
|
||
// container, and, per https://material.io/go/design-text-fields#text-fields-layout,
|
||
// 16 pixels more padding between the left edge of the container and the left edge
|
||
// of the input and label.
|
||
expect(iconRight + 16.0 + 16.0, equals(tester.getTopLeft(find.text('label')).dx));
|
||
expect(iconRight + 16.0 + 16.0, equals(tester.getTopLeft(find.byType(EditableText)).dx));
|
||
});
|
||
|
||
testWidgets('Collapsed hint text placement', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: const TextField(
|
||
decoration: InputDecoration.collapsed(hintText: 'hint'),
|
||
strutStyle: StrutStyle.disabled,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
tester.getTopLeft(find.text('hint')),
|
||
equals(tester.getTopLeft(find.byType(EditableText))),
|
||
);
|
||
});
|
||
|
||
testWidgets('Can align to center', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: const SizedBox(
|
||
width: 300.0,
|
||
child: TextField(textAlign: TextAlign.center, decoration: null),
|
||
),
|
||
),
|
||
);
|
||
|
||
final RenderEditable editable = findRenderEditable(tester);
|
||
assert(editable.size.width == 300);
|
||
Offset topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft,
|
||
);
|
||
|
||
// The overlay() function centers its child within a 800x600 view.
|
||
// Default cursorWidth is 2.0, test viewWidth is 800
|
||
// Centered cursor topLeft.dx: 399 == viewWidth/2 - cursorWidth/2
|
||
expect(topLeft.dx, equals(399.0));
|
||
|
||
await tester.enterText(find.byType(TextField), 'abcd');
|
||
await tester.pump();
|
||
|
||
topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
|
||
);
|
||
|
||
// TextPosition(offset: 2) - center of 'abcd'
|
||
expect(topLeft.dx, equals(399.0));
|
||
});
|
||
|
||
testWidgets('Can align to center within center', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: const SizedBox(
|
||
width: 300.0,
|
||
child: Center(child: TextField(textAlign: TextAlign.center, decoration: null)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final RenderEditable editable = findRenderEditable(tester);
|
||
Offset topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft,
|
||
);
|
||
|
||
// The overlay() function centers its child within a 800x600 view.
|
||
// Default cursorWidth is 2.0, test viewWidth is 800
|
||
// Centered cursor topLeft.dx: 399 == viewWidth/2 - cursorWidth/2
|
||
expect(topLeft.dx, equals(399.0));
|
||
|
||
await tester.enterText(find.byType(TextField), 'abcd');
|
||
await tester.pump();
|
||
|
||
topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
|
||
);
|
||
|
||
// TextPosition(offset: 2) - center of 'abcd'
|
||
expect(topLeft.dx, equals(399.0));
|
||
});
|
||
|
||
testWidgets('Controller can update server', (WidgetTester tester) async {
|
||
final TextEditingController controller1 = _textEditingController(text: 'Initial Text');
|
||
final TextEditingController controller2 = _textEditingController(text: 'More Text');
|
||
|
||
TextEditingController? currentController;
|
||
late StateSetter setState;
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: StatefulBuilder(
|
||
builder: (BuildContext context, StateSetter setter) {
|
||
setState = setter;
|
||
return TextField(controller: currentController);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
expect(tester.testTextInput.editingState, isNull);
|
||
|
||
// Initial state with null controller.
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump();
|
||
expect(tester.testTextInput.editingState!['text'], isEmpty);
|
||
|
||
// Update the controller from null to controller1.
|
||
setState(() {
|
||
currentController = controller1;
|
||
});
|
||
await tester.pump();
|
||
expect(tester.testTextInput.editingState!['text'], equals('Initial Text'));
|
||
|
||
// Verify that updates to controller1 are handled.
|
||
controller1.text = 'Updated Text';
|
||
await tester.idle();
|
||
expect(tester.testTextInput.editingState!['text'], equals('Updated Text'));
|
||
|
||
// Verify that switching from controller1 to controller2 is handled.
|
||
setState(() {
|
||
currentController = controller2;
|
||
});
|
||
await tester.pump();
|
||
expect(tester.testTextInput.editingState!['text'], equals('More Text'));
|
||
|
||
// Verify that updates to controller1 are ignored.
|
||
controller1.text = 'Ignored Text';
|
||
await tester.idle();
|
||
expect(tester.testTextInput.editingState!['text'], equals('More Text'));
|
||
|
||
// Verify that updates to controller text are handled.
|
||
controller2.text = 'Additional Text';
|
||
await tester.idle();
|
||
expect(tester.testTextInput.editingState!['text'], equals('Additional Text'));
|
||
|
||
// Verify that updates to controller selection are handled.
|
||
controller2.selection = const TextSelection(baseOffset: 0, extentOffset: 5);
|
||
await tester.idle();
|
||
expect(tester.testTextInput.editingState!['selectionBase'], equals(0));
|
||
expect(tester.testTextInput.editingState!['selectionExtent'], equals(5));
|
||
|
||
// Verify that calling clear() clears the text.
|
||
controller2.clear();
|
||
await tester.idle();
|
||
expect(tester.testTextInput.editingState!['text'], equals(''));
|
||
|
||
// Verify that switching from controller2 to null preserves current text.
|
||
controller2.text = 'The Final Cut';
|
||
await tester.idle();
|
||
expect(tester.testTextInput.editingState!['text'], equals('The Final Cut'));
|
||
setState(() {
|
||
currentController = null;
|
||
});
|
||
await tester.pump();
|
||
expect(tester.testTextInput.editingState!['text'], equals('The Final Cut'));
|
||
|
||
// Verify that changes to controller2 are ignored.
|
||
controller2.text = 'Goodbye Cruel World';
|
||
expect(tester.testTextInput.editingState!['text'], equals('The Final Cut'));
|
||
});
|
||
|
||
testWidgets('Cannot enter new lines onto single line TextField', (WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(child: TextField(controller: textController, decoration: null)),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), 'abc\ndef');
|
||
|
||
expect(textController.text, 'abcdef');
|
||
});
|
||
|
||
testWidgets('Injected formatters are chained', (WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: TextField(
|
||
controller: textController,
|
||
decoration: null,
|
||
inputFormatters: <TextInputFormatter>[
|
||
FilteringTextInputFormatter.deny(RegExp(r'[a-z]'), replacementString: '#'),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六');
|
||
// The default single line formatter replaces \n with empty string.
|
||
expect(textController.text, '#一#二#三#四#五#六');
|
||
});
|
||
|
||
testWidgets('Injected formatters are chained (deprecated names)', (WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: TextField(
|
||
controller: textController,
|
||
decoration: null,
|
||
inputFormatters: <TextInputFormatter>[
|
||
FilteringTextInputFormatter.deny(RegExp(r'[a-z]'), replacementString: '#'),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六');
|
||
// The default single line formatter replaces \n with empty string.
|
||
expect(textController.text, '#一#二#三#四#五#六');
|
||
});
|
||
|
||
testWidgets('Chained formatters are in sequence', (WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: TextField(
|
||
controller: textController,
|
||
decoration: null,
|
||
maxLines: 2,
|
||
inputFormatters: <TextInputFormatter>[
|
||
FilteringTextInputFormatter.deny(RegExp(r'[a-z]'), replacementString: '12\n'),
|
||
FilteringTextInputFormatter.allow(RegExp(r'\n[0-9]')),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), 'a1b2c3');
|
||
// The first formatter turns it into
|
||
// 12\n112\n212\n3
|
||
// The second formatter turns it into
|
||
// \n1\n2\n3
|
||
// Multiline is allowed since maxLine != 1.
|
||
expect(textController.text, '\n1\n2\n3');
|
||
});
|
||
|
||
testWidgets('Chained formatters are in sequence (deprecated names)', (WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: TextField(
|
||
controller: textController,
|
||
decoration: null,
|
||
maxLines: 2,
|
||
inputFormatters: <TextInputFormatter>[
|
||
FilteringTextInputFormatter.deny(RegExp(r'[a-z]'), replacementString: '12\n'),
|
||
FilteringTextInputFormatter.allow(RegExp(r'\n[0-9]')),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), 'a1b2c3');
|
||
// The first formatter turns it into
|
||
// 12\n112\n212\n3
|
||
// The second formatter turns it into
|
||
// \n1\n2\n3
|
||
// Multiline is allowed since maxLine != 1.
|
||
expect(textController.text, '\n1\n2\n3');
|
||
});
|
||
|
||
testWidgets(
|
||
'Pasted values are formatted',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
controller: textController,
|
||
decoration: null,
|
||
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), 'a1b\n2c3');
|
||
expect(textController.text, '123');
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2')));
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(textController.selection),
|
||
renderEditable,
|
||
);
|
||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
|
||
Clipboard.setData(const ClipboardData(text: '一4二\n5三6'));
|
||
await tester.tap(find.text('Paste'));
|
||
await tester.pump();
|
||
// Puts 456 before the 2 in 123.
|
||
expect(textController.text, '145623');
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Pasted values are formatted (deprecated names)',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
controller: textController,
|
||
decoration: null,
|
||
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), 'a1b\n2c3');
|
||
expect(textController.text, '123');
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2')));
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(textController.selection),
|
||
renderEditable,
|
||
);
|
||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is zero
|
||
|
||
Clipboard.setData(const ClipboardData(text: '一4二\n5三6'));
|
||
await tester.tap(find.text('Paste'));
|
||
await tester.pump();
|
||
// Puts 456 before the 2 in 123.
|
||
expect(textController.text, '145623');
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets('Do not add LengthLimiting formatter to the user supplied list', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final formatters = <TextInputFormatter>[];
|
||
|
||
await tester.pumpWidget(
|
||
overlay(child: TextField(decoration: null, maxLength: 5, inputFormatters: formatters)),
|
||
);
|
||
|
||
expect(formatters.isEmpty, isTrue);
|
||
});
|
||
|
||
testWidgets('Text field scrolls the caret into view', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: SizedBox(width: 100.0, child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final String longText = 'a' * 20;
|
||
await tester.enterText(find.byType(TextField), longText);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
ScrollableState scrollableState = tester.firstState(find.byType(Scrollable));
|
||
expect(scrollableState.position.pixels, equals(0.0));
|
||
|
||
// Move the caret to the end of the text and check that the text field
|
||
// scrolls to make the caret visible.
|
||
scrollableState = tester.firstState(find.byType(Scrollable));
|
||
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
|
||
editableTextState.userUpdateTextEditingValue(
|
||
editableTextState.textEditingValue.copyWith(
|
||
selection: TextSelection.collapsed(offset: longText.length),
|
||
),
|
||
null,
|
||
);
|
||
|
||
await tester.pump(); // TODO(ianh): Figure out why this extra pump is needed.
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
scrollableState = tester.firstState(find.byType(Scrollable));
|
||
// For a horizontal input, scrolls to the exact position of the caret.
|
||
expect(scrollableState.position.pixels, equals(222.0));
|
||
});
|
||
|
||
testWidgets('Multiline text field scrolls the caret into view', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller, maxLines: 6)));
|
||
|
||
const tallText = 'a\nb\nc\nd\ne\nf\ng'; // One line over max
|
||
await tester.enterText(find.byType(TextField), tallText);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
ScrollableState scrollableState = tester.firstState(find.byType(Scrollable));
|
||
expect(scrollableState.position.pixels, equals(0.0));
|
||
|
||
// Move the caret to the end of the text and check that the text field
|
||
// scrolls to make the caret visible.
|
||
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
|
||
editableTextState.userUpdateTextEditingValue(
|
||
editableTextState.textEditingValue.copyWith(
|
||
selection: const TextSelection.collapsed(offset: tallText.length),
|
||
),
|
||
null,
|
||
);
|
||
await tester.pump();
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Should have scrolled down exactly one line height (7 lines of text in 6
|
||
// line text field).
|
||
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
|
||
scrollableState = tester.firstState(find.byType(Scrollable));
|
||
expect(scrollableState.position.pixels, moreOrLessEquals(lineHeight, epsilon: 0.1));
|
||
});
|
||
|
||
testWidgets('haptic feedback', (WidgetTester tester) async {
|
||
final feedback = FeedbackTester();
|
||
addTearDown(feedback.dispose);
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: SizedBox(width: 100.0, child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||
expect(feedback.clickSoundCount, 0);
|
||
expect(feedback.hapticCount, 0);
|
||
|
||
await tester.longPress(find.byType(TextField));
|
||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||
expect(feedback.clickSoundCount, 0);
|
||
expect(feedback.hapticCount, 1);
|
||
});
|
||
|
||
testWidgets('Text field drops selection color when losing focus', (WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/103341.
|
||
final Key key1 = UniqueKey();
|
||
final Key key2 = UniqueKey();
|
||
final TextEditingController controller1 = _textEditingController();
|
||
const Color selectionColor = Colors.orange;
|
||
const Color cursorColor = Colors.red;
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: DefaultSelectionStyle(
|
||
selectionColor: selectionColor,
|
||
cursorColor: cursorColor,
|
||
child: Column(
|
||
children: <Widget>[
|
||
TextField(key: key1, controller: controller1),
|
||
TextField(key: key2),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const selection = TextSelection(baseOffset: 0, extentOffset: 4);
|
||
final EditableTextState state1 = tester.state<EditableTextState>(
|
||
find.byType(EditableText).first,
|
||
);
|
||
final EditableTextState state2 = tester.state<EditableTextState>(
|
||
find.byType(EditableText).last,
|
||
);
|
||
|
||
await tester.tap(find.byKey(key1));
|
||
await tester.enterText(find.byKey(key1), 'abcd');
|
||
await tester.pump();
|
||
|
||
await tester.tap(find.byKey(key2));
|
||
await tester.enterText(find.byKey(key2), 'dcba');
|
||
await tester.pump();
|
||
|
||
// Focus and selection is active on first TextField, so the second TextFields
|
||
// selectionColor should be dropped.
|
||
await tester.tap(find.byKey(key1));
|
||
controller1.selection = const TextSelection(baseOffset: 0, extentOffset: 4);
|
||
await tester.pump();
|
||
expect(controller1.selection, selection);
|
||
expect(state1.widget.selectionColor, selectionColor);
|
||
expect(state2.widget.selectionColor, null);
|
||
|
||
// Focus and selection is active on second TextField, so the first TextFields
|
||
// selectionColor should be dropped.
|
||
await tester.tap(find.byKey(key2));
|
||
await tester.pump();
|
||
expect(state1.widget.selectionColor, null);
|
||
expect(state2.widget.selectionColor, selectionColor);
|
||
});
|
||
|
||
testWidgets('Selection is consistent with text length', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
controller.text = 'abcde';
|
||
controller.selection = const TextSelection.collapsed(offset: 5);
|
||
|
||
controller.text = '';
|
||
expect(controller.selection.start, lessThanOrEqualTo(0));
|
||
expect(controller.selection.end, lessThanOrEqualTo(0));
|
||
|
||
late FlutterError error;
|
||
try {
|
||
controller.selection = const TextSelection.collapsed(offset: 10);
|
||
} on FlutterError catch (e) {
|
||
error = e;
|
||
} finally {
|
||
expect(error.diagnostics.length, 1);
|
||
expect(
|
||
error.toStringDeep(),
|
||
equalsIgnoringHashCodes(
|
||
'FlutterError\n'
|
||
' invalid text selection: TextSelection.collapsed(offset: 10,\n'
|
||
' affinity: TextAffinity.downstream, isDirectional: false)\n',
|
||
),
|
||
);
|
||
}
|
||
});
|
||
|
||
// Regression test for https://github.com/flutter/flutter/issues/35848
|
||
testWidgets('Clearing text field with suffixIcon does not cause text selection exception', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller = _textEditingController(text: 'Prefilled text.');
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: TextField(
|
||
controller: controller,
|
||
decoration: InputDecoration(
|
||
suffixIcon: IconButton(icon: const Icon(Icons.close), onPressed: controller.clear),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byType(IconButton));
|
||
expect(controller.text, '');
|
||
});
|
||
|
||
testWidgets('maxLength limits input.', (WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(child: TextField(controller: textController, maxLength: 10)),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), '0123456789101112');
|
||
expect(textController.text, '0123456789');
|
||
});
|
||
|
||
testWidgets('maxLength limits input with surrogate pairs.', (WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(child: TextField(controller: textController, maxLength: 10)),
|
||
);
|
||
|
||
const surrogatePair = '😆';
|
||
await tester.enterText(find.byType(TextField), '${surrogatePair}0123456789101112');
|
||
expect(textController.text, '${surrogatePair}012345678');
|
||
});
|
||
|
||
testWidgets('maxLength limits input with grapheme clusters.', (WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(child: TextField(controller: textController, maxLength: 10)),
|
||
);
|
||
|
||
const graphemeCluster = '👨👩👦';
|
||
await tester.enterText(find.byType(TextField), '${graphemeCluster}0123456789101112');
|
||
expect(textController.text, '${graphemeCluster}012345678');
|
||
});
|
||
|
||
testWidgets('maxLength limits input in the center of a maxed-out field.', (
|
||
WidgetTester tester,
|
||
) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/37420.
|
||
final TextEditingController textController = _textEditingController();
|
||
const testValue = '0123456789';
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(child: TextField(controller: textController, maxLength: 10)),
|
||
);
|
||
|
||
// Max out the character limit in the field.
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
expect(textController.text, testValue);
|
||
|
||
// Entering more characters at the end does nothing.
|
||
await tester.enterText(find.byType(TextField), '${testValue}9999999');
|
||
expect(textController.text, testValue);
|
||
|
||
// Entering text in the middle of the field also does nothing.
|
||
await tester.enterText(find.byType(TextField), '0123455555555556789');
|
||
expect(textController.text, testValue);
|
||
});
|
||
|
||
testWidgets(
|
||
'maxLength limits input in the center of a maxed-out field, with collapsed selection',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
const testValue = '0123456789';
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(child: TextField(controller: textController, maxLength: 10)),
|
||
);
|
||
|
||
// Max out the character limit in the field.
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(text: testValue, selection: TextSelection.collapsed(offset: 10)),
|
||
);
|
||
await tester.pump();
|
||
expect(textController.text, testValue);
|
||
|
||
// Entering more characters at the end does nothing.
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(
|
||
text: '${testValue}9999999',
|
||
selection: TextSelection.collapsed(offset: 10 + 7),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
expect(textController.text, testValue);
|
||
|
||
// Entering text in the middle of the field also does nothing.
|
||
// Entering more characters at the end does nothing.
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(
|
||
text: '0123455555555556789',
|
||
selection: TextSelection.collapsed(offset: 19),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
expect(textController.text, testValue);
|
||
},
|
||
);
|
||
|
||
testWidgets(
|
||
'maxLength limits input in the center of a maxed-out field, with non-collapsed selection',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
const testValue = '0123456789';
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: TextField(
|
||
controller: textController,
|
||
maxLength: 10,
|
||
maxLengthEnforcement: MaxLengthEnforcement.enforced,
|
||
),
|
||
),
|
||
);
|
||
|
||
// Max out the character limit in the field.
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(
|
||
text: testValue,
|
||
selection: TextSelection(baseOffset: 8, extentOffset: 10),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
expect(textController.text, testValue);
|
||
|
||
// Entering more characters at the end does nothing.
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(
|
||
text: '01234569999999',
|
||
selection: TextSelection.collapsed(offset: 14),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
expect(textController.text, '0123456999');
|
||
},
|
||
);
|
||
|
||
testWidgets('maxLength limits input length even if decoration is null.', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(child: TextField(controller: textController, decoration: null, maxLength: 10)),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), '0123456789101112');
|
||
expect(textController.text, '0123456789');
|
||
});
|
||
|
||
testWidgets('maxLength still works with other formatters', (WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: TextField(
|
||
controller: textController,
|
||
maxLength: 10,
|
||
inputFormatters: <TextInputFormatter>[
|
||
FilteringTextInputFormatter.deny(RegExp(r'[a-z]'), replacementString: '#'),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六');
|
||
// The default single line formatter replaces \n with empty string.
|
||
expect(textController.text, '#一#二#三#四#五');
|
||
});
|
||
|
||
testWidgets('maxLength still works with other formatters (deprecated names)', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: TextField(
|
||
controller: textController,
|
||
maxLength: 10,
|
||
inputFormatters: <TextInputFormatter>[
|
||
FilteringTextInputFormatter.deny(RegExp(r'[a-z]'), replacementString: '#'),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六');
|
||
// The default single line formatter replaces \n with empty string.
|
||
expect(textController.text, '#一#二#三#四#五');
|
||
});
|
||
|
||
testWidgets("maxLength isn't enforced when maxLengthEnforcement.none.", (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: TextField(
|
||
controller: textController,
|
||
maxLength: 10,
|
||
maxLengthEnforcement: MaxLengthEnforcement.none,
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), '0123456789101112');
|
||
expect(textController.text, '0123456789101112');
|
||
});
|
||
|
||
testWidgets('maxLength shows warning when maxLengthEnforcement.none.', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
const testStyle = TextStyle(color: Colors.deepPurpleAccent);
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: TextField(
|
||
decoration: const InputDecoration(errorStyle: testStyle),
|
||
controller: textController,
|
||
maxLength: 10,
|
||
maxLengthEnforcement: MaxLengthEnforcement.none,
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), '0123456789101112');
|
||
await tester.pump();
|
||
|
||
expect(textController.text, '0123456789101112');
|
||
expect(find.text('16/10'), findsOneWidget);
|
||
Text counterTextWidget = tester.widget(find.text('16/10'));
|
||
expect(counterTextWidget.style!.color, equals(Colors.deepPurpleAccent));
|
||
|
||
await tester.enterText(find.byType(TextField), '0123456789');
|
||
await tester.pump();
|
||
|
||
expect(textController.text, '0123456789');
|
||
expect(find.text('10/10'), findsOneWidget);
|
||
counterTextWidget = tester.widget(find.text('10/10'));
|
||
expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent)));
|
||
});
|
||
|
||
testWidgets('maxLength shows warning in Material 3', (WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
final theme = ThemeData.from(
|
||
colorScheme: const ColorScheme.light().copyWith(error: Colors.deepPurpleAccent),
|
||
);
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
theme: theme,
|
||
child: TextField(
|
||
controller: textController,
|
||
maxLength: 10,
|
||
maxLengthEnforcement: MaxLengthEnforcement.none,
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), '0123456789101112');
|
||
await tester.pump();
|
||
|
||
expect(textController.text, '0123456789101112');
|
||
expect(find.text('16/10'), findsOneWidget);
|
||
Text counterTextWidget = tester.widget(find.text('16/10'));
|
||
expect(counterTextWidget.style!.color, equals(Colors.deepPurpleAccent));
|
||
|
||
await tester.enterText(find.byType(TextField), '0123456789');
|
||
await tester.pump();
|
||
|
||
expect(textController.text, '0123456789');
|
||
expect(find.text('10/10'), findsOneWidget);
|
||
counterTextWidget = tester.widget(find.text('10/10'));
|
||
expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent)));
|
||
});
|
||
|
||
testWidgets('maxLength shows warning when maxLengthEnforcement.none with surrogate pairs.', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
const testStyle = TextStyle(color: Colors.deepPurpleAccent);
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: TextField(
|
||
decoration: const InputDecoration(errorStyle: testStyle),
|
||
controller: textController,
|
||
maxLength: 10,
|
||
maxLengthEnforcement: MaxLengthEnforcement.none,
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), '😆012345678910111');
|
||
await tester.pump();
|
||
|
||
expect(textController.text, '😆012345678910111');
|
||
expect(find.text('16/10'), findsOneWidget);
|
||
Text counterTextWidget = tester.widget(find.text('16/10'));
|
||
expect(counterTextWidget.style!.color, equals(Colors.deepPurpleAccent));
|
||
|
||
await tester.enterText(find.byType(TextField), '😆012345678');
|
||
await tester.pump();
|
||
|
||
expect(textController.text, '😆012345678');
|
||
expect(find.text('10/10'), findsOneWidget);
|
||
counterTextWidget = tester.widget(find.text('10/10'));
|
||
expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent)));
|
||
});
|
||
|
||
testWidgets('maxLength shows warning when maxLengthEnforcement.none with grapheme clusters.', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
const testStyle = TextStyle(color: Colors.deepPurpleAccent);
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: TextField(
|
||
decoration: const InputDecoration(errorStyle: testStyle),
|
||
controller: textController,
|
||
maxLength: 10,
|
||
maxLengthEnforcement: MaxLengthEnforcement.none,
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), '👨👩👦012345678910111');
|
||
await tester.pump();
|
||
|
||
expect(textController.text, '👨👩👦012345678910111');
|
||
expect(find.text('16/10'), findsOneWidget);
|
||
Text counterTextWidget = tester.widget(find.text('16/10'));
|
||
expect(counterTextWidget.style!.color, equals(Colors.deepPurpleAccent));
|
||
|
||
await tester.enterText(find.byType(TextField), '👨👩👦012345678');
|
||
await tester.pump();
|
||
|
||
expect(textController.text, '👨👩👦012345678');
|
||
expect(find.text('10/10'), findsOneWidget);
|
||
counterTextWidget = tester.widget(find.text('10/10'));
|
||
expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent)));
|
||
});
|
||
|
||
testWidgets('maxLength limits input with surrogate pairs.', (WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(child: TextField(controller: textController, maxLength: 10)),
|
||
);
|
||
|
||
const surrogatePair = '😆';
|
||
await tester.enterText(find.byType(TextField), '${surrogatePair}0123456789101112');
|
||
expect(textController.text, '${surrogatePair}012345678');
|
||
});
|
||
|
||
testWidgets('maxLength limits input with grapheme clusters.', (WidgetTester tester) async {
|
||
final TextEditingController textController = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(child: TextField(controller: textController, maxLength: 10)),
|
||
);
|
||
|
||
const graphemeCluster = '👨👩👦';
|
||
await tester.enterText(find.byType(TextField), '${graphemeCluster}0123456789101112');
|
||
expect(textController.text, '${graphemeCluster}012345678');
|
||
});
|
||
|
||
testWidgets('setting maxLength shows counter', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(child: Center(child: TextField(maxLength: 10))),
|
||
),
|
||
);
|
||
|
||
expect(find.text('0/10'), findsOneWidget);
|
||
|
||
await tester.enterText(find.byType(TextField), '01234');
|
||
await tester.pump();
|
||
|
||
expect(find.text('5/10'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('maxLength counter measures surrogate pairs as one character', (
|
||
WidgetTester tester,
|
||
) async {
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(child: Center(child: TextField(maxLength: 10))),
|
||
),
|
||
);
|
||
|
||
expect(find.text('0/10'), findsOneWidget);
|
||
|
||
const surrogatePair = '😆';
|
||
await tester.enterText(find.byType(TextField), surrogatePair);
|
||
await tester.pump();
|
||
|
||
expect(find.text('1/10'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('maxLength counter measures grapheme clusters as one character', (
|
||
WidgetTester tester,
|
||
) async {
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(child: Center(child: TextField(maxLength: 10))),
|
||
),
|
||
);
|
||
|
||
expect(find.text('0/10'), findsOneWidget);
|
||
|
||
const familyEmoji = '👨👩👦';
|
||
await tester.enterText(find.byType(TextField), familyEmoji);
|
||
await tester.pump();
|
||
|
||
expect(find.text('1/10'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('setting maxLength to TextField.noMaxLength shows only entered length', (
|
||
WidgetTester tester,
|
||
) async {
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(maxLength: TextField.noMaxLength)),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('0'), findsOneWidget);
|
||
|
||
await tester.enterText(find.byType(TextField), '01234');
|
||
await tester.pump();
|
||
|
||
expect(find.text('5'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('passing a buildCounter shows returned widget', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
buildCounter:
|
||
(
|
||
BuildContext context, {
|
||
required int currentLength,
|
||
int? maxLength,
|
||
required bool isFocused,
|
||
}) {
|
||
return Text('$currentLength of $maxLength');
|
||
},
|
||
maxLength: 10,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('0 of 10'), findsOneWidget);
|
||
|
||
await tester.enterText(find.byType(TextField), '01234');
|
||
await tester.pump();
|
||
|
||
expect(find.text('5 of 10'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('TextField identifies as text field in semantics', (WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(child: Center(child: TextField(maxLength: 10))),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
semantics,
|
||
includesNodeWith(
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 0,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
),
|
||
);
|
||
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('Can scroll multiline input when disabled', (WidgetTester tester) async {
|
||
final Key textFieldKey = UniqueKey();
|
||
final TextEditingController controller = _textEditingController(text: kMoreThanFourLines);
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
key: textFieldKey,
|
||
controller: controller,
|
||
ignorePointers: false,
|
||
enabled: false,
|
||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||
maxLines: 2,
|
||
),
|
||
),
|
||
);
|
||
|
||
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
|
||
final RenderBox inputBox = findInputBox();
|
||
|
||
// Check that the last line of text is not displayed.
|
||
final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
|
||
final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
|
||
expect(firstPos.dx, fourthPos.dx);
|
||
expect(firstPos.dy, lessThan(fourthPos.dy));
|
||
expect(
|
||
inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(firstPos)),
|
||
isTrue,
|
||
);
|
||
expect(
|
||
inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(fourthPos)),
|
||
isFalse,
|
||
);
|
||
|
||
final TestGesture gesture = await tester.startGesture(firstPos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.moveBy(const Offset(0.0, -1000.0));
|
||
await tester.pumpAndSettle();
|
||
await gesture.moveBy(const Offset(0.0, -1000.0));
|
||
await tester.pumpAndSettle();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// Now the first line is scrolled up, and the fourth line is visible.
|
||
final Offset finalFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
|
||
final Offset finalFourthPos = textOffsetToPosition(
|
||
tester,
|
||
kMoreThanFourLines.indexOf('Fourth'),
|
||
);
|
||
|
||
expect(finalFirstPos.dy, lessThan(firstPos.dy));
|
||
expect(
|
||
inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(finalFirstPos)),
|
||
isFalse,
|
||
);
|
||
expect(
|
||
inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(finalFourthPos)),
|
||
isTrue,
|
||
);
|
||
});
|
||
|
||
testWidgets('Disabled text field does not have tap action', (WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(child: Center(child: TextField(maxLength: 10, enabled: false))),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
semantics,
|
||
isNot(
|
||
includesNodeWith(actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus]),
|
||
),
|
||
);
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('Disabled text field semantics node still contains value', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final semantics = SemanticsTester(tester);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
controller: _textEditingController(text: 'text'),
|
||
maxLength: 10,
|
||
enabled: false,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[], value: 'text'));
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('Readonly text field does not have tap action', (WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(child: Center(child: TextField(maxLength: 10, readOnly: true))),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
semantics,
|
||
isNot(
|
||
includesNodeWith(actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus]),
|
||
),
|
||
);
|
||
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('Disabled text field hides helper and counter', (WidgetTester tester) async {
|
||
const helperText = 'helper text';
|
||
const counterText = 'counter text';
|
||
const errorText = 'error text';
|
||
Widget buildFrame(bool enabled, bool hasError) {
|
||
return MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
decoration: InputDecoration(
|
||
labelText: 'label text',
|
||
helperText: helperText,
|
||
counterText: counterText,
|
||
errorText: hasError ? errorText : null,
|
||
enabled: enabled,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(buildFrame(true, false));
|
||
Text helperWidget = tester.widget(find.text(helperText));
|
||
Text counterWidget = tester.widget(find.text(counterText));
|
||
expect(helperWidget.style!.color, isNot(equals(Colors.transparent)));
|
||
expect(counterWidget.style!.color, isNot(equals(Colors.transparent)));
|
||
await tester.pumpWidget(buildFrame(true, true));
|
||
counterWidget = tester.widget(find.text(counterText));
|
||
Text errorWidget = tester.widget(find.text(errorText));
|
||
expect(helperWidget.style!.color, isNot(equals(Colors.transparent)));
|
||
expect(errorWidget.style!.color, isNot(equals(Colors.transparent)));
|
||
|
||
// When enabled is false, the helper/error and counter are not visible.
|
||
await tester.pumpWidget(buildFrame(false, false));
|
||
helperWidget = tester.widget(find.text(helperText));
|
||
counterWidget = tester.widget(find.text(counterText));
|
||
expect(helperWidget.style!.color, equals(Colors.transparent));
|
||
expect(counterWidget.style!.color, equals(Colors.transparent));
|
||
await tester.pumpWidget(buildFrame(false, true));
|
||
errorWidget = tester.widget(find.text(errorText));
|
||
counterWidget = tester.widget(find.text(counterText));
|
||
expect(counterWidget.style!.color, equals(Colors.transparent));
|
||
expect(errorWidget.style!.color, equals(Colors.transparent));
|
||
});
|
||
|
||
testWidgets('Disabled text field has default M2 disabled text style for the input text', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller, enabled: false)),
|
||
),
|
||
),
|
||
);
|
||
final EditableText editableText = tester.widget(find.byType(EditableText));
|
||
expect(
|
||
editableText.style.color,
|
||
Colors.black38,
|
||
); // Colors.black38 is the default disabled color for ThemeData.light().
|
||
});
|
||
|
||
testWidgets('Disabled text field has default M3 disabled text style for the input text', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
|
||
final theme = ThemeData();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: theme,
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller, enabled: false)),
|
||
),
|
||
),
|
||
);
|
||
final EditableText editableText = tester.widget(find.byType(EditableText));
|
||
expect(editableText.style.color, theme.textTheme.bodyLarge!.color!.withOpacity(0.38));
|
||
});
|
||
|
||
testWidgets('Enabled TextField statesController', (WidgetTester tester) async {
|
||
final textEditingController = TextEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
addTearDown(textEditingController.dispose);
|
||
|
||
var count = 0;
|
||
void valueChanged() {
|
||
count += 1;
|
||
}
|
||
|
||
final statesController = MaterialStatesController();
|
||
addTearDown(statesController.dispose);
|
||
|
||
statesController.addListener(valueChanged);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(statesController: statesController, controller: textEditingController),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||
final Offset center = tester.getCenter(find.byType(EditableText).first);
|
||
await gesture.moveTo(center);
|
||
await tester.pump();
|
||
expect(statesController.value, <WidgetState>{WidgetState.hovered});
|
||
expect(count, 1);
|
||
|
||
await gesture.moveTo(Offset.zero);
|
||
await tester.pump();
|
||
expect(statesController.value, <WidgetState>{});
|
||
expect(count, 2);
|
||
|
||
await gesture.down(center);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
expect(statesController.value, <WidgetState>{WidgetState.hovered, WidgetState.focused});
|
||
expect(count, 4); // adds hovered and pressed - two changes.
|
||
|
||
await gesture.moveTo(Offset.zero);
|
||
await tester.pump();
|
||
expect(statesController.value, <WidgetState>{WidgetState.focused});
|
||
expect(count, 5);
|
||
|
||
await gesture.down(Offset.zero);
|
||
await tester.pump();
|
||
expect(statesController.value, <WidgetState>{});
|
||
expect(count, 6);
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
await gesture.down(center);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
expect(statesController.value, <WidgetState>{WidgetState.hovered, WidgetState.focused});
|
||
expect(count, 8); // adds hovered and pressed - two changes.
|
||
|
||
// If the text field is rebuilt disabled, then the focused state is
|
||
// removed.
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
statesController: statesController,
|
||
controller: textEditingController,
|
||
enabled: false,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
expect(statesController.value, <WidgetState>{WidgetState.hovered, WidgetState.disabled});
|
||
expect(count, 10); // removes focused and adds disabled - two changes.
|
||
|
||
await gesture.moveTo(Offset.zero);
|
||
await tester.pump();
|
||
expect(statesController.value, <WidgetState>{WidgetState.disabled});
|
||
expect(count, 11);
|
||
|
||
// If the text field is rebuilt enabled and in an error state, then the error
|
||
// state is added.
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
statesController: statesController,
|
||
controller: textEditingController,
|
||
decoration: const InputDecoration(errorText: 'error'),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
expect(statesController.value, <WidgetState>{WidgetState.error});
|
||
expect(count, 13); // removes disabled and adds error - two changes.
|
||
|
||
// If the text field is rebuilt without an error, then the error
|
||
// state is removed.
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(statesController: statesController, controller: textEditingController),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
expect(statesController.value, <WidgetState>{});
|
||
expect(count, 14);
|
||
});
|
||
|
||
testWidgets('Disabled TextField statesController', (WidgetTester tester) async {
|
||
var count = 0;
|
||
void valueChanged() {
|
||
count += 1;
|
||
}
|
||
|
||
final controller = MaterialStatesController();
|
||
addTearDown(controller.dispose);
|
||
controller.addListener(valueChanged);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(statesController: controller, enabled: false)),
|
||
),
|
||
),
|
||
);
|
||
expect(controller.value, <WidgetState>{WidgetState.disabled});
|
||
expect(count, 1);
|
||
});
|
||
|
||
testWidgets('Provided style correctly resolves for material states', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
|
||
final theme = ThemeData();
|
||
|
||
Widget buildFrame(bool enabled) {
|
||
return MaterialApp(
|
||
theme: theme,
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
controller: controller,
|
||
enabled: enabled,
|
||
style: WidgetStateTextStyle.resolveWith((Set<WidgetState> states) {
|
||
if (states.contains(WidgetState.disabled)) {
|
||
return const TextStyle(color: Colors.red);
|
||
}
|
||
return const TextStyle(color: Colors.blue);
|
||
}),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(buildFrame(false));
|
||
EditableText editableText = tester.widget(find.byType(EditableText));
|
||
expect(editableText.style.color, Colors.red);
|
||
await tester.pumpWidget(buildFrame(true));
|
||
editableText = tester.widget(find.byType(EditableText));
|
||
expect(editableText.style.color, Colors.blue);
|
||
});
|
||
|
||
testWidgets('currentValueLength/maxValueLength are in the tree', (WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller, maxLength: 10)),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
semantics,
|
||
includesNodeWith(
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 0,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
maxValueLength: 10,
|
||
),
|
||
);
|
||
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
const testValue = '123';
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(
|
||
text: testValue,
|
||
selection: TextSelection.collapsed(offset: 3),
|
||
composing: TextRange(start: 0, end: testValue.length),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
includesNodeWith(
|
||
inputType: ui.SemanticsInputType.text,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isFocused,
|
||
],
|
||
maxValueLength: 10,
|
||
currentValueLength: 3,
|
||
),
|
||
);
|
||
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('Read only TextField identifies as read only text field in semantics', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final semantics = SemanticsTester(tester);
|
||
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(child: Center(child: TextField(maxLength: 10, readOnly: true))),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
semantics,
|
||
includesNodeWith(
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 0,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isReadOnly,
|
||
],
|
||
),
|
||
);
|
||
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets("Disabled TextField can't be traversed to.", (WidgetTester tester) async {
|
||
final focusNode1 = FocusNode(debugLabel: 'TextField 1');
|
||
addTearDown(focusNode1.dispose);
|
||
final focusNode2 = FocusNode(debugLabel: 'TextField 2');
|
||
addTearDown(focusNode2.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: FocusScope(
|
||
child: Center(
|
||
child: Column(
|
||
children: <Widget>[
|
||
TextField(focusNode: focusNode1, autofocus: true, maxLength: 10, enabled: true),
|
||
TextField(focusNode: focusNode2, maxLength: 10, enabled: false),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.pump();
|
||
expect(focusNode1.hasPrimaryFocus, isTrue);
|
||
expect(focusNode2.hasPrimaryFocus, isFalse);
|
||
|
||
expect(focusNode1.nextFocus(), isFalse);
|
||
await tester.pump();
|
||
|
||
expect(focusNode1.hasPrimaryFocus, isTrue);
|
||
expect(focusNode2.hasPrimaryFocus, isFalse);
|
||
});
|
||
|
||
group(
|
||
'Keyboard Tests',
|
||
() {
|
||
late TextEditingController controller;
|
||
|
||
setUp(() {
|
||
controller = _textEditingController();
|
||
});
|
||
|
||
Future<void> setupWidget(WidgetTester tester) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: KeyboardListener(
|
||
focusNode: focusNode,
|
||
child: TextField(controller: controller, maxLines: 3),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
}
|
||
|
||
testWidgets('Shift test 1', (WidgetTester tester) async {
|
||
await setupWidget(tester);
|
||
const testValue = 'a big house';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
|
||
await tester.idle();
|
||
// Need to wait for selection to catch up.
|
||
await tester.pump();
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
|
||
}, variant: KeySimulatorTransitModeVariant.all());
|
||
|
||
testWidgets('Shift test 2', (WidgetTester tester) async {
|
||
await setupWidget(tester);
|
||
|
||
const testValue = 'abcdefghi';
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(
|
||
text: testValue,
|
||
selection: TextSelection.collapsed(offset: 3),
|
||
composing: TextRange(start: 0, end: testValue.length),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
|
||
}, variant: KeySimulatorTransitModeVariant.all());
|
||
|
||
testWidgets('Control Shift test', (WidgetTester tester) async {
|
||
await setupWidget(tester);
|
||
const testValue = 'their big house';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
|
||
await tester.idle();
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.extentOffset - controller.selection.baseOffset, 5);
|
||
}, variant: KeySimulatorTransitModeVariant.all());
|
||
|
||
testWidgets('Down and up test', (WidgetTester tester) async {
|
||
await setupWidget(tester);
|
||
const testValue = 'a big house';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
|
||
await tester.idle();
|
||
// Need to wait for selection to catch up.
|
||
await tester.pump();
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowUp);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.extentOffset - controller.selection.baseOffset, -11);
|
||
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowUp);
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
|
||
}, variant: KeySimulatorTransitModeVariant.all());
|
||
|
||
testWidgets('Down and up test 2', (WidgetTester tester) async {
|
||
await setupWidget(tester);
|
||
const testValue = 'a big house\njumped over a mouse\nOne more line yay'; // 11 \n 19
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
|
||
await tester.idle();
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
|
||
for (var i = 0; i < 5; i += 1) {
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowRight);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.extentOffset - controller.selection.baseOffset, 12);
|
||
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.extentOffset - controller.selection.baseOffset, 32);
|
||
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.extentOffset - controller.selection.baseOffset, 12);
|
||
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
|
||
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
|
||
}, variant: KeySimulatorTransitModeVariant.all());
|
||
|
||
testWidgets('Read only keyboard selection test', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'readonly');
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller, readOnly: true)));
|
||
|
||
await tester.idle();
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);
|
||
expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
|
||
}, variant: KeySimulatorTransitModeVariant.all());
|
||
},
|
||
// [intended] only applies to platforms where we handle key events.
|
||
skip: areKeyEventsHandledByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Copy paste test',
|
||
(WidgetTester tester) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
final TextEditingController controller = _textEditingController();
|
||
final textField = TextField(controller: controller, maxLines: 3);
|
||
|
||
var clipboardContent = '';
|
||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (
|
||
MethodCall methodCall,
|
||
) async {
|
||
if (methodCall.method == 'Clipboard.setData') {
|
||
// ignore: avoid_dynamic_calls
|
||
clipboardContent = methodCall.arguments['text'] as String;
|
||
} else if (methodCall.method == 'Clipboard.getData') {
|
||
return <String, dynamic>{'text': clipboardContent};
|
||
}
|
||
return null;
|
||
});
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: KeyboardListener(focusNode: focusNode, child: textField),
|
||
),
|
||
),
|
||
);
|
||
focusNode.requestFocus();
|
||
await tester.pump();
|
||
|
||
const testValue = 'a big house\njumped over a mouse'; // 11 \n 19
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
|
||
await tester.idle();
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Select the first 5 characters
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
for (var i = 0; i < 5; i += 1) {
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
|
||
// Copy them
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.keyC);
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(clipboardContent, 'a big');
|
||
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||
await tester.pumpAndSettle();
|
||
|
||
// Paste them
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV);
|
||
await tester.pumpAndSettle();
|
||
await tester.pump(const Duration(milliseconds: 200));
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV);
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
|
||
await tester.pumpAndSettle();
|
||
|
||
const expected = 'a biga big house\njumped over a mouse';
|
||
expect(
|
||
find.text(expected),
|
||
findsOneWidget,
|
||
reason: 'Because text contains ${controller.text}',
|
||
);
|
||
},
|
||
// [intended] only applies to platforms where we handle key events.
|
||
skip: areKeyEventsHandledByPlatform,
|
||
variant: KeySimulatorTransitModeVariant.all(),
|
||
);
|
||
|
||
// Regression test for https://github.com/flutter/flutter/issues/78219
|
||
testWidgets(
|
||
'Paste does not crash after calling TextController.text setter',
|
||
(WidgetTester tester) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
final TextEditingController controller = _textEditingController();
|
||
final textField = TextField(controller: controller, obscureText: true);
|
||
|
||
const clipboardContent = 'I love Flutter!';
|
||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (
|
||
MethodCall methodCall,
|
||
) async {
|
||
if (methodCall.method == 'Clipboard.getData') {
|
||
return <String, dynamic>{'text': clipboardContent};
|
||
}
|
||
return null;
|
||
});
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: KeyboardListener(focusNode: focusNode, child: textField),
|
||
),
|
||
),
|
||
);
|
||
focusNode.requestFocus();
|
||
await tester.pump();
|
||
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Clear the text.
|
||
controller.text = '';
|
||
|
||
// Paste clipboardContent to the text field.
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV);
|
||
await tester.pumpAndSettle();
|
||
await tester.pump(const Duration(milliseconds: 200));
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV);
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
|
||
await tester.pumpAndSettle();
|
||
|
||
// Clipboard content is correctly pasted.
|
||
expect(find.text(clipboardContent), findsOneWidget);
|
||
},
|
||
// [intended] only applies to platforms where we handle key events.
|
||
skip: areKeyEventsHandledByPlatform,
|
||
variant: KeySimulatorTransitModeVariant.all(),
|
||
);
|
||
|
||
testWidgets(
|
||
'Cut test',
|
||
(WidgetTester tester) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
final TextEditingController controller = _textEditingController();
|
||
final textField = TextField(controller: controller, maxLines: 3);
|
||
var clipboardContent = '';
|
||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (
|
||
MethodCall methodCall,
|
||
) async {
|
||
if (methodCall.method == 'Clipboard.setData') {
|
||
// ignore: avoid_dynamic_calls
|
||
clipboardContent = methodCall.arguments['text'] as String;
|
||
} else if (methodCall.method == 'Clipboard.getData') {
|
||
return <String, dynamic>{'text': clipboardContent};
|
||
}
|
||
return null;
|
||
});
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: KeyboardListener(focusNode: focusNode, child: textField),
|
||
),
|
||
),
|
||
);
|
||
focusNode.requestFocus();
|
||
await tester.pump();
|
||
|
||
const testValue = 'a big house\njumped over a mouse'; // 11 \n 19
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
|
||
await tester.idle();
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Select the first 5 characters
|
||
for (var i = 0; i < 5; i += 1) {
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
|
||
// Cut them
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.keyX);
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(clipboardContent, 'a big');
|
||
|
||
for (var i = 0; i < 5; i += 1) {
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
|
||
// Paste them
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV);
|
||
await tester.pumpAndSettle();
|
||
await tester.pump(const Duration(milliseconds: 200));
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV);
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
|
||
await tester.pumpAndSettle();
|
||
|
||
const expected = ' housa bige\njumped over a mouse';
|
||
expect(find.text(expected), findsOneWidget);
|
||
},
|
||
// [intended] only applies to platforms where we handle key events.
|
||
skip: areKeyEventsHandledByPlatform,
|
||
variant: KeySimulatorTransitModeVariant.all(),
|
||
);
|
||
|
||
testWidgets(
|
||
'Select all test',
|
||
(WidgetTester tester) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
final TextEditingController controller = _textEditingController();
|
||
final textField = TextField(controller: controller, maxLines: 3);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: KeyboardListener(focusNode: focusNode, child: textField),
|
||
),
|
||
),
|
||
);
|
||
focusNode.requestFocus();
|
||
await tester.pump();
|
||
|
||
const testValue = 'a big house\njumped over a mouse'; // 11 \n 19
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
|
||
await tester.idle();
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Select All
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.control);
|
||
await tester.pumpAndSettle();
|
||
|
||
// Delete them
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.delete);
|
||
await tester.pumpAndSettle();
|
||
await tester.pump(const Duration(milliseconds: 200));
|
||
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.delete);
|
||
await tester.pumpAndSettle();
|
||
|
||
const expected = '';
|
||
expect(find.text(expected), findsOneWidget);
|
||
},
|
||
// [intended] only applies to platforms where we handle key events.
|
||
skip: areKeyEventsHandledByPlatform,
|
||
variant: KeySimulatorTransitModeVariant.all(),
|
||
);
|
||
|
||
testWidgets(
|
||
'Delete test',
|
||
(WidgetTester tester) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
final TextEditingController controller = _textEditingController();
|
||
final textField = TextField(controller: controller, maxLines: 3);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: KeyboardListener(focusNode: focusNode, child: textField),
|
||
),
|
||
),
|
||
);
|
||
focusNode.requestFocus();
|
||
await tester.pump();
|
||
|
||
const testValue = 'a big house\njumped over a mouse'; // 11 \n 19
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
|
||
await tester.idle();
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Delete
|
||
for (var i = 0; i < 6; i += 1) {
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.delete);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
|
||
const expected = 'house\njumped over a mouse';
|
||
expect(find.text(expected), findsOneWidget);
|
||
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.control);
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.delete);
|
||
await tester.pumpAndSettle();
|
||
|
||
const expected2 = '';
|
||
expect(find.text(expected2), findsOneWidget);
|
||
},
|
||
// [intended] only applies to platforms where we handle key events.
|
||
skip: areKeyEventsHandledByPlatform,
|
||
variant: KeySimulatorTransitModeVariant.all(),
|
||
);
|
||
|
||
testWidgets(
|
||
'Changing positions of text fields',
|
||
(WidgetTester tester) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
final events = <KeyEvent>[];
|
||
|
||
final TextEditingController c1 = _textEditingController();
|
||
final TextEditingController c2 = _textEditingController();
|
||
final Key key1 = UniqueKey();
|
||
final Key key2 = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: KeyboardListener(
|
||
focusNode: focusNode,
|
||
onKeyEvent: events.add,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: <Widget>[
|
||
TextField(key: key1, controller: c1, maxLines: 3),
|
||
TextField(key: key2, controller: c2, maxLines: 3),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'a big house';
|
||
await tester.enterText(find.byType(TextField).first, testValue);
|
||
|
||
await tester.idle();
|
||
// Need to wait for selection to catch up.
|
||
await tester.pump();
|
||
await tester.tap(find.byType(TextField).first);
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
for (var i = 0; i < 5; i += 1) {
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
|
||
expect(c1.selection.extentOffset - c1.selection.baseOffset, -5);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: KeyboardListener(
|
||
focusNode: focusNode,
|
||
onKeyEvent: events.add,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: <Widget>[
|
||
TextField(key: key2, controller: c2, maxLines: 3),
|
||
TextField(key: key1, controller: c1, maxLines: 3),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
for (var i = 0; i < 5; i += 1) {
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
|
||
expect(c1.selection.extentOffset - c1.selection.baseOffset, -10);
|
||
},
|
||
// [intended] only applies to platforms where we handle key events.
|
||
skip: areKeyEventsHandledByPlatform,
|
||
variant: KeySimulatorTransitModeVariant.all(),
|
||
);
|
||
|
||
testWidgets(
|
||
'Changing focus test',
|
||
(WidgetTester tester) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
final events = <KeyEvent>[];
|
||
|
||
final TextEditingController c1 = _textEditingController();
|
||
final TextEditingController c2 = _textEditingController();
|
||
final Key key1 = UniqueKey();
|
||
final Key key2 = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: KeyboardListener(
|
||
focusNode: focusNode,
|
||
onKeyEvent: events.add,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: <Widget>[
|
||
TextField(key: key1, controller: c1, maxLines: 3),
|
||
TextField(key: key2, controller: c2, maxLines: 3),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'a big house';
|
||
await tester.enterText(find.byType(TextField).first, testValue);
|
||
await tester.idle();
|
||
await tester.pump();
|
||
|
||
await tester.idle();
|
||
await tester.tap(find.byType(TextField).first);
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
for (var i = 0; i < 5; i += 1) {
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
|
||
expect(c1.selection.extentOffset - c1.selection.baseOffset, -5);
|
||
expect(c2.selection.extentOffset - c2.selection.baseOffset, 0);
|
||
|
||
await tester.enterText(find.byType(TextField).last, testValue);
|
||
await tester.idle();
|
||
await tester.pump();
|
||
|
||
await tester.idle();
|
||
await tester.tap(find.byType(TextField).last);
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
for (var i = 0; i < 5; i += 1) {
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
|
||
expect(c1.selection.extentOffset - c1.selection.baseOffset, -5);
|
||
expect(c2.selection.extentOffset - c2.selection.baseOffset, -5);
|
||
},
|
||
// [intended] only applies to platforms where we handle key events.
|
||
skip: areKeyEventsHandledByPlatform,
|
||
variant: KeySimulatorTransitModeVariant.all(),
|
||
);
|
||
|
||
testWidgets('Caret works when maxLines is null', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(overlay(child: TextField(controller: controller, maxLines: null)));
|
||
|
||
const testValue = 'x';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.length);
|
||
|
||
// Tap the selection handle to bring up the "paste / select all" menu.
|
||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||
await tester.pump();
|
||
await tester.pump(
|
||
const Duration(milliseconds: 200),
|
||
); // skip past the frame where the opacity is
|
||
|
||
// Confirm that the selection was updated.
|
||
expect(controller.selection.baseOffset, 0);
|
||
});
|
||
|
||
testWidgets('TextField baseline alignment no-strut', (WidgetTester tester) async {
|
||
final TextEditingController controllerA = _textEditingController(text: 'A');
|
||
final TextEditingController controllerB = _textEditingController(text: 'B');
|
||
final Key keyA = UniqueKey();
|
||
final Key keyB = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||
textBaseline: TextBaseline.alphabetic,
|
||
children: <Widget>[
|
||
Expanded(
|
||
child: TextField(
|
||
key: keyA,
|
||
decoration: null,
|
||
controller: controllerA,
|
||
// The point size of the font must be a multiple of 4 until
|
||
// https://github.com/flutter/flutter/issues/122066 is resolved.
|
||
style: const TextStyle(fontFamily: 'FlutterTest', fontSize: 12.0),
|
||
strutStyle: StrutStyle.disabled,
|
||
),
|
||
),
|
||
const Text(
|
||
'abc',
|
||
// The point size of the font must be a multiple of 4 until
|
||
// https://github.com/flutter/flutter/issues/122066 is resolved.
|
||
style: TextStyle(fontFamily: 'FlutterTest', fontSize: 24.0),
|
||
),
|
||
Expanded(
|
||
child: TextField(
|
||
key: keyB,
|
||
decoration: null,
|
||
controller: controllerB,
|
||
// The point size of the font must be a multiple of 4 until
|
||
// https://github.com/flutter/flutter/issues/122066 is resolved.
|
||
style: const TextStyle(fontFamily: 'FlutterTest', fontSize: 36.0),
|
||
strutStyle: StrutStyle.disabled,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// The test font extends 0.25 * fontSize below the baseline.
|
||
// So the three row elements line up like this:
|
||
//
|
||
// A abc B
|
||
// --------- baseline
|
||
// 3 6 9 space below the baseline = 0.25 * fontSize
|
||
// --------- rowBottomY
|
||
|
||
final double rowBottomY = tester.getBottomLeft(find.byType(Row)).dy;
|
||
expect(tester.getBottomLeft(find.byKey(keyA)).dy, rowBottomY - 6.0);
|
||
expect(tester.getBottomLeft(find.text('abc')).dy, rowBottomY - 3.0);
|
||
expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY);
|
||
});
|
||
|
||
testWidgets('TextField baseline alignment', (WidgetTester tester) async {
|
||
final TextEditingController controllerA = _textEditingController(text: 'A');
|
||
final TextEditingController controllerB = _textEditingController(text: 'B');
|
||
final Key keyA = UniqueKey();
|
||
final Key keyB = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: overlay(
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||
textBaseline: TextBaseline.alphabetic,
|
||
children: <Widget>[
|
||
Expanded(
|
||
child: TextField(
|
||
key: keyA,
|
||
decoration: null,
|
||
controller: controllerA,
|
||
// The point size of the font must be a multiple of 4 until
|
||
// https://github.com/flutter/flutter/issues/122066 is resolved.
|
||
style: const TextStyle(fontFamily: 'FlutterTest', fontSize: 12.0),
|
||
),
|
||
),
|
||
const Text(
|
||
'abc',
|
||
// The point size of the font must be a multiple of 4 until
|
||
// https://github.com/flutter/flutter/issues/122066 is resolved.
|
||
style: TextStyle(fontFamily: 'FlutterTest', fontSize: 24.0),
|
||
),
|
||
Expanded(
|
||
child: TextField(
|
||
key: keyB,
|
||
decoration: null,
|
||
controller: controllerB,
|
||
// The point size of the font must be a multiple of 4 until
|
||
// https://github.com/flutter/flutter/issues/122066 is resolved.
|
||
style: const TextStyle(fontFamily: 'FlutterTest', fontSize: 36.0),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// The test font extends 0.25 * fontSize below the baseline.
|
||
// So the three row elements line up like this:
|
||
//
|
||
// A abc B
|
||
// --------- baseline
|
||
// 3 6 9 space below the baseline = 0.25 * fontSize
|
||
// --------- rowBottomY
|
||
|
||
final double rowBottomY = tester.getBottomLeft(find.byType(Row)).dy;
|
||
// The values here should match the version with strut disabled ('TextField baseline alignment no-strut')
|
||
expect(tester.getBottomLeft(find.byKey(keyA)).dy, rowBottomY - 6.0);
|
||
expect(tester.getBottomLeft(find.text('abc')).dy, rowBottomY - 3.0);
|
||
expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY);
|
||
});
|
||
|
||
testWidgets(
|
||
'TextField semantics include label when unfocused and label/hint when focused if input is empty',
|
||
(WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final TextEditingController controller = _textEditingController();
|
||
final Key key = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
key: key,
|
||
controller: controller,
|
||
decoration: const InputDecoration(hintText: 'hint', labelText: 'label'),
|
||
),
|
||
),
|
||
);
|
||
|
||
final SemanticsNode node = tester.getSemantics(find.byKey(key));
|
||
|
||
expect(node.label, 'label');
|
||
expect(node.value, '');
|
||
|
||
// Focus text field.
|
||
await tester.tap(find.byKey(key));
|
||
await tester.pump();
|
||
|
||
expect(node.label, 'label');
|
||
expect(node.value, '');
|
||
semantics.dispose();
|
||
},
|
||
);
|
||
|
||
testWidgets(
|
||
'TextField semantics always include label and not hint when input value is not empty',
|
||
(WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final TextEditingController controller = _textEditingController(text: 'value');
|
||
final Key key = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
key: key,
|
||
controller: controller,
|
||
decoration: const InputDecoration(hintText: 'hint', labelText: 'label'),
|
||
),
|
||
),
|
||
);
|
||
|
||
final SemanticsNode node = tester.getSemantics(find.byKey(key));
|
||
|
||
expect(node.label, 'label');
|
||
expect(node.value, 'value');
|
||
|
||
// Focus text field.
|
||
await tester.tap(find.byKey(key));
|
||
await tester.pump();
|
||
|
||
expect(node.label, 'label');
|
||
expect(node.value, 'value');
|
||
semantics.dispose();
|
||
},
|
||
);
|
||
|
||
testWidgets('TextField semantics always include label when no hint is given', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final TextEditingController controller = _textEditingController(text: 'value');
|
||
final Key key = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
key: key,
|
||
controller: controller,
|
||
decoration: const InputDecoration(labelText: 'label'),
|
||
),
|
||
),
|
||
);
|
||
|
||
final SemanticsNode node = tester.getSemantics(find.byKey(key));
|
||
|
||
expect(node.label, 'label');
|
||
expect(node.value, 'value');
|
||
|
||
// Focus text field.
|
||
await tester.tap(find.byKey(key));
|
||
await tester.pump();
|
||
|
||
expect(node.label, 'label');
|
||
expect(node.value, 'value');
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('TextField semantics only include hint when it is visible', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final TextEditingController controller = _textEditingController(text: 'value');
|
||
final Key key = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
key: key,
|
||
controller: controller,
|
||
decoration: const InputDecoration(hintText: 'hint'),
|
||
),
|
||
),
|
||
);
|
||
|
||
final SemanticsNode node = tester.getSemantics(find.byKey(key));
|
||
|
||
expect(node.label, '');
|
||
expect(node.value, 'value');
|
||
|
||
// Focus text field.
|
||
await tester.tap(find.byKey(key));
|
||
await tester.pump();
|
||
|
||
expect(node.label, '');
|
||
expect(node.value, 'value');
|
||
|
||
// Clear the Text.
|
||
await tester.enterText(find.byType(TextField), '');
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(node.value, '');
|
||
expect(node.label, 'hint');
|
||
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('TextField semantics', (WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final TextEditingController controller = _textEditingController();
|
||
final Key key = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(key: key, controller: controller),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
textDirection: TextDirection.ltr,
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 0,
|
||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
|
||
controller.text = 'Guten Tag';
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
textDirection: TextDirection.ltr,
|
||
value: 'Guten Tag',
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 9,
|
||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byKey(key));
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
textDirection: TextDirection.ltr,
|
||
value: 'Guten Tag',
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 9,
|
||
textSelection: const TextSelection.collapsed(offset: 9),
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
SemanticsAction.focus,
|
||
SemanticsAction.moveCursorBackwardByCharacter,
|
||
SemanticsAction.moveCursorBackwardByWord,
|
||
SemanticsAction.setSelection,
|
||
SemanticsAction.setText,
|
||
SemanticsAction.paste,
|
||
],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isFocused,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
|
||
controller.selection = const TextSelection.collapsed(offset: 4);
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
textDirection: TextDirection.ltr,
|
||
textSelection: const TextSelection.collapsed(offset: 4),
|
||
value: 'Guten Tag',
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 9,
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
SemanticsAction.focus,
|
||
SemanticsAction.moveCursorBackwardByCharacter,
|
||
SemanticsAction.moveCursorForwardByCharacter,
|
||
SemanticsAction.moveCursorBackwardByWord,
|
||
SemanticsAction.moveCursorForwardByWord,
|
||
SemanticsAction.setSelection,
|
||
SemanticsAction.setText,
|
||
SemanticsAction.paste,
|
||
],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isFocused,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
|
||
controller.text = 'Schönen Feierabend';
|
||
controller.selection = const TextSelection.collapsed(offset: 0);
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
textDirection: TextDirection.ltr,
|
||
textSelection: const TextSelection.collapsed(offset: 0),
|
||
value: 'Schönen Feierabend',
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 18,
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
SemanticsAction.focus,
|
||
SemanticsAction.moveCursorForwardByCharacter,
|
||
SemanticsAction.moveCursorForwardByWord,
|
||
SemanticsAction.setSelection,
|
||
SemanticsAction.setText,
|
||
SemanticsAction.paste,
|
||
],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isFocused,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
|
||
semantics.dispose();
|
||
});
|
||
|
||
// Regressing test for https://github.com/flutter/flutter/issues/99763
|
||
testWidgets('Update textField semantics when obscureText changes', (WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final TextEditingController controller = _textEditingController();
|
||
await tester.pumpWidget(_ObscureTextTestWidget(controller: controller));
|
||
|
||
controller.text = 'Hello';
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
includesNodeWith(
|
||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||
textDirection: TextDirection.ltr,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
value: 'Hello',
|
||
inputType: ui.SemanticsInputType.text,
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byType(ElevatedButton));
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
includesNodeWith(
|
||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||
textDirection: TextDirection.ltr,
|
||
inputType: ui.SemanticsInputType.text,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isObscured,
|
||
],
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byType(ElevatedButton));
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
includesNodeWith(
|
||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||
textDirection: TextDirection.ltr,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
value: 'Hello',
|
||
inputType: ui.SemanticsInputType.text,
|
||
),
|
||
);
|
||
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('TextField semantics, enableInteractiveSelection = false', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final TextEditingController controller = _textEditingController();
|
||
final Key key = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(key: key, controller: controller, enableInteractiveSelection: false),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byKey(key));
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
textDirection: TextDirection.ltr,
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 0,
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
SemanticsAction.focus,
|
||
SemanticsAction.setText,
|
||
// Absent the following because enableInteractiveSelection: false
|
||
// SemanticsAction.moveCursorBackwardByCharacter,
|
||
// SemanticsAction.moveCursorBackwardByWord,
|
||
// SemanticsAction.setSelection,
|
||
// SemanticsAction.paste,
|
||
],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isFocused,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('TextField semantics for selections', (WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final TextEditingController controller = _textEditingController()..text = 'Hello';
|
||
final Key key = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(key: key, controller: controller),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
value: 'Hello',
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 5,
|
||
textDirection: TextDirection.ltr,
|
||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
|
||
// Focus the text field
|
||
await tester.tap(find.byKey(key));
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
value: 'Hello',
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 5,
|
||
textSelection: const TextSelection.collapsed(offset: 5),
|
||
textDirection: TextDirection.ltr,
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
SemanticsAction.focus,
|
||
SemanticsAction.moveCursorBackwardByCharacter,
|
||
SemanticsAction.moveCursorBackwardByWord,
|
||
SemanticsAction.setSelection,
|
||
SemanticsAction.setText,
|
||
SemanticsAction.paste,
|
||
],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isFocused,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
|
||
controller.selection = const TextSelection(baseOffset: 5, extentOffset: 3);
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
value: 'Hello',
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 5,
|
||
textSelection: const TextSelection(baseOffset: 5, extentOffset: 3),
|
||
textDirection: TextDirection.ltr,
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
SemanticsAction.focus,
|
||
SemanticsAction.moveCursorBackwardByCharacter,
|
||
SemanticsAction.moveCursorForwardByCharacter,
|
||
SemanticsAction.moveCursorBackwardByWord,
|
||
SemanticsAction.moveCursorForwardByWord,
|
||
SemanticsAction.setSelection,
|
||
SemanticsAction.setText,
|
||
SemanticsAction.paste,
|
||
SemanticsAction.cut,
|
||
SemanticsAction.copy,
|
||
],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isFocused,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('TextField change selection with semantics', (WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
||
final TextEditingController controller = _textEditingController()..text = 'Hello';
|
||
final Key key = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(key: key, controller: controller),
|
||
),
|
||
);
|
||
|
||
// Focus the text field
|
||
await tester.tap(find.byKey(key));
|
||
await tester.pump();
|
||
|
||
const inputFieldId = 2;
|
||
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 5, affinity: TextAffinity.upstream),
|
||
);
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
id: 1,
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: inputFieldId,
|
||
value: 'Hello',
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 5,
|
||
textSelection: const TextSelection.collapsed(offset: 5),
|
||
textDirection: TextDirection.ltr,
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
SemanticsAction.focus,
|
||
SemanticsAction.moveCursorBackwardByCharacter,
|
||
SemanticsAction.moveCursorBackwardByWord,
|
||
SemanticsAction.setSelection,
|
||
SemanticsAction.setText,
|
||
SemanticsAction.paste,
|
||
],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isFocused,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
|
||
// move cursor back once
|
||
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <dynamic, dynamic>{
|
||
'base': 4,
|
||
'extent': 4,
|
||
});
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 4));
|
||
|
||
// move cursor to front
|
||
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <dynamic, dynamic>{
|
||
'base': 0,
|
||
'extent': 0,
|
||
});
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 0));
|
||
|
||
// select all
|
||
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <dynamic, dynamic>{
|
||
'base': 0,
|
||
'extent': 5,
|
||
});
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
id: 1,
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: inputFieldId,
|
||
value: 'Hello',
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 5,
|
||
textSelection: const TextSelection(baseOffset: 0, extentOffset: 5),
|
||
textDirection: TextDirection.ltr,
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
SemanticsAction.focus,
|
||
SemanticsAction.moveCursorBackwardByCharacter,
|
||
SemanticsAction.moveCursorBackwardByWord,
|
||
SemanticsAction.setSelection,
|
||
SemanticsAction.setText,
|
||
SemanticsAction.paste,
|
||
SemanticsAction.cut,
|
||
SemanticsAction.copy,
|
||
],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isFocused,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('Can activate TextField with explicit controller via semantics ', (
|
||
WidgetTester tester,
|
||
) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/17801
|
||
|
||
const textInTextField = 'Hello';
|
||
|
||
final semantics = SemanticsTester(tester);
|
||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
||
final TextEditingController controller = _textEditingController()..text = textInTextField;
|
||
final Key key = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(key: key, controller: controller),
|
||
),
|
||
);
|
||
|
||
const inputFieldId = 2;
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
id: 1,
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: inputFieldId,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||
value: textInTextField,
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 5,
|
||
textDirection: TextDirection.ltr,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreRect: true,
|
||
ignoreTransform: true,
|
||
),
|
||
);
|
||
|
||
semanticsOwner.performAction(inputFieldId, SemanticsAction.tap);
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
id: 1,
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: inputFieldId,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isFocused,
|
||
],
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
SemanticsAction.focus,
|
||
SemanticsAction.moveCursorBackwardByCharacter,
|
||
SemanticsAction.moveCursorBackwardByWord,
|
||
SemanticsAction.setSelection,
|
||
SemanticsAction.setText,
|
||
SemanticsAction.paste,
|
||
],
|
||
value: textInTextField,
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 5,
|
||
textDirection: TextDirection.ltr,
|
||
textSelection: const TextSelection(
|
||
baseOffset: textInTextField.length,
|
||
extentOffset: textInTextField.length,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreRect: true,
|
||
ignoreTransform: true,
|
||
),
|
||
);
|
||
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('When clipboard empty, no semantics paste option', (WidgetTester tester) async {
|
||
const textInTextField = 'Hello';
|
||
|
||
final semantics = SemanticsTester(tester);
|
||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
||
final TextEditingController controller = _textEditingController()..text = textInTextField;
|
||
final Key key = UniqueKey();
|
||
|
||
// Clear the clipboard.
|
||
await Clipboard.setData(const ClipboardData(text: ''));
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(key: key, controller: controller),
|
||
),
|
||
);
|
||
|
||
const inputFieldId = 2;
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
id: 1,
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: inputFieldId,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||
value: textInTextField,
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 5,
|
||
textDirection: TextDirection.ltr,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreRect: true,
|
||
ignoreTransform: true,
|
||
),
|
||
);
|
||
|
||
semanticsOwner.performAction(inputFieldId, SemanticsAction.tap);
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
id: 1,
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: inputFieldId,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isFocused,
|
||
],
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
SemanticsAction.focus,
|
||
SemanticsAction.moveCursorBackwardByCharacter,
|
||
SemanticsAction.moveCursorBackwardByWord,
|
||
SemanticsAction.setSelection,
|
||
SemanticsAction.setText,
|
||
// No paste option.
|
||
],
|
||
value: textInTextField,
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 5,
|
||
textDirection: TextDirection.ltr,
|
||
textSelection: const TextSelection(
|
||
baseOffset: textInTextField.length,
|
||
extentOffset: textInTextField.length,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreRect: true,
|
||
ignoreTransform: true,
|
||
),
|
||
);
|
||
|
||
semantics.dispose();
|
||
|
||
// On web, we don't check for pasteability because that triggers a
|
||
// permission dialog in the browser.
|
||
// https://github.com/flutter/flutter/pull/57139#issuecomment-629048058
|
||
}, skip: isBrowser); // [intended] see above.
|
||
|
||
testWidgets('TextField throws when not descended from a Material widget', (
|
||
WidgetTester tester,
|
||
) async {
|
||
const Widget textField = TextField();
|
||
await tester.pumpWidget(textField);
|
||
final dynamic exception = tester.takeException();
|
||
expect(exception, isFlutterError);
|
||
expect(exception.toString(), startsWith('No Material widget found.'));
|
||
});
|
||
|
||
testWidgets('TextField loses focus when disabled', (WidgetTester tester) async {
|
||
final focusNode = FocusNode(debugLabel: 'TextField Focus Node');
|
||
addTearDown(focusNode.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(child: TextField(focusNode: focusNode, autofocus: true, enabled: true)),
|
||
);
|
||
expect(focusNode.hasFocus, isTrue);
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(child: TextField(focusNode: focusNode, autofocus: true, enabled: false)),
|
||
);
|
||
expect(focusNode.hasFocus, isFalse);
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: Builder(
|
||
builder: (BuildContext context) {
|
||
return MediaQuery(
|
||
data: MediaQuery.of(context).copyWith(navigationMode: NavigationMode.directional),
|
||
child: TextField(focusNode: focusNode, autofocus: true, enabled: true),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
focusNode.requestFocus();
|
||
await tester.pump();
|
||
|
||
expect(focusNode.hasFocus, isTrue);
|
||
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: Builder(
|
||
builder: (BuildContext context) {
|
||
return MediaQuery(
|
||
data: MediaQuery.of(context).copyWith(navigationMode: NavigationMode.directional),
|
||
child: TextField(focusNode: focusNode, autofocus: true, enabled: false),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
expect(focusNode.hasFocus, isTrue);
|
||
});
|
||
|
||
testWidgets('TextField displays text with text direction', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: const Material(child: TextField(textDirection: TextDirection.rtl)),
|
||
),
|
||
);
|
||
|
||
RenderEditable editable = findRenderEditable(tester);
|
||
|
||
await tester.enterText(find.byType(TextField), '0123456789101112');
|
||
await tester.pumpAndSettle();
|
||
Offset topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 10)).topLeft,
|
||
);
|
||
|
||
expect(topLeft.dx, equals(701));
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: const Material(child: TextField(textDirection: TextDirection.ltr)),
|
||
),
|
||
);
|
||
|
||
editable = findRenderEditable(tester);
|
||
|
||
await tester.enterText(find.byType(TextField), '0123456789101112');
|
||
await tester.pumpAndSettle();
|
||
topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 10)).topLeft,
|
||
);
|
||
|
||
expect(topLeft.dx, equals(160.0));
|
||
});
|
||
|
||
testWidgets('TextField semantics', (WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final TextEditingController controller = _textEditingController();
|
||
final Key key = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
key: key,
|
||
controller: controller,
|
||
maxLength: 10,
|
||
decoration: const InputDecoration(
|
||
labelText: 'label',
|
||
hintText: 'hint',
|
||
helperText: 'helper',
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
label: 'label',
|
||
textDirection: TextDirection.ltr,
|
||
inputType: ui.SemanticsInputType.text,
|
||
maxValueLength: 10,
|
||
currentValueLength: 0,
|
||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
children: <TestSemantics>[
|
||
TestSemantics(label: 'helper', textDirection: TextDirection.ltr),
|
||
TestSemantics(
|
||
label: '10 characters remaining',
|
||
textDirection: TextDirection.ltr,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump();
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
label: 'label',
|
||
textDirection: TextDirection.ltr,
|
||
textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 0,
|
||
maxValueLength: 10,
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
SemanticsAction.focus,
|
||
SemanticsAction.setSelection,
|
||
SemanticsAction.setText,
|
||
SemanticsAction.paste,
|
||
],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
SemanticsFlag.isFocused,
|
||
],
|
||
children: <TestSemantics>[
|
||
TestSemantics(id: 2, label: 'helper', textDirection: TextDirection.ltr),
|
||
TestSemantics(
|
||
label: '10 characters remaining',
|
||
flags: <SemanticsFlag>[SemanticsFlag.isLiveRegion],
|
||
textDirection: TextDirection.ltr,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreId: true,
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
),
|
||
);
|
||
|
||
controller.text = 'hello';
|
||
await tester.pump();
|
||
semantics.dispose();
|
||
});
|
||
|
||
testWidgets('InputDecoration counterText can have a semanticCounterText', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final TextEditingController controller = _textEditingController();
|
||
final Key key = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
key: key,
|
||
controller: controller,
|
||
decoration: const InputDecoration(
|
||
labelText: 'label',
|
||
hintText: 'hint',
|
||
helperText: 'helper',
|
||
counterText: '0/10',
|
||
semanticCounterText: '0 out of 10',
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
label: 'label',
|
||
textDirection: TextDirection.ltr,
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 0,
|
||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
children: <TestSemantics>[
|
||
TestSemantics(label: 'helper', textDirection: TextDirection.ltr),
|
||
TestSemantics(label: '0 out of 10', textDirection: TextDirection.ltr),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
ignoreId: true,
|
||
),
|
||
);
|
||
|
||
semantics.dispose();
|
||
});
|
||
|
||
for (final supportsAnnounce in <bool>[true, false]) {
|
||
testWidgets('InputDecoration errorText semantics (supportsAnnounce=$supportsAnnounce)', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final TextEditingController controller = _textEditingController();
|
||
final Key key = UniqueKey();
|
||
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: MediaQuery(
|
||
data: MediaQueryData(supportsAnnounce: supportsAnnounce),
|
||
child: TextField(
|
||
key: key,
|
||
controller: controller,
|
||
decoration: const InputDecoration(
|
||
labelText: 'label',
|
||
hintText: 'hint',
|
||
errorText: 'oh no!',
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
children: <TestSemantics>[
|
||
TestSemantics.rootChild(
|
||
label: 'label',
|
||
textDirection: TextDirection.ltr,
|
||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 0,
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
label: 'oh no!',
|
||
textDirection: TextDirection.ltr,
|
||
flags: <SemanticsFlag>[if (!supportsAnnounce) SemanticsFlag.isLiveRegion],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreTransform: true,
|
||
ignoreRect: true,
|
||
ignoreId: true,
|
||
),
|
||
);
|
||
|
||
semantics.dispose();
|
||
debugDefaultTargetPlatformOverride = null;
|
||
});
|
||
}
|
||
|
||
testWidgets('floating label does not overlap with value at large textScaleFactors', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller = _textEditingController(text: 'Just some text');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Scaffold(
|
||
body: MediaQuery(
|
||
data: const MediaQueryData(textScaler: TextScaler.linear(4.0)),
|
||
child: Center(
|
||
child: TextField(
|
||
decoration: const InputDecoration(
|
||
labelText: 'Label',
|
||
border: UnderlineInputBorder(),
|
||
),
|
||
controller: controller,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byType(TextField));
|
||
final Rect labelRect = tester.getRect(find.text('Label'));
|
||
final Rect fieldRect = tester.getRect(find.text('Just some text'));
|
||
expect(labelRect.bottom, lessThanOrEqualTo(fieldRect.top));
|
||
});
|
||
|
||
testWidgets('TextField scrolls into view but does not bounce (SingleChildScrollView)', (
|
||
WidgetTester tester,
|
||
) async {
|
||
// This is a regression test for https://github.com/flutter/flutter/issues/20485
|
||
|
||
final Key textField1 = UniqueKey();
|
||
final Key textField2 = UniqueKey();
|
||
final scrollController = ScrollController();
|
||
addTearDown(scrollController.dispose);
|
||
|
||
double? minOffset;
|
||
double? maxOffset;
|
||
|
||
scrollController.addListener(() {
|
||
final double offset = scrollController.offset;
|
||
minOffset = math.min(minOffset ?? offset, offset);
|
||
maxOffset = math.max(maxOffset ?? offset, offset);
|
||
});
|
||
|
||
Widget buildFrame(Axis scrollDirection) {
|
||
return MaterialApp(
|
||
home: Scaffold(
|
||
body: SafeArea(
|
||
child: SingleChildScrollView(
|
||
physics: const BouncingScrollPhysics(),
|
||
controller: scrollController,
|
||
child: Column(
|
||
children: <Widget>[
|
||
SizedBox(
|
||
// visible when scrollOffset is 0.0
|
||
height: 100.0,
|
||
width: 100.0,
|
||
child: TextField(key: textField1, scrollPadding: const EdgeInsets.all(200.0)),
|
||
),
|
||
const SizedBox(
|
||
height: 600.0, // Same size as the frame. Initially
|
||
width: 800.0, // textField2 is not visible
|
||
),
|
||
SizedBox(
|
||
// visible when scrollOffset is 200.0
|
||
height: 100.0,
|
||
width: 100.0,
|
||
child: TextField(key: textField2, scrollPadding: const EdgeInsets.all(200.0)),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(buildFrame(Axis.vertical));
|
||
await tester.enterText(find.byKey(textField1), '1');
|
||
await tester.pumpAndSettle();
|
||
await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
|
||
await tester.pumpAndSettle();
|
||
await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(minOffset, 0.0);
|
||
expect(maxOffset, 200.0);
|
||
|
||
minOffset = null;
|
||
maxOffset = null;
|
||
|
||
await tester.pumpWidget(buildFrame(Axis.horizontal));
|
||
await tester.enterText(find.byKey(textField1), '1');
|
||
await tester.pumpAndSettle();
|
||
await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
|
||
await tester.pumpAndSettle();
|
||
await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(minOffset, 0.0);
|
||
expect(maxOffset, 200.0);
|
||
});
|
||
|
||
testWidgets('TextField scrolls into view but does not bounce (ListView)', (
|
||
WidgetTester tester,
|
||
) async {
|
||
// This is a regression test for https://github.com/flutter/flutter/issues/20485
|
||
|
||
final Key textField1 = UniqueKey();
|
||
final Key textField2 = UniqueKey();
|
||
final scrollController = ScrollController();
|
||
addTearDown(scrollController.dispose);
|
||
|
||
double? minOffset;
|
||
double? maxOffset;
|
||
|
||
scrollController.addListener(() {
|
||
final double offset = scrollController.offset;
|
||
minOffset = math.min(minOffset ?? offset, offset);
|
||
maxOffset = math.max(maxOffset ?? offset, offset);
|
||
});
|
||
|
||
Widget buildFrame(Axis scrollDirection) {
|
||
return MaterialApp(
|
||
home: Scaffold(
|
||
body: SafeArea(
|
||
child: ListView(
|
||
physics: const BouncingScrollPhysics(),
|
||
controller: scrollController,
|
||
children: <Widget>[
|
||
SizedBox(
|
||
// visible when scrollOffset is 0.0
|
||
height: 100.0,
|
||
width: 100.0,
|
||
child: TextField(key: textField1, scrollPadding: const EdgeInsets.all(200.0)),
|
||
),
|
||
const SizedBox(
|
||
height: 450.0, // 50.0 smaller than the overall frame so that both
|
||
width: 650.0, // textfields are always partially visible.
|
||
),
|
||
SizedBox(
|
||
// visible when scrollOffset = 50.0
|
||
height: 100.0,
|
||
width: 100.0,
|
||
child: TextField(key: textField2, scrollPadding: const EdgeInsets.all(200.0)),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(buildFrame(Axis.vertical));
|
||
await tester.enterText(find.byKey(textField1), '1'); // textfield1 is visible
|
||
await tester.pumpAndSettle();
|
||
await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
|
||
await tester.pumpAndSettle();
|
||
await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(minOffset, 0.0);
|
||
expect(maxOffset, 50.0);
|
||
|
||
minOffset = null;
|
||
maxOffset = null;
|
||
|
||
await tester.pumpWidget(buildFrame(Axis.horizontal));
|
||
await tester.enterText(find.byKey(textField1), '1'); // textfield1 is visible
|
||
await tester.pumpAndSettle();
|
||
await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
|
||
await tester.pumpAndSettle();
|
||
await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(minOffset, 0.0);
|
||
expect(maxOffset, 50.0);
|
||
});
|
||
|
||
testWidgets('onTap is called upon tap', (WidgetTester tester) async {
|
||
var tapCount = 0;
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
onTap: () {
|
||
tapCount += 1;
|
||
},
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(tapCount, 0);
|
||
await tester.tap(find.byType(TextField));
|
||
// Wait a bit so they're all single taps and not double taps.
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
expect(tapCount, 3);
|
||
});
|
||
|
||
testWidgets('onTap is not called, field is disabled', (WidgetTester tester) async {
|
||
var tapCount = 0;
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
enabled: false,
|
||
onTap: () {
|
||
tapCount += 1;
|
||
},
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(tapCount, 0);
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.tap(find.byType(TextField));
|
||
expect(tapCount, 0);
|
||
});
|
||
|
||
testWidgets('Includes cursor for TextField', (WidgetTester tester) async {
|
||
// This is a regression test for https://github.com/flutter/flutter/issues/24612
|
||
|
||
Widget buildFrame({
|
||
double? stepWidth,
|
||
required double cursorWidth,
|
||
required TextAlign textAlign,
|
||
}) {
|
||
return MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: <Widget>[
|
||
IntrinsicWidth(
|
||
stepWidth: stepWidth,
|
||
child: TextField(textAlign: textAlign, cursorWidth: cursorWidth),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// A cursor of default size doesn't cause the TextField to increase its
|
||
// width.
|
||
const text = '1234';
|
||
double? stepWidth = 80.0;
|
||
await tester.pumpWidget(
|
||
buildFrame(stepWidth: 80.0, cursorWidth: 2.0, textAlign: TextAlign.left),
|
||
);
|
||
await tester.enterText(find.byType(TextField), text);
|
||
await tester.pumpAndSettle();
|
||
expect(tester.getSize(find.byType(TextField)).width, stepWidth);
|
||
|
||
// A wide cursor is counted in the width of the text and causes the
|
||
// TextField to increase to twice the stepWidth.
|
||
await tester.pumpWidget(
|
||
buildFrame(stepWidth: stepWidth, cursorWidth: 18.0, textAlign: TextAlign.left),
|
||
);
|
||
await tester.enterText(find.byType(TextField), text);
|
||
await tester.pumpAndSettle();
|
||
expect(tester.getSize(find.byType(TextField)).width, 2 * stepWidth);
|
||
|
||
// A null stepWidth causes the TextField to perfectly wrap the text plus
|
||
// the cursor regardless of alignment.
|
||
stepWidth = null;
|
||
const WIDTH_OF_CHAR = 16.0;
|
||
const CARET_GAP = 1.0;
|
||
await tester.pumpWidget(
|
||
buildFrame(stepWidth: stepWidth, cursorWidth: 18.0, textAlign: TextAlign.left),
|
||
);
|
||
await tester.enterText(find.byType(TextField), text);
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
tester.getSize(find.byType(TextField)).width,
|
||
WIDTH_OF_CHAR * text.length + 18.0 + CARET_GAP,
|
||
);
|
||
await tester.pumpWidget(
|
||
buildFrame(stepWidth: stepWidth, cursorWidth: 18.0, textAlign: TextAlign.right),
|
||
);
|
||
await tester.enterText(find.byType(TextField), text);
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
tester.getSize(find.byType(TextField)).width,
|
||
WIDTH_OF_CHAR * text.length + 18.0 + CARET_GAP,
|
||
);
|
||
});
|
||
|
||
testWidgets('TextField style is merged with theme', (WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/23994
|
||
|
||
final themeData = ThemeData(
|
||
useMaterial3: false,
|
||
textTheme: TextTheme(titleMedium: TextStyle(color: Colors.blue[500])),
|
||
);
|
||
|
||
Widget buildFrame(TextStyle style) {
|
||
return MaterialApp(
|
||
theme: themeData,
|
||
home: Material(
|
||
child: Center(child: TextField(style: style)),
|
||
),
|
||
);
|
||
}
|
||
|
||
// Empty TextStyle is overridden by theme
|
||
await tester.pumpWidget(buildFrame(const TextStyle()));
|
||
EditableText editableText = tester.widget(find.byType(EditableText));
|
||
expect(editableText.style.color, themeData.textTheme.titleMedium!.color);
|
||
expect(editableText.style.background, themeData.textTheme.titleMedium!.background);
|
||
expect(editableText.style.shadows, themeData.textTheme.titleMedium!.shadows);
|
||
expect(editableText.style.decoration, themeData.textTheme.titleMedium!.decoration);
|
||
expect(editableText.style.locale, themeData.textTheme.titleMedium!.locale);
|
||
expect(editableText.style.wordSpacing, themeData.textTheme.titleMedium!.wordSpacing);
|
||
|
||
// Properties set on TextStyle override theme
|
||
const Color setColor = Colors.red;
|
||
await tester.pumpWidget(buildFrame(const TextStyle(color: setColor)));
|
||
editableText = tester.widget(find.byType(EditableText));
|
||
expect(editableText.style.color, setColor);
|
||
|
||
// inherit: false causes nothing to be merged in from theme
|
||
await tester.pumpWidget(
|
||
buildFrame(
|
||
const TextStyle(fontSize: 24.0, textBaseline: TextBaseline.alphabetic, inherit: false),
|
||
),
|
||
);
|
||
editableText = tester.widget(find.byType(EditableText));
|
||
expect(editableText.style.color, isNull);
|
||
});
|
||
|
||
testWidgets('TextField style is merged with theme in Material 3', (WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/23994
|
||
|
||
final themeData = ThemeData(
|
||
textTheme: TextTheme(bodyLarge: TextStyle(color: Colors.blue[500])),
|
||
);
|
||
|
||
Widget buildFrame(TextStyle style) {
|
||
return MaterialApp(
|
||
theme: themeData,
|
||
home: Material(
|
||
child: Center(child: TextField(style: style)),
|
||
),
|
||
);
|
||
}
|
||
|
||
// Empty TextStyle is overridden by theme
|
||
await tester.pumpWidget(buildFrame(const TextStyle()));
|
||
EditableText editableText = tester.widget(find.byType(EditableText));
|
||
|
||
// According to material 3 spec, the input text should be the color of onSurface.
|
||
// https://github.com/flutter/flutter/issues/107686 is tracking this issue.
|
||
expect(editableText.style.color, themeData.textTheme.bodyLarge!.color);
|
||
|
||
expect(editableText.style.background, themeData.textTheme.bodyLarge!.background);
|
||
expect(editableText.style.shadows, themeData.textTheme.bodyLarge!.shadows);
|
||
expect(editableText.style.decoration, themeData.textTheme.bodyLarge!.decoration);
|
||
expect(editableText.style.locale, themeData.textTheme.bodyLarge!.locale);
|
||
expect(editableText.style.wordSpacing, themeData.textTheme.bodyLarge!.wordSpacing);
|
||
|
||
// Properties set on TextStyle override theme
|
||
const Color setColor = Colors.red;
|
||
await tester.pumpWidget(buildFrame(const TextStyle(color: setColor)));
|
||
editableText = tester.widget(find.byType(EditableText));
|
||
expect(editableText.style.color, setColor);
|
||
|
||
// inherit: false causes nothing to be merged in from theme
|
||
await tester.pumpWidget(
|
||
buildFrame(
|
||
const TextStyle(fontSize: 24.0, textBaseline: TextBaseline.alphabetic, inherit: false),
|
||
),
|
||
);
|
||
editableText = tester.widget(find.byType(EditableText));
|
||
expect(editableText.style.color, isNull);
|
||
});
|
||
|
||
testWidgets(
|
||
'selection handles color respects Theme',
|
||
(WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/74890.
|
||
const expectedSelectionHandleColor = Color.fromARGB(255, 10, 200, 255);
|
||
|
||
final controller = TextEditingController(text: 'Some text.');
|
||
addTearDown(controller.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(
|
||
textSelectionTheme: const TextSelectionThemeData(selectionHandleColor: Colors.red),
|
||
),
|
||
home: Material(
|
||
child: Theme(
|
||
data: ThemeData(
|
||
textSelectionTheme: const TextSelectionThemeData(
|
||
selectionHandleColor: expectedSelectionHandleColor,
|
||
),
|
||
),
|
||
child: TextField(controller: controller),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.longPressAt(textOffsetToPosition(tester, 0));
|
||
await tester.pumpAndSettle();
|
||
final Iterable<RenderBox> boxes = tester.renderObjectList<RenderBox>(
|
||
find.descendant(
|
||
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
|
||
matching: find.byType(CustomPaint),
|
||
),
|
||
);
|
||
expect(boxes.length, 2);
|
||
|
||
for (final box in boxes) {
|
||
expect(box, paints..path(color: expectedSelectionHandleColor));
|
||
}
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
}),
|
||
);
|
||
|
||
testWidgets('style enforces required fields', (WidgetTester tester) async {
|
||
Widget buildFrame(TextStyle style) {
|
||
return MaterialApp(
|
||
home: Material(child: TextField(style: style)),
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(
|
||
buildFrame(
|
||
const TextStyle(inherit: false, fontSize: 12.0, textBaseline: TextBaseline.alphabetic),
|
||
),
|
||
);
|
||
expect(tester.takeException(), isNull);
|
||
|
||
// With inherit not set to false, will pickup required fields from theme
|
||
await tester.pumpWidget(buildFrame(const TextStyle(fontSize: 12.0)));
|
||
expect(tester.takeException(), isNull);
|
||
|
||
await tester.pumpWidget(buildFrame(const TextStyle(inherit: false, fontSize: 12.0)));
|
||
expect(tester.takeException(), isNotNull);
|
||
});
|
||
|
||
testWidgets(
|
||
'tap moves cursor to the edge of the word it tapped',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pump();
|
||
|
||
// We moved the cursor.
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
||
);
|
||
|
||
// But don't trigger the toolbar.
|
||
expectNoCupertinoToolbar();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets(
|
||
'tap with a mouse does not move cursor to the edge of the word',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textfieldStart + const Offset(50.0, 9.0),
|
||
pointer: 1,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await gesture.up();
|
||
|
||
// Cursor at tap position, not at word edge.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 3));
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'tap moves cursor to the position tapped',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pump();
|
||
|
||
// We moved the cursor.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 3));
|
||
|
||
// But don't trigger the toolbar.
|
||
expectNoMaterialToolbar();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
TargetPlatform.linux,
|
||
TargetPlatform.windows,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'two slow taps do not trigger a word selection',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
|
||
// On macOS, we select the precise position of the tap.
|
||
final isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset pos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'.
|
||
|
||
await tester.tapAt(pos);
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
await tester.tapAt(pos);
|
||
await tester.pump();
|
||
|
||
// Plain collapsed selection.
|
||
expect(controller.selection.isCollapsed, isTrue);
|
||
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6);
|
||
|
||
// Toolbar shows on mobile only.
|
||
if (isTargetPlatformMobile) {
|
||
expectCupertinoToolbarForCollapsedSelection();
|
||
} else {
|
||
// After a tap, macOS does not show a selection toolbar for a collapsed selection.
|
||
expectNoCupertinoToolbar();
|
||
}
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Tapping on a collapsed selection toggles the toolbar',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text:
|
||
'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neigse Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
|
||
);
|
||
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller, maxLines: 2)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
|
||
final Offset begPos = textOffsetToPosition(tester, 0);
|
||
final Offset endPos =
|
||
textOffsetToPosition(tester, 35) +
|
||
const Offset(
|
||
200.0,
|
||
0.0,
|
||
); // Index of 'Bonaventure|' + Offset(200.0,0), which is at the end of the first line.
|
||
final Offset vPos = textOffsetToPosition(tester, 29); // Index of 'Bonav|enture'.
|
||
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'.
|
||
|
||
// This tap just puts the cursor somewhere different than where the double
|
||
// tap will occur to test that the double tap moves the existing cursor first.
|
||
await tester.tapAt(wPos);
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
await tester.tapAt(vPos);
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
// First tap moved the cursor. Here we tap the position where 'v' is located.
|
||
// On iOS this will select the closest word edge, in this case the cursor is placed
|
||
// at the end of the word 'Bonaventure|'.
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 35);
|
||
expectNoCupertinoToolbar();
|
||
|
||
await tester.tapAt(vPos);
|
||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||
// Second tap toggles the toolbar. Here we tap on 'v' again, and select the word edge. Since
|
||
// the selection has not changed we toggle the toolbar.
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 35);
|
||
expectCupertinoToolbarForCollapsedSelection();
|
||
|
||
// Tap the 'v' position again to hide the toolbar.
|
||
await tester.tapAt(vPos);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 35);
|
||
expectNoCupertinoToolbar();
|
||
|
||
// Long press at the end of the first line to move the cursor to the end of the first line
|
||
// where the word wrap is. Since there is a word wrap here, and the direction of the text is LTR,
|
||
// the TextAffinity will be upstream and against the natural direction. The toolbar is also
|
||
// shown after a long press.
|
||
await tester.longPressAt(endPos);
|
||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 46);
|
||
expect(controller.selection.affinity, TextAffinity.upstream);
|
||
expectCupertinoToolbarForCollapsedSelection();
|
||
|
||
// Tap at the same position to toggle the toolbar.
|
||
await tester.tapAt(endPos);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 46);
|
||
expect(controller.selection.affinity, TextAffinity.upstream);
|
||
expectNoCupertinoToolbar();
|
||
|
||
// Tap at the beginning of the second line to move the cursor to the front of the first word on the
|
||
// second line, where the word wrap is. Since there is a word wrap here, and the direction of the text is LTR,
|
||
// the TextAffinity will be downstream and following the natural direction. The toolbar will be hidden after this tap.
|
||
await tester.tapAt(begPos + Offset(0.0, lineHeight));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 46);
|
||
expect(controller.selection.affinity, TextAffinity.downstream);
|
||
expectNoCupertinoToolbar();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Tapping on a non-collapsed selection toggles the toolbar and retains the selection',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset vPos = textOffsetToPosition(tester, 29); // Index of 'Bonav|enture'.
|
||
final Offset ePos =
|
||
textOffsetToPosition(tester, 35) +
|
||
const Offset(
|
||
7.0,
|
||
0.0,
|
||
); // Index of 'Bonaventure|' + Offset(7.0,0), which taps slightly to the right of the end of the text.
|
||
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'.
|
||
|
||
// This tap just puts the cursor somewhere different than where the double
|
||
// tap will occur to test that the double tap moves the existing cursor first.
|
||
await tester.tapAt(wPos);
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
await tester.tapAt(vPos);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap moved the cursor.
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 35);
|
||
await tester.tapAt(vPos);
|
||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||
|
||
// Second tap selects the word around the cursor.
|
||
expect(controller.selection, const TextSelection(baseOffset: 24, extentOffset: 35));
|
||
|
||
// The toolbar shows up.
|
||
expectCupertinoToolbarForPartialSelection();
|
||
|
||
// Tap the selected word to hide the toolbar and retain the selection.
|
||
await tester.tapAt(vPos);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 24, extentOffset: 35));
|
||
expectNoCupertinoToolbar();
|
||
|
||
// Tap the selected word to show the toolbar and retain the selection.
|
||
await tester.tapAt(vPos);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 24, extentOffset: 35));
|
||
expectCupertinoToolbarForPartialSelection();
|
||
|
||
// Tap past the selected word to move the cursor and hide the toolbar.
|
||
await tester.tapAt(ePos);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 35);
|
||
expectNoCupertinoToolbar();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets(
|
||
'double tap selects word and first tap of double tap moves cursor (iOS)',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'.
|
||
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'.
|
||
|
||
// This tap just puts the cursor somewhere different than where the double
|
||
// tap will occur to test that the double tap moves the existing cursor first.
|
||
await tester.tapAt(wPos);
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
await tester.tapAt(pPos);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap moved the cursor.
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
|
||
);
|
||
await tester.tapAt(pPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
// Second tap selects the word around the cursor.
|
||
expect(controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12));
|
||
|
||
// The toolbar shows up.
|
||
expectCupertinoToolbarForPartialSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets('iOS selectWordEdge works correctly', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'blah1 blah2');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Initially, the menu is not shown and there is no selection.
|
||
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
|
||
final Offset pos1 = textOffsetToPosition(tester, 1);
|
||
TestGesture gesture = await tester.startGesture(pos1);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 5, affinity: TextAffinity.upstream),
|
||
);
|
||
|
||
final Offset pos0 = textOffsetToPosition(tester, 0);
|
||
gesture = await tester.startGesture(pos0);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 0));
|
||
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
|
||
|
||
testWidgets(
|
||
'double tap does not select word on read-only obscured field',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(obscureText: true, readOnly: true, controller: controller),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
// This tap just puts the cursor somewhere different than where the double
|
||
// tap will occur to test that the double tap moves the existing cursor first.
|
||
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap moved the cursor.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 35));
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Second tap doesn't select anything.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 35));
|
||
|
||
// Selected text shows no toolbar.
|
||
expectNoCupertinoToolbar();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'double tap selects word and first tap of double tap moves cursor and shows toolbar',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
// This tap just puts the cursor somewhere different than where the double
|
||
// tap will occur to test that the double tap moves the existing cursor first.
|
||
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap moved the cursor.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 9));
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Second tap selects the word around the cursor.
|
||
expect(controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12));
|
||
|
||
// The toolbar shows up.
|
||
expectMaterialToolbarForPartialSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
TargetPlatform.linux,
|
||
TargetPlatform.windows,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Custom toolbar test - Android text selection controls',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
controller: controller,
|
||
selectionControls: materialTextSelectionControls,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Selected text shows 4 toolbar buttons: cut, copy, paste, select all
|
||
expect(find.byType(TextButton), findsNWidgets(4));
|
||
expect(find.text('Cut'), findsOneWidget);
|
||
expect(find.text('Copy'), findsOneWidget);
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
expect(find.text('Select all'), findsOneWidget);
|
||
},
|
||
variant: TargetPlatformVariant.all(),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Custom toolbar test - Cupertino text selection controls',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
controller: controller,
|
||
selectionControls: cupertinoTextSelectionControls,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Selected text shows 3 toolbar buttons: cut, copy, paste
|
||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||
},
|
||
variant: TargetPlatformVariant.all(),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets('selectionControls is passed to EditableText', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Scaffold(body: TextField(selectionControls: materialTextSelectionControls)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final EditableText widget = tester.widget(find.byType(EditableText));
|
||
expect(widget.selectionControls, equals(materialTextSelectionControls));
|
||
});
|
||
|
||
testWidgets('Can double click + drag with a mouse to select word by word', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
final Offset hPos = textOffsetToPosition(tester, testValue.indexOf('h'));
|
||
|
||
// Tap on text field to gain focus, and set selection to '|e'.
|
||
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('e'));
|
||
|
||
// Here we tap on '|e' again, to register a double tap. This will select
|
||
// the word at the tapped position.
|
||
await gesture.down(ePos);
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 7);
|
||
|
||
// Drag, right after the double tap, to select word by word.
|
||
// Moving to the position of 'h', will extend the selection to 'ghi'.
|
||
await gesture.moveTo(hPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, testValue.indexOf('d'));
|
||
expect(controller.selection.extentOffset, testValue.indexOf('i') + 1);
|
||
});
|
||
|
||
testWidgets('Can double tap + drag to select word by word', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
final Offset hPos = textOffsetToPosition(tester, testValue.indexOf('h'));
|
||
|
||
// Tap on text field to gain focus, and set selection to '|e'.
|
||
final TestGesture gesture = await tester.startGesture(ePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, testValue.indexOf('e'));
|
||
|
||
// Here we tap on '|e' again, to register a double tap. This will select
|
||
// the word at the tapped position.
|
||
await gesture.down(ePos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 7);
|
||
|
||
// Drag, right after the double tap, to select word by word.
|
||
// Moving to the position of 'h', will extend the selection to 'ghi'.
|
||
await gesture.moveTo(hPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
// Toolbar should be hidden during a drag.
|
||
expectNoMaterialToolbar();
|
||
expect(controller.selection.baseOffset, testValue.indexOf('d'));
|
||
expect(controller.selection.extentOffset, testValue.indexOf('i') + 1);
|
||
|
||
// Toolbar should re-appear after a drag.
|
||
await gesture.up();
|
||
await tester.pump();
|
||
expectMaterialToolbarForPartialSelection();
|
||
});
|
||
|
||
group('Triple tap/click', () {
|
||
const testValueA =
|
||
'Now is the time for\n' // 20
|
||
'all good people\n' // 20 + 16 => 36
|
||
'to come to the aid\n' // 36 + 19 => 55
|
||
'of their country.'; // 55 + 17 => 72
|
||
const testValueB =
|
||
'Today is the time for\n' // 22
|
||
'all good people\n' // 22 + 16 => 38
|
||
'to come to the aid\n' // 38 + 19 => 57
|
||
'of their country.'; // 57 + 17 => 74
|
||
testWidgets(
|
||
'Can triple tap to select a paragraph on mobile platforms when tapping at a word edge',
|
||
(WidgetTester tester) async {
|
||
// TODO(Renzo-Olivares): Enable for iOS, currently broken because selection overlay blocks the TextSelectionGestureDetector https://github.com/flutter/flutter/issues/123415.
|
||
final TextEditingController controller = _textEditingController();
|
||
final isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: null,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), testValueA);
|
||
await skipPastScrollingAnimation(tester);
|
||
expect(controller.value.text, testValueA);
|
||
|
||
final Offset firstLinePos = textOffsetToPosition(tester, 6);
|
||
|
||
// Tap on text field to gain focus, and set selection to 'is|' on the first line.
|
||
final TestGesture gesture = await tester.startGesture(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 6);
|
||
|
||
// Here we tap on same position again, to register a double tap. This will select
|
||
// the word at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 6);
|
||
expect(controller.selection.extentOffset, isTargetPlatformApple ? 6 : 7);
|
||
|
||
// Here we tap on same position again, to register a triple tap. This will select
|
||
// the paragraph at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 20);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can triple tap to select a paragraph on mobile platforms',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
final isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: null,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), testValueB);
|
||
await skipPastScrollingAnimation(tester);
|
||
expect(controller.value.text, testValueB);
|
||
|
||
final Offset firstLinePos =
|
||
tester.getTopLeft(find.byType(TextField)) + const Offset(50.0, 9.0);
|
||
|
||
// Tap on text field to gain focus, and move the selection.
|
||
final TestGesture gesture = await tester.startGesture(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3);
|
||
|
||
// Here we tap on same position again, to register a double tap. This will select
|
||
// the word at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 5);
|
||
|
||
// Here we tap on same position again, to register a triple tap. This will select
|
||
// the paragraph at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 22);
|
||
},
|
||
variant: TargetPlatformVariant.mobile(),
|
||
);
|
||
|
||
testWidgets(
|
||
'Triple click at the beginning of a line should not select the previous paragraph',
|
||
(WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/132126
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: null,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), testValueB);
|
||
await skipPastScrollingAnimation(tester);
|
||
expect(controller.value.text, testValueB);
|
||
|
||
final Offset thirdLinePos = textOffsetToPosition(tester, 38);
|
||
|
||
// Click on text field to gain focus, and move the selection.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
thirdLinePos,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 38);
|
||
|
||
// Here we click on same position again, to register a double click. This will select
|
||
// the word at the clicked position.
|
||
await gesture.down(thirdLinePos);
|
||
await gesture.up();
|
||
|
||
expect(controller.selection.baseOffset, 38);
|
||
expect(controller.selection.extentOffset, 40);
|
||
|
||
// Here we click on same position again, to register a triple click. This will select
|
||
// the paragraph at the clicked position.
|
||
await gesture.down(thirdLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 38);
|
||
expect(controller.selection.extentOffset, 57);
|
||
},
|
||
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{TargetPlatform.linux}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Triple click at the end of text should select the previous paragraph',
|
||
(WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/132126.
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: null,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), testValueB);
|
||
await skipPastScrollingAnimation(tester);
|
||
expect(controller.value.text, testValueB);
|
||
|
||
final Offset endOfTextPos = textOffsetToPosition(tester, 74);
|
||
|
||
// Click on text field to gain focus, and move the selection.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
endOfTextPos,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 74);
|
||
|
||
// Here we click on same position again, to register a double click.
|
||
await gesture.down(endOfTextPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 74);
|
||
expect(controller.selection.extentOffset, 74);
|
||
|
||
// Here we click on same position again, to register a triple click. This will select
|
||
// the paragraph at the clicked position.
|
||
await gesture.down(endOfTextPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 57);
|
||
expect(controller.selection.extentOffset, 74);
|
||
},
|
||
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{TargetPlatform.linux}),
|
||
);
|
||
|
||
testWidgets(
|
||
'triple tap chains work on Non-Apple mobile platforms',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 3);
|
||
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
expectMaterialToolbarForPartialSelection();
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 35));
|
||
// Triple tap selecting the same paragraph somewhere else is fine.
|
||
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap hides the toolbar and moves the selection.
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 6);
|
||
expectNoMaterialToolbar();
|
||
// Second tap shows the toolbar and selects the word.
|
||
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
expectMaterialToolbarForPartialSelection();
|
||
|
||
// Third tap shows the toolbar and selects the paragraph.
|
||
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 35));
|
||
expectMaterialToolbarForFullSelection();
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap moved the cursor and hid the toolbar.
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 9);
|
||
expectNoMaterialToolbar();
|
||
// Second tap selects the word.
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12));
|
||
expectMaterialToolbarForPartialSelection();
|
||
|
||
// Third tap selects the paragraph and shows the toolbar.
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 35));
|
||
expectMaterialToolbarForFullSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'triple tap chains work on Apple platforms',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure\nThe fox jumped over the fence.',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller, maxLines: null)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 7);
|
||
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
expectCupertinoToolbarForPartialSelection();
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 36));
|
||
// Triple tap selecting the same paragraph somewhere else is fine.
|
||
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap hides the toolbar and retains the selection.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 36));
|
||
expectNoCupertinoToolbar();
|
||
// Second tap shows the toolbar and selects the word.
|
||
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
expectCupertinoToolbarForPartialSelection();
|
||
|
||
// Third tap shows the toolbar and selects the paragraph.
|
||
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 36));
|
||
expectCupertinoToolbarForPartialSelection();
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 50.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap moved the cursor and hid the toolbar.
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 50, affinity: TextAffinity.upstream),
|
||
);
|
||
expectNoCupertinoToolbar();
|
||
// Second tap selects the word.
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 50.0));
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 44, extentOffset: 50));
|
||
expectCupertinoToolbarForPartialSelection();
|
||
|
||
// Third tap selects the paragraph and shows the toolbar.
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 50.0));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 36, extentOffset: 66));
|
||
expectCupertinoToolbarForPartialSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets('triple click chains work', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: testValueA);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller, maxLines: null)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
|
||
final platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;
|
||
|
||
// First click moves the cursor to the point of the click, not the edge of
|
||
// the clicked word.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textFieldStart + const Offset(210.0, 9.0),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 13);
|
||
|
||
// Second click selects the word.
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 11, extentOffset: 15));
|
||
|
||
// Triple click selects the paragraph.
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
// Wait for the consecutive tap timer to timeout so the next
|
||
// tap is not detected as a triple tap.
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
expect(
|
||
controller.selection,
|
||
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
|
||
);
|
||
|
||
// Triple click selecting the same paragraph somewhere else is fine.
|
||
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First click moved the cursor.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 6));
|
||
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
// Second click selected the word.
|
||
expect(controller.selection, const TextSelection(baseOffset: 6, extentOffset: 7));
|
||
|
||
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
// Wait for the consecutive tap timer to timeout so the tap count
|
||
// is reset.
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
// Third click selected the paragraph.
|
||
expect(
|
||
controller.selection,
|
||
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
|
||
);
|
||
|
||
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First click moved the cursor.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 9));
|
||
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
// Second click selected the word.
|
||
expect(controller.selection, const TextSelection(baseOffset: 7, extentOffset: 10));
|
||
|
||
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
// Third click selects the paragraph.
|
||
expect(
|
||
controller.selection,
|
||
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
|
||
);
|
||
}, variant: TargetPlatformVariant.desktop());
|
||
|
||
testWidgets('triple click after a click on desktop platforms', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: testValueA);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller, maxLines: null)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
|
||
final platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;
|
||
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textFieldStart + const Offset(50.0, 9.0),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 3));
|
||
// First click moves the selection.
|
||
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 9));
|
||
|
||
// Double click selection to select a word.
|
||
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 7, extentOffset: 10));
|
||
|
||
// Triple click selection to select a paragraph.
|
||
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
controller.selection,
|
||
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
|
||
);
|
||
}, variant: TargetPlatformVariant.desktop());
|
||
|
||
testWidgets(
|
||
'Can triple tap to select all on a single-line textfield on mobile platforms',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: testValueB);
|
||
final isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
final Offset firstLinePos =
|
||
tester.getTopLeft(find.byType(TextField)) + const Offset(50.0, 9.0);
|
||
|
||
// Tap on text field to gain focus, and set selection somewhere on the first word.
|
||
final TestGesture gesture = await tester.startGesture(firstLinePos, pointer: 7);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3);
|
||
|
||
// Here we tap on same position again, to register a double tap. This will select
|
||
// the word at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 5);
|
||
|
||
// Here we tap on same position again, to register a triple tap. This will select
|
||
// the entire text field if it is a single-line field.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 74);
|
||
},
|
||
variant: TargetPlatformVariant.mobile(),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can triple click to select all on a single-line textfield on desktop platforms',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: testValueA);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset firstLinePos = textOffsetToPosition(tester, 5);
|
||
|
||
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
firstLinePos,
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 5);
|
||
|
||
// Here we tap on same position again, to register a double tap. This will select
|
||
// the word at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 6);
|
||
|
||
// Here we tap on same position again, to register a triple tap. This will select
|
||
// the entire text field if it is a single-line field.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 72);
|
||
},
|
||
variant: TargetPlatformVariant.desktop(),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can triple click to select a line on Linux',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: null,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), testValueA);
|
||
await skipPastScrollingAnimation(tester);
|
||
expect(controller.value.text, testValueA);
|
||
|
||
final Offset firstLinePos = textOffsetToPosition(tester, 5);
|
||
|
||
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
firstLinePos,
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 5);
|
||
|
||
// Here we tap on same position again, to register a double tap. This will select
|
||
// the word at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 6);
|
||
|
||
// Here we tap on same position again, to register a triple tap. This will select
|
||
// the paragraph at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 19);
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.linux),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can triple click to select a paragraph',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: null,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), testValueA);
|
||
await skipPastScrollingAnimation(tester);
|
||
expect(controller.value.text, testValueA);
|
||
|
||
final Offset firstLinePos = textOffsetToPosition(tester, 5);
|
||
|
||
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
firstLinePos,
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 5);
|
||
|
||
// Here we tap on same position again, to register a double tap. This will select
|
||
// the word at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 6);
|
||
|
||
// Here we tap on same position again, to register a triple tap. This will select
|
||
// the paragraph at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 20);
|
||
},
|
||
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{TargetPlatform.linux}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can triple click + drag to select line by line on Linux',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: null,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), testValueA);
|
||
await skipPastScrollingAnimation(tester);
|
||
expect(controller.value.text, testValueA);
|
||
|
||
final Offset firstLinePos = textOffsetToPosition(tester, 5);
|
||
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
|
||
|
||
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
firstLinePos,
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 5);
|
||
|
||
// Here we tap on same position again, to register a double tap. This will select
|
||
// the word at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 6);
|
||
|
||
// Here we tap on the same position again, to register a triple tap. This will select
|
||
// the line at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 19);
|
||
|
||
// Drag, down after the triple tap, to select line by line.
|
||
// Moving down will extend the selection to the second line.
|
||
await gesture.moveTo(firstLinePos + Offset(0, lineHeight));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 35);
|
||
|
||
// Moving down will extend the selection to the third line.
|
||
await gesture.moveTo(firstLinePos + Offset(0, lineHeight * 2));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 54);
|
||
|
||
// Moving down will extend the selection to the last line.
|
||
await gesture.moveTo(firstLinePos + Offset(0, lineHeight * 4));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 72);
|
||
|
||
// Moving up will extend the selection to the third line.
|
||
await gesture.moveTo(firstLinePos + Offset(0, lineHeight * 2));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 54);
|
||
|
||
// Moving up will extend the selection to the second line.
|
||
await gesture.moveTo(firstLinePos + Offset(0, lineHeight * 1));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 35);
|
||
|
||
// Moving up will extend the selection to the first line.
|
||
await gesture.moveTo(firstLinePos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 19);
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.linux),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can triple click + drag to select paragraph by paragraph',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
maxLines: null,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextField), testValueA);
|
||
await skipPastScrollingAnimation(tester);
|
||
expect(controller.value.text, testValueA);
|
||
|
||
final Offset firstLinePos = textOffsetToPosition(tester, 5);
|
||
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
|
||
|
||
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
firstLinePos,
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 5);
|
||
|
||
// Here we tap on same position again, to register a double tap. This will select
|
||
// the word at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(controller.selection.baseOffset, 4);
|
||
expect(controller.selection.extentOffset, 6);
|
||
|
||
// Here we tap on the same position again, to register a triple tap. This will select
|
||
// the paragraph at the tapped position.
|
||
await gesture.down(firstLinePos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 20);
|
||
|
||
// Drag, down after the triple tap, to select paragraph by paragraph.
|
||
// Moving down will extend the selection to the second line.
|
||
await gesture.moveTo(firstLinePos + Offset(0, lineHeight));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 36);
|
||
|
||
// Moving down will extend the selection to the third line.
|
||
await gesture.moveTo(firstLinePos + Offset(0, lineHeight * 2));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 55);
|
||
|
||
// Moving down will extend the selection to the last line.
|
||
await gesture.moveTo(firstLinePos + Offset(0, lineHeight * 4));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 72);
|
||
|
||
// Moving up will extend the selection to the third line.
|
||
await gesture.moveTo(firstLinePos + Offset(0, lineHeight * 2));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 55);
|
||
|
||
// Moving up will extend the selection to the second line.
|
||
await gesture.moveTo(firstLinePos + Offset(0, lineHeight));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 36);
|
||
|
||
// Moving up will extend the selection to the first line.
|
||
await gesture.moveTo(firstLinePos);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 20);
|
||
},
|
||
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{TargetPlatform.linux}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Going past triple click retains the selection on Apple platforms',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: testValueA);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller, maxLines: null)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
// First click moves the cursor to the point of the click, not the edge of
|
||
// the clicked word.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textFieldStart + const Offset(210.0, 9.0),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 13);
|
||
|
||
// Second click selects the word.
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 11, extentOffset: 15));
|
||
|
||
// Triple click selects the paragraph.
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 20));
|
||
|
||
// Clicking again retains the selection.
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 20));
|
||
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
// Clicking again retains the selection.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 20));
|
||
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
// Clicking again retains the selection.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 20));
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Tap count resets when going past a triple tap on Android, Fuchsia, and Linux',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: testValueA);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller, maxLines: null)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
|
||
final platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;
|
||
|
||
// First click moves the cursor to the point of the click, not the edge of
|
||
// the clicked word.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textFieldStart + const Offset(210.0, 9.0),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 13);
|
||
|
||
// Second click selects the word.
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 11, extentOffset: 15));
|
||
|
||
// Triple click selects the paragraph.
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
controller.selection,
|
||
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
|
||
);
|
||
|
||
// Clicking again moves the caret to the tapped position.
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 13);
|
||
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
// Clicking again selects the word.
|
||
expect(controller.selection, const TextSelection(baseOffset: 11, extentOffset: 15));
|
||
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
// Clicking again selects the paragraph.
|
||
expect(
|
||
controller.selection,
|
||
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
|
||
);
|
||
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// Clicking again moves the caret to the tapped position.
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 13);
|
||
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
// Clicking again selects the word.
|
||
expect(controller.selection, const TextSelection(baseOffset: 11, extentOffset: 15));
|
||
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
// Clicking again selects the paragraph.
|
||
expect(
|
||
controller.selection,
|
||
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
|
||
);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
TargetPlatform.linux,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Double click and triple click alternate on Windows',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: testValueA);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller, maxLines: null)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
// First click moves the cursor to the point of the click, not the edge of
|
||
// the clicked word.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textFieldStart + const Offset(210.0, 9.0),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 13);
|
||
|
||
// Second click selects the word.
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 11, extentOffset: 15));
|
||
|
||
// Triple click selects the paragraph.
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 20));
|
||
|
||
// Clicking again selects the word.
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection, const TextSelection(baseOffset: 11, extentOffset: 15));
|
||
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
// Clicking again selects the paragraph.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 20));
|
||
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
// Clicking again selects the word.
|
||
expect(controller.selection, const TextSelection(baseOffset: 11, extentOffset: 15));
|
||
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// Clicking again selects the paragraph.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 20));
|
||
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
// Clicking again selects the word.
|
||
expect(controller.selection, const TextSelection(baseOffset: 11, extentOffset: 15));
|
||
|
||
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
// Clicking again selects the paragraph.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 20));
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.windows),
|
||
);
|
||
});
|
||
|
||
testWidgets(
|
||
'double tap on top of cursor also selects word',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Tap to put the cursor after the "w".
|
||
const index = 3;
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
expect(controller.selection, const TextSelection.collapsed(offset: index));
|
||
|
||
// Double tap on the same location.
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
|
||
// First tap doesn't change the selection
|
||
expect(controller.selection, const TextSelection.collapsed(offset: index));
|
||
|
||
// Second tap selects the word around the cursor.
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
|
||
// The toolbar shows up.
|
||
expectMaterialToolbarForPartialSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
TargetPlatform.linux,
|
||
TargetPlatform.windows,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'double double tap just shows the selection menu',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Double tap on the same location shows the selection menu.
|
||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
|
||
// Double tap again keeps the selection menu visible.
|
||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'double long press just shows the selection menu',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Long press shows the selection menu.
|
||
await tester.longPressAt(textOffsetToPosition(tester, 0));
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
|
||
// Long press again keeps the selection menu visible.
|
||
await tester.longPressAt(textOffsetToPosition(tester, 0));
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'A single tap hides the selection menu',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Long press shows the selection menu.
|
||
await tester.longPress(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
|
||
// Tap hides the selection menu.
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump();
|
||
expect(find.text('Paste'), findsNothing);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Drag selection hides the selection menu',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'blah1 blah2');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Initially, the menu is not shown and there is no selection.
|
||
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
|
||
final Offset midBlah1 = textOffsetToPosition(tester, 2);
|
||
final Offset midBlah2 = textOffsetToPosition(tester, 8);
|
||
|
||
// Right click the second word.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
midBlah2,
|
||
kind: PointerDeviceKind.mouse,
|
||
buttons: kSecondaryMouseButton,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// The toolbar is shown.
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
|
||
// Drag the mouse to the first word.
|
||
final TestGesture gesture2 = await tester.startGesture(
|
||
midBlah1,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture2.moveTo(midBlah2);
|
||
await tester.pump();
|
||
await gesture2.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// The toolbar is hidden.
|
||
expect(find.text('Paste'), findsNothing);
|
||
},
|
||
variant: TargetPlatformVariant.desktop(),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Long press on an autofocused field shows the selection menu',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(autofocus: true, controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
// This extra pump allows the selection set by autofocus to propagate to
|
||
// the RenderEditable.
|
||
await tester.pump();
|
||
|
||
// Long press shows the selection menu.
|
||
expect(find.text('Paste'), findsNothing);
|
||
await tester.longPress(find.byType(TextField));
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'double tap hold selects word',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textfieldStart + const Offset(150.0, 9.0),
|
||
);
|
||
// Hold the press.
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12));
|
||
|
||
// The toolbar shows up.
|
||
expectCupertinoToolbarForPartialSelection();
|
||
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
// Still selected.
|
||
expect(controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12));
|
||
|
||
// The toolbar is still showing.
|
||
expectCupertinoToolbarForPartialSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'tap after a double tap select is not affected',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
|
||
// On macOS, we select the precise position of the tap.
|
||
final isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'.
|
||
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
|
||
|
||
await tester.tapAt(pPos);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap moved the cursor.
|
||
expect(
|
||
controller.selection,
|
||
isTargetPlatformMobile
|
||
? const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream)
|
||
: const TextSelection.collapsed(offset: 9),
|
||
);
|
||
await tester.tapAt(pPos);
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
await tester.tapAt(ePos);
|
||
await tester.pump();
|
||
|
||
// Plain collapsed selection at the edge of first word on iOS. In iOS 12,
|
||
// the first tap after a double tap ends up putting the cursor at where
|
||
// you tapped instead of the edge like every other single tap. This is
|
||
// likely a bug in iOS 12 and not present in other versions.
|
||
expect(controller.selection.isCollapsed, isTrue);
|
||
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6);
|
||
|
||
// No toolbar.
|
||
expectNoCupertinoToolbar();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'long press moves cursor to the exact long press position and shows toolbar when the field is focused',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(autofocus: true, controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// This extra pump allows the selection set by autofocus to propagate to
|
||
// the RenderEditable.
|
||
await tester.pump();
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Collapsed cursor for iOS long press.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 3));
|
||
|
||
expectCupertinoToolbarForCollapsedSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'long press that starts on an unfocused TextField selects the word at the exact long press position and shows toolbar',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Collapsed cursor for iOS long press.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
|
||
// The toolbar shows up.
|
||
expectCupertinoToolbarForPartialSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'long press selects word and shows toolbar',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
|
||
// The toolbar shows up.
|
||
expectMaterialToolbarForPartialSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
TargetPlatform.linux,
|
||
TargetPlatform.windows,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Toolbar hides on scroll start and re-appears on scroll end on Android',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure ' * 20,
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
final RenderEditable renderEditable = state.renderEditable;
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Long press should select word at position and show toolbar.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
|
||
final targetPlatformIsiOS = defaultTargetPlatform == TargetPlatform.iOS;
|
||
final Finder contextMenuButtonFinder = targetPlatformIsiOS
|
||
? find.byType(CupertinoButton)
|
||
: find.byType(TextButton);
|
||
// Context menu shows 5 buttons: cut, copy, paste, select all, share on Android.
|
||
// Context menu shows 6 buttons: cut, copy, paste, select all, lookup, share on iOS.
|
||
final numberOfContextMenuButtons = targetPlatformIsiOS ? 6 : 5;
|
||
|
||
expect(
|
||
contextMenuButtonFinder,
|
||
isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(numberOfContextMenuButtons),
|
||
);
|
||
|
||
// Scroll to the left, the toolbar should be hidden since we are scrolling.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
tester.getCenter(find.byType(TextField)),
|
||
);
|
||
await tester.pump();
|
||
await gesture.moveTo(tester.getBottomLeft(find.byType(TextField)));
|
||
await tester.pumpAndSettle();
|
||
expect(contextMenuButtonFinder, findsNothing);
|
||
|
||
// Scroll back to center, the toolbar should still be hidden since
|
||
// we are still scrolling.
|
||
await gesture.moveTo(tester.getCenter(find.byType(TextField)));
|
||
await tester.pumpAndSettle();
|
||
expect(contextMenuButtonFinder, findsNothing);
|
||
|
||
// Release finger to end scroll, toolbar should now be visible.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
contextMenuButtonFinder,
|
||
isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(numberOfContextMenuButtons),
|
||
);
|
||
expect(renderEditable.selectionStartInViewport.value, true);
|
||
expect(renderEditable.selectionEndInViewport.value, true);
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
||
);
|
||
|
||
testWidgets(
|
||
'Toolbar hides on parent scrollable scroll start and re-appears on scroll end on Android and iOS',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure ' * 20,
|
||
);
|
||
final Key key1 = UniqueKey();
|
||
final Key key2 = UniqueKey();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: ListView(
|
||
children: <Widget>[
|
||
Container(height: 400, key: key1),
|
||
TextField(controller: controller),
|
||
Container(height: 1000, key: key2),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
final RenderEditable renderEditable = state.renderEditable;
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Long press should select word at position and show toolbar.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
final targetPlatformIsiOS = defaultTargetPlatform == TargetPlatform.iOS;
|
||
final Finder contextMenuButtonFinder = targetPlatformIsiOS
|
||
? find.byType(CupertinoButton)
|
||
: find.byType(TextButton);
|
||
// Context menu shows 5 buttons: cut, copy, paste, select all, share on Android.
|
||
// Context menu shows 6 buttons: cut, copy, paste, select all, lookup, share on iOS.
|
||
final numberOfContextMenuButtons = targetPlatformIsiOS ? 6 : 5;
|
||
expect(
|
||
contextMenuButtonFinder,
|
||
isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(numberOfContextMenuButtons),
|
||
);
|
||
|
||
// Scroll down, the toolbar should be hidden since we are scrolling.
|
||
final TestGesture gesture = await tester.startGesture(tester.getBottomLeft(find.byKey(key1)));
|
||
await tester.pump();
|
||
await gesture.moveTo(tester.getTopLeft(find.byKey(key1)));
|
||
await tester.pumpAndSettle();
|
||
expect(contextMenuButtonFinder, findsNothing);
|
||
|
||
// Release finger to end scroll, toolbar should now be visible.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
contextMenuButtonFinder,
|
||
isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(numberOfContextMenuButtons),
|
||
);
|
||
expect(renderEditable.selectionStartInViewport.value, true);
|
||
expect(renderEditable.selectionEndInViewport.value, true);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.iOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Toolbar can re-appear after being scrolled out of view on Android and iOS',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure ' * 20,
|
||
);
|
||
final scrollController = ScrollController();
|
||
addTearDown(scrollController.dispose);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(controller: controller, scrollController: scrollController),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
final RenderEditable renderEditable = state.renderEditable;
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
expect(renderEditable.selectionStartInViewport.value, false);
|
||
expect(renderEditable.selectionEndInViewport.value, false);
|
||
|
||
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Long press should select word at position and show toolbar.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
|
||
final targetPlatformIsiOS = defaultTargetPlatform == TargetPlatform.iOS;
|
||
final Finder contextMenuButtonFinder = targetPlatformIsiOS
|
||
? find.byType(CupertinoButton)
|
||
: find.byType(TextButton);
|
||
// Context menu shows 5 buttons: cut, copy, paste, select all, share on Android.
|
||
// Context menu shows 6 buttons: cut, copy, paste, select all, lookup, share on iOS.
|
||
final numberOfContextMenuButtons = targetPlatformIsiOS ? 6 : 5;
|
||
|
||
expect(
|
||
contextMenuButtonFinder,
|
||
isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(numberOfContextMenuButtons),
|
||
);
|
||
expect(renderEditable.selectionStartInViewport.value, true);
|
||
expect(renderEditable.selectionEndInViewport.value, true);
|
||
|
||
// Scroll to the end so the selection is no longer visible. This should
|
||
// hide the toolbar, but schedule it to be shown once the selection is
|
||
// visible again.
|
||
scrollController.animateTo(
|
||
500.0,
|
||
duration: const Duration(milliseconds: 100),
|
||
curve: Curves.linear,
|
||
);
|
||
await tester.pumpAndSettle();
|
||
expect(contextMenuButtonFinder, findsNothing);
|
||
expect(renderEditable.selectionStartInViewport.value, false);
|
||
expect(renderEditable.selectionEndInViewport.value, false);
|
||
|
||
// Scroll to the beginning where the selection is in view
|
||
// and the toolbar should show again.
|
||
scrollController.animateTo(
|
||
0.0,
|
||
duration: const Duration(milliseconds: 100),
|
||
curve: Curves.linear,
|
||
);
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
contextMenuButtonFinder,
|
||
isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(numberOfContextMenuButtons),
|
||
);
|
||
expect(renderEditable.selectionStartInViewport.value, true);
|
||
expect(renderEditable.selectionEndInViewport.value, true);
|
||
|
||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, 0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await gesture.down(textOffsetToPosition(tester, 0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// Double tap should select word at position and show toolbar.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
expect(
|
||
contextMenuButtonFinder,
|
||
isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(numberOfContextMenuButtons),
|
||
);
|
||
expect(renderEditable.selectionStartInViewport.value, true);
|
||
expect(renderEditable.selectionEndInViewport.value, true);
|
||
|
||
// Scroll to the end so the selection is no longer visible. This should
|
||
// hide the toolbar, but schedule it to be shown once the selection is
|
||
// visible again.
|
||
scrollController.animateTo(
|
||
500.0,
|
||
duration: const Duration(milliseconds: 100),
|
||
curve: Curves.linear,
|
||
);
|
||
await tester.pumpAndSettle();
|
||
expect(contextMenuButtonFinder, findsNothing);
|
||
expect(renderEditable.selectionStartInViewport.value, false);
|
||
expect(renderEditable.selectionEndInViewport.value, false);
|
||
|
||
// Tap to change the selection. This will invalidate the scheduled
|
||
// toolbar.
|
||
await gesture.down(tester.getCenter(find.byType(TextField)));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// Scroll to the beginning where the selection was previously
|
||
// and the toolbar should not show because it was invalidated.
|
||
scrollController.animateTo(
|
||
0.0,
|
||
duration: const Duration(milliseconds: 100),
|
||
curve: Curves.linear,
|
||
);
|
||
await tester.pumpAndSettle();
|
||
expect(contextMenuButtonFinder, findsNothing);
|
||
expect(renderEditable.selectionStartInViewport.value, false);
|
||
expect(renderEditable.selectionEndInViewport.value, false);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.iOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Toolbar can re-appear after parent scrollable scrolls selection out of view on Android and iOS',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
final scrollController = ScrollController();
|
||
addTearDown(scrollController.dispose);
|
||
final Key key1 = UniqueKey();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: ListView(
|
||
controller: scrollController,
|
||
children: <Widget>[
|
||
TextField(controller: controller),
|
||
Container(height: 1500.0, key: key1),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
final RenderEditable renderEditable = state.renderEditable;
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Long press should select word at position and show toolbar.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
final targetPlatformIsiOS = defaultTargetPlatform == TargetPlatform.iOS;
|
||
final Finder contextMenuButtonFinder = targetPlatformIsiOS
|
||
? find.byType(CupertinoButton)
|
||
: find.byType(TextButton);
|
||
// Context menu shows 5 buttons: cut, copy, paste, select all, share on Android.
|
||
// Context menu shows 6 buttons: cut, copy, paste, select all, lookup, share on iOS.
|
||
final numberOfContextMenuButtons = targetPlatformIsiOS ? 6 : 5;
|
||
expect(
|
||
contextMenuButtonFinder,
|
||
isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(numberOfContextMenuButtons),
|
||
);
|
||
|
||
// Scroll down, the TextField should no longer be in the viewport.
|
||
scrollController.animateTo(
|
||
scrollController.position.maxScrollExtent,
|
||
duration: const Duration(milliseconds: 100),
|
||
curve: Curves.linear,
|
||
);
|
||
await tester.pumpAndSettle();
|
||
expect(find.byType(TextField), findsNothing);
|
||
expect(contextMenuButtonFinder, findsNothing);
|
||
|
||
// Scroll back up so the TextField is inside the viewport.
|
||
scrollController.animateTo(
|
||
0.0,
|
||
duration: const Duration(milliseconds: 100),
|
||
curve: Curves.linear,
|
||
);
|
||
await tester.pumpAndSettle();
|
||
expect(find.byType(TextField), findsOneWidget);
|
||
expect(
|
||
contextMenuButtonFinder,
|
||
isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(numberOfContextMenuButtons),
|
||
);
|
||
expect(renderEditable.selectionStartInViewport.value, true);
|
||
expect(renderEditable.selectionEndInViewport.value, true);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.iOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'long press tap cannot initiate a double tap',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(autofocus: true, controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// This extra pump is so autofocus can propagate to renderEditable.
|
||
await tester.pump();
|
||
|
||
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
|
||
|
||
await tester.longPressAt(ePos);
|
||
await tester.pumpAndSettle(const Duration(milliseconds: 50));
|
||
|
||
// Tap slightly behind the previous tap to avoid tapping the context menu
|
||
// on desktop.
|
||
final isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
|
||
final Offset secondTapPos = isTargetPlatformMobile ? ePos : ePos + const Offset(-1.0, 0.0);
|
||
await tester.tapAt(secondTapPos);
|
||
await tester.pump();
|
||
|
||
// The cursor does not move and the toolbar is toggled.
|
||
expect(controller.selection.isCollapsed, isTrue);
|
||
expect(controller.selection.baseOffset, 6);
|
||
|
||
// The toolbar from the long press is now dismissed by the second tap.
|
||
expectNoCupertinoToolbar();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'long press drag extends the selection to the word under the drag and shows toolbar on lift on non-Apple platforms',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, 18));
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
// Long press selects the word at the long presses position.
|
||
expect(controller.selection, const TextSelection(baseOffset: 13, extentOffset: 23));
|
||
// Cursor move doesn't trigger a toolbar initially.
|
||
expectNoMaterialToolbar();
|
||
|
||
await gesture.moveBy(const Offset(100, 0));
|
||
await tester.pump();
|
||
|
||
// The selection is now moved with the drag.
|
||
expect(controller.selection, const TextSelection(baseOffset: 13, extentOffset: 35));
|
||
// Still no toolbar.
|
||
expectNoMaterialToolbar();
|
||
|
||
// The selection is moved on a backwards drag.
|
||
await gesture.moveBy(const Offset(-200, 0));
|
||
await tester.pump();
|
||
|
||
// The selection is now moved with the drag.
|
||
expect(controller.selection, const TextSelection(baseOffset: 23, extentOffset: 8));
|
||
// Still no toolbar.
|
||
expectNoMaterialToolbar();
|
||
|
||
await gesture.moveBy(const Offset(-100, 0));
|
||
await tester.pump();
|
||
|
||
// The selection is now moved with the drag.
|
||
expect(controller.selection, const TextSelection(baseOffset: 23, extentOffset: 0));
|
||
// Still no toolbar.
|
||
expectNoMaterialToolbar();
|
||
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// The selection isn't affected by the gesture lift.
|
||
expect(controller.selection, const TextSelection(baseOffset: 23, extentOffset: 0));
|
||
// The toolbar now shows up.
|
||
expectMaterialToolbarForPartialSelection();
|
||
},
|
||
variant: TargetPlatformVariant.all(
|
||
excluding: <TargetPlatform>{TargetPlatform.iOS, TargetPlatform.macOS},
|
||
),
|
||
);
|
||
|
||
testWidgets(
|
||
'long press drag on a focused TextField moves the cursor under the drag and shows toolbar on lift',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(autofocus: true, controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// This extra pump is so autofocus can propagate to renderEditable.
|
||
await tester.pump();
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textfieldStart + const Offset(50.0, 9.0),
|
||
);
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
// Long press on iOS shows collapsed selection cursor.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 3));
|
||
// Cursor move doesn't trigger a toolbar initially.
|
||
expectNoCupertinoToolbar();
|
||
|
||
await gesture.moveBy(const Offset(50, 0));
|
||
await tester.pump();
|
||
|
||
// The selection position is now moved with the drag.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 6));
|
||
// Still no toolbar.
|
||
expectNoCupertinoToolbar();
|
||
|
||
await gesture.moveBy(const Offset(50, 0));
|
||
await tester.pump();
|
||
|
||
// The selection position is now moved with the drag.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 9));
|
||
// Still no toolbar.
|
||
expectNoCupertinoToolbar();
|
||
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// The selection isn't affected by the gesture lift.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 9));
|
||
// The toolbar now shows up.
|
||
expectCupertinoToolbarForCollapsedSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'long press drag on an unfocused TextField selects word-by-word and shows toolbar on lift',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textfieldStart + const Offset(50.0, 9.0),
|
||
);
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
// Long press on iOS shows collapsed selection cursor.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
// Cursor move doesn't trigger a toolbar initially.
|
||
expectNoCupertinoToolbar();
|
||
|
||
await gesture.moveBy(const Offset(100, 0));
|
||
await tester.pump();
|
||
|
||
// The selection position is now moved with the drag.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 12));
|
||
// Still no toolbar.
|
||
expectNoCupertinoToolbar();
|
||
|
||
await gesture.moveBy(const Offset(100, 0));
|
||
await tester.pump();
|
||
|
||
// The selection position is now moved with the drag.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 23));
|
||
// Still no toolbar.
|
||
expectNoCupertinoToolbar();
|
||
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// The selection isn't affected by the gesture lift.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 23));
|
||
// The toolbar now shows up.
|
||
expectCupertinoToolbarForPartialSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'long press drag can edge scroll on non-Apple platforms',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
|
||
List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
||
const TextSelection.collapsed(offset: 66), // Last character's position.
|
||
);
|
||
|
||
expect(lastCharEndpoint.length, 1);
|
||
// Just testing the text and making sure that the last character is off
|
||
// the right side of the screen.
|
||
expect(lastCharEndpoint[0].point.dx, 1056);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
final TestGesture gesture = await tester.startGesture(textfieldStart);
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.upstream),
|
||
);
|
||
expectNoMaterialToolbar();
|
||
|
||
await gesture.moveBy(const Offset(900, 5));
|
||
// To the edge of the screen basically.
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 59));
|
||
// Keep moving out.
|
||
await gesture.moveBy(const Offset(1, 0));
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 66));
|
||
await gesture.moveBy(const Offset(1, 0));
|
||
await tester.pump();
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream),
|
||
); // We're at the edge now.
|
||
expectNoMaterialToolbar();
|
||
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// The selection isn't affected by the gesture lift.
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream),
|
||
);
|
||
// The toolbar now shows up.
|
||
expectMaterialToolbarForFullSelection();
|
||
|
||
lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
||
const TextSelection.collapsed(offset: 66), // Last character's position.
|
||
);
|
||
|
||
expect(lastCharEndpoint.length, 1);
|
||
// The last character is now on screen near the right edge.
|
||
expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(798, epsilon: 1));
|
||
|
||
final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection(
|
||
const TextSelection.collapsed(offset: 0), // First character's position.
|
||
);
|
||
expect(firstCharEndpoint.length, 1);
|
||
// The first character is now offscreen to the left.
|
||
expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257.0, epsilon: 1));
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
TargetPlatform.linux,
|
||
TargetPlatform.windows,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'long press drag can edge scroll on Apple platforms - unfocused TextField',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
|
||
List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
||
const TextSelection.collapsed(offset: 66), // Last character's position.
|
||
);
|
||
|
||
expect(lastCharEndpoint.length, 1);
|
||
// Just testing the test and making sure that the last character is off
|
||
// the right side of the screen.
|
||
expect(lastCharEndpoint[0].point.dx, 1056);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
final TestGesture gesture = await tester.startGesture(textfieldStart);
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.upstream),
|
||
);
|
||
expectNoCupertinoToolbar();
|
||
|
||
await gesture.moveBy(const Offset(900, 5));
|
||
// To the edge of the screen basically.
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 59));
|
||
// Keep moving out.
|
||
await gesture.moveBy(const Offset(1, 0));
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 66));
|
||
await gesture.moveBy(const Offset(1, 0));
|
||
await tester.pump();
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream),
|
||
); // We're at the edge now.
|
||
expectNoCupertinoToolbar();
|
||
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// The selection isn't affected by the gesture lift.
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream),
|
||
);
|
||
// The toolbar now shows up.
|
||
expectCupertinoToolbarForFullSelection();
|
||
|
||
lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
||
const TextSelection.collapsed(offset: 66), // Last character's position.
|
||
);
|
||
|
||
expect(lastCharEndpoint.length, 1);
|
||
// The last character is now on screen near the right edge.
|
||
expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(798, epsilon: 1));
|
||
|
||
final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection(
|
||
const TextSelection.collapsed(offset: 0), // First character's position.
|
||
);
|
||
expect(firstCharEndpoint.length, 1);
|
||
// The first character is now offscreen to the left.
|
||
expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257.0, epsilon: 1));
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'long press drag can edge scroll on Apple platforms - focused TextField',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(child: TextField(autofocus: true, controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// This extra pump is so autofocus can propagate to renderEditable.
|
||
await tester.pump();
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
|
||
List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
||
const TextSelection.collapsed(offset: 66), // Last character's position.
|
||
);
|
||
|
||
expect(lastCharEndpoint.length, 1);
|
||
// Just testing the test and making sure that the last character is off
|
||
// the right side of the screen.
|
||
expect(lastCharEndpoint[0].point.dx, 1056);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
final TestGesture gesture = await tester.startGesture(textfieldStart + const Offset(300, 5));
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 19, affinity: TextAffinity.upstream),
|
||
);
|
||
expectNoCupertinoToolbar();
|
||
|
||
await gesture.moveBy(const Offset(600, 0));
|
||
// To the edge of the screen basically.
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 56));
|
||
// Keep moving out.
|
||
await gesture.moveBy(const Offset(1, 0));
|
||
await tester.pump();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 62));
|
||
await gesture.moveBy(const Offset(1, 0));
|
||
await tester.pump();
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
|
||
); // We're at the edge now.
|
||
expectNoCupertinoToolbar();
|
||
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// The selection isn't affected by the gesture lift.
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
|
||
);
|
||
// The toolbar now shows up.
|
||
expectCupertinoToolbarForCollapsedSelection();
|
||
|
||
lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
||
const TextSelection.collapsed(offset: 66), // Last character's position.
|
||
);
|
||
|
||
expect(lastCharEndpoint.length, 1);
|
||
// The last character is now on screen near the right edge.
|
||
expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(798, epsilon: 1));
|
||
|
||
final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection(
|
||
const TextSelection.collapsed(offset: 0), // First character's position.
|
||
);
|
||
expect(firstCharEndpoint.length, 1);
|
||
// The first character is now offscreen to the left.
|
||
expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257.0, epsilon: 1));
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets('mouse click and drag can edge scroll', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
final Size screenSize = MediaQuery.of(tester.element(find.byType(TextField))).size;
|
||
// Just testing the test and making sure that the last character is off
|
||
// the right side of the screen.
|
||
expect(textOffsetToPosition(tester, 66).dx, greaterThan(screenSize.width));
|
||
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, 19),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
|
||
await gesture.moveTo(textOffsetToPosition(tester, 56));
|
||
// To the edge of the screen basically.
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 19, extentOffset: 56));
|
||
|
||
// Keep moving out.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 62));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 19, extentOffset: 62));
|
||
await gesture.moveTo(textOffsetToPosition(tester, 66));
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection(baseOffset: 19, extentOffset: 66),
|
||
); // We're at the edge now.
|
||
expectNoCupertinoToolbar();
|
||
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// The selection isn't affected by the gesture lift.
|
||
expect(controller.selection, const TextSelection(baseOffset: 19, extentOffset: 66));
|
||
|
||
// The last character is now on screen near the right edge.
|
||
expect(
|
||
textOffsetToPosition(tester, 66).dx,
|
||
moreOrLessEquals(TestSemantics.fullScreen.width, epsilon: 2.0),
|
||
);
|
||
|
||
// The first character is now offscreen to the left.
|
||
expect(textOffsetToPosition(tester, 0).dx, lessThan(-100.0));
|
||
}, variant: TargetPlatformVariant.all());
|
||
|
||
testWidgets(
|
||
'keyboard selection change scrolls the field',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Just testing the test and making sure that the last character is off
|
||
// the right side of the screen.
|
||
expect(textOffsetToPosition(tester, 66).dx, 1056);
|
||
|
||
await tester.tapAt(textOffsetToPosition(tester, 13));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 13));
|
||
|
||
// Move to position 56 with the right arrow (near the edge of the screen).
|
||
for (var i = 0; i < (56 - 13); i += 1) {
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||
}
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
controller.selection,
|
||
// arrowRight always sets the affinity to downstream.
|
||
const TextSelection.collapsed(offset: 56),
|
||
);
|
||
|
||
// Keep moving out.
|
||
for (var i = 0; i < (62 - 56); i += 1) {
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||
}
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 62));
|
||
for (var i = 0; i < (66 - 62); i += 1) {
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||
}
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 66),
|
||
); // We're at the edge now.
|
||
|
||
await tester.pumpAndSettle();
|
||
|
||
// The last character is now on screen near the right edge.
|
||
expect(
|
||
textOffsetToPosition(tester, 66).dx,
|
||
moreOrLessEquals(TestSemantics.fullScreen.width, epsilon: 2.0),
|
||
);
|
||
|
||
// The first character is now offscreen to the left.
|
||
expect(textOffsetToPosition(tester, 0).dx, moreOrLessEquals(-257.0, epsilon: 1));
|
||
},
|
||
variant: TargetPlatformVariant.all(),
|
||
skip: isBrowser, // [intended] Browser handles arrow keys differently.
|
||
);
|
||
|
||
testWidgets(
|
||
'long press drag can edge scroll vertically',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text:
|
||
'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neigse Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(autofocus: true, maxLines: 2, controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// This extra pump is so autofocus can propagate to renderEditable.
|
||
await tester.pump();
|
||
|
||
// Just testing the test and making sure that the last character is outside
|
||
// the bottom of the field.
|
||
final int textLength = controller.text.length;
|
||
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
|
||
final double firstCharY = textOffsetToPosition(tester, 0).dy;
|
||
expect(
|
||
textOffsetToPosition(tester, textLength).dy,
|
||
moreOrLessEquals(firstCharY + lineHeight * 2, epsilon: 1),
|
||
);
|
||
|
||
// Start long pressing on the first line.
|
||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, 19));
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 19));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Move down to the second line.
|
||
await gesture.moveBy(Offset(0.0, lineHeight));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 65));
|
||
|
||
// Still hasn't scrolled.
|
||
expect(
|
||
textOffsetToPosition(tester, 65).dy,
|
||
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
|
||
);
|
||
|
||
// Keep selecting down to the third and final line.
|
||
await gesture.moveBy(Offset(0.0, lineHeight));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 110));
|
||
|
||
// The last character is no longer three line heights down from the top of
|
||
// the field, it's now only two line heights down, because it has scrolled
|
||
// down by one line.
|
||
expect(
|
||
textOffsetToPosition(tester, 110).dy,
|
||
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
|
||
);
|
||
|
||
// Likewise, the first character is now scrolled out of the top of the field
|
||
// by one line.
|
||
expect(
|
||
textOffsetToPosition(tester, 0).dy,
|
||
moreOrLessEquals(firstCharY - lineHeight, epsilon: 1),
|
||
);
|
||
|
||
// End gesture and skip the magnifier hide animation, so it can release
|
||
// resources.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'keyboard selection change scrolls the field vertically',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text:
|
||
'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(maxLines: 2, controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Just testing the test and making sure that the last character is outside
|
||
// the bottom of the field.
|
||
final int textLength = controller.text.length;
|
||
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
|
||
final double firstCharY = textOffsetToPosition(tester, 0).dy;
|
||
expect(
|
||
textOffsetToPosition(tester, textLength).dy,
|
||
moreOrLessEquals(firstCharY + lineHeight * 2, epsilon: 1),
|
||
);
|
||
|
||
await tester.tapAt(textOffsetToPosition(tester, 13));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 13));
|
||
|
||
// Move down to the second line.
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 59));
|
||
|
||
// Still hasn't scrolled.
|
||
expect(
|
||
textOffsetToPosition(tester, 66).dy,
|
||
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
|
||
);
|
||
|
||
// Move down to the third and final line.
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 104));
|
||
|
||
// The last character is no longer three line heights down from the top of
|
||
// the field, it's now only two line heights down, because it has scrolled
|
||
// down by one line.
|
||
expect(
|
||
textOffsetToPosition(tester, textLength).dy,
|
||
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
|
||
);
|
||
|
||
// Likewise, the first character is now scrolled out of the top of the field
|
||
// by one line.
|
||
expect(
|
||
textOffsetToPosition(tester, 0).dy,
|
||
moreOrLessEquals(firstCharY - lineHeight, epsilon: 1),
|
||
);
|
||
},
|
||
variant: TargetPlatformVariant.all(),
|
||
skip: isBrowser, // [intended] Browser handles arrow keys differently.
|
||
);
|
||
|
||
testWidgets('mouse click and drag can edge scroll vertically', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text:
|
||
'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(maxLines: 2, controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Just testing the test and making sure that the last character is outside
|
||
// the bottom of the field.
|
||
final int textLength = controller.text.length;
|
||
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
|
||
final double firstCharY = textOffsetToPosition(tester, 0).dy;
|
||
expect(
|
||
textOffsetToPosition(tester, textLength).dy,
|
||
moreOrLessEquals(firstCharY + lineHeight * 2, epsilon: 1),
|
||
);
|
||
|
||
// Start selecting on the first line.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, 19),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
|
||
// Still hasn't scrolled.
|
||
expect(
|
||
textOffsetToPosition(tester, 60).dy,
|
||
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
|
||
);
|
||
|
||
// Select down to the second line.
|
||
await gesture.moveBy(Offset(0.0, lineHeight));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 19, extentOffset: 65));
|
||
|
||
// Still hasn't scrolled.
|
||
expect(
|
||
textOffsetToPosition(tester, 60).dy,
|
||
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
|
||
);
|
||
|
||
// Keep selecting down to the third and final line.
|
||
await gesture.moveBy(Offset(0.0, lineHeight));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 19, extentOffset: 110));
|
||
|
||
// The last character is no longer three line heights down from the top of
|
||
// the field, it's now only two line heights down, because it has scrolled
|
||
// down by one line.
|
||
expect(
|
||
textOffsetToPosition(tester, textLength).dy,
|
||
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
|
||
);
|
||
|
||
// Likewise, the first character is now scrolled out of the top of the field
|
||
// by one line.
|
||
expect(
|
||
textOffsetToPosition(tester, 0).dy,
|
||
moreOrLessEquals(firstCharY - lineHeight, epsilon: 1),
|
||
);
|
||
}, variant: TargetPlatformVariant.all());
|
||
|
||
testWidgets(
|
||
'long tap after a double tap select is not affected',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
|
||
// On macOS, we select the precise position of the tap.
|
||
final isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
|
||
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
|
||
|
||
await tester.tapAt(pPos);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap moved the cursor to the beginning of the second word.
|
||
expect(
|
||
controller.selection,
|
||
isTargetPlatformMobile
|
||
? const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream)
|
||
: const TextSelection.collapsed(offset: 9),
|
||
);
|
||
await tester.tapAt(pPos);
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
|
||
await tester.longPressAt(ePos);
|
||
await tester.pumpAndSettle();
|
||
|
||
// Plain collapsed selection at the exact tap position.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 6));
|
||
|
||
// The toolbar shows up.
|
||
expectCupertinoToolbarForCollapsedSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'double tap after a long tap is not affected',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
|
||
// On macOS, we select the precise position of the tap.
|
||
final isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(child: TextField(autofocus: true, controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// This extra pump is so autofocus can propagate to renderEditable.
|
||
await tester.pump();
|
||
|
||
// The second tap is slightly higher to avoid tapping the context menu on
|
||
// desktop.
|
||
final Offset pPos =
|
||
textOffsetToPosition(tester, 9) + const Offset(0.0, -20.0); // Index of 'P|eel'
|
||
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'
|
||
|
||
await tester.longPressAt(wPos);
|
||
await tester.pumpAndSettle(const Duration(milliseconds: 50));
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 3));
|
||
|
||
await tester.tapAt(pPos);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap moved the cursor.
|
||
expect(
|
||
controller.selection,
|
||
isTargetPlatformMobile
|
||
? const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream)
|
||
: const TextSelection.collapsed(offset: 9),
|
||
);
|
||
await tester.tapAt(pPos);
|
||
await tester.pumpAndSettle();
|
||
|
||
// Double tap selection.
|
||
expect(controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12));
|
||
expectCupertinoToolbarForPartialSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets('double click after a click on desktop platforms', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textFieldStart + const Offset(50.0, 9.0),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 3));
|
||
|
||
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First click moved the cursor to the precise location, not the start of
|
||
// the word.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 9));
|
||
|
||
// Double click selection.
|
||
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12));
|
||
// The text selection toolbar isn't shown on Mac without a right click.
|
||
expectNoCupertinoToolbar();
|
||
}, variant: TargetPlatformVariant.desktop());
|
||
|
||
testWidgets(
|
||
'double tap chains work',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
||
);
|
||
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
expectCupertinoToolbarForPartialSelection();
|
||
|
||
// Double tap selecting the same word somewhere else is fine.
|
||
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap hides the toolbar and retains the selection.
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
expectNoCupertinoToolbar();
|
||
|
||
// Second tap shows the toolbar and retains the selection.
|
||
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
||
// Wait for the consecutive tap timer to timeout so the next
|
||
// tap is not detected as a triple tap.
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
expectCupertinoToolbarForPartialSelection();
|
||
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap moved the cursor and hid the toolbar.
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
|
||
);
|
||
expectNoCupertinoToolbar();
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12));
|
||
expectCupertinoToolbarForPartialSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets(
|
||
'double click chains work',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
// First click moves the cursor to the point of the click, not the edge of
|
||
// the clicked word.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textFieldStart + const Offset(50.0, 9.0),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 3));
|
||
|
||
// Second click selects.
|
||
await gesture.down(textFieldStart + const Offset(50.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
// Wait for the consecutive tap timer to timeout so the next
|
||
// tap is not detected as a triple tap.
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
expectNoCupertinoToolbar();
|
||
|
||
// Double tap selecting the same word somewhere else is fine.
|
||
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap moved the cursor.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 6));
|
||
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
// Wait for the consecutive tap timer to timeout so the next
|
||
// tap is not detected as a triple tap.
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
|
||
expectNoCupertinoToolbar();
|
||
|
||
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
// First tap moved the cursor.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 9));
|
||
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12));
|
||
expectNoCupertinoToolbar();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.macOS,
|
||
TargetPlatform.windows,
|
||
TargetPlatform.linux,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'double tapping a space selects the previous word on iOS',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: ' blah blah \n blah');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(maxLines: null, controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, -1);
|
||
expect(controller.value.selection.extentOffset, -1);
|
||
|
||
// Put the cursor at the end of the field.
|
||
await tester.tapAt(textOffsetToPosition(tester, 19));
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, 19);
|
||
expect(controller.value.selection.extentOffset, 19);
|
||
|
||
// Double tapping does the same thing.
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.extentOffset, 5);
|
||
expect(controller.value.selection.baseOffset, 1);
|
||
|
||
// Put the cursor at the end of the field.
|
||
await tester.tapAt(textOffsetToPosition(tester, 19));
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, 19);
|
||
expect(controller.value.selection.extentOffset, 19);
|
||
|
||
// Double tapping does the same thing for the first space.
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, 0);
|
||
expect(controller.value.selection.extentOffset, 1);
|
||
|
||
// Put the cursor at the end of the field.
|
||
await tester.tapAt(textOffsetToPosition(tester, 19));
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, 19);
|
||
expect(controller.value.selection.extentOffset, 19);
|
||
|
||
// Double tapping the last space selects all previous contiguous spaces on
|
||
// both lines and the previous word.
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
await tester.tapAt(textOffsetToPosition(tester, 14));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, 14));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, 6);
|
||
expect(controller.value.selection.extentOffset, 14);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets(
|
||
'selecting a space selects the space on non-iOS platforms',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: ' blah blah');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, -1);
|
||
expect(controller.value.selection.extentOffset, -1);
|
||
|
||
// Put the cursor at the end of the field.
|
||
await tester.tapAt(textOffsetToPosition(tester, 10));
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, 10);
|
||
expect(controller.value.selection.extentOffset, 10);
|
||
|
||
// Double tapping the second space selects it.
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, 5);
|
||
expect(controller.value.selection.extentOffset, 6);
|
||
|
||
// Tap at the end of the text to move the selection to the end. On some
|
||
// platforms, the context menu "Cut" button blocks this tap, so move it out
|
||
// of the way by an Offset.
|
||
await tester.tapAt(textOffsetToPosition(tester, 10) + const Offset(200.0, 0.0));
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, 10);
|
||
expect(controller.value.selection.extentOffset, 10);
|
||
|
||
// Double tapping the second space selects it.
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, 0);
|
||
expect(controller.value.selection.extentOffset, 1);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.macOS,
|
||
TargetPlatform.windows,
|
||
TargetPlatform.linux,
|
||
TargetPlatform.fuchsia,
|
||
TargetPlatform.android,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'selecting a space selects the space on Desktop platforms',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: ' blah blah');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, -1);
|
||
expect(controller.value.selection.extentOffset, -1);
|
||
|
||
// Put the cursor at the end of the field.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, 10),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, 10);
|
||
expect(controller.value.selection.extentOffset, 10);
|
||
|
||
// Double clicking the second space selects it.
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
await gesture.down(textOffsetToPosition(tester, 5));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await gesture.down(textOffsetToPosition(tester, 5));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
// Wait for the consecutive tap timer to timeout so our next tap is not
|
||
// detected as a triple tap.
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, 5);
|
||
expect(controller.value.selection.extentOffset, 6);
|
||
|
||
// Put the cursor at the end of the field.
|
||
await gesture.down(textOffsetToPosition(tester, 10));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, 10);
|
||
expect(controller.value.selection.extentOffset, 10);
|
||
|
||
// Double tapping the second space selects it.
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
await gesture.down(textOffsetToPosition(tester, 0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await gesture.down(textOffsetToPosition(tester, 0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(controller.value.selection, isNotNull);
|
||
expect(controller.value.selection.baseOffset, 0);
|
||
expect(controller.value.selection.extentOffset, 1);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.macOS,
|
||
TargetPlatform.windows,
|
||
TargetPlatform.linux,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Force press does not set selection on Android or Fuchsia touch devices',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
final Offset offset = tester.getTopLeft(find.byType(TextField)) + const Offset(150.0, 9.0);
|
||
|
||
final int pointerValue = tester.nextPointer;
|
||
final TestGesture gesture = await tester.createGesture();
|
||
await gesture.downWithCustomEvent(
|
||
offset,
|
||
PointerDownEvent(
|
||
pointer: pointerValue,
|
||
position: offset,
|
||
pressure: 0.0,
|
||
pressureMax: 6.0,
|
||
pressureMin: 0.0,
|
||
),
|
||
);
|
||
await gesture.updateWithCustomEvent(
|
||
PointerMoveEvent(
|
||
pointer: pointerValue,
|
||
position: offset + const Offset(150.0, 9.0),
|
||
pressure: 0.5,
|
||
pressureMin: 0,
|
||
),
|
||
);
|
||
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
// We don't want this gesture to select any word on Android.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: -1));
|
||
expectNoMaterialToolbar();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Force press sets selection on desktop platforms that do not support it',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
final Offset offset = tester.getTopLeft(find.byType(TextField)) + const Offset(150.0, 9.0);
|
||
|
||
final int pointerValue = tester.nextPointer;
|
||
final TestGesture gesture = await tester.createGesture();
|
||
await gesture.downWithCustomEvent(
|
||
offset,
|
||
PointerDownEvent(
|
||
pointer: pointerValue,
|
||
position: offset,
|
||
pressure: 0.0,
|
||
pressureMax: 6.0,
|
||
pressureMin: 0.0,
|
||
),
|
||
);
|
||
await gesture.updateWithCustomEvent(
|
||
PointerMoveEvent(
|
||
pointer: pointerValue,
|
||
position: offset + const Offset(150.0, 9.0),
|
||
pressure: 0.5,
|
||
pressureMin: 0,
|
||
),
|
||
);
|
||
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
// We don't want this gesture to select any word on Android.
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 9));
|
||
expectNoMaterialToolbar();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.linux,
|
||
TargetPlatform.windows,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'force press selects word',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
final int pointerValue = tester.nextPointer;
|
||
final Offset offset = textfieldStart + const Offset(150.0, 9.0);
|
||
final TestGesture gesture = await tester.createGesture();
|
||
await gesture.downWithCustomEvent(
|
||
offset,
|
||
PointerDownEvent(
|
||
pointer: pointerValue,
|
||
position: offset,
|
||
pressure: 0.0,
|
||
pressureMax: 6.0,
|
||
pressureMin: 0.0,
|
||
),
|
||
);
|
||
|
||
await gesture.updateWithCustomEvent(
|
||
PointerMoveEvent(
|
||
pointer: pointerValue,
|
||
position: textfieldStart + const Offset(150.0, 9.0),
|
||
pressure: 0.5,
|
||
pressureMin: 0,
|
||
),
|
||
);
|
||
// We expect the force press to select a word at the given location.
|
||
expect(controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12));
|
||
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expectCupertinoToolbarForPartialSelection();
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets(
|
||
'tap on non-force-press-supported devices work',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(Container(key: GlobalKey()));
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
final int pointerValue = tester.nextPointer;
|
||
final Offset offset = textfieldStart + const Offset(150.0, 9.0);
|
||
final TestGesture gesture = await tester.createGesture();
|
||
await gesture.downWithCustomEvent(
|
||
offset,
|
||
PointerDownEvent(
|
||
pointer: pointerValue,
|
||
position: offset,
|
||
// iPhone 6 and below report 0 across the board.
|
||
pressure: 0,
|
||
pressureMax: 0,
|
||
pressureMin: 0,
|
||
),
|
||
);
|
||
|
||
await gesture.updateWithCustomEvent(
|
||
PointerMoveEvent(
|
||
pointer: pointerValue,
|
||
position: textfieldStart + const Offset(150.0, 9.0),
|
||
pressure: 0.5,
|
||
pressureMin: 0,
|
||
),
|
||
);
|
||
await gesture.up();
|
||
// The event should fallback to a normal tap and move the cursor.
|
||
// Single taps selects the edge of the word.
|
||
expect(
|
||
controller.selection,
|
||
const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
|
||
);
|
||
|
||
await tester.pump();
|
||
// Single taps shouldn't trigger the toolbar.
|
||
expectNoCupertinoToolbar();
|
||
|
||
// TODO(gspencergoog): Add in TargetPlatform.macOS in the line below when we figure out what global state is leaking.
|
||
// https://github.com/flutter/flutter/issues/43445
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
||
);
|
||
|
||
testWidgets('default TextField debugFillProperties', (WidgetTester tester) async {
|
||
final builder = DiagnosticPropertiesBuilder();
|
||
|
||
const TextField().debugFillProperties(builder);
|
||
|
||
final List<String> description = builder.properties
|
||
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
|
||
.map((DiagnosticsNode node) => node.toString())
|
||
.toList();
|
||
|
||
expect(description, <String>[]);
|
||
});
|
||
|
||
testWidgets('TextField implements debugFillProperties', (WidgetTester tester) async {
|
||
final builder = DiagnosticPropertiesBuilder();
|
||
|
||
// Not checking controller, inputFormatters, focusNode
|
||
const TextField(
|
||
decoration: InputDecoration(labelText: 'foo'),
|
||
keyboardType: TextInputType.text,
|
||
textInputAction: TextInputAction.done,
|
||
style: TextStyle(color: Color(0xff00ff00)),
|
||
textAlign: TextAlign.end,
|
||
textDirection: TextDirection.ltr,
|
||
autofocus: true,
|
||
autocorrect: false,
|
||
maxLines: 10,
|
||
maxLength: 100,
|
||
maxLengthEnforcement: MaxLengthEnforcement.none,
|
||
smartDashesType: SmartDashesType.disabled,
|
||
smartQuotesType: SmartQuotesType.disabled,
|
||
enabled: false,
|
||
cursorWidth: 1.0,
|
||
cursorHeight: 1.0,
|
||
cursorRadius: Radius.zero,
|
||
cursorColor: Color(0xff00ff00),
|
||
keyboardAppearance: Brightness.dark,
|
||
scrollPadding: EdgeInsets.zero,
|
||
scrollPhysics: ClampingScrollPhysics(),
|
||
enableInteractiveSelection: false,
|
||
hintLocales: <Locale>[Locale('en'), Locale('fr')],
|
||
).debugFillProperties(builder);
|
||
|
||
final List<String> description = builder.properties
|
||
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
|
||
.map((DiagnosticsNode node) => node.toString())
|
||
.toList();
|
||
|
||
expect(description, <String>[
|
||
'enabled: false',
|
||
'decoration: InputDecoration(labelText: "foo")',
|
||
'style: TextStyle(inherit: true, color: ${const Color(0xff00ff00)})',
|
||
'autofocus: true',
|
||
'autocorrect: false',
|
||
'smartDashesType: disabled',
|
||
'smartQuotesType: disabled',
|
||
'maxLines: 10',
|
||
'maxLength: 100',
|
||
'maxLengthEnforcement: none',
|
||
'textInputAction: done',
|
||
'textAlign: end',
|
||
'textDirection: ltr',
|
||
'cursorWidth: 1.0',
|
||
'cursorHeight: 1.0',
|
||
'cursorRadius: Radius.circular(0.0)',
|
||
'cursorColor: ${const Color(0xff00ff00)}',
|
||
'keyboardAppearance: Brightness.dark',
|
||
'scrollPadding: EdgeInsets.zero',
|
||
'selection disabled',
|
||
'scrollPhysics: ClampingScrollPhysics',
|
||
'hintLocales: [en, fr]',
|
||
]);
|
||
});
|
||
|
||
testWidgets('strut basic single line', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(platform: TargetPlatform.android),
|
||
home: const Material(child: Center(child: TextField())),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
tester.getSize(find.byType(TextField)),
|
||
// The TextField will be as tall as the decoration (24) plus the metrics
|
||
// from the default TextStyle of the theme (16), or 40 altogether.
|
||
// Because this is less than the kMinInteractiveDimension, it will be
|
||
// increased to that value (48).
|
||
const Size(800, kMinInteractiveDimension),
|
||
);
|
||
});
|
||
|
||
testWidgets('strut TextStyle increases height', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(platform: TargetPlatform.android, useMaterial3: false),
|
||
home: const Material(
|
||
child: Center(child: TextField(style: TextStyle(fontSize: 20))),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
tester.getSize(find.byType(TextField)),
|
||
// Strut should inherit the TextStyle.fontSize by default and produce the
|
||
// same height as if it were disabled.
|
||
const Size(800, kMinInteractiveDimension), // Because 44 < 48.
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(platform: TargetPlatform.android),
|
||
home: const Material(
|
||
child: Center(
|
||
child: TextField(style: TextStyle(fontSize: 20), strutStyle: StrutStyle.disabled),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
tester.getSize(find.byType(TextField)),
|
||
// The height here should match the previous version with strut enabled.
|
||
const Size(800, kMinInteractiveDimension), // Because 44 < 48.
|
||
);
|
||
});
|
||
|
||
testWidgets('strut basic multi line', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(platform: TargetPlatform.android, useMaterial3: false),
|
||
home: const Material(child: Center(child: TextField(maxLines: 6))),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
tester.getSize(find.byType(TextField)),
|
||
// The height should be the input decoration (24) plus 6x the strut height (16).
|
||
const Size(800, 120),
|
||
);
|
||
});
|
||
|
||
testWidgets('strut no force small strut', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(platform: TargetPlatform.android, useMaterial3: false),
|
||
home: const Material(
|
||
child: Center(
|
||
child: TextField(
|
||
maxLines: 6,
|
||
strutStyle: StrutStyle(
|
||
// The small strut is overtaken by the larger
|
||
// TextStyle fontSize.
|
||
fontSize: 5,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
tester.getSize(find.byType(TextField)),
|
||
// When the strut's height is smaller than TextStyle's and forceStrutHeight
|
||
// is disabled, then the TextStyle takes precedence. Should be the same height
|
||
// as 'strut basic multi line'.
|
||
const Size(800, 120),
|
||
);
|
||
});
|
||
|
||
testWidgets(
|
||
'strut no force large strut',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(platform: TargetPlatform.android, useMaterial3: false),
|
||
home: const Material(
|
||
child: Center(child: TextField(maxLines: 6, strutStyle: StrutStyle(fontSize: 25))),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
tester.getSize(find.byType(TextField)),
|
||
// When the strut's height is larger than TextStyle's and forceStrutHeight
|
||
// is disabled, then the StrutStyle takes precedence.
|
||
const Size(800, 174),
|
||
);
|
||
},
|
||
skip: isBrowser, // TODO(mdebbar): https://github.com/flutter/flutter/issues/32243
|
||
);
|
||
|
||
testWidgets(
|
||
'strut height override',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(platform: TargetPlatform.android, useMaterial3: false),
|
||
home: const Material(
|
||
child: Center(
|
||
child: TextField(
|
||
maxLines: 3,
|
||
strutStyle: StrutStyle(fontSize: 8, forceStrutHeight: true),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
tester.getSize(find.byType(TextField)),
|
||
// The smaller font size of strut make the field shorter than normal.
|
||
const Size(800, 48),
|
||
);
|
||
},
|
||
skip: isBrowser, // TODO(mdebbar): https://github.com/flutter/flutter/issues/32243
|
||
);
|
||
|
||
testWidgets(
|
||
'strut forces field taller',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(platform: TargetPlatform.android, useMaterial3: false),
|
||
home: const Material(
|
||
child: Center(
|
||
child: TextField(
|
||
maxLines: 3,
|
||
style: TextStyle(fontSize: 10),
|
||
strutStyle: StrutStyle(fontSize: 18, forceStrutHeight: true),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
tester.getSize(find.byType(TextField)),
|
||
// When the strut fontSize is larger than a provided TextStyle, the
|
||
// strut's height takes precedence.
|
||
const Size(800, 78),
|
||
);
|
||
},
|
||
skip: isBrowser, // TODO(mdebbar): https://github.com/flutter/flutter/issues/32243
|
||
);
|
||
|
||
testWidgets('Caret center position', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: const SizedBox(
|
||
width: 300.0,
|
||
child: TextField(textAlign: TextAlign.center, decoration: null),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final RenderEditable editable = findRenderEditable(tester);
|
||
|
||
await tester.enterText(find.byType(TextField), 'abcd');
|
||
await tester.pump();
|
||
|
||
Offset topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 4)).topLeft,
|
||
);
|
||
expect(topLeft.dx, equals(431));
|
||
|
||
topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 3)).topLeft,
|
||
);
|
||
expect(topLeft.dx, equals(415));
|
||
|
||
topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
|
||
);
|
||
expect(topLeft.dx, equals(399));
|
||
|
||
topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 1)).topLeft,
|
||
);
|
||
expect(topLeft.dx, equals(383));
|
||
});
|
||
|
||
testWidgets('Caret indexes into trailing whitespace center align', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: Theme(
|
||
data: ThemeData(useMaterial3: false),
|
||
child: const SizedBox(
|
||
width: 300.0,
|
||
child: TextField(textAlign: TextAlign.center, decoration: null),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final RenderEditable editable = findRenderEditable(tester);
|
||
|
||
await tester.enterText(find.byType(TextField), 'abcd ');
|
||
await tester.pump();
|
||
|
||
Offset topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 7)).topLeft,
|
||
);
|
||
expect(topLeft.dx, equals(479));
|
||
|
||
topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 8)).topLeft,
|
||
);
|
||
expect(topLeft.dx, equals(495));
|
||
|
||
topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 4)).topLeft,
|
||
);
|
||
expect(topLeft.dx, equals(431));
|
||
|
||
topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 3)).topLeft,
|
||
);
|
||
expect(topLeft.dx, equals(415)); // Should be same as equivalent in 'Caret center position'
|
||
|
||
topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
|
||
);
|
||
expect(topLeft.dx, equals(399)); // Should be same as equivalent in 'Caret center position'
|
||
|
||
topLeft = editable.localToGlobal(
|
||
editable.getLocalRectForCaret(const TextPosition(offset: 1)).topLeft,
|
||
);
|
||
expect(topLeft.dx, equals(383)); // Should be same as equivalent in 'Caret center position'
|
||
});
|
||
|
||
testWidgets('selection handles are rendered and not faded away', (WidgetTester tester) async {
|
||
const testText = 'lorem ipsum';
|
||
final TextEditingController controller = _textEditingController(text: testText);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
final RenderEditable renderEditable = state.renderEditable;
|
||
|
||
await tester.tapAt(const Offset(20, 10));
|
||
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
|
||
await tester.pumpAndSettle();
|
||
|
||
final List<FadeTransition> transitions = find
|
||
.descendant(
|
||
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
|
||
matching: find.byType(FadeTransition),
|
||
)
|
||
.evaluate()
|
||
.map((Element e) => e.widget)
|
||
.cast<FadeTransition>()
|
||
.toList();
|
||
expect(transitions.length, 2);
|
||
final FadeTransition left = transitions[0];
|
||
final FadeTransition right = transitions[1];
|
||
expect(left.opacity.value, equals(1.0));
|
||
expect(right.opacity.value, equals(1.0));
|
||
});
|
||
|
||
testWidgets(
|
||
'iOS selection handles are rendered and not faded away',
|
||
(WidgetTester tester) async {
|
||
const testText = 'lorem ipsum';
|
||
final TextEditingController controller = _textEditingController(text: testText);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
final RenderEditable renderEditable = tester
|
||
.state<EditableTextState>(find.byType(EditableText))
|
||
.renderEditable;
|
||
|
||
await tester.tapAt(const Offset(20, 10));
|
||
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
|
||
await tester.pumpAndSettle();
|
||
|
||
final List<FadeTransition> transitions = find
|
||
.byType(FadeTransition)
|
||
.evaluate()
|
||
.map((Element e) => e.widget)
|
||
.cast<FadeTransition>()
|
||
.toList();
|
||
expect(transitions.length, 2);
|
||
final FadeTransition left = transitions[0];
|
||
final FadeTransition right = transitions[1];
|
||
|
||
expect(left.opacity.value, equals(1.0));
|
||
expect(right.opacity.value, equals(1.0));
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'iPad Scribble selection change shows selection handles',
|
||
(WidgetTester tester) async {
|
||
const testText = 'lorem ipsum';
|
||
final TextEditingController controller = _textEditingController(text: testText);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
await tester.showKeyboard(find.byType(EditableText));
|
||
await tester.testTextInput.startScribbleInteraction();
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(
|
||
text: testText,
|
||
selection: TextSelection(baseOffset: 2, extentOffset: 7),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
|
||
final List<FadeTransition> transitions = find
|
||
.byType(FadeTransition)
|
||
.evaluate()
|
||
.map((Element e) => e.widget)
|
||
.cast<FadeTransition>()
|
||
.toList();
|
||
expect(transitions.length, 2);
|
||
final FadeTransition left = transitions[0];
|
||
final FadeTransition right = transitions[1];
|
||
|
||
expect(left.opacity.value, equals(1.0));
|
||
expect(right.opacity.value, equals(1.0));
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
|
||
);
|
||
|
||
testWidgets('Tap shows handles but not toolbar', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'abc def ghi');
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Tap to trigger the text field.
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump();
|
||
|
||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
|
||
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
|
||
});
|
||
|
||
testWidgets('Tap in empty text field does not show handles nor toolbar', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Tap to trigger the text field.
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump();
|
||
|
||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
|
||
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
|
||
});
|
||
|
||
testWidgets('Long press shows handles and toolbar', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'abc def ghi');
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Long press to trigger the text field.
|
||
await tester.longPress(find.byType(TextField));
|
||
await tester.pump();
|
||
|
||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
|
||
expect(
|
||
editableText.selectionOverlay!.toolbarIsVisible,
|
||
isContextMenuProvidedByPlatform ? isFalse : isTrue,
|
||
);
|
||
});
|
||
|
||
testWidgets('Long press in empty text field shows handles and toolbar', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Tap to trigger the text field.
|
||
await tester.longPress(find.byType(TextField));
|
||
await tester.pump();
|
||
|
||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
|
||
expect(
|
||
editableText.selectionOverlay!.toolbarIsVisible,
|
||
isContextMenuProvidedByPlatform ? isFalse : isTrue,
|
||
);
|
||
});
|
||
|
||
testWidgets('Double tap shows handles and toolbar', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'abc def ghi');
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Double tap to trigger the text field.
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump();
|
||
|
||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
|
||
expect(
|
||
editableText.selectionOverlay!.toolbarIsVisible,
|
||
isContextMenuProvidedByPlatform ? isFalse : isTrue,
|
||
);
|
||
});
|
||
|
||
testWidgets('Double tap in empty text field shows toolbar but not handles', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Double tap to trigger the text field.
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump();
|
||
|
||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
|
||
expect(
|
||
editableText.selectionOverlay!.toolbarIsVisible,
|
||
isContextMenuProvidedByPlatform ? isFalse : isTrue,
|
||
);
|
||
});
|
||
|
||
testWidgets('Mouse tap does not show handles nor toolbar', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'abc def ghi');
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Long press to trigger the text field.
|
||
final Offset textFieldPos = tester.getCenter(find.byType(TextField));
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textFieldPos,
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
|
||
});
|
||
|
||
testWidgets('Mouse long press does not show handles nor toolbar', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'abc def ghi');
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Long press to trigger the text field.
|
||
final Offset textFieldPos = tester.getCenter(find.byType(TextField));
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textFieldPos,
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump(const Duration(seconds: 2));
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
|
||
});
|
||
|
||
testWidgets('Mouse double tap does not show handles nor toolbar', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'abc def ghi');
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Double tap to trigger the text field.
|
||
final Offset textFieldPos = tester.getCenter(find.byType(TextField));
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textFieldPos,
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await gesture.up();
|
||
await tester.pump();
|
||
await gesture.down(textFieldPos);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
|
||
});
|
||
|
||
testWidgets('Does not show handles when updated from the web engine', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller = _textEditingController(text: 'abc def ghi');
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Interact with the text field to establish the input connection.
|
||
final Offset topLeft = tester.getTopLeft(find.byType(EditableText));
|
||
final TestGesture gesture = await tester.startGesture(
|
||
topLeft + const Offset(0.0, 5.0),
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
final EditableTextState state = tester.state(find.byType(EditableText));
|
||
expect(state.selectionOverlay!.handlesAreVisible, isFalse);
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 0));
|
||
|
||
if (kIsWeb) {
|
||
tester.testTextInput.updateEditingValue(
|
||
const TextEditingValue(
|
||
text: 'abc def ghi',
|
||
selection: TextSelection(baseOffset: 2, extentOffset: 7),
|
||
),
|
||
);
|
||
// Wait for all the `setState` calls to be flushed.
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
state.currentTextEditingValue.selection,
|
||
const TextSelection(baseOffset: 2, extentOffset: 7),
|
||
);
|
||
expect(state.selectionOverlay!.handlesAreVisible, isFalse);
|
||
}
|
||
});
|
||
|
||
testWidgets('Tapping selection handles toggles the toolbar', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'abc def ghi');
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Tap to position the cursor and show the selection handles.
|
||
final Offset ePos = textOffsetToPosition(tester, 5); // Index of 'e'.
|
||
await tester.tapAt(ePos, pointer: 7);
|
||
await tester.pumpAndSettle();
|
||
|
||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
|
||
expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(controller.selection),
|
||
renderEditable,
|
||
);
|
||
expect(endpoints.length, 1);
|
||
|
||
// Tap the handle to show the toolbar.
|
||
final Offset handlePos = endpoints[0].point + const Offset(0.0, 1.0);
|
||
await tester.tapAt(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
expect(
|
||
editableText.selectionOverlay!.toolbarIsVisible,
|
||
isContextMenuProvidedByPlatform ? isFalse : isTrue,
|
||
);
|
||
|
||
// Tap the handle again to hide the toolbar.
|
||
await tester.tapAt(handlePos, pointer: 7);
|
||
await tester.pump();
|
||
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
|
||
});
|
||
|
||
testWidgets(
|
||
'when TextField would be blocked by keyboard, it is shown with enough space for the selection handle',
|
||
(WidgetTester tester) async {
|
||
final scrollController = ScrollController();
|
||
addTearDown(scrollController.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: ListView(
|
||
controller: scrollController,
|
||
children: <Widget>[
|
||
Container(height: 579), // Push field almost off screen.
|
||
const TextField(),
|
||
Container(height: 1000),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Tap the TextField to put the cursor into it and bring it into view.
|
||
expect(scrollController.offset, 0.0);
|
||
await tester.tapAt(tester.getTopLeft(find.byType(TextField)));
|
||
await tester.pumpAndSettle();
|
||
|
||
// The ListView has scrolled to keep the TextField and cursor handle
|
||
// visible.
|
||
expect(scrollController.offset, 50.0);
|
||
},
|
||
);
|
||
|
||
// Regression test for https://github.com/flutter/flutter/issues/74566
|
||
testWidgets(
|
||
'TextField and last input character are visible on the screen when the cursor is not shown',
|
||
(WidgetTester tester) async {
|
||
final scrollController = ScrollController();
|
||
final textFieldScrollController = ScrollController();
|
||
addTearDown(() {
|
||
scrollController.dispose();
|
||
textFieldScrollController.dispose();
|
||
});
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: ListView(
|
||
controller: scrollController,
|
||
children: <Widget>[
|
||
Container(height: 579), // Push field almost off screen.
|
||
TextField(scrollController: textFieldScrollController, showCursor: false),
|
||
Container(height: 1000),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Tap the TextField to bring it into view.
|
||
expect(scrollController.offset, 0.0);
|
||
await tester.tapAt(tester.getTopLeft(find.byType(TextField)));
|
||
await tester.pumpAndSettle();
|
||
|
||
// The ListView has scrolled to keep the TextField visible.
|
||
expect(scrollController.offset, 50.0);
|
||
expect(textFieldScrollController.offset, 0.0);
|
||
|
||
// After entering some long text, the last input character remains on the screen.
|
||
final String testValue = 'I love Flutter!' * 10;
|
||
tester.testTextInput.updateEditingValue(
|
||
TextEditingValue(
|
||
text: testValue,
|
||
selection: TextSelection.collapsed(offset: testValue.length),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
await tester.pumpAndSettle(); // Text scroll animation.
|
||
|
||
expect(textFieldScrollController.offset, 1602.0);
|
||
},
|
||
);
|
||
|
||
group('height', () {
|
||
testWidgets('By default, TextField is at least kMinInteractiveDimension high', (
|
||
WidgetTester tester,
|
||
) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(),
|
||
home: const Scaffold(body: Center(child: TextField())),
|
||
),
|
||
);
|
||
|
||
final RenderBox renderBox = tester.renderObject(find.byType(TextField));
|
||
expect(renderBox.size.height, greaterThanOrEqualTo(kMinInteractiveDimension));
|
||
});
|
||
|
||
testWidgets(
|
||
"When text is very small, TextField still doesn't go below kMinInteractiveDimension height",
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(),
|
||
home: const Scaffold(
|
||
body: Center(child: TextField(style: TextStyle(fontSize: 2.0))),
|
||
),
|
||
),
|
||
);
|
||
|
||
final RenderBox renderBox = tester.renderObject(find.byType(TextField));
|
||
expect(renderBox.size.height, kMinInteractiveDimension);
|
||
},
|
||
);
|
||
|
||
testWidgets('When isDense, TextField can go below kMinInteractiveDimension height', (
|
||
WidgetTester tester,
|
||
) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(),
|
||
home: const Scaffold(
|
||
body: Center(child: TextField(decoration: InputDecoration(isDense: true))),
|
||
),
|
||
),
|
||
);
|
||
|
||
final RenderBox renderBox = tester.renderObject(find.byType(TextField));
|
||
expect(renderBox.size.height, lessThan(kMinInteractiveDimension));
|
||
});
|
||
|
||
group('intrinsics', () {
|
||
Widget buildTest({required bool isDense}) {
|
||
return MaterialApp(
|
||
home: Scaffold(
|
||
body: CustomScrollView(
|
||
slivers: <Widget>[
|
||
SliverFillRemaining(
|
||
hasScrollBody: false,
|
||
child: Column(
|
||
children: <Widget>[
|
||
TextField(decoration: InputDecoration(isDense: isDense)),
|
||
Container(height: 1000),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
testWidgets('By default, intrinsic height is at least kMinInteractiveDimension high', (
|
||
WidgetTester tester,
|
||
) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/54729
|
||
// If the intrinsic height does not match that of the height after
|
||
// performLayout, this will fail.
|
||
await tester.pumpWidget(buildTest(isDense: false));
|
||
});
|
||
|
||
testWidgets('When isDense, intrinsic height can go below kMinInteractiveDimension height', (
|
||
WidgetTester tester,
|
||
) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/54729
|
||
// If the intrinsic height does not match that of the height after
|
||
// performLayout, this will fail.
|
||
await tester.pumpWidget(buildTest(isDense: true));
|
||
});
|
||
});
|
||
});
|
||
testWidgets("Arrow keys don't move input focus", (WidgetTester tester) async {
|
||
final TextEditingController controller1 = _textEditingController();
|
||
final TextEditingController controller2 = _textEditingController();
|
||
final TextEditingController controller3 = _textEditingController();
|
||
final TextEditingController controller4 = _textEditingController();
|
||
final TextEditingController controller5 = _textEditingController();
|
||
final focusNode1 = FocusNode(debugLabel: 'Field 1');
|
||
final focusNode2 = FocusNode(debugLabel: 'Field 2');
|
||
final focusNode3 = FocusNode(debugLabel: 'Field 3');
|
||
final focusNode4 = FocusNode(debugLabel: 'Field 4');
|
||
final focusNode5 = FocusNode(debugLabel: 'Field 5');
|
||
|
||
addTearDown(() {
|
||
focusNode1.dispose();
|
||
focusNode2.dispose();
|
||
focusNode3.dispose();
|
||
focusNode4.dispose();
|
||
focusNode5.dispose();
|
||
});
|
||
|
||
// Lay out text fields in a "+" formation, and focus the center one.
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(),
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: <Widget>[
|
||
SizedBox(
|
||
width: 100.0,
|
||
child: TextField(controller: controller1, focusNode: focusNode1),
|
||
),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: <Widget>[
|
||
SizedBox(
|
||
width: 100.0,
|
||
child: TextField(controller: controller2, focusNode: focusNode2),
|
||
),
|
||
SizedBox(
|
||
width: 100.0,
|
||
child: TextField(controller: controller3, focusNode: focusNode3),
|
||
),
|
||
SizedBox(
|
||
width: 100.0,
|
||
child: TextField(controller: controller4, focusNode: focusNode4),
|
||
),
|
||
],
|
||
),
|
||
SizedBox(
|
||
width: 100.0,
|
||
child: TextField(controller: controller5, focusNode: focusNode5),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
focusNode3.requestFocus();
|
||
await tester.pump();
|
||
expect(focusNode3.hasPrimaryFocus, isTrue);
|
||
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||
await tester.pump();
|
||
expect(focusNode3.hasPrimaryFocus, isTrue);
|
||
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||
await tester.pump();
|
||
expect(focusNode3.hasPrimaryFocus, isTrue);
|
||
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||
await tester.pump();
|
||
expect(focusNode3.hasPrimaryFocus, isTrue);
|
||
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||
await tester.pump();
|
||
expect(focusNode3.hasPrimaryFocus, isTrue);
|
||
});
|
||
|
||
testWidgets('Scrolling shortcuts are disabled in text fields', (WidgetTester tester) async {
|
||
var scrollInvoked = false;
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Actions(
|
||
actions: <Type, Action<Intent>>{
|
||
ScrollIntent: CallbackAction<ScrollIntent>(
|
||
onInvoke: (Intent intent) {
|
||
scrollInvoked = true;
|
||
return null;
|
||
},
|
||
),
|
||
},
|
||
child: Material(
|
||
child: ListView(
|
||
children: const <Widget>[
|
||
Padding(padding: EdgeInsets.symmetric(vertical: 200)),
|
||
TextField(),
|
||
Padding(padding: EdgeInsets.symmetric(vertical: 800)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
expect(scrollInvoked, isFalse);
|
||
|
||
// Set focus on the text field.
|
||
await tester.tapAt(tester.getTopLeft(find.byType(TextField)));
|
||
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||
expect(scrollInvoked, isFalse);
|
||
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||
expect(scrollInvoked, isFalse);
|
||
});
|
||
|
||
testWidgets("A buildCounter that returns null doesn't affect the size of the TextField", (
|
||
WidgetTester tester,
|
||
) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/44909
|
||
|
||
final GlobalKey textField1Key = GlobalKey();
|
||
final GlobalKey textField2Key = GlobalKey();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Column(
|
||
children: <Widget>[
|
||
TextField(key: textField1Key),
|
||
TextField(
|
||
key: textField2Key,
|
||
maxLength: 1,
|
||
buildCounter:
|
||
(
|
||
BuildContext context, {
|
||
required int currentLength,
|
||
required bool isFocused,
|
||
int? maxLength,
|
||
}) => null,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.pumpAndSettle();
|
||
final Size textFieldSize1 = tester.getSize(find.byKey(textField1Key));
|
||
final Size textFieldSize2 = tester.getSize(find.byKey(textField2Key));
|
||
|
||
expect(textFieldSize1, equals(textFieldSize2));
|
||
});
|
||
|
||
testWidgets('The selection menu displays in an Overlay without error', (
|
||
WidgetTester tester,
|
||
) async {
|
||
// This is a regression test for
|
||
// https://github.com/flutter/flutter/issues/43787
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'This is a test that shows some odd behavior with Text Selection!',
|
||
);
|
||
|
||
late final OverlayEntry overlayEntry;
|
||
addTearDown(
|
||
() => overlayEntry
|
||
..remove()
|
||
..dispose(),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: ColoredBox(
|
||
color: Colors.grey,
|
||
child: Center(
|
||
child: Container(
|
||
color: Colors.red,
|
||
width: 300,
|
||
height: 600,
|
||
child: Overlay(
|
||
initialEntries: <OverlayEntry>[
|
||
overlayEntry = OverlayEntry(
|
||
builder: (BuildContext context) =>
|
||
Center(child: TextField(controller: controller)),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await showSelectionMenuAt(tester, controller, controller.text.indexOf('test'));
|
||
await tester.pumpAndSettle();
|
||
expect(tester.takeException(), isNull);
|
||
});
|
||
|
||
testWidgets(
|
||
'clipboard status is checked via hasStrings without getting the full clipboard contents',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
var calledGetData = false;
|
||
var calledHasStrings = false;
|
||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (
|
||
MethodCall methodCall,
|
||
) async {
|
||
switch (methodCall.method) {
|
||
case 'Clipboard.getData':
|
||
calledGetData = true;
|
||
case 'Clipboard.hasStrings':
|
||
calledHasStrings = true;
|
||
default:
|
||
break;
|
||
}
|
||
return null;
|
||
});
|
||
|
||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||
|
||
// Double tap like when showing the text selection menu on Android/iOS.
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||
await tester.pump();
|
||
|
||
// getData is not called unless something is pasted. hasStrings is used to
|
||
// check the status of the clipboard.
|
||
expect(calledGetData, false);
|
||
// hasStrings is checked in order to decide if the content can be pasted.
|
||
expect(calledHasStrings, true);
|
||
},
|
||
skip: kIsWeb, // [intended] web doesn't call hasStrings.
|
||
);
|
||
|
||
testWidgets('TextField changes mouse cursor when hovered', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(
|
||
child: MouseRegion(
|
||
cursor: SystemMouseCursors.forbidden,
|
||
child: TextField(
|
||
mouseCursor: SystemMouseCursors.grab,
|
||
decoration: InputDecoration(
|
||
// Add an icon so that the left edge is not the text area
|
||
icon: Icon(Icons.person),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Center, which is within the text area
|
||
final Offset center = tester.getCenter(find.byType(TextField));
|
||
// Top left, which is not the text area
|
||
final Offset edge = tester.getTopLeft(find.byType(TextField)) + const Offset(1, 1);
|
||
|
||
final TestGesture gesture = await tester.createGesture(
|
||
kind: PointerDeviceKind.mouse,
|
||
pointer: 1,
|
||
);
|
||
await gesture.addPointer(location: center);
|
||
|
||
await tester.pump();
|
||
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
SystemMouseCursors.grab,
|
||
);
|
||
|
||
// Test default cursor
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(
|
||
child: MouseRegion(
|
||
cursor: SystemMouseCursors.forbidden,
|
||
child: TextField(decoration: InputDecoration(icon: Icon(Icons.person))),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
SystemMouseCursors.text,
|
||
);
|
||
await gesture.moveTo(edge);
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
SystemMouseCursors.text,
|
||
);
|
||
await gesture.moveTo(center);
|
||
|
||
// Test default cursor when disabled
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(
|
||
child: MouseRegion(
|
||
cursor: SystemMouseCursors.forbidden,
|
||
child: TextField(enabled: false, decoration: InputDecoration(icon: Icon(Icons.person))),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
SystemMouseCursors.basic,
|
||
);
|
||
await gesture.moveTo(edge);
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
SystemMouseCursors.basic,
|
||
);
|
||
await gesture.moveTo(center);
|
||
});
|
||
|
||
testWidgets('TextField icons change mouse cursor when hovered', (WidgetTester tester) async {
|
||
// Test default cursor in icons area.
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(
|
||
child: MouseRegion(
|
||
cursor: SystemMouseCursors.forbidden,
|
||
child: TextField(
|
||
decoration: InputDecoration(
|
||
icon: Icon(Icons.label),
|
||
prefixIcon: Icon(Icons.cabin),
|
||
suffixIcon: Icon(Icons.person),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Center, which is within the text area
|
||
final Offset center = tester.getCenter(find.byType(TextField));
|
||
// The Icon area
|
||
final Offset iconArea = tester.getCenter(find.byIcon(Icons.label));
|
||
// The prefix Icon area
|
||
final Offset prefixIconArea = tester.getCenter(find.byIcon(Icons.cabin));
|
||
// The suffix Icon area
|
||
final Offset suffixIconArea = tester.getCenter(find.byIcon(Icons.person));
|
||
|
||
final TestGesture gesture = await tester.createGesture(
|
||
kind: PointerDeviceKind.mouse,
|
||
pointer: 1,
|
||
);
|
||
await gesture.addPointer(location: center);
|
||
|
||
await tester.pump();
|
||
|
||
await gesture.moveTo(center);
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
SystemMouseCursors.text,
|
||
);
|
||
|
||
await gesture.moveTo(iconArea);
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
SystemMouseCursors.basic,
|
||
);
|
||
|
||
await gesture.moveTo(prefixIconArea);
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
SystemMouseCursors.basic,
|
||
);
|
||
|
||
await gesture.moveTo(suffixIconArea);
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
SystemMouseCursors.basic,
|
||
);
|
||
await gesture.moveTo(center);
|
||
|
||
// Test click cursor in icons area for buttons.
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: MouseRegion(
|
||
cursor: SystemMouseCursors.forbidden,
|
||
child: TextField(
|
||
decoration: InputDecoration(
|
||
icon: IconButton(icon: const Icon(Icons.label), onPressed: () {}),
|
||
prefixIcon: IconButton(icon: const Icon(Icons.cabin), onPressed: () {}),
|
||
suffixIcon: IconButton(icon: const Icon(Icons.person), onPressed: () {}),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.pump();
|
||
|
||
await gesture.moveTo(center);
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
SystemMouseCursors.text,
|
||
);
|
||
|
||
await gesture.moveTo(iconArea);
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
|
||
);
|
||
|
||
await gesture.moveTo(prefixIconArea);
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
|
||
);
|
||
|
||
await gesture.moveTo(suffixIconArea);
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
|
||
);
|
||
});
|
||
|
||
testWidgets(
|
||
'Text selection menu does not change mouse cursor when hovered',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: MouseRegion(
|
||
cursor: SystemMouseCursors.forbidden,
|
||
child: TextField(controller: controller),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Copy'), findsNothing);
|
||
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, 3),
|
||
kind: PointerDeviceKind.mouse,
|
||
buttons: kSecondaryMouseButton,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
SystemMouseCursors.text,
|
||
);
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
|
||
await gesture.moveTo(tester.getCenter(find.text('Paste')));
|
||
expect(
|
||
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||
SystemMouseCursors.basic,
|
||
);
|
||
},
|
||
variant: TargetPlatformVariant.desktop(),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets('Caret rtl with changing width', (WidgetTester tester) async {
|
||
late StateSetter setState;
|
||
var isWide = false;
|
||
const wideWidth = 300.0;
|
||
const narrowWidth = 200.0;
|
||
const style = TextStyle(fontSize: 10, height: 1.0, letterSpacing: 0.0, wordSpacing: 0.0);
|
||
const caretWidth = 2.0;
|
||
final TextEditingController controller = _textEditingController();
|
||
await tester.pumpWidget(
|
||
boilerplate(
|
||
child: StatefulBuilder(
|
||
builder: (BuildContext context, StateSetter setter) {
|
||
setState = setter;
|
||
return SizedBox(
|
||
width: isWide ? wideWidth : narrowWidth,
|
||
child: TextField(
|
||
key: textFieldKey,
|
||
controller: controller,
|
||
textDirection: TextDirection.rtl,
|
||
style: style,
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
|
||
// The cursor is on the right of the input because it's RTL.
|
||
RenderEditable editable = findRenderEditable(tester);
|
||
double cursorRight = editable
|
||
.getLocalRectForCaret(TextPosition(offset: controller.value.text.length))
|
||
.topRight
|
||
.dx;
|
||
double inputWidth = editable.size.width;
|
||
expect(inputWidth, narrowWidth);
|
||
expect(cursorRight, inputWidth - kCaretGap);
|
||
|
||
const text = '12345';
|
||
// After entering some text, the cursor is placed to the left of the text
|
||
// because the paragraph's writing direction is RTL.
|
||
await tester.enterText(find.byType(TextField), text);
|
||
await tester.pump();
|
||
editable = findRenderEditable(tester);
|
||
cursorRight = editable
|
||
.getLocalRectForCaret(TextPosition(offset: controller.value.text.length))
|
||
.topRight
|
||
.dx;
|
||
inputWidth = editable.size.width;
|
||
expect(cursorRight, inputWidth - kCaretGap - text.length * 10 - caretWidth);
|
||
|
||
// Since increasing the width of the input moves its right edge further to
|
||
// the right, the cursor has followed this change and still appears on the
|
||
// right of the input.
|
||
setState(() {
|
||
isWide = true;
|
||
});
|
||
await tester.pump();
|
||
editable = findRenderEditable(tester);
|
||
cursorRight = editable
|
||
.getLocalRectForCaret(TextPosition(offset: controller.value.text.length))
|
||
.topRight
|
||
.dx;
|
||
inputWidth = editable.size.width;
|
||
expect(inputWidth, wideWidth);
|
||
expect(cursorRight, inputWidth - kCaretGap - text.length * 10 - caretWidth);
|
||
});
|
||
|
||
testWidgets(
|
||
'Text selection menu hides after select all on desktop',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
final selectAll = defaultTargetPlatform == TargetPlatform.macOS ? 'Select All' : 'Select all';
|
||
|
||
expect(find.text(selectAll), findsNothing);
|
||
expect(find.text('Copy'), findsNothing);
|
||
|
||
final TestGesture gesture = await tester.startGesture(
|
||
const Offset(10.0, 0.0) + textOffsetToPosition(tester, controller.text.length),
|
||
kind: PointerDeviceKind.mouse,
|
||
buttons: kSecondaryMouseButton,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(
|
||
controller.value.selection,
|
||
TextSelection.collapsed(offset: controller.text.length, affinity: TextAffinity.upstream),
|
||
);
|
||
expect(find.text(selectAll), findsOneWidget);
|
||
|
||
await tester.tapAt(tester.getCenter(find.text(selectAll)));
|
||
|
||
await tester.pump();
|
||
expect(find.text(selectAll), findsNothing);
|
||
expect(find.text('Copy'), findsNothing);
|
||
},
|
||
// All desktop platforms except MacOS, which has no select all button.
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.linux,
|
||
TargetPlatform.windows,
|
||
}),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
// Regressing test for https://github.com/flutter/flutter/issues/70625
|
||
testWidgets('TextFields can inherit [FloatingLabelBehaviour] from input decoration theme', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
Widget textFieldBuilder({FloatingLabelBehavior behavior = FloatingLabelBehavior.auto}) {
|
||
return MaterialApp(
|
||
theme: ThemeData(
|
||
useMaterial3: false,
|
||
inputDecorationTheme: InputDecorationThemeData(floatingLabelBehavior: behavior),
|
||
),
|
||
home: Scaffold(
|
||
body: TextField(
|
||
focusNode: focusNode,
|
||
decoration: const InputDecoration(labelText: 'Label'),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(textFieldBuilder());
|
||
// The label will be positioned within the content when unfocused.
|
||
expect(tester.getTopLeft(find.text('Label')).dy, 20.0);
|
||
|
||
focusNode.requestFocus();
|
||
await tester.pumpAndSettle(); // label animation.
|
||
// The label will float above the content when focused.
|
||
expect(tester.getTopLeft(find.text('Label')).dy, 12.0);
|
||
|
||
focusNode.unfocus();
|
||
await tester.pumpAndSettle(); // label animation.
|
||
|
||
await tester.pumpWidget(textFieldBuilder(behavior: FloatingLabelBehavior.never));
|
||
await tester.pumpAndSettle(); // theme animation.
|
||
// The label will be positioned within the content.
|
||
expect(tester.getTopLeft(find.text('Label')).dy, 20.0);
|
||
|
||
focusNode.requestFocus();
|
||
await tester.pumpAndSettle(); // label animation.
|
||
// The label will always be positioned within the content.
|
||
expect(tester.getTopLeft(find.text('Label')).dy, 20.0);
|
||
|
||
await tester.pumpWidget(textFieldBuilder(behavior: FloatingLabelBehavior.always));
|
||
await tester.pumpAndSettle(); // theme animation.
|
||
// The label will always float above the content.
|
||
expect(tester.getTopLeft(find.text('Label')).dy, 12.0);
|
||
|
||
focusNode.unfocus();
|
||
await tester.pumpAndSettle(); // label animation.
|
||
// The label will always float above the content.
|
||
expect(tester.getTopLeft(find.text('Label')).dy, 12.0);
|
||
});
|
||
|
||
// Regression test for https://github.com/flutter/flutter/issues/140607.
|
||
testWidgets('TextFields can inherit errorStyle color from InputDecorationThemeData', (
|
||
WidgetTester tester,
|
||
) async {
|
||
const decorationTheme = InputDecorationThemeData(errorStyle: TextStyle(color: Colors.green));
|
||
|
||
EditableTextState getEditableTextState() {
|
||
return tester.state<EditableTextState>(find.byType(EditableText));
|
||
}
|
||
|
||
// Global theme.
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(inputDecorationTheme: decorationTheme),
|
||
home: const Scaffold(
|
||
body: TextField(decoration: InputDecoration(errorText: 'error')),
|
||
),
|
||
),
|
||
);
|
||
expect(getEditableTextState().widget.cursorColor, Colors.green);
|
||
|
||
// Local theme.
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Scaffold(
|
||
body: InputDecorationTheme(
|
||
data: decorationTheme,
|
||
child: TextField(decoration: InputDecoration(errorText: 'error')),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
expect(getEditableTextState().widget.cursorColor, Colors.green);
|
||
});
|
||
|
||
testWidgets('TextField can inherit decoration from local InputDecorationThemeData', (
|
||
WidgetTester tester,
|
||
) async {
|
||
const decoration = InputDecoration(labelText: 'Label');
|
||
const decorationTheme = InputDecorationThemeData(errorStyle: TextStyle(color: Colors.green));
|
||
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Scaffold(
|
||
body: InputDecorationTheme(
|
||
data: decorationTheme,
|
||
child: TextField(decoration: decoration),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final InputDecorator decorator = tester.widget(find.byType(InputDecorator));
|
||
final InputDecoration expectedDecoration = decoration
|
||
.applyDefaults(decorationTheme)
|
||
.copyWith(enabled: true, hintMaxLines: 1);
|
||
expect(decorator.decoration, expectedDecoration);
|
||
});
|
||
|
||
group('MaxLengthEnforcement', () {
|
||
const maxLength = 5;
|
||
|
||
Future<void> setupWidget(WidgetTester tester, MaxLengthEnforcement? enforcement) async {
|
||
final Widget widget = MaterialApp(
|
||
home: Material(
|
||
child: TextField(maxLength: maxLength, maxLengthEnforcement: enforcement),
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(widget);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
|
||
testWidgets('using none enforcement.', (WidgetTester tester) async {
|
||
const MaxLengthEnforcement enforcement = MaxLengthEnforcement.none;
|
||
|
||
await setupWidget(tester, enforcement);
|
||
|
||
final EditableTextState state = tester.state(find.byType(EditableText));
|
||
|
||
state.updateEditingValue(const TextEditingValue(text: 'abc'));
|
||
expect(state.currentTextEditingValue.text, 'abc');
|
||
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
||
|
||
state.updateEditingValue(
|
||
const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6)),
|
||
);
|
||
expect(state.currentTextEditingValue.text, 'abcdef');
|
||
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6));
|
||
|
||
state.updateEditingValue(const TextEditingValue(text: 'abcdef'));
|
||
expect(state.currentTextEditingValue.text, 'abcdef');
|
||
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
||
});
|
||
|
||
testWidgets('using enforced.', (WidgetTester tester) async {
|
||
const MaxLengthEnforcement enforcement = MaxLengthEnforcement.enforced;
|
||
|
||
await setupWidget(tester, enforcement);
|
||
|
||
final EditableTextState state = tester.state(find.byType(EditableText));
|
||
|
||
state.updateEditingValue(const TextEditingValue(text: 'abc'));
|
||
expect(state.currentTextEditingValue.text, 'abc');
|
||
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
||
|
||
state.updateEditingValue(
|
||
const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5)),
|
||
);
|
||
expect(state.currentTextEditingValue.text, 'abcde');
|
||
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
|
||
|
||
state.updateEditingValue(
|
||
const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6)),
|
||
);
|
||
expect(state.currentTextEditingValue.text, 'abcde');
|
||
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
|
||
|
||
state.updateEditingValue(const TextEditingValue(text: 'abcdef'));
|
||
expect(state.currentTextEditingValue.text, 'abcde');
|
||
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
|
||
});
|
||
|
||
testWidgets('using truncateAfterCompositionEnds.', (WidgetTester tester) async {
|
||
const MaxLengthEnforcement enforcement = MaxLengthEnforcement.truncateAfterCompositionEnds;
|
||
|
||
await setupWidget(tester, enforcement);
|
||
|
||
final EditableTextState state = tester.state(find.byType(EditableText));
|
||
|
||
state.updateEditingValue(const TextEditingValue(text: 'abc'));
|
||
expect(state.currentTextEditingValue.text, 'abc');
|
||
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
||
|
||
state.updateEditingValue(
|
||
const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5)),
|
||
);
|
||
expect(state.currentTextEditingValue.text, 'abcde');
|
||
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
|
||
|
||
state.updateEditingValue(
|
||
const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6)),
|
||
);
|
||
expect(state.currentTextEditingValue.text, 'abcdef');
|
||
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6));
|
||
|
||
state.updateEditingValue(const TextEditingValue(text: 'abcdef'));
|
||
expect(state.currentTextEditingValue.text, 'abcde');
|
||
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
||
});
|
||
|
||
testWidgets('using default behavior for different platforms.', (WidgetTester tester) async {
|
||
await setupWidget(tester, null);
|
||
|
||
final EditableTextState state = tester.state(find.byType(EditableText));
|
||
|
||
state.updateEditingValue(const TextEditingValue(text: '侬好啊'));
|
||
expect(state.currentTextEditingValue.text, '侬好啊');
|
||
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
||
|
||
state.updateEditingValue(
|
||
const TextEditingValue(text: '侬好啊旁友', composing: TextRange(start: 3, end: 5)),
|
||
);
|
||
expect(state.currentTextEditingValue.text, '侬好啊旁友');
|
||
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
|
||
|
||
state.updateEditingValue(
|
||
const TextEditingValue(text: '侬好啊旁友们', composing: TextRange(start: 3, end: 6)),
|
||
);
|
||
if (kIsWeb ||
|
||
defaultTargetPlatform == TargetPlatform.iOS ||
|
||
defaultTargetPlatform == TargetPlatform.macOS ||
|
||
defaultTargetPlatform == TargetPlatform.linux ||
|
||
defaultTargetPlatform == TargetPlatform.fuchsia) {
|
||
expect(state.currentTextEditingValue.text, '侬好啊旁友们');
|
||
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6));
|
||
} else {
|
||
expect(state.currentTextEditingValue.text, '侬好啊旁友');
|
||
expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
|
||
}
|
||
|
||
state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友'));
|
||
expect(state.currentTextEditingValue.text, '侬好啊旁友');
|
||
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
||
});
|
||
});
|
||
|
||
testWidgets('TextField does not leak touch events when deadline has exceeded', (
|
||
WidgetTester tester,
|
||
) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/118340.
|
||
var textFieldTapCount = 0;
|
||
var prefixTapCount = 0;
|
||
var suffixTapCount = 0;
|
||
|
||
final FocusNode focusNode = _focusNode();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: TextField(
|
||
focusNode: focusNode,
|
||
onTap: () {
|
||
textFieldTapCount += 1;
|
||
},
|
||
decoration: InputDecoration(
|
||
labelText: 'Label',
|
||
prefix: ElevatedButton(
|
||
onPressed: () {
|
||
prefixTapCount += 1;
|
||
},
|
||
child: const Text('prefix'),
|
||
),
|
||
suffix: ElevatedButton(
|
||
onPressed: () {
|
||
suffixTapCount += 1;
|
||
},
|
||
child: const Text('suffix'),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Focus to show the prefix and suffix buttons.
|
||
focusNode.requestFocus();
|
||
await tester.pump();
|
||
|
||
TestGesture gesture = await tester.startGesture(
|
||
tester.getRect(find.text('prefix')).center,
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pumpAndSettle();
|
||
await gesture.up();
|
||
expect(textFieldTapCount, 0);
|
||
expect(prefixTapCount, 1);
|
||
expect(suffixTapCount, 0);
|
||
|
||
gesture = await tester.startGesture(
|
||
tester.getRect(find.text('suffix')).center,
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pumpAndSettle();
|
||
await gesture.up();
|
||
expect(textFieldTapCount, 0);
|
||
expect(prefixTapCount, 1);
|
||
expect(suffixTapCount, 1);
|
||
});
|
||
|
||
testWidgets('prefix/suffix buttons do not leak touch events', (WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/39376.
|
||
|
||
var textFieldTapCount = 0;
|
||
var prefixTapCount = 0;
|
||
var suffixTapCount = 0;
|
||
|
||
final FocusNode focusNode = _focusNode();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: TextField(
|
||
focusNode: focusNode,
|
||
onTap: () {
|
||
textFieldTapCount += 1;
|
||
},
|
||
decoration: InputDecoration(
|
||
labelText: 'Label',
|
||
prefix: ElevatedButton(
|
||
onPressed: () {
|
||
prefixTapCount += 1;
|
||
},
|
||
child: const Text('prefix'),
|
||
),
|
||
suffix: ElevatedButton(
|
||
onPressed: () {
|
||
suffixTapCount += 1;
|
||
},
|
||
child: const Text('suffix'),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Focus to show the prefix and suffix buttons.
|
||
focusNode.requestFocus();
|
||
await tester.pump();
|
||
|
||
await tester.tap(find.text('prefix'));
|
||
expect(textFieldTapCount, 0);
|
||
expect(prefixTapCount, 1);
|
||
expect(suffixTapCount, 0);
|
||
|
||
await tester.tap(find.text('suffix'));
|
||
expect(textFieldTapCount, 0);
|
||
expect(prefixTapCount, 1);
|
||
expect(suffixTapCount, 1);
|
||
});
|
||
|
||
testWidgets('autofill info has hint text', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(decoration: InputDecoration(hintText: 'placeholder text')),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byType(TextField));
|
||
|
||
expect(
|
||
tester.testTextInput.setClientArgs?['autofill'],
|
||
containsPair('hintText', 'placeholder text'),
|
||
);
|
||
});
|
||
|
||
testWidgets('TextField at rest does not push any layers with alwaysNeedsAddToScene', (
|
||
WidgetTester tester,
|
||
) async {
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(child: Center(child: TextField())),
|
||
),
|
||
);
|
||
|
||
expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse);
|
||
});
|
||
|
||
testWidgets('Focused TextField does not push any layers with alwaysNeedsAddToScene', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(focusNode: focusNode)),
|
||
),
|
||
),
|
||
);
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
|
||
expect(focusNode.hasFocus, isTrue);
|
||
expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse);
|
||
});
|
||
|
||
testWidgets(
|
||
'TextField does not push any layers with alwaysNeedsAddToScene after toolbar is dismissed',
|
||
(WidgetTester tester) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(focusNode: focusNode)),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
|
||
// Bring up the toolbar.
|
||
const testValue = 'A B C';
|
||
tester.testTextInput.updateEditingValue(const TextEditingValue(text: testValue));
|
||
await tester.pump();
|
||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
state.renderEditable.selectWordsInRange(from: Offset.zero, cause: SelectionChangedCause.tap);
|
||
expect(state.showToolbar(), true);
|
||
await tester.pumpAndSettle();
|
||
await tester.pump(const Duration(seconds: 1));
|
||
expect(find.text('Copy'), findsOneWidget); // Toolbar is visible
|
||
|
||
// Hide the toolbar
|
||
focusNode.unfocus();
|
||
await tester.pumpAndSettle();
|
||
await tester.pump(const Duration(seconds: 1));
|
||
expect(find.text('Copy'), findsNothing); // Toolbar is not visible
|
||
|
||
expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets('cursor blinking respects TickerMode', (WidgetTester tester) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
Widget builder({required bool tickerMode}) {
|
||
return MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TickerMode(
|
||
enabled: tickerMode,
|
||
child: TextField(focusNode: focusNode),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// TickerMode is on, cursor is blinking.
|
||
await tester.pumpWidget(builder(tickerMode: true));
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
final RenderEditable editable = state.renderEditable;
|
||
expect(editable.showCursor.value, isTrue);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isTrue);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
|
||
// TickerMode is off, cursor does not blink.
|
||
await tester.pumpWidget(builder(tickerMode: false));
|
||
expect(editable.showCursor.value, isFalse);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
|
||
// TickerMode is on, cursor blinks again.
|
||
await tester.pumpWidget(builder(tickerMode: true));
|
||
expect(editable.showCursor.value, isTrue);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isTrue);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
|
||
// Dismissing focus while tickerMode is off does not start cursor blinking
|
||
// when tickerMode is turned on again.
|
||
await tester.pumpWidget(builder(tickerMode: false));
|
||
focusNode.unfocus();
|
||
await tester.pump();
|
||
expect(editable.showCursor.value, isFalse);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
await tester.pumpWidget(builder(tickerMode: true));
|
||
expect(editable.showCursor.value, isFalse);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
|
||
// Focusing while tickerMode is off does not start cursor blinking...
|
||
await tester.pumpWidget(builder(tickerMode: false));
|
||
await tester.showKeyboard(find.byType(TextField));
|
||
expect(editable.showCursor.value, isFalse);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
|
||
// ... but it does start when tickerMode is switched on again.
|
||
await tester.pumpWidget(builder(tickerMode: true));
|
||
expect(editable.showCursor.value, isTrue);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isFalse);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(editable.showCursor.value, isTrue);
|
||
});
|
||
|
||
testWidgets(
|
||
'can shift + tap to select with a keyboard (Apple platforms)',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tapAt(textOffsetToPosition(tester, 13));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 13);
|
||
expect(controller.selection.extentOffset, 13);
|
||
|
||
await tester.pump(kDoubleTapTimeout);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.tapAt(textOffsetToPosition(tester, 20));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 13);
|
||
expect(controller.selection.extentOffset, 20);
|
||
|
||
await tester.pump(kDoubleTapTimeout);
|
||
await tester.tapAt(textOffsetToPosition(tester, 23));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 13);
|
||
expect(controller.selection.extentOffset, 23);
|
||
|
||
await tester.pump(kDoubleTapTimeout);
|
||
await tester.tapAt(textOffsetToPosition(tester, 4));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 4);
|
||
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 4);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'can shift + tap to select with a keyboard (non-Apple platforms)',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tapAt(textOffsetToPosition(tester, 13));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 13);
|
||
expect(controller.selection.extentOffset, 13);
|
||
|
||
await tester.pump(kDoubleTapTimeout);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.tapAt(textOffsetToPosition(tester, 20));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 13);
|
||
expect(controller.selection.extentOffset, 20);
|
||
|
||
await tester.pump(kDoubleTapTimeout);
|
||
await tester.tapAt(textOffsetToPosition(tester, 23));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 13);
|
||
expect(controller.selection.extentOffset, 23);
|
||
|
||
await tester.pump(kDoubleTapTimeout);
|
||
await tester.tapAt(textOffsetToPosition(tester, 4));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 13);
|
||
expect(controller.selection.extentOffset, 4);
|
||
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
expect(controller.selection.baseOffset, 13);
|
||
expect(controller.selection.extentOffset, 4);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
TargetPlatform.linux,
|
||
TargetPlatform.windows,
|
||
}),
|
||
);
|
||
|
||
testWidgets('shift tapping an unfocused field', (WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
final FocusNode focusNode = _focusNode();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(controller: controller, focusNode: focusNode),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
expect(focusNode.hasFocus, isFalse);
|
||
|
||
// Put the cursor at the end of the field.
|
||
await tester.tapAt(textOffsetToPosition(tester, controller.text.length));
|
||
await tester.pump(kDoubleTapTimeout);
|
||
await tester.pumpAndSettle();
|
||
expect(focusNode.hasFocus, isTrue);
|
||
expect(controller.selection.baseOffset, 35);
|
||
expect(controller.selection.extentOffset, 35);
|
||
|
||
// Unfocus the field, but the selection remains.
|
||
focusNode.unfocus();
|
||
await tester.pumpAndSettle();
|
||
expect(focusNode.hasFocus, isFalse);
|
||
expect(controller.selection.baseOffset, 35);
|
||
expect(controller.selection.extentOffset, 35);
|
||
|
||
// Shift tap in the middle of the field.
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
await tester.tapAt(textOffsetToPosition(tester, 20));
|
||
await tester.pumpAndSettle();
|
||
expect(focusNode.hasFocus, isTrue);
|
||
switch (defaultTargetPlatform) {
|
||
// Apple platforms start the selection from 0.
|
||
case TargetPlatform.iOS:
|
||
case TargetPlatform.macOS:
|
||
expect(controller.selection.baseOffset, 0);
|
||
|
||
// Other platforms start from the previous selection.
|
||
case TargetPlatform.android:
|
||
case TargetPlatform.fuchsia:
|
||
case TargetPlatform.linux:
|
||
case TargetPlatform.windows:
|
||
expect(controller.selection.baseOffset, 35);
|
||
}
|
||
expect(controller.selection.extentOffset, 20);
|
||
}, variant: TargetPlatformVariant.all());
|
||
|
||
testWidgets(
|
||
'can shift + tap + drag to select with a keyboard (Apple platforms)',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
final isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tapAt(textOffsetToPosition(tester, 8));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 8);
|
||
|
||
await tester.pump(kDoubleTapTimeout);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, 23),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
if (isTargetPlatformMobile) {
|
||
await gesture.up();
|
||
}
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 23);
|
||
|
||
// Expand the selection a bit.
|
||
if (isTargetPlatformMobile) {
|
||
await gesture.down(textOffsetToPosition(tester, 24));
|
||
}
|
||
await tester.pumpAndSettle();
|
||
await gesture.moveTo(textOffsetToPosition(tester, 28));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 28);
|
||
|
||
// Move back to the original selection.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 23));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 23);
|
||
|
||
// Collapse the selection.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 8));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 8);
|
||
|
||
// Invert the selection. The base jumps to the original extent.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 7));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 7);
|
||
|
||
// Continuing to move in the inverted direction expands the selection.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 4));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 4);
|
||
|
||
// Move back to the original base.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 8));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 8);
|
||
|
||
// Continue to move past the original base, which will cause the selection
|
||
// to invert back to the original orientation.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 9));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 9);
|
||
|
||
// Continuing to select in this direction selects just like it did
|
||
// originally.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 24));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 24);
|
||
|
||
// Releasing the shift key has no effect; the selection continues as the
|
||
// mouse continues to move.
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 24);
|
||
await gesture.moveTo(textOffsetToPosition(tester, 26));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 26);
|
||
|
||
await gesture.up();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 26);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'can shift + tap + drag to select with a keyboard (non-Apple platforms)',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
final bool isTargetPlatformMobile =
|
||
defaultTargetPlatform == TargetPlatform.android ||
|
||
defaultTargetPlatform == TargetPlatform.fuchsia;
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tapAt(textOffsetToPosition(tester, 8));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 8);
|
||
|
||
await tester.pump(kDoubleTapTimeout);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, 23),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pumpAndSettle();
|
||
if (isTargetPlatformMobile) {
|
||
await gesture.up();
|
||
// Not a double tap + drag.
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
}
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 23);
|
||
|
||
// Expand the selection a bit.
|
||
if (isTargetPlatformMobile) {
|
||
await gesture.down(textOffsetToPosition(tester, 23));
|
||
await tester.pumpAndSettle();
|
||
}
|
||
await gesture.moveTo(textOffsetToPosition(tester, 28));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 28);
|
||
|
||
// Move back to the original selection.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 23));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 23);
|
||
|
||
// Collapse the selection.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 8));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 8);
|
||
|
||
// Invert the selection. The original selection is not restored like on iOS
|
||
// and Mac.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 7));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 7);
|
||
|
||
// Continuing to move in the inverted direction expands the selection.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 4));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 4);
|
||
|
||
// Move back to the original base.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 8));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 8);
|
||
|
||
// Continue to move past the original base.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 9));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 9);
|
||
|
||
// Continuing to select in this direction selects just like it did
|
||
// originally.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 24));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 24);
|
||
|
||
// Releasing the shift key has no effect; the selection continues as the
|
||
// mouse continues to move.
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 24);
|
||
await gesture.moveTo(textOffsetToPosition(tester, 26));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 26);
|
||
|
||
await gesture.up();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 26);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.linux,
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
TargetPlatform.windows,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'can shift + tap + drag to select with a keyboard, reversed (Apple platforms)',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
final isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Make a selection from right to left.
|
||
await tester.tapAt(textOffsetToPosition(tester, 23));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 23);
|
||
await tester.pump(kDoubleTapTimeout);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, 8),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
if (isTargetPlatformMobile) {
|
||
await gesture.up();
|
||
}
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 8);
|
||
|
||
// Expand the selection a bit.
|
||
if (isTargetPlatformMobile) {
|
||
await gesture.down(textOffsetToPosition(tester, 7));
|
||
}
|
||
await gesture.moveTo(textOffsetToPosition(tester, 5));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 5);
|
||
|
||
// Move back to the original selection.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 8));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 8);
|
||
|
||
// Collapse the selection.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 23));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 23);
|
||
|
||
// Invert the selection. The base jumps to the original extent.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 24));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 24);
|
||
|
||
// Continuing to move in the inverted direction expands the selection.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 27));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 27);
|
||
|
||
// Move back to the original base.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 23));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 8);
|
||
expect(controller.selection.extentOffset, 23);
|
||
|
||
// Continue to move past the original base, which will cause the selection
|
||
// to invert back to the original orientation.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 22));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 22);
|
||
|
||
// Continuing to select in this direction selects just like it did
|
||
// originally.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 16));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 16);
|
||
|
||
// Releasing the shift key has no effect; the selection continues as the
|
||
// mouse continues to move.
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 16);
|
||
await gesture.moveTo(textOffsetToPosition(tester, 14));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 14);
|
||
|
||
await gesture.up();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 14);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'can shift + tap + drag to select with a keyboard, reversed (non-Apple platforms)',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(
|
||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||
);
|
||
final bool isTargetPlatformMobile =
|
||
defaultTargetPlatform == TargetPlatform.android ||
|
||
defaultTargetPlatform == TargetPlatform.fuchsia;
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(useMaterial3: false),
|
||
home: Material(
|
||
child: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Make a selection from right to left.
|
||
await tester.tapAt(textOffsetToPosition(tester, 23));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 23);
|
||
await tester.pump(kDoubleTapTimeout);
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, 8),
|
||
pointer: 7,
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pumpAndSettle();
|
||
if (isTargetPlatformMobile) {
|
||
await gesture.up();
|
||
// Not a double tap + drag.
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
}
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 8);
|
||
|
||
// Expand the selection a bit.
|
||
if (isTargetPlatformMobile) {
|
||
await gesture.down(textOffsetToPosition(tester, 8));
|
||
await tester.pumpAndSettle();
|
||
}
|
||
await gesture.moveTo(textOffsetToPosition(tester, 5));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 5);
|
||
|
||
// Move back to the original selection.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 8));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 8);
|
||
|
||
// Collapse the selection.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 23));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 23);
|
||
|
||
// Invert the selection. The selection is not restored like it would be on
|
||
// iOS and Mac.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 24));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 24);
|
||
|
||
// Continuing to move in the inverted direction expands the selection.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 27));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 27);
|
||
|
||
// Move back to the original base.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 23));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 23);
|
||
|
||
// Continue to move past the original base.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 22));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 22);
|
||
|
||
// Continuing to select in this direction selects just like it did
|
||
// originally.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 16));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 16);
|
||
|
||
// Releasing the shift key has no effect; the selection continues as the
|
||
// mouse continues to move.
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 16);
|
||
await gesture.moveTo(textOffsetToPosition(tester, 14));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 14);
|
||
|
||
await gesture.up();
|
||
expect(controller.selection.baseOffset, 23);
|
||
expect(controller.selection.extentOffset, 14);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.linux,
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
TargetPlatform.windows,
|
||
}),
|
||
);
|
||
|
||
// Regression test for https://github.com/flutter/flutter/issues/101587.
|
||
testWidgets(
|
||
'Right clicking menu behavior',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'blah1 blah2');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Initially, the menu is not shown and there is no selection.
|
||
expectNoCupertinoToolbar();
|
||
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
|
||
|
||
final Offset midBlah1 = textOffsetToPosition(tester, 2);
|
||
final Offset midBlah2 = textOffsetToPosition(tester, 8);
|
||
|
||
// Right click the second word.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
midBlah2,
|
||
kind: PointerDeviceKind.mouse,
|
||
buttons: kSecondaryMouseButton,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.iOS:
|
||
case TargetPlatform.macOS:
|
||
expect(controller.selection, const TextSelection(baseOffset: 6, extentOffset: 11));
|
||
expect(find.text('Cut'), findsOneWidget);
|
||
expect(find.text('Copy'), findsOneWidget);
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
|
||
case TargetPlatform.android:
|
||
case TargetPlatform.fuchsia:
|
||
case TargetPlatform.linux:
|
||
case TargetPlatform.windows:
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 8));
|
||
expect(find.text('Cut'), findsNothing);
|
||
expect(find.text('Copy'), findsNothing);
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
expect(find.text('Select all'), findsOneWidget);
|
||
}
|
||
|
||
// Right click the first word.
|
||
await gesture.down(midBlah1);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.iOS:
|
||
case TargetPlatform.macOS:
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
|
||
expect(find.text('Cut'), findsOneWidget);
|
||
expect(find.text('Copy'), findsOneWidget);
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
|
||
case TargetPlatform.android:
|
||
case TargetPlatform.fuchsia:
|
||
case TargetPlatform.linux:
|
||
case TargetPlatform.windows:
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 8));
|
||
expect(find.text('Cut'), findsNothing);
|
||
expect(find.text('Copy'), findsNothing);
|
||
expect(find.text('Paste'), findsNothing);
|
||
expect(find.text('Select all'), findsNothing);
|
||
}
|
||
},
|
||
variant: TargetPlatformVariant.all(),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Selection handles should not show when using a mouse on non-Apple platforms',
|
||
(WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/pull/168252.
|
||
final TextEditingController controller = _textEditingController(text: 'blah1 blah2');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Initially, the menu is not shown and there is no selection.
|
||
expectNoMaterialToolbar();
|
||
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
|
||
|
||
final Offset secondBlah = textOffsetToPosition(tester, 8);
|
||
|
||
// Right click the second word using a mouse.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
secondBlah,
|
||
kind: PointerDeviceKind.mouse,
|
||
buttons: kSecondaryMouseButton,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.iOS:
|
||
case TargetPlatform.macOS:
|
||
return;
|
||
case TargetPlatform.android:
|
||
case TargetPlatform.fuchsia:
|
||
case TargetPlatform.linux:
|
||
case TargetPlatform.windows:
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 8));
|
||
expect(find.text('Cut'), findsNothing);
|
||
expect(find.text('Copy'), findsNothing);
|
||
expect(find.text('Paste'), findsOneWidget);
|
||
expect(find.text('Select all'), findsOneWidget);
|
||
}
|
||
|
||
// Press select all.
|
||
await tester.tap(find.text('Select all'), kind: PointerDeviceKind.mouse);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 11));
|
||
|
||
// Selection handles are hidden.
|
||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
expect(state.selectionOverlay, isNotNull);
|
||
expect(state.selectionOverlay!.handlesAreVisible, isFalse);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.fuchsia,
|
||
TargetPlatform.linux,
|
||
TargetPlatform.windows,
|
||
}),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Selection handles should not show when using a mouse on Apple platforms using Flutter context menu',
|
||
(WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/pull/168252.
|
||
final TextEditingController controller = _textEditingController(text: 'blah1 blah2');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Initially, the menu is not shown and there is no selection.
|
||
expectNoCupertinoToolbar();
|
||
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
|
||
|
||
final Offset firstBlah = textOffsetToPosition(tester, 5);
|
||
|
||
// Click at the end of blah1.
|
||
await tester.tapAt(firstBlah, kind: PointerDeviceKind.mouse);
|
||
await tester.pumpAndSettle();
|
||
|
||
// Right click the same position to reveal the context menu.
|
||
await tester.tapAt(firstBlah, kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection.collapsed(offset: 5));
|
||
expectCupertinoToolbarForCollapsedSelection();
|
||
|
||
// Press select all.
|
||
await tester.tap(find.text('Select All'), kind: PointerDeviceKind.mouse);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 11));
|
||
|
||
// Selection handles are hidden.
|
||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
expect(state.selectionOverlay, isNotNull);
|
||
expect(state.selectionOverlay!.handlesAreVisible, isFalse);
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets('Cannot request focus when canRequestFocus is false', (WidgetTester tester) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
|
||
// Default test. The canRequestFocus is true by default and the text field can be focused
|
||
await tester.pumpWidget(boilerplate(child: TextField(focusNode: focusNode)));
|
||
expect(focusNode.hasFocus, isFalse);
|
||
focusNode.requestFocus();
|
||
await tester.pump();
|
||
expect(focusNode.hasFocus, isTrue);
|
||
|
||
// Set canRequestFocus to false: the text field cannot be focused when it is tapped/long pressed.
|
||
await tester.pumpWidget(
|
||
boilerplate(child: TextField(focusNode: focusNode, canRequestFocus: false)),
|
||
);
|
||
|
||
expect(focusNode.hasFocus, isFalse);
|
||
focusNode.requestFocus();
|
||
await tester.pump();
|
||
expect(focusNode.hasFocus, isFalse);
|
||
|
||
// The text field cannot be focused if it is tapped.
|
||
await tester.tap(find.byType(TextField));
|
||
await tester.pump();
|
||
expect(focusNode.hasFocus, isFalse);
|
||
|
||
// The text field cannot be focused if it is long pressed.
|
||
await tester.longPress(find.byType(TextField));
|
||
await tester.pump();
|
||
expect(focusNode.hasFocus, isFalse);
|
||
});
|
||
|
||
group('Right click focus', () {
|
||
testWidgets('Can right click to focus multiple times', (WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/pull/103228
|
||
final FocusNode focusNode1 = _focusNode();
|
||
final FocusNode focusNode2 = _focusNode();
|
||
final key1 = UniqueKey();
|
||
final key2 = UniqueKey();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Column(
|
||
children: <Widget>[
|
||
TextField(key: key1, focusNode: focusNode1),
|
||
const SizedBox(height: 100.0),
|
||
TextField(key: key2, focusNode: focusNode2),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Interact with the field to establish the input connection.
|
||
await tester.tapAt(tester.getCenter(find.byKey(key1)), buttons: kSecondaryButton);
|
||
await tester.pump();
|
||
|
||
expect(focusNode1.hasFocus, isTrue);
|
||
expect(focusNode2.hasFocus, isFalse);
|
||
|
||
await tester.tapAt(tester.getCenter(find.byKey(key2)), buttons: kSecondaryButton);
|
||
await tester.pump();
|
||
|
||
expect(focusNode1.hasFocus, isFalse);
|
||
expect(focusNode2.hasFocus, isTrue);
|
||
|
||
await tester.tapAt(tester.getCenter(find.byKey(key1)), buttons: kSecondaryButton);
|
||
await tester.pump();
|
||
|
||
expect(focusNode1.hasFocus, isTrue);
|
||
expect(focusNode2.hasFocus, isFalse);
|
||
});
|
||
|
||
testWidgets(
|
||
'Can right click to focus on previously selected word on Apple platforms',
|
||
(WidgetTester tester) async {
|
||
final FocusNode focusNode1 = _focusNode();
|
||
final FocusNode focusNode2 = _focusNode();
|
||
final TextEditingController controller = _textEditingController(text: 'first second');
|
||
final key1 = UniqueKey();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Column(
|
||
children: <Widget>[
|
||
TextField(key: key1, controller: controller, focusNode: focusNode1),
|
||
Focus(focusNode: focusNode2, child: const Text('focusable')),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Interact with the field to establish the input connection.
|
||
await tester.tapAt(tester.getCenter(find.byKey(key1)), buttons: kSecondaryButton);
|
||
await tester.pump();
|
||
|
||
expect(focusNode1.hasFocus, isTrue);
|
||
expect(focusNode2.hasFocus, isFalse);
|
||
|
||
// Select the second word.
|
||
controller.selection = const TextSelection(baseOffset: 6, extentOffset: 12);
|
||
await tester.pump();
|
||
|
||
expect(focusNode1.hasFocus, isTrue);
|
||
expect(focusNode2.hasFocus, isFalse);
|
||
expect(controller.selection.isCollapsed, isFalse);
|
||
expect(controller.selection.baseOffset, 6);
|
||
expect(controller.selection.extentOffset, 12);
|
||
|
||
// Unfocus the first field.
|
||
focusNode2.requestFocus();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(focusNode1.hasFocus, isFalse);
|
||
expect(focusNode2.hasFocus, isTrue);
|
||
|
||
// Right click the second word in the first field, which is still selected
|
||
// even though the selection is not visible.
|
||
await tester.tapAt(textOffsetToPosition(tester, 8), buttons: kSecondaryButton);
|
||
await tester.pump();
|
||
|
||
expect(focusNode1.hasFocus, isTrue);
|
||
expect(focusNode2.hasFocus, isFalse);
|
||
expect(controller.selection.baseOffset, 6);
|
||
expect(controller.selection.extentOffset, 12);
|
||
|
||
// Select everything.
|
||
controller.selection = const TextSelection(baseOffset: 0, extentOffset: 12);
|
||
await tester.pump();
|
||
|
||
expect(focusNode1.hasFocus, isTrue);
|
||
expect(focusNode2.hasFocus, isFalse);
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 12);
|
||
|
||
// Unfocus the first field.
|
||
focusNode2.requestFocus();
|
||
await tester.pumpAndSettle();
|
||
|
||
// Right click the first word in the first field.
|
||
await tester.tapAt(textOffsetToPosition(tester, 2), buttons: kSecondaryButton);
|
||
await tester.pump();
|
||
|
||
expect(focusNode1.hasFocus, isTrue);
|
||
expect(focusNode2.hasFocus, isFalse);
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 5);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.macOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets('Right clicking cannot request focus if canRequestFocus is false', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final FocusNode focusNode = _focusNode();
|
||
final key = UniqueKey();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Column(
|
||
children: <Widget>[TextField(key: key, focusNode: focusNode, canRequestFocus: false)],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tapAt(tester.getCenter(find.byKey(key)), buttons: kSecondaryButton);
|
||
await tester.pump();
|
||
|
||
expect(focusNode.hasFocus, isFalse);
|
||
});
|
||
});
|
||
|
||
group('context menu', () {
|
||
testWidgets(
|
||
'builds AdaptiveTextSelectionToolbar by default',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Column(children: <Widget>[TextField(controller: controller)]),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.pump(); // Wait for autofocus to take effect.
|
||
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||
|
||
// Long-press to bring up the context menu.
|
||
final Finder textFinder = find.byType(EditableText);
|
||
await tester.longPress(textFinder);
|
||
tester.state<EditableTextState>(textFinder).showToolbar();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||
},
|
||
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||
);
|
||
|
||
testWidgets(
|
||
'contextMenuBuilder is used in place of the default text selection toolbar',
|
||
(WidgetTester tester) async {
|
||
final GlobalKey key = GlobalKey();
|
||
final TextEditingController controller = _textEditingController();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Column(
|
||
children: <Widget>[
|
||
TextField(
|
||
controller: controller,
|
||
contextMenuBuilder:
|
||
(BuildContext context, EditableTextState editableTextState) {
|
||
return Placeholder(key: key);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.pump(); // Wait for autofocus to take effect.
|
||
|
||
expect(find.byKey(key), findsNothing);
|
||
|
||
// Long-press to bring up the context menu.
|
||
final Finder textFinder = find.byType(EditableText);
|
||
await tester.longPress(textFinder);
|
||
tester.state<EditableTextState>(textFinder).showToolbar();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byKey(key), findsOneWidget);
|
||
},
|
||
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||
);
|
||
|
||
testWidgets(
|
||
'contextMenuBuilder changes from default to null',
|
||
(WidgetTester tester) async {
|
||
final GlobalKey key = GlobalKey();
|
||
final TextEditingController controller = _textEditingController();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(key: key, controller: controller),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.pump(); // Wait for autofocus to take effect.
|
||
|
||
// Long-press to bring up the context menu.
|
||
final Finder textFinder = find.byType(EditableText);
|
||
await tester.longPress(textFinder);
|
||
tester.state<EditableTextState>(textFinder).showToolbar();
|
||
await tester.pump();
|
||
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||
|
||
// Set contextMenuBuilder to null.
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: TextField(key: key, controller: controller, contextMenuBuilder: null),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Trigger build one more time...
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Padding(
|
||
padding: EdgeInsets.zero,
|
||
child: TextField(key: key, controller: controller, contextMenuBuilder: null),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||
);
|
||
|
||
testWidgets(
|
||
'iOS uses the system context menu by default if supported',
|
||
(WidgetTester tester) async {
|
||
tester.platformDispatcher.supportsShowingSystemContextMenu = true;
|
||
addTearDown(() {
|
||
tester.platformDispatcher.resetSupportsShowingSystemContextMenu();
|
||
tester.view.reset();
|
||
});
|
||
|
||
await tester.pumpWidget(
|
||
// Don't wrap with the global View so that the change to
|
||
// platformDispatcher is read.
|
||
wrapWithView: false,
|
||
View(
|
||
view: tester.view,
|
||
child: MaterialApp(
|
||
home: Material(
|
||
child: TextField(controller: _textEditingController(text: 'one two three')),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// No context menu shown.
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||
expect(find.byType(SystemContextMenu), findsNothing);
|
||
|
||
// Double tap to select the first word and show the menu.
|
||
await tester.tapAt(textOffsetToPosition(tester, 1));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, 1));
|
||
await tester.pump(SelectionOverlay.fadeDuration);
|
||
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||
expect(find.byType(SystemContextMenu), findsOneWidget);
|
||
},
|
||
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
||
);
|
||
|
||
testWidgets(
|
||
'iOS system context menu does not hide selection handles on onSystemHide',
|
||
(WidgetTester tester) async {
|
||
tester.platformDispatcher.supportsShowingSystemContextMenu = true;
|
||
addTearDown(() {
|
||
tester.platformDispatcher.resetSupportsShowingSystemContextMenu();
|
||
tester.view.reset();
|
||
});
|
||
|
||
await tester.pumpWidget(
|
||
// Don't wrap with the global View so that the change to
|
||
// platformDispatcher is read.
|
||
wrapWithView: false,
|
||
View(
|
||
view: tester.view,
|
||
child: MaterialApp(
|
||
home: Material(
|
||
child: TextField(controller: _textEditingController(text: 'one two three')),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// No context menu shown.
|
||
expect(find.byType(SystemContextMenu), findsNothing);
|
||
|
||
// Double tap to select the first word and show the menu.
|
||
await tester.tapAt(textOffsetToPosition(tester, 1));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, 1));
|
||
await tester.pump(SelectionOverlay.fadeDuration);
|
||
|
||
expect(find.byType(SystemContextMenu), findsOneWidget);
|
||
|
||
// Simulate system hiding the menu.
|
||
final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
|
||
'method': 'ContextMenu.onDismissSystemContextMenu',
|
||
});
|
||
|
||
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
|
||
'flutter/platform',
|
||
messageBytes,
|
||
(ByteData? data) {},
|
||
);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byType(SystemContextMenu), findsNothing);
|
||
|
||
// Selection handles are not hidden.
|
||
final Iterable<RenderBox> boxes = tester.renderObjectList<RenderBox>(
|
||
find.descendant(
|
||
of: find.byWidgetPredicate(
|
||
(Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay',
|
||
),
|
||
matching: find.byType(CustomPaint),
|
||
),
|
||
);
|
||
expect(boxes.length, 2);
|
||
},
|
||
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
||
);
|
||
});
|
||
|
||
group('magnifier builder', () {
|
||
testWidgets('should build custom magnifier if given', (WidgetTester tester) async {
|
||
final Widget customMagnifier = Container(key: UniqueKey());
|
||
final textField = TextField(
|
||
magnifierConfiguration: TextMagnifierConfiguration(
|
||
magnifierBuilder:
|
||
(
|
||
BuildContext context,
|
||
MagnifierController controller,
|
||
ValueNotifier<MagnifierInfo>? info,
|
||
) => customMagnifier,
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(const MaterialApp(home: Placeholder()));
|
||
|
||
final BuildContext context = tester.firstElement(find.byType(Placeholder));
|
||
final magnifierInfo = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
|
||
addTearDown(magnifierInfo.dispose);
|
||
|
||
expect(
|
||
textField.magnifierConfiguration!.magnifierBuilder(
|
||
context,
|
||
MagnifierController(),
|
||
magnifierInfo,
|
||
),
|
||
isA<Widget>().having(
|
||
(Widget widget) => widget.key,
|
||
'built magnifier key equal to passed in magnifier key',
|
||
equals(customMagnifier.key),
|
||
),
|
||
);
|
||
});
|
||
|
||
group('defaults', () {
|
||
testWidgets(
|
||
'should build Magnifier on Android',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: TextField())));
|
||
|
||
final BuildContext context = tester.firstElement(find.byType(TextField));
|
||
final EditableText editableText = tester.widget(find.byType(EditableText));
|
||
final magnifierInfo = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
|
||
addTearDown(magnifierInfo.dispose);
|
||
|
||
expect(
|
||
editableText.magnifierConfiguration.magnifierBuilder(
|
||
context,
|
||
MagnifierController(),
|
||
magnifierInfo,
|
||
),
|
||
isA<TextMagnifier>(),
|
||
);
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
||
);
|
||
|
||
testWidgets(
|
||
'should build CupertinoMagnifier on iOS',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: TextField())));
|
||
|
||
final BuildContext context = tester.firstElement(find.byType(TextField));
|
||
final EditableText editableText = tester.widget(find.byType(EditableText));
|
||
final magnifierInfo = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
|
||
addTearDown(magnifierInfo.dispose);
|
||
|
||
expect(
|
||
editableText.magnifierConfiguration.magnifierBuilder(
|
||
context,
|
||
MagnifierController(),
|
||
magnifierInfo,
|
||
),
|
||
isA<CupertinoTextMagnifier>(),
|
||
);
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
||
);
|
||
|
||
testWidgets(
|
||
'should build nothing on all platforms but iOS and Android',
|
||
(WidgetTester tester) async {
|
||
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: TextField())));
|
||
|
||
final BuildContext context = tester.firstElement(find.byType(TextField));
|
||
final EditableText editableText = tester.widget(find.byType(EditableText));
|
||
final magnifierInfo = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
|
||
addTearDown(magnifierInfo.dispose);
|
||
|
||
expect(
|
||
editableText.magnifierConfiguration.magnifierBuilder(
|
||
context,
|
||
MagnifierController(),
|
||
magnifierInfo,
|
||
),
|
||
isNull,
|
||
);
|
||
},
|
||
variant: TargetPlatformVariant.all(
|
||
excluding: <TargetPlatform>{TargetPlatform.iOS, TargetPlatform.android},
|
||
),
|
||
);
|
||
});
|
||
});
|
||
|
||
group('magnifier', () {
|
||
late ValueNotifier<MagnifierInfo> magnifierInfo;
|
||
final Widget fakeMagnifier = Container(key: UniqueKey());
|
||
|
||
testWidgets('Can drag handles to show, unshow, and update magnifier', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
await tester.pumpWidget(
|
||
overlay(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
magnifierConfiguration: TextMagnifierConfiguration(
|
||
magnifierBuilder:
|
||
(
|
||
BuildContext context,
|
||
MagnifierController controller,
|
||
ValueNotifier<MagnifierInfo> localMagnifierInfo,
|
||
) {
|
||
magnifierInfo = localMagnifierInfo;
|
||
return fakeMagnifier;
|
||
},
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Double tap the 'e' to select 'def'.
|
||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
||
await tester.pump(const Duration(milliseconds: 30));
|
||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
||
await tester.pump(const Duration(milliseconds: 30));
|
||
|
||
final TextSelection selection = controller.selection;
|
||
|
||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||
final List<TextSelectionPoint> endpoints = globalize(
|
||
renderEditable.getEndpointsForSelection(selection),
|
||
renderEditable,
|
||
);
|
||
|
||
// Drag the right handle 2 letters to the right.
|
||
final Offset handlePos = endpoints.last.point + const Offset(1.0, 1.0);
|
||
final TestGesture gesture = await tester.startGesture(handlePos);
|
||
|
||
await gesture.moveTo(textOffsetToPosition(tester, testValue.length - 2));
|
||
await tester.pump();
|
||
|
||
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
|
||
final Offset firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;
|
||
|
||
await gesture.moveTo(textOffsetToPosition(tester, testValue.length));
|
||
await tester.pump();
|
||
|
||
// Expect the position the magnifier gets to have moved.
|
||
expect(firstDragGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));
|
||
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
||
});
|
||
|
||
testWidgets(
|
||
'Can drag to show, unshow, and update magnifier',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
magnifierConfiguration: TextMagnifierConfiguration(
|
||
magnifierBuilder:
|
||
(
|
||
BuildContext context,
|
||
MagnifierController controller,
|
||
ValueNotifier<MagnifierInfo> localMagnifierInfo,
|
||
) {
|
||
magnifierInfo = localMagnifierInfo;
|
||
return fakeMagnifier;
|
||
},
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Tap at '|a' to move the selection to position 0.
|
||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
||
|
||
// Start a drag gesture to move the selection to the dragged position, showing
|
||
// the magnifier.
|
||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, 0));
|
||
await tester.pump();
|
||
|
||
await gesture.moveTo(textOffsetToPosition(tester, 5));
|
||
await tester.pump();
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 5);
|
||
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
|
||
|
||
Offset firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;
|
||
|
||
await gesture.moveTo(textOffsetToPosition(tester, 10));
|
||
await tester.pump();
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 10);
|
||
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
|
||
// Expect the position the magnifier gets to have moved.
|
||
expect(firstDragGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));
|
||
|
||
// The magnifier should hide when the drag ends.
|
||
await gesture.up();
|
||
await tester.pump();
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 10);
|
||
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
||
|
||
// Start a double-tap select the word at the tapped position.
|
||
await gesture.down(textOffsetToPosition(tester, 1));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
await gesture.down(textOffsetToPosition(tester, 1));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 3);
|
||
|
||
// Start a drag gesture to extend the selection word-by-word, showing the
|
||
// magnifier.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 5));
|
||
await tester.pump();
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 7);
|
||
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
|
||
|
||
firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;
|
||
|
||
await gesture.moveTo(textOffsetToPosition(tester, 10));
|
||
await tester.pump();
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 11);
|
||
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
|
||
// Expect the position the magnifier gets to have moved.
|
||
expect(firstDragGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));
|
||
|
||
// The magnifier should hide when the drag ends.
|
||
await gesture.up();
|
||
await tester.pump();
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 11);
|
||
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.iOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can long press to show, unshow, and update magnifier',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
final isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
magnifierConfiguration: TextMagnifierConfiguration(
|
||
magnifierBuilder:
|
||
(
|
||
BuildContext context,
|
||
MagnifierController controller,
|
||
ValueNotifier<MagnifierInfo> localMagnifierInfo,
|
||
) {
|
||
magnifierInfo = localMagnifierInfo;
|
||
return fakeMagnifier;
|
||
},
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'abc def ghi';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Tap at 'e' to set the selection to position 5 on Android.
|
||
// Tap at 'e' to set the selection to the closest word edge, which is position 4 on iOS.
|
||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
||
await tester.pumpAndSettle(const Duration(milliseconds: 300));
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 7);
|
||
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
||
|
||
// Long press the 'e' to select 'def' on Android and show magnifier.
|
||
// Long press the 'e' to move the cursor in front of the 'e' on iOS and show the magnifier.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, testValue.indexOf('e')),
|
||
);
|
||
await tester.pumpAndSettle(const Duration(milliseconds: 1000));
|
||
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 4 : 5);
|
||
expect(controller.selection.extentOffset, isTargetPlatformAndroid ? 7 : 5);
|
||
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
|
||
|
||
final Offset firstLongPressGesturePosition = magnifierInfo.value.globalGesturePosition;
|
||
|
||
// Move the gesture to 'h' on Android to update the magnifier and select 'ghi'.
|
||
// Move the gesture to 'h' on iOS to update the magnifier and move the cursor to 'h'.
|
||
await gesture.moveTo(textOffsetToPosition(tester, testValue.indexOf('h')));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 4 : 9);
|
||
expect(controller.selection.extentOffset, isTargetPlatformAndroid ? 11 : 9);
|
||
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
|
||
// Expect the position the magnifier gets to have moved.
|
||
expect(firstLongPressGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));
|
||
|
||
// End the long press to hide the magnifier.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.iOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Can double tap and drag to show, unshow, and update magnifier',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController();
|
||
MagnifierController? magnifierController;
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
controller: controller,
|
||
magnifierConfiguration: TextMagnifierConfiguration(
|
||
magnifierBuilder:
|
||
(
|
||
BuildContext context,
|
||
MagnifierController controller,
|
||
ValueNotifier<MagnifierInfo> localMagnifierInfo,
|
||
) {
|
||
magnifierController = controller;
|
||
return TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder(
|
||
context,
|
||
controller,
|
||
localMagnifierInfo,
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
const testValue = 'one two three four five six seven';
|
||
await tester.enterText(find.byType(TextField), testValue);
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// Tap at 'e' to set the selection to the closest word edge, which is position 3 on iOS.
|
||
final Offset initialPosition = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||
await tester.tapAt(initialPosition);
|
||
await tester.pumpAndSettle(const Duration(milliseconds: 300));
|
||
expect(controller.selection.isCollapsed, true);
|
||
expect(controller.selection.baseOffset, 3);
|
||
expect(magnifierController, isNull);
|
||
|
||
// Double tap the 'e' to select 'one'.
|
||
final TestGesture gesture = await tester.startGesture(initialPosition);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
await gesture.down(initialPosition);
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.isCollapsed, false);
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 3);
|
||
expect(magnifierController, isNull);
|
||
|
||
// Drag immediately after the double tap to select 'one two three four' and show the magnifier.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 16));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(controller.selection.isCollapsed, false);
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 18);
|
||
expect(magnifierController, isNotNull);
|
||
expect(magnifierController!.shown, true);
|
||
|
||
// Dragging down at the same position should hide the cupertino magnifier when it
|
||
// exceeds its `hideBelowThreshold`.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 16) + const Offset(0.0, 50.0));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.isCollapsed, false);
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 18);
|
||
expect(magnifierController, isNotNull);
|
||
expect(magnifierController!.shown, false);
|
||
|
||
// Keep draging to select 'one two three four five' while the position continues to
|
||
// exceed the `hideBelowThreshold` keeping the magnifier hidden.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 20) + const Offset(0.0, 50.0));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.isCollapsed, false);
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 23);
|
||
expect(magnifierController, isNotNull);
|
||
expect(magnifierController!.shown, false);
|
||
|
||
// Remove offset that is used to exceed threshold, this should reveal the magnifier.
|
||
await gesture.moveTo(textOffsetToPosition(tester, 20));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection.isCollapsed, false);
|
||
expect(controller.selection.baseOffset, 0);
|
||
expect(controller.selection.extentOffset, 23);
|
||
expect(magnifierController, isNotNull);
|
||
expect(magnifierController!.shown, true);
|
||
|
||
// End the drag to hide the magnifier.
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
expect(magnifierController, isNotNull);
|
||
expect(magnifierController!.shown, false);
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
||
);
|
||
|
||
testWidgets(
|
||
'cancelling long press hides magnifier',
|
||
(WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/167879
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
magnifierConfiguration: TextMagnifierConfiguration(
|
||
magnifierBuilder:
|
||
(
|
||
BuildContext context,
|
||
MagnifierController controller,
|
||
ValueNotifier<MagnifierInfo> localMagnifierInfo,
|
||
) {
|
||
magnifierInfo = localMagnifierInfo;
|
||
return fakeMagnifier;
|
||
},
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
||
|
||
final TestGesture gesture = await tester.startGesture(
|
||
tester.getCenter(find.byType(TextField)),
|
||
);
|
||
|
||
await tester.pumpAndSettle(kLongPressTimeout);
|
||
|
||
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
|
||
|
||
// Cancel the long press to hide the magnifier.
|
||
await gesture.cancel();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.iOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'TextField cursor appears only when focused',
|
||
(WidgetTester tester) async {
|
||
final focusNode = FocusNode(debugLabel: 'Test Node');
|
||
addTearDown(focusNode.dispose);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
focusNode: focusNode,
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
magnifierConfiguration: TextMagnifierConfiguration(
|
||
magnifierBuilder:
|
||
(
|
||
BuildContext context,
|
||
MagnifierController controller,
|
||
ValueNotifier<MagnifierInfo> localMagnifierInfo,
|
||
) {
|
||
magnifierInfo = localMagnifierInfo;
|
||
return fakeMagnifier;
|
||
},
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Offset fieldCenter = tester.getCenter(find.byType(EditableText));
|
||
final TestGesture gesture = await tester.startGesture(fieldCenter);
|
||
await gesture.moveBy(const Offset(30, 0));
|
||
await tester.pumpAndSettle();
|
||
|
||
// The blinking cursor should NOT be shown.
|
||
final EditableTextState editableTextState = tester.state<EditableTextState>(
|
||
find.byType(EditableText),
|
||
);
|
||
expect(focusNode.hasFocus, isFalse);
|
||
expect(editableTextState.cursorCurrentlyVisible, isFalse);
|
||
|
||
// Simulate long press again.
|
||
await tester.pump();
|
||
await tester.longPress(find.byType(EditableText));
|
||
await tester.pumpAndSettle();
|
||
|
||
// The blinking cursor should now be shown.
|
||
expect(focusNode.hasFocus, isTrue);
|
||
expect(editableTextState.cursorCurrentlyVisible, isTrue);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.iOS,
|
||
TargetPlatform.android,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'magnifier does not show when tapping outside field',
|
||
(WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/128321
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Padding(
|
||
padding: const EdgeInsets.all(20),
|
||
child: TextField(
|
||
magnifierConfiguration: TextMagnifierConfiguration(
|
||
magnifierBuilder:
|
||
(
|
||
BuildContext context,
|
||
MagnifierController controller,
|
||
ValueNotifier<MagnifierInfo> localMagnifierInfo,
|
||
) {
|
||
magnifierInfo = localMagnifierInfo;
|
||
return fakeMagnifier;
|
||
},
|
||
),
|
||
onTapOutside: (PointerDownEvent event) {
|
||
FocusManager.instance.primaryFocus?.unfocus();
|
||
},
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tapAt(tester.getCenter(find.byType(TextField)));
|
||
await tester.pump();
|
||
|
||
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
||
|
||
final TestGesture gesture = await tester.startGesture(
|
||
tester.getBottomLeft(find.byType(TextField)) - const Offset(10.0, 20.0),
|
||
);
|
||
await tester.pump();
|
||
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
||
await gesture.up();
|
||
await tester.pump();
|
||
|
||
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.android}),
|
||
);
|
||
});
|
||
|
||
group('TapRegion integration', () {
|
||
testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async {
|
||
final focusNode = FocusNode(debugLabel: 'Test Node');
|
||
addTearDown(focusNode.dispose);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: SizedBox(
|
||
width: 100,
|
||
height: 100,
|
||
child: Opacity(
|
||
opacity: 0.5,
|
||
child: TextField(
|
||
autofocus: true,
|
||
focusNode: focusNode,
|
||
decoration: const InputDecoration(
|
||
hintText: 'Placeholder',
|
||
border: OutlineInputBorder(),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||
|
||
await tester.tapAt(const Offset(10, 10));
|
||
await tester.pump();
|
||
|
||
expect(focusNode.hasPrimaryFocus, isFalse);
|
||
}, variant: TargetPlatformVariant.desktop());
|
||
|
||
testWidgets("Tapping outside doesn't lose focus on mobile", (WidgetTester tester) async {
|
||
final focusNode = FocusNode(debugLabel: 'Test Node');
|
||
addTearDown(focusNode.dispose);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: SizedBox(
|
||
width: 100,
|
||
height: 100,
|
||
child: Opacity(
|
||
opacity: 0.5,
|
||
child: TextField(
|
||
autofocus: true,
|
||
focusNode: focusNode,
|
||
decoration: const InputDecoration(
|
||
hintText: 'Placeholder',
|
||
border: OutlineInputBorder(),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||
|
||
await tester.tapAt(const Offset(10, 10));
|
||
await tester.pump();
|
||
|
||
// Focus is lost on mobile browsers, but not mobile apps.
|
||
expect(focusNode.hasPrimaryFocus, kIsWeb ? isFalse : isTrue);
|
||
}, variant: TargetPlatformVariant.mobile());
|
||
|
||
testWidgets(
|
||
"Tapping on toolbar doesn't lose focus",
|
||
(WidgetTester tester) async {
|
||
final focusNode = FocusNode(debugLabel: 'Test Node');
|
||
final TextEditingController controller = _textEditingController(text: 'A B C');
|
||
addTearDown(focusNode.dispose);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: SizedBox(
|
||
width: 100,
|
||
height: 100,
|
||
child: Opacity(
|
||
opacity: 0.5,
|
||
child: TextField(
|
||
controller: controller,
|
||
focusNode: focusNode,
|
||
decoration: const InputDecoration(hintText: 'Placeholder'),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// The selectWordsInRange with SelectionChangedCause.tap seems to be needed to show the toolbar.
|
||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||
state.renderEditable.selectWordsInRange(
|
||
from: Offset.zero,
|
||
cause: SelectionChangedCause.tap,
|
||
);
|
||
|
||
final Offset aPosition = textOffsetToPosition(tester, 1);
|
||
|
||
// Right clicking shows the menu.
|
||
final TestGesture gesture = await tester.startGesture(
|
||
aPosition,
|
||
kind: PointerDeviceKind.mouse,
|
||
buttons: kSecondaryMouseButton,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
// Sanity check that the toolbar widget exists.
|
||
expect(find.text('Copy'), findsOneWidget);
|
||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||
|
||
// Now tap on it to see if we lose focus.
|
||
await tester.tap(find.text('Copy'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||
},
|
||
variant: TargetPlatformVariant.all(),
|
||
skip: isBrowser, // [intended] On the web, the toolbar isn't rendered by Flutter.
|
||
);
|
||
|
||
testWidgets("Tapping on input decorator doesn't lose focus", (WidgetTester tester) async {
|
||
final focusNode = FocusNode(debugLabel: 'Test Node');
|
||
addTearDown(focusNode.dispose);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: SizedBox(
|
||
width: 100,
|
||
height: 100,
|
||
child: Opacity(
|
||
opacity: 0.5,
|
||
child: TextField(
|
||
autofocus: true,
|
||
focusNode: focusNode,
|
||
decoration: const InputDecoration(
|
||
hintText: 'Placeholder',
|
||
border: OutlineInputBorder(),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.pump();
|
||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||
|
||
final Rect decorationBox = tester.getRect(find.byType(TextField));
|
||
// Tap just inside the decoration, but not inside the EditableText.
|
||
await tester.tapAt(decorationBox.topLeft + const Offset(1, 1));
|
||
await tester.pump();
|
||
|
||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||
}, variant: TargetPlatformVariant.all());
|
||
|
||
// PointerDownEvents can't be trackpad events, apparently, so we skip that one.
|
||
for (final PointerDeviceKind pointerDeviceKind
|
||
in PointerDeviceKind.values.toSet()..remove(PointerDeviceKind.trackpad)) {
|
||
testWidgets(
|
||
'Default TextField handling of onTapOutside follows platform conventions for ${pointerDeviceKind.name}',
|
||
(WidgetTester tester) async {
|
||
final focusNode = FocusNode(debugLabel: 'Test');
|
||
addTearDown(focusNode.dispose);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Column(
|
||
children: <Widget>[
|
||
const Text('Outside'),
|
||
TextField(autofocus: true, focusNode: focusNode),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
Future<void> click(Finder finder) async {
|
||
final TestGesture gesture = await tester.startGesture(
|
||
tester.getCenter(finder),
|
||
kind: pointerDeviceKind,
|
||
);
|
||
await gesture.up();
|
||
await gesture.removePointer();
|
||
}
|
||
|
||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||
|
||
await click(find.text('Outside'));
|
||
|
||
switch (pointerDeviceKind) {
|
||
case PointerDeviceKind.touch:
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.iOS:
|
||
case TargetPlatform.android:
|
||
case TargetPlatform.fuchsia:
|
||
expect(focusNode.hasPrimaryFocus, equals(!kIsWeb));
|
||
case TargetPlatform.linux:
|
||
case TargetPlatform.macOS:
|
||
case TargetPlatform.windows:
|
||
expect(focusNode.hasPrimaryFocus, isFalse);
|
||
}
|
||
case PointerDeviceKind.mouse:
|
||
case PointerDeviceKind.stylus:
|
||
case PointerDeviceKind.invertedStylus:
|
||
case PointerDeviceKind.trackpad:
|
||
case PointerDeviceKind.unknown:
|
||
expect(focusNode.hasPrimaryFocus, isFalse);
|
||
}
|
||
},
|
||
variant: TargetPlatformVariant.all(),
|
||
);
|
||
}
|
||
});
|
||
|
||
testWidgets(
|
||
'Builds the corresponding default spell check toolbar by platform',
|
||
(WidgetTester tester) async {
|
||
tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true;
|
||
late final BuildContext builderContext;
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: Builder(
|
||
builder: (BuildContext context) {
|
||
builderContext = context;
|
||
return const TextField(
|
||
autofocus: true,
|
||
spellCheckConfiguration: SpellCheckConfiguration(),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Allow the autofocus to take effect.
|
||
await tester.pump();
|
||
|
||
final EditableTextState editableTextState = tester.state<EditableTextState>(
|
||
find.byType(EditableText),
|
||
);
|
||
editableTextState.spellCheckResults = const SpellCheckResults('', <SuggestionSpan>[
|
||
SuggestionSpan(TextRange(start: 0, end: 0), <String>['something']),
|
||
]);
|
||
final Widget spellCheckToolbar = TextField.defaultSpellCheckSuggestionsToolbarBuilder(
|
||
builderContext,
|
||
editableTextState,
|
||
);
|
||
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.iOS:
|
||
case TargetPlatform.macOS:
|
||
expect(spellCheckToolbar, isA<CupertinoSpellCheckSuggestionsToolbar>());
|
||
case TargetPlatform.android:
|
||
case TargetPlatform.fuchsia:
|
||
case TargetPlatform.linux:
|
||
case TargetPlatform.windows:
|
||
expect(spellCheckToolbar, isA<SpellCheckSuggestionsToolbar>());
|
||
}
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.iOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'Builds the corresponding default spell check configuration by platform',
|
||
(WidgetTester tester) async {
|
||
tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true;
|
||
|
||
final SpellCheckConfiguration expectedConfiguration;
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.iOS:
|
||
case TargetPlatform.macOS:
|
||
expectedConfiguration = SpellCheckConfiguration(
|
||
misspelledTextStyle: CupertinoTextField.cupertinoMisspelledTextStyle,
|
||
misspelledSelectionColor: CupertinoTextField.kMisspelledSelectionColor,
|
||
spellCheckService: DefaultSpellCheckService(),
|
||
spellCheckSuggestionsToolbarBuilder:
|
||
CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder,
|
||
);
|
||
case TargetPlatform.android:
|
||
case TargetPlatform.fuchsia:
|
||
case TargetPlatform.linux:
|
||
case TargetPlatform.windows:
|
||
expectedConfiguration = SpellCheckConfiguration(
|
||
misspelledTextStyle: TextField.materialMisspelledTextStyle,
|
||
spellCheckService: DefaultSpellCheckService(),
|
||
spellCheckSuggestionsToolbarBuilder:
|
||
TextField.defaultSpellCheckSuggestionsToolbarBuilder,
|
||
);
|
||
}
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: TextField(autofocus: true, spellCheckConfiguration: SpellCheckConfiguration()),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final EditableTextState editableTextState = tester.state<EditableTextState>(
|
||
find.byType(EditableText),
|
||
);
|
||
|
||
expect(
|
||
editableTextState.spellCheckConfiguration.misspelledTextStyle,
|
||
expectedConfiguration.misspelledTextStyle,
|
||
);
|
||
expect(
|
||
editableTextState.spellCheckConfiguration.misspelledSelectionColor,
|
||
expectedConfiguration.misspelledSelectionColor,
|
||
);
|
||
expect(
|
||
editableTextState.spellCheckConfiguration.spellCheckService.runtimeType,
|
||
expectedConfiguration.spellCheckService.runtimeType,
|
||
);
|
||
expect(
|
||
editableTextState.spellCheckConfiguration.spellCheckSuggestionsToolbarBuilder,
|
||
expectedConfiguration.spellCheckSuggestionsToolbarBuilder,
|
||
);
|
||
},
|
||
variant: const TargetPlatformVariant(<TargetPlatform>{
|
||
TargetPlatform.android,
|
||
TargetPlatform.iOS,
|
||
}),
|
||
);
|
||
|
||
testWidgets(
|
||
'text selection toolbar is hidden on tap down on desktop platforms',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'blah1 blah2');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||
|
||
TestGesture gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, 8),
|
||
kind: PointerDeviceKind.mouse,
|
||
buttons: kSecondaryMouseButton,
|
||
);
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||
|
||
gesture = await tester.startGesture(
|
||
textOffsetToPosition(tester, 2),
|
||
kind: PointerDeviceKind.mouse,
|
||
);
|
||
await tester.pump();
|
||
|
||
// After the gesture is down but not up, the toolbar is already gone.
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||
|
||
await gesture.up();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
variant: TargetPlatformVariant.all(excluding: TargetPlatformVariant.mobile().values),
|
||
);
|
||
|
||
testWidgets(
|
||
'Text processing actions are added to the toolbar',
|
||
(WidgetTester tester) async {
|
||
const initialText = 'I love Flutter';
|
||
final TextEditingController controller = _textEditingController(text: initialText);
|
||
final mockProcessTextHandler = MockProcessTextHandler();
|
||
TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.processText,
|
||
mockProcessTextHandler.handleMethodCall,
|
||
);
|
||
addTearDown(
|
||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.processText,
|
||
null,
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Long press to put the cursor after the "F".
|
||
final int index = initialText.indexOf('F');
|
||
await tester.longPressAt(textOffsetToPosition(tester, index));
|
||
await tester.pump();
|
||
|
||
// Double tap on the same location to select the word around the cursor.
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 7, extentOffset: 14));
|
||
|
||
// The toolbar is visible and the text processing actions are visible on Android.
|
||
final areTextActionsSupported = defaultTargetPlatform == TargetPlatform.android;
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||
expect(find.text(fakeAction1Label), areTextActionsSupported ? findsOneWidget : findsNothing);
|
||
expect(find.text(fakeAction2Label), areTextActionsSupported ? findsOneWidget : findsNothing);
|
||
},
|
||
variant: TargetPlatformVariant.all(),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Text processing actions are not added to the toolbar for obscured text',
|
||
(WidgetTester tester) async {
|
||
const initialText = 'I love Flutter';
|
||
final TextEditingController controller = _textEditingController(text: initialText);
|
||
final mockProcessTextHandler = MockProcessTextHandler();
|
||
TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.processText,
|
||
mockProcessTextHandler.handleMethodCall,
|
||
);
|
||
addTearDown(
|
||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.processText,
|
||
null,
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(obscureText: true, controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Long press to put the cursor after the "F".
|
||
final int index = initialText.indexOf('F');
|
||
await tester.longPressAt(textOffsetToPosition(tester, index));
|
||
await tester.pump();
|
||
|
||
// Double tap on the same location to select the word around the cursor.
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 14));
|
||
|
||
// The toolbar is visible but does not contain the text processing actions.
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||
expect(find.text(fakeAction1Label), findsNothing);
|
||
expect(find.text(fakeAction2Label), findsNothing);
|
||
},
|
||
variant: TargetPlatformVariant.all(),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Text processing actions are not added to the toolbar if selection is collapsed (Android only)',
|
||
(WidgetTester tester) async {
|
||
const initialText = 'I love Flutter';
|
||
final TextEditingController controller = _textEditingController(text: initialText);
|
||
final mockProcessTextHandler = MockProcessTextHandler();
|
||
TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.processText,
|
||
mockProcessTextHandler.handleMethodCall,
|
||
);
|
||
addTearDown(
|
||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.processText,
|
||
null,
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Open the text selection toolbar.
|
||
await showSelectionMenuAt(tester, controller, initialText.indexOf('F'));
|
||
await skipPastScrollingAnimation(tester);
|
||
|
||
// The toolbar is visible but does not contain the text processing actions.
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||
expect(controller.selection.isCollapsed, true);
|
||
|
||
expect(find.text(fakeAction1Label), findsNothing);
|
||
expect(find.text(fakeAction2Label), findsNothing);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Invoke a text processing action that does not return a value (Android only)',
|
||
(WidgetTester tester) async {
|
||
const initialText = 'I love Flutter';
|
||
final TextEditingController controller = _textEditingController(text: initialText);
|
||
final mockProcessTextHandler = MockProcessTextHandler();
|
||
TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.processText,
|
||
mockProcessTextHandler.handleMethodCall,
|
||
);
|
||
addTearDown(
|
||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.processText,
|
||
null,
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Long press to put the cursor after the "F".
|
||
final int index = initialText.indexOf('F');
|
||
await tester.longPressAt(textOffsetToPosition(tester, index));
|
||
await tester.pump();
|
||
|
||
// Double tap on the same location to select the word around the cursor.
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 7, extentOffset: 14));
|
||
|
||
// Run an action that does not return a processed text.
|
||
await tester.tap(find.text(fakeAction2Label));
|
||
await tester.pump(const Duration(milliseconds: 200));
|
||
|
||
// The action was correctly called.
|
||
expect(mockProcessTextHandler.lastCalledActionId, fakeAction2Id);
|
||
expect(mockProcessTextHandler.lastTextToProcess, 'Flutter');
|
||
|
||
// The text field was not updated.
|
||
expect(controller.text, initialText);
|
||
|
||
// The toolbar is no longer visible.
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Invoking a text processing action that returns a value replaces the selection (Android only)',
|
||
(WidgetTester tester) async {
|
||
const initialText = 'I love Flutter';
|
||
final TextEditingController controller = _textEditingController(text: initialText);
|
||
final mockProcessTextHandler = MockProcessTextHandler();
|
||
TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.processText,
|
||
mockProcessTextHandler.handleMethodCall,
|
||
);
|
||
addTearDown(
|
||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.processText,
|
||
null,
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Long press to put the cursor after the "F".
|
||
final int index = initialText.indexOf('F');
|
||
await tester.longPressAt(textOffsetToPosition(tester, index));
|
||
await tester.pump();
|
||
|
||
// Double tap on the same location to select the word around the cursor.
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 7, extentOffset: 14));
|
||
|
||
// Run an action that returns a processed text.
|
||
await tester.tap(find.text(fakeAction1Label));
|
||
await tester.pump(const Duration(milliseconds: 200));
|
||
|
||
// The action was correctly called.
|
||
expect(mockProcessTextHandler.lastCalledActionId, fakeAction1Id);
|
||
expect(mockProcessTextHandler.lastTextToProcess, 'Flutter');
|
||
|
||
// The text field was updated.
|
||
expect(controller.text, 'I love Flutter!!!');
|
||
|
||
// The toolbar is no longer visible.
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets(
|
||
'Invoking a text processing action that returns a value does not replace the selection of a readOnly text field (Android only)',
|
||
(WidgetTester tester) async {
|
||
const initialText = 'I love Flutter';
|
||
final TextEditingController controller = _textEditingController(text: initialText);
|
||
final mockProcessTextHandler = MockProcessTextHandler();
|
||
TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.processText,
|
||
mockProcessTextHandler.handleMethodCall,
|
||
);
|
||
addTearDown(
|
||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||
SystemChannels.processText,
|
||
null,
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(readOnly: true, controller: controller)),
|
||
),
|
||
);
|
||
|
||
// Long press to put the cursor after the "F".
|
||
final int index = initialText.indexOf('F');
|
||
await tester.longPressAt(textOffsetToPosition(tester, index));
|
||
await tester.pump();
|
||
|
||
// Double tap on the same location to select the word around the cursor.
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tapAt(textOffsetToPosition(tester, index));
|
||
await tester.pumpAndSettle();
|
||
expect(controller.selection, const TextSelection(baseOffset: 7, extentOffset: 14));
|
||
|
||
// Run an action that returns a processed text.
|
||
await tester.tap(find.text(fakeAction1Label));
|
||
await tester.pump(const Duration(milliseconds: 200));
|
||
|
||
// The Action was correctly called.
|
||
expect(mockProcessTextHandler.lastCalledActionId, fakeAction1Id);
|
||
expect(mockProcessTextHandler.lastTextToProcess, 'Flutter');
|
||
|
||
// The text field was not updated.
|
||
expect(controller.text, initialText);
|
||
|
||
// The toolbar is no longer visible.
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||
},
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: isContextMenuProvidedByPlatform,
|
||
);
|
||
|
||
testWidgets('Start the floating cursor on long tap', (WidgetTester tester) async {
|
||
EditableText.debugDeterministicCursor = true;
|
||
final TextEditingController controller = _textEditingController(text: 'abcd');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: RepaintBoundary(
|
||
key: const ValueKey<int>(1),
|
||
child: TextField(autofocus: true, controller: controller),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
// Wait for autofocus.
|
||
await tester.pumpAndSettle();
|
||
final Offset textFieldCenter = tester.getCenter(find.byType(TextField));
|
||
final TestGesture gesture = await tester.startGesture(textFieldCenter);
|
||
await tester.pump(kLongPressTimeout);
|
||
await expectLater(
|
||
find.byKey(const ValueKey<int>(1)),
|
||
matchesGoldenFile('text_field_floating_cursor.regular_and_floating_both.material.0.png'),
|
||
);
|
||
await gesture.moveTo(Offset(10, textFieldCenter.dy));
|
||
await tester.pump();
|
||
await expectLater(
|
||
find.byKey(const ValueKey<int>(1)),
|
||
matchesGoldenFile('text_field_floating_cursor.only_floating_cursor.material.0.png'),
|
||
);
|
||
await gesture.up();
|
||
EditableText.debugDeterministicCursor = false;
|
||
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
|
||
|
||
testWidgets(
|
||
'Cursor should not blink when long-pressing to show floating cursor.',
|
||
(WidgetTester tester) async {
|
||
final TextEditingController controller = _textEditingController(text: 'abcdefghijklmnopqr');
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(
|
||
child: TextField(
|
||
autofocus: true,
|
||
controller: controller,
|
||
cursorOpacityAnimates: false,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
final EditableTextState state = tester.state(find.byType(EditableText));
|
||
Future<void> checkCursorBlinking({bool isBlinking = true}) async {
|
||
var initialShowCursor = true;
|
||
if (isBlinking) {
|
||
initialShowCursor = state.renderEditable.showCursor.value;
|
||
}
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(
|
||
state.cursorCurrentlyVisible,
|
||
equals(isBlinking ? !initialShowCursor : initialShowCursor),
|
||
);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(state.cursorCurrentlyVisible, equals(initialShowCursor));
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(
|
||
state.cursorCurrentlyVisible,
|
||
equals(isBlinking ? !initialShowCursor : initialShowCursor),
|
||
);
|
||
await tester.pump(state.cursorBlinkInterval);
|
||
expect(state.cursorCurrentlyVisible, equals(initialShowCursor));
|
||
}
|
||
|
||
// Wait for autofocus.
|
||
await tester.pumpAndSettle();
|
||
// Before long-pressing, the cursor should blink.
|
||
await checkCursorBlinking();
|
||
|
||
final TestGesture gesture = await tester.startGesture(
|
||
tester.getTopLeft(find.byType(TextField)),
|
||
);
|
||
await tester.pump(kLongPressTimeout);
|
||
// When long-pressing, the cursor shouldn't blink.
|
||
await checkCursorBlinking(isBlinking: false);
|
||
await gesture.moveBy(const Offset(20, 0));
|
||
await tester.pump();
|
||
// When long-pressing and dragging to move the cursor, the cursor shouldn't blink.
|
||
await checkCursorBlinking(isBlinking: false);
|
||
await gesture.up();
|
||
// After finishing the long-press, the cursor should blink.
|
||
await checkCursorBlinking();
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
||
);
|
||
|
||
testWidgets('when enabled listens to onFocus events and gains focus', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
||
final focusNode = FocusNode();
|
||
addTearDown(focusNode.dispose);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(focusNode: focusNode)),
|
||
),
|
||
),
|
||
);
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: 1,
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: 2,
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: 3,
|
||
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: 4,
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 0,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isEnabled,
|
||
],
|
||
actions: <SemanticsAction>[
|
||
SemanticsAction.tap,
|
||
if (defaultTargetPlatform == TargetPlatform.windows ||
|
||
defaultTargetPlatform == TargetPlatform.macOS ||
|
||
defaultTargetPlatform == TargetPlatform.linux)
|
||
SemanticsAction.didGainAccessibilityFocus,
|
||
if (defaultTargetPlatform == TargetPlatform.windows ||
|
||
defaultTargetPlatform == TargetPlatform.macOS ||
|
||
defaultTargetPlatform == TargetPlatform.linux)
|
||
SemanticsAction.didLoseAccessibilityFocus,
|
||
SemanticsAction.focus,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreRect: true,
|
||
ignoreTransform: true,
|
||
),
|
||
);
|
||
|
||
expect(focusNode.hasFocus, isFalse);
|
||
semanticsOwner.performAction(4, SemanticsAction.focus);
|
||
await tester.pumpAndSettle();
|
||
expect(focusNode.hasFocus, isTrue);
|
||
semantics.dispose();
|
||
}, variant: TargetPlatformVariant.all());
|
||
|
||
testWidgets(
|
||
'when disabled does not listen to onFocus events or gain focus',
|
||
(WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
||
final focusNode = FocusNode();
|
||
addTearDown(focusNode.dispose);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(focusNode: focusNode, enabled: false)),
|
||
),
|
||
),
|
||
);
|
||
expect(
|
||
semantics,
|
||
hasSemantics(
|
||
TestSemantics.root(
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: 1,
|
||
textDirection: TextDirection.ltr,
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: 2,
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: 3,
|
||
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
||
children: <TestSemantics>[
|
||
TestSemantics(
|
||
id: 4,
|
||
inputType: ui.SemanticsInputType.text,
|
||
currentValueLength: 0,
|
||
flags: <SemanticsFlag>[
|
||
SemanticsFlag.isTextField,
|
||
SemanticsFlag.isFocusable,
|
||
SemanticsFlag.hasEnabledState,
|
||
SemanticsFlag.isReadOnly,
|
||
],
|
||
actions: <SemanticsAction>[
|
||
if (defaultTargetPlatform == TargetPlatform.windows ||
|
||
defaultTargetPlatform == TargetPlatform.macOS ||
|
||
defaultTargetPlatform == TargetPlatform.linux)
|
||
SemanticsAction.didGainAccessibilityFocus,
|
||
if (defaultTargetPlatform == TargetPlatform.windows ||
|
||
defaultTargetPlatform == TargetPlatform.macOS ||
|
||
defaultTargetPlatform == TargetPlatform.linux)
|
||
SemanticsAction.didLoseAccessibilityFocus,
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
ignoreRect: true,
|
||
ignoreTransform: true,
|
||
),
|
||
);
|
||
|
||
expect(focusNode.hasFocus, isFalse);
|
||
semanticsOwner.performAction(4, SemanticsAction.focus);
|
||
await tester.pumpAndSettle();
|
||
expect(focusNode.hasFocus, isFalse);
|
||
semantics.dispose();
|
||
},
|
||
variant: TargetPlatformVariant.all(),
|
||
);
|
||
|
||
testWidgets(
|
||
'when receives SemanticsAction.focus while already focused, shows keyboard',
|
||
(WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
||
final focusNode = FocusNode();
|
||
addTearDown(focusNode.dispose);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(focusNode: focusNode)),
|
||
),
|
||
),
|
||
);
|
||
focusNode.requestFocus();
|
||
await tester.pumpAndSettle();
|
||
|
||
tester.testTextInput.log.clear();
|
||
expect(focusNode.hasFocus, isTrue);
|
||
semanticsOwner.performAction(4, SemanticsAction.focus);
|
||
await tester.pumpAndSettle();
|
||
expect(focusNode.hasFocus, isTrue);
|
||
expect(tester.testTextInput.log.single.method, 'TextInput.show');
|
||
|
||
semantics.dispose();
|
||
},
|
||
variant: TargetPlatformVariant.all(),
|
||
);
|
||
|
||
testWidgets(
|
||
'when receives SemanticsAction.focus while focused but read-only, does not show keyboard',
|
||
(WidgetTester tester) async {
|
||
final semantics = SemanticsTester(tester);
|
||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
||
final focusNode = FocusNode();
|
||
addTearDown(focusNode.dispose);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: Center(child: TextField(focusNode: focusNode, readOnly: true)),
|
||
),
|
||
),
|
||
);
|
||
focusNode.requestFocus();
|
||
await tester.pumpAndSettle();
|
||
|
||
tester.testTextInput.log.clear();
|
||
expect(focusNode.hasFocus, isTrue);
|
||
semanticsOwner.performAction(4, SemanticsAction.focus);
|
||
await tester.pumpAndSettle();
|
||
expect(focusNode.hasFocus, isTrue);
|
||
expect(tester.testTextInput.log, isEmpty);
|
||
|
||
semantics.dispose();
|
||
},
|
||
variant: TargetPlatformVariant.all(),
|
||
);
|
||
|
||
testWidgets('hintLocales is passed to EditableText', (WidgetTester tester) async {
|
||
const hintLocales = <Locale>[Locale('en'), Locale('fr')];
|
||
await tester.pumpWidget(
|
||
const MaterialApp(
|
||
home: Material(child: TextField(hintLocales: hintLocales)),
|
||
),
|
||
);
|
||
|
||
final EditableText editableText = tester.widget(find.byType(EditableText));
|
||
expect(editableText.hintLocales, hintLocales);
|
||
});
|
||
|
||
testWidgets(
|
||
'readOnly disallows SystemContextMenu',
|
||
(WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/170521.
|
||
tester.platformDispatcher.supportsShowingSystemContextMenu = true;
|
||
final controller = TextEditingController(text: 'abcdefghijklmnopqr');
|
||
addTearDown(() {
|
||
tester.platformDispatcher.resetSupportsShowingSystemContextMenu();
|
||
tester.view.reset();
|
||
controller.dispose();
|
||
});
|
||
|
||
var readOnly = true;
|
||
late StateSetter setState;
|
||
|
||
await tester.pumpWidget(
|
||
// Don't wrap with the global View so that the change to
|
||
// platformDispatcher is read.
|
||
wrapWithView: false,
|
||
View(
|
||
view: tester.view,
|
||
child: MaterialApp(
|
||
home: Material(
|
||
child: StatefulBuilder(
|
||
builder: (BuildContext context, StateSetter setter) {
|
||
setState = setter;
|
||
return TextField(readOnly: readOnly, controller: controller);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Duration waitDuration = SelectionOverlay.fadeDuration > kDoubleTapTimeout
|
||
? SelectionOverlay.fadeDuration
|
||
: kDoubleTapTimeout;
|
||
|
||
// Double tap to select the text.
|
||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||
await tester.pump(kDoubleTapTimeout ~/ 2);
|
||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||
await tester.pump(waitDuration);
|
||
|
||
// No error as in https://github.com/flutter/flutter/issues/170521.
|
||
|
||
// The Flutter-drawn context menu is shown. The SystemContextMenu is not
|
||
// shown because readOnly is true.
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||
expect(find.byType(SystemContextMenu), findsNothing);
|
||
|
||
// Turn off readOnly and hide the context menu.
|
||
setState(() {
|
||
readOnly = false;
|
||
});
|
||
await tester.tap(find.text('Copy'));
|
||
await tester.pump(waitDuration);
|
||
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||
expect(find.byType(SystemContextMenu), findsNothing);
|
||
|
||
// Double tap to show the context menu again.
|
||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||
await tester.pump(kDoubleTapTimeout ~/ 2);
|
||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||
await tester.pump(waitDuration);
|
||
|
||
// Now iOS is showing the SystemContextMenu while others continue to show
|
||
// the Flutter-drawn context menu.
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.iOS:
|
||
expect(find.byType(SystemContextMenu), findsOneWidget);
|
||
case TargetPlatform.macOS:
|
||
case TargetPlatform.android:
|
||
case TargetPlatform.fuchsia:
|
||
case TargetPlatform.linux:
|
||
case TargetPlatform.windows:
|
||
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||
}
|
||
},
|
||
variant: TargetPlatformVariant.all(),
|
||
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||
);
|
||
|
||
testWidgets('TextField does not crash at zero area', (WidgetTester tester) async {
|
||
tester.view.physicalSize = Size.zero;
|
||
final controller = TextEditingController(text: 'X');
|
||
addTearDown(tester.view.reset);
|
||
addTearDown(controller.dispose);
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Center(child: TextField(controller: controller)),
|
||
),
|
||
),
|
||
);
|
||
expect(tester.getSize(find.byType(TextField)), Size.zero);
|
||
controller.selection = const TextSelection.collapsed(offset: 0);
|
||
tester.pump();
|
||
});
|
||
|
||
testWidgets(
|
||
'Does not crash when editing value changes between consecutive scrolls',
|
||
(WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/179164.
|
||
final controller = TextEditingController(text: 'text ' * 10000);
|
||
addTearDown(controller.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller, maxLines: null)),
|
||
),
|
||
);
|
||
|
||
final Finder textField = find.byType(TextField);
|
||
final EditableTextState editableTextState = tester.state<EditableTextState>(
|
||
find.byType(EditableText),
|
||
);
|
||
// Long press to select the first word and show the toolbar.
|
||
await tester.longPressAt(textOffsetToPosition(tester, 0));
|
||
await tester.pumpAndSettle();
|
||
expect(editableTextState.selectionOverlay?.toolbarIsVisible, true);
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4));
|
||
|
||
// Scroll down so selection is not visible, and toolbar is scheduled to be shown
|
||
// when the selection is once again visible.
|
||
final TestGesture gesture = await tester.startGesture(tester.getCenter(textField));
|
||
await gesture.moveBy(const Offset(0.0, -200.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
|
||
// Scroll again before the post-frame callback from the first scroll is run to invalidate
|
||
// the data from the first scroll.
|
||
controller.value = const TextEditingValue(text: 'a different value');
|
||
|
||
await gesture.down(tester.getCenter(textField));
|
||
await gesture.moveBy(const Offset(0.0, -100.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
await tester.pump();
|
||
// This test should reach the end without crashing.
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: kIsWeb,
|
||
);
|
||
|
||
testWidgets(
|
||
'toolbar should not reappear when editing value changes during a scroll',
|
||
(WidgetTester tester) async {
|
||
// Regression test for https://github.com/flutter/flutter/issues/179164.
|
||
final controller = TextEditingController(text: 'text ' * 10000);
|
||
addTearDown(controller.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(child: TextField(controller: controller, maxLines: null)),
|
||
),
|
||
);
|
||
|
||
final Finder textField = find.byType(TextField);
|
||
final EditableTextState editableTextState = tester.state<EditableTextState>(
|
||
find.byType(EditableText),
|
||
);
|
||
// Long press to select the first word and show the toolbar.
|
||
await tester.longPressAt(textOffsetToPosition(tester, 0));
|
||
await tester.pumpAndSettle();
|
||
expect(editableTextState.selectionOverlay?.toolbarIsVisible, true);
|
||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4));
|
||
|
||
// Scroll down so selection is not visible, and toolbar is scheduled to be shown
|
||
// when the selection is once again visible.
|
||
final TestGesture gesture = await tester.startGesture(tester.getCenter(textField));
|
||
await gesture.moveBy(const Offset(0.0, -200.0));
|
||
await tester.pump();
|
||
await gesture.up();
|
||
// Change the editing value before the post-frame callback from the scroll is run,
|
||
// this should invalidate the data from the scroll and cause the toolbar to not
|
||
// reappear.
|
||
controller.value = const TextEditingValue(text: 'a different value');
|
||
// Pump and settle to allow postFrameCallbacks to complete.
|
||
await tester.pumpAndSettle();
|
||
expect(editableTextState.selectionOverlay?.toolbarIsVisible, false);
|
||
},
|
||
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
||
// [intended] only applies to platforms where we supply the context menu.
|
||
skip: kIsWeb,
|
||
);
|
||
}
|
||
|
||
/// A Simple widget for testing the obscure text.
|
||
class _ObscureTextTestWidget extends StatefulWidget {
|
||
const _ObscureTextTestWidget({required this.controller});
|
||
|
||
final TextEditingController controller;
|
||
@override
|
||
_ObscureTextTestWidgetState createState() => _ObscureTextTestWidgetState();
|
||
}
|
||
|
||
class _ObscureTextTestWidgetState extends State<_ObscureTextTestWidget> {
|
||
bool _obscureText = false;
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MaterialApp(
|
||
home: Scaffold(
|
||
body: Builder(
|
||
builder: (BuildContext context) {
|
||
return Column(
|
||
children: <Widget>[
|
||
TextField(obscureText: _obscureText, controller: widget.controller),
|
||
ElevatedButton(
|
||
onPressed: () => setState(() {
|
||
_obscureText = !_obscureText;
|
||
}),
|
||
child: const SizedBox.shrink(),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
typedef FormatEditUpdateCallback =
|
||
void Function(TextEditingValue oldValue, TextEditingValue newValue);
|
||
|
||
// On web, key events in text fields are handled by the browser.
|
||
const bool areKeyEventsHandledByPlatform = isBrowser;
|
||
|
||
class CupertinoLocalizationsDelegate extends LocalizationsDelegate<CupertinoLocalizations> {
|
||
@override
|
||
bool isSupported(Locale locale) => true;
|
||
|
||
@override
|
||
Future<CupertinoLocalizations> load(Locale locale) => DefaultCupertinoLocalizations.load(locale);
|
||
|
||
@override
|
||
bool shouldReload(CupertinoLocalizationsDelegate old) => false;
|
||
}
|
||
|
||
class MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
|
||
@override
|
||
bool isSupported(Locale locale) => true;
|
||
|
||
@override
|
||
Future<MaterialLocalizations> load(Locale locale) => DefaultMaterialLocalizations.load(locale);
|
||
|
||
@override
|
||
bool shouldReload(MaterialLocalizationsDelegate old) => false;
|
||
}
|
||
|
||
class WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
|
||
@override
|
||
bool isSupported(Locale locale) => true;
|
||
|
||
@override
|
||
Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale);
|
||
|
||
@override
|
||
bool shouldReload(WidgetsLocalizationsDelegate old) => false;
|
||
}
|
||
|
||
Widget overlay({required Widget child}) {
|
||
final entry = OverlayEntry(
|
||
builder: (BuildContext context) {
|
||
return Center(child: Material(child: child));
|
||
},
|
||
);
|
||
addTearDown(
|
||
() => entry
|
||
..remove()
|
||
..dispose(),
|
||
);
|
||
return overlayWithEntry(entry);
|
||
}
|
||
|
||
Widget overlayWithEntry(OverlayEntry entry) {
|
||
return Localizations(
|
||
locale: const Locale('en', 'US'),
|
||
delegates: <LocalizationsDelegate<dynamic>>[
|
||
WidgetsLocalizationsDelegate(),
|
||
MaterialLocalizationsDelegate(),
|
||
CupertinoLocalizationsDelegate(),
|
||
],
|
||
child: DefaultTextEditingShortcuts(
|
||
child: Directionality(
|
||
textDirection: TextDirection.ltr,
|
||
child: MediaQuery(
|
||
data: const MediaQueryData(size: Size(800.0, 600.0)),
|
||
child: Overlay(initialEntries: <OverlayEntry>[entry]),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget boilerplate({required Widget child, ThemeData? theme}) {
|
||
return MaterialApp(
|
||
theme: theme,
|
||
home: Localizations(
|
||
locale: const Locale('en', 'US'),
|
||
delegates: <LocalizationsDelegate<dynamic>>[
|
||
WidgetsLocalizationsDelegate(),
|
||
MaterialLocalizationsDelegate(),
|
||
],
|
||
child: Directionality(
|
||
textDirection: TextDirection.ltr,
|
||
child: MediaQuery(
|
||
data: const MediaQueryData(size: Size(800.0, 600.0)),
|
||
child: Center(child: Material(child: child)),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> skipPastScrollingAnimation(WidgetTester tester) async {
|
||
await tester.pump();
|
||
await tester.pump(const Duration(milliseconds: 200));
|
||
}
|
||
|
||
double getOpacity(WidgetTester tester, Finder finder) {
|
||
return tester
|
||
.widget<FadeTransition>(find.ancestor(of: finder, matching: find.byType(FadeTransition)))
|
||
.opacity
|
||
.value;
|
||
}
|
||
|
||
class TestFormatter extends TextInputFormatter {
|
||
TestFormatter(this.onFormatEditUpdate);
|
||
FormatEditUpdateCallback onFormatEditUpdate;
|
||
@override
|
||
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
||
onFormatEditUpdate(oldValue, newValue);
|
||
return newValue;
|
||
}
|
||
}
|
||
|
||
FocusNode _focusNode() {
|
||
final result = FocusNode();
|
||
addTearDown(result.dispose);
|
||
return result;
|
||
}
|
||
|
||
TextEditingController _textEditingController({String text = ''}) {
|
||
final result = TextEditingController(text: text);
|
||
addTearDown(result.dispose);
|
||
return result;
|
||
}
|