blob: 2dba2fe4d43e770422822b6972e205afff21d23e [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';
import 'package:mockito/mockito.dart';
import 'semantics_tester.dart';
void main() {
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
final FocusScopeNode focusScopeNode = FocusScopeNode();
const TextStyle textStyle = TextStyle();
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
setUp(() {
debugResetSemanticsIdCounter();
});
// Tests that the desired keyboard action button is requested.
//
// More technically, when an EditableText is given a particular [action], Flutter
// requests [serializedActionName] when attaching to the platform's input
// system.
Future<Null> _desiredKeyboardActionIsRequested({
WidgetTester tester,
TextInputAction action,
String serializedActionName,
}) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
focusNode: focusNode,
textInputAction: action,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals(serializedActionName));
}
testWidgets('has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
);
final EditableText editableText =
tester.firstWidget(find.byType(EditableText));
expect(editableText.maxLines, equals(1));
expect(editableText.obscureText, isFalse);
expect(editableText.autocorrect, isTrue);
expect(editableText.cursorWidth, 2.0);
});
testWidgets('cursor has expected width and radius',
(WidgetTester tester) async {
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
cursorWidth: 10.0,
cursorRadius: const Radius.circular(2.0),
)));
final EditableText editableText =
tester.firstWidget(find.byType(EditableText));
expect(editableText.cursorWidth, 10.0);
expect(editableText.cursorRadius.x, 2.0);
});
testWidgets('text keyboard is requested when maxLines is default',
(WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
final EditableText editableText =
tester.firstWidget(find.byType(EditableText));
expect(editableText.maxLines, equals(1));
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'],
equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done'));
});
testWidgets(
'Keyboard is configured for "unspecified" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.unspecified,
serializedActionName: 'TextInputAction.unspecified',
);
});
testWidgets(
'Keyboard is configured for "none" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.none,
serializedActionName: 'TextInputAction.none',
);
});
testWidgets(
'Keyboard is configured for "done" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.done,
serializedActionName: 'TextInputAction.done',
);
});
testWidgets(
'Keyboard is configured for "send" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.send,
serializedActionName: 'TextInputAction.send',
);
});
testWidgets(
'Keyboard is configured for "go" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.go,
serializedActionName: 'TextInputAction.go',
);
});
testWidgets(
'Keyboard is configured for "search" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.search,
serializedActionName: 'TextInputAction.search',
);
});
testWidgets(
'Keyboard is configured for "send" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.send,
serializedActionName: 'TextInputAction.send',
);
});
testWidgets(
'Keyboard is configured for "next" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.next,
serializedActionName: 'TextInputAction.next',
);
});
testWidgets(
'Keyboard is configured for "previous" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.previous,
serializedActionName: 'TextInputAction.previous',
);
});
testWidgets(
'Keyboard is configured for "continue" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.continueAction,
serializedActionName: 'TextInputAction.continueAction',
);
});
testWidgets(
'Keyboard is configured for "join" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.join,
serializedActionName: 'TextInputAction.join',
);
});
testWidgets(
'Keyboard is configured for "route" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.route,
serializedActionName: 'TextInputAction.route',
);
});
testWidgets(
'Keyboard is configured for "emergencyCall" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.emergencyCall,
serializedActionName: 'TextInputAction.emergencyCall',
);
});
testWidgets('multiline keyboard is requested when set explicitly',
(WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
focusNode: focusNode,
keyboardType: TextInputType.multiline,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'],
equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.newline'));
});
testWidgets('Multiline keyboard with newline action is requested when maxLines = null', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
focusNode: focusNode,
maxLines: null,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'],
equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.newline'));
});
testWidgets('Text keyboard is requested when explicitly set and maxLines = null', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
focusNode: focusNode,
maxLines: null,
keyboardType: TextInputType.text,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'],
equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done'));
});
testWidgets(
'Correct keyboard is requested when set explicitly and maxLines > 1',
(WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
focusNode: focusNode,
keyboardType: TextInputType.phone,
maxLines: 3,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'],
equals('TextInputType.phone'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done'));
});
testWidgets('multiline keyboard is requested when set implicitly',
(WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
focusNode: focusNode,
maxLines: 3, // Sets multiline keyboard implicitly.
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'],
equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.newline'));
});
testWidgets('single line inputs have correct default keyboard',
(WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
focusNode: focusNode,
maxLines: 1, // Sets text keyboard implicitly.
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'],
equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done'));
});
testWidgets('Fires onChanged when text changes via TextSelectionOverlay',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
String changedValue;
final Widget widget = MaterialApp(
home: EditableText(
key: editableTextKey,
controller: TextEditingController(),
focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {
changedValue = value;
},
),
);
await tester.pumpWidget(widget);
// Populate a fake clipboard.
const String clipboardContent = 'Dobunezumi mitai ni utsukushiku naritai';
SystemChannels.platform
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.getData')
return const <String, dynamic>{'text': clipboardContent};
return null;
});
// Long-press to bring up the text editing controls.
final Finder textFinder = find.byKey(editableTextKey);
await tester.longPress(textFinder);
await tester.pump();
await tester.tap(find.text('PASTE'));
await tester.pump();
expect(changedValue, clipboardContent);
});
testWidgets('cursor layout has correct width', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
String changedValue;
final Widget widget = MaterialApp(
home: RepaintBoundary(
key: const ValueKey<int>(1),
child: EditableText(
key: editableTextKey,
controller: TextEditingController(),
focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {
changedValue = value;
},
cursorWidth: 15.0,
),
),
);
await tester.pumpWidget(widget);
// Populate a fake clipboard.
const String clipboardContent = ' ';
SystemChannels.platform
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.getData')
return const <String, dynamic>{'text': clipboardContent};
return null;
});
// Long-press to bring up the text editing controls.
final Finder textFinder = find.byKey(editableTextKey);
await tester.longPress(textFinder);
await tester.pump();
await tester.tap(find.text('PASTE'));
await tester.pump();
expect(changedValue, clipboardContent);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('editable_text_test.0.0.png'),
);
}, skip: !Platform.isLinux);
testWidgets('cursor layout has correct radius', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
String changedValue;
final Widget widget = MaterialApp(
home: RepaintBoundary(
key: const ValueKey<int>(1),
child: EditableText(
key: editableTextKey,
controller: TextEditingController(),
focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {
changedValue = value;
},
cursorWidth: 15.0,
cursorRadius: const Radius.circular(3.0),
),
),
);
await tester.pumpWidget(widget);
// Populate a fake clipboard.
const String clipboardContent = ' ';
SystemChannels.platform
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.getData')
return const <String, dynamic>{'text': clipboardContent};
return null;
});
// Long-press to bring up the text editing controls.
final Finder textFinder = find.byKey(editableTextKey);
await tester.longPress(textFinder);
await tester.pump();
await tester.tap(find.text('PASTE'));
await tester.pump();
expect(changedValue, clipboardContent);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('editable_text_test.1.0.png'),
);
}, skip: !Platform.isLinux);
testWidgets('Does not lose focus by default when "next" action is pressed',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
final FocusNode focusNode = FocusNode();
final Widget widget = MaterialApp(
home: EditableText(
key: editableTextKey,
controller: TextEditingController(),
focusNode: focusNode,
style: Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
await tester.testTextInput.receiveAction(TextInputAction.next);
await tester.pump();
// Still has focus after pressing "next".
expect(focusNode.hasFocus, true);
});
testWidgets(
'Does not lose focus by default when "done" action is pressed and onEditingComplete is provided',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
final FocusNode focusNode = FocusNode();
final Widget widget = MaterialApp(
home: EditableText(
key: editableTextKey,
controller: TextEditingController(),
focusNode: focusNode,
style: Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onEditingComplete: () {
// This prevents the default focus change behavior on submission.
},
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
// Still has focus even though "done" was pressed because onEditingComplete
// was provided and it overrides the default behavior.
expect(focusNode.hasFocus, true);
});
testWidgets(
'When "done" is pressed callbacks are invoked: onEditingComplete > onSubmitted',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
final FocusNode focusNode = FocusNode();
bool onEditingCompleteCalled = false;
bool onSubmittedCalled = false;
final Widget widget = MaterialApp(
home: EditableText(
key: editableTextKey,
controller: TextEditingController(),
focusNode: focusNode,
style: Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
onEditingComplete: () {
onEditingCompleteCalled = true;
expect(onSubmittedCalled, false);
},
onSubmitted: (String value) {
onSubmittedCalled = true;
expect(onEditingCompleteCalled, true);
},
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
// The execution path starting with receiveAction() will trigger the
// onEditingComplete and onSubmission callbacks.
await tester.testTextInput.receiveAction(TextInputAction.done);
// The expectations we care about are up above in the onEditingComplete
// and onSubmission callbacks.
});
testWidgets(
'When "next" is pressed callbacks are invoked: onEditingComplete > onSubmitted',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
final FocusNode focusNode = FocusNode();
bool onEditingCompleteCalled = false;
bool onSubmittedCalled = false;
final Widget widget = MaterialApp(
home: EditableText(
key: editableTextKey,
controller: TextEditingController(),
focusNode: focusNode,
style: Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
onEditingComplete: () {
onEditingCompleteCalled = true;
assert(!onSubmittedCalled);
},
onSubmitted: (String value) {
onSubmittedCalled = true;
assert(onEditingCompleteCalled);
},
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
// The execution path starting with receiveAction() will trigger the
// onEditingComplete and onSubmission callbacks.
await tester.testTextInput.receiveAction(TextInputAction.done);
// The expectations we care about are up above in the onEditingComplete
// and onSubmission callbacks.
});
testWidgets('Changing controller updates EditableText',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
final TextEditingController controller1 =
TextEditingController(text: 'Wibble');
final TextEditingController controller2 =
TextEditingController(text: 'Wobble');
TextEditingController currentController = controller1;
StateSetter setState;
Widget builder() {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Material(
child: EditableText(
key: editableTextKey,
controller: currentController,
focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.android)
.black
.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {},
),
),
),
);
},
);
}
await tester.pumpWidget(builder());
await tester.showKeyboard(find.byType(EditableText));
// Verify TextInput.setEditingState is fired with updated text when controller is replaced.
final List<MethodCall> log = <MethodCall>[];
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
setState(() {
currentController = controller2;
});
await tester.pump();
expect(log, hasLength(1));
expect(
log.single,
isMethodCall(
'TextInput.setEditingState',
arguments: const <String, dynamic>{
'text': 'Wobble',
'selectionBase': -1,
'selectionExtent': -1,
'selectionAffinity': 'TextAffinity.downstream',
'selectionIsDirectional': false,
'composingBase': -1,
'composingExtent': -1,
},
));
});
testWidgets('EditableText identifies as text field (w/ focus) in semantics',
(WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
expect(semantics,
includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField]));
await tester.tap(find.byType(EditableText));
await tester.idle();
await tester.pump();
expect(
semantics,
includesNodeWith(flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused
]));
semantics.dispose();
});
testWidgets('EditableText includes text as value in semantics',
(WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const String value1 = 'EditableText content';
controller.text = value1;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
child: EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
expect(
semantics,
includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
value: value1,
));
const String value2 = 'Changed the EditableText content';
controller.text = value2;
await tester.idle();
await tester.pump();
expect(
semantics,
includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
value: value2,
));
semantics.dispose();
});
testWidgets('changing selection with keyboard does not show handles',
(WidgetTester tester) async {
const String value1 = 'Hello World';
controller.text = value1;
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
selectionControls: materialTextSelectionControls,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
);
// Simulate selection change via tap to show handles.
final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
render.onSelectionChanged(const TextSelection.collapsed(offset: 4), render,
SelectionChangedCause.tap);
await tester.pumpAndSettle();
final EditableTextState textState = tester.state(find.byType(EditableText));
expect(textState.selectionOverlay.handlesAreVisible, isTrue);
expect(
textState.selectionOverlay.selectionDelegate.textEditingValue.selection,
const TextSelection.collapsed(offset: 4));
// Simulate selection change via keyboard and expect handles to disappear.
render.onSelectionChanged(const TextSelection.collapsed(offset: 10), render,
SelectionChangedCause.keyboard);
await tester.pumpAndSettle();
expect(textState.selectionOverlay.handlesAreVisible, isFalse);
expect(
textState.selectionOverlay.selectionDelegate.textEditingValue.selection,
const TextSelection.collapsed(offset: 10));
});
testWidgets('exposes correct cursor movement semantics',
(WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
controller.text = 'test';
await tester.pumpWidget(MaterialApp(
home: EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
expect(
semantics,
includesNodeWith(
value: 'test',
));
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
await tester.pumpAndSettle();
// At end, can only go backwards.
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
],
));
controller.selection =
TextSelection.collapsed(offset: controller.text.length - 2);
await tester.pumpAndSettle();
// Somewhere in the middle, can go in both directions.
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
));
controller.selection = const TextSelection.collapsed(offset: 0);
await tester.pumpAndSettle();
// At beginning, can only go forward.
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
));
semantics.dispose();
});
testWidgets('can move cursor with a11y means - character', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const bool doNotExtendSelection = false;
controller.text = 'test';
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
await tester.pumpWidget(MaterialApp(
home: EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
],
));
final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
final int semanticsId = render.debugSemantics.id;
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 4);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 3);
expect(controller.selection.extentOffset, 3);
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 0);
await tester.pumpAndSettle();
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1);
expect(controller.selection.extentOffset, 1);
semantics.dispose();
});
testWidgets('can move cursor with a11y means - word', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const bool doNotExtendSelection = false;
controller.text = 'test for words';
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
await tester.pumpWidget(MaterialApp(
home: EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
expect(
semantics,
includesNodeWith(
value: 'test for words',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
],
));
final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
final int semanticsId = render.debugSemantics.id;
expect(controller.selection.baseOffset, 14);
expect(controller.selection.extentOffset, 14);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 9);
expect(controller.selection.extentOffset, 9);
expect(
semantics,
includesNodeWith(
value: 'test for words',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 0);
await tester.pumpAndSettle();
expect(
semantics,
includesNodeWith(
value: 'test for words',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByWord, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByWord, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 9);
expect(controller.selection.extentOffset, 9);
semantics.dispose();
});
testWidgets('can extend selection with a11y means - character',
(WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const bool extendSelection = true;
const bool doNotExtendSelection = false;
controller.text = 'test';
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
await tester.pumpWidget(MaterialApp(
home: EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
],
));
final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
final int semanticsId = render.debugSemantics.id;
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 4);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 3);
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 0);
await tester.pumpAndSettle();
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1);
expect(controller.selection.extentOffset, 1);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1);
expect(controller.selection.extentOffset, 2);
semantics.dispose();
});
testWidgets('can extend selection with a11y means - word',
(WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const bool extendSelection = true;
const bool doNotExtendSelection = false;
controller.text = 'test for words';
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
await tester.pumpWidget(MaterialApp(
home: EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
expect(
semantics,
includesNodeWith(
value: 'test for words',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
],
));
final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
final int semanticsId = render.debugSemantics.id;
expect(controller.selection.baseOffset, 14);
expect(controller.selection.extentOffset, 14);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 14);
expect(controller.selection.extentOffset, 9);
expect(
semantics,
includesNodeWith(
value: 'test for words',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 14);
expect(controller.selection.extentOffset, 5);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByWord, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 14);
expect(controller.selection.extentOffset, 0);
await tester.pumpAndSettle();
expect(
semantics,
includesNodeWith(
value: 'test for words',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.moveCursorForwardByWord,
SemanticsAction.setSelection,
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByWord, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByWord, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 9);
semantics.dispose();
});
testWidgets('password fields have correct semantics',
(WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
controller.text = 'super-secret-password!!1';
await tester.pumpWidget(MaterialApp(
home: EditableText(
obscureText: true,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
final String expectedValue = '•' * controller.text.length;
expect(
semantics,
hasSemantics(
TestSemantics(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isObscured
],
value: expectedValue,
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true));
semantics.dispose();
});
group('a11y copy/cut/paste', () {
Future<Null> _buildApp(
MockTextSelectionControls controls, WidgetTester tester) {
return tester.pumpWidget(MaterialApp(
home: EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: controls,
),
));
}
MockTextSelectionControls controls;
setUp(() {
controller.text = 'test';
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
controls = MockTextSelectionControls();
when(controls.buildHandle(any, any, any)).thenReturn(Container());
when(controls.buildToolbar(any, any, any, any))
.thenReturn(Container());
});
testWidgets('are exposed', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
when(controls.canCopy(any)).thenReturn(false);
when(controls.canCut(any)).thenReturn(false);
when(controls.canPaste(any)).thenReturn(false);
await _buildApp(controls, tester);
await tester.tap(find.byType(EditableText));
await tester.pump();
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
],
));
when(controls.canCopy(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.copy,
],
));
when(controls.canCopy(any)).thenReturn(false);
when(controls.canPaste(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.paste,
],
));
when(controls.canPaste(any)).thenReturn(false);
when(controls.canCut(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.cut,
],
));
when(controls.canCopy(any)).thenReturn(true);
when(controls.canCut(any)).thenReturn(true);
when(controls.canPaste(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.cut,
SemanticsAction.copy,
SemanticsAction.paste,
],
));
semantics.dispose();
});
testWidgets('can copy/cut/paste with a11y', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
when(controls.canCopy(any)).thenReturn(true);
when(controls.canCut(any)).thenReturn(true);
when(controls.canPaste(any)).thenReturn(true);
await _buildApp(controls, tester);
await tester.tap(find.byType(EditableText));
await tester.pump();
final SemanticsOwner owner = tester.binding.pipelineOwner.semanticsOwner;
const int expectedNodeId = 4;
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
children: <TestSemantics>[
TestSemantics(
id: 2,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics.rootChild(
id: expectedNodeId,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused
],
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorBackwardByWord,
SemanticsAction.setSelection,
SemanticsAction.copy,
SemanticsAction.cut,
SemanticsAction.paste
],
value: 'test',
textSelection: TextSelection.collapsed(
offset: controller.text.length),
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true));
owner.performAction(expectedNodeId, SemanticsAction.copy);
verify(controls.handleCopy(any)).called(1);
owner.performAction(expectedNodeId, SemanticsAction.cut);
verify(controls.handleCut(any)).called(1);
owner.performAction(expectedNodeId, SemanticsAction.paste);
verify(controls.handlePaste(any)).called(1);
semantics.dispose();
});
});
testWidgets('allows customizing text style in subclasses',
(WidgetTester tester) async {
controller.text = 'Hello World';
await tester.pumpWidget(MaterialApp(
home: CustomStyleEditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
// Simulate selection change via tap to show handles.
final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
expect(render.text.style.fontStyle, FontStyle.italic);
});
testWidgets('autofocus sets cursor to the end of text',
(WidgetTester tester) async {
const String text = 'hello world';
final FocusScopeNode focusScopeNode = FocusScopeNode();
final FocusNode focusNode = FocusNode();
controller.text = text;
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
focusNode: focusNode,
autofocus: true,
style: textStyle,
cursorColor: cursorColor,
),
),
));
expect(focusNode.hasFocus, true);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, text.length);
});
}
class MockTextSelectionControls extends Mock implements TextSelectionControls {}
class CustomStyleEditableText extends EditableText {
CustomStyleEditableText({
TextEditingController controller,
Color cursorColor,
FocusNode focusNode,
TextStyle style,
}) : super(
controller: controller,
cursorColor: cursorColor,
focusNode: focusNode,
style: style,
);
@override
CustomStyleEditableTextState createState() =>
CustomStyleEditableTextState();
}
class CustomStyleEditableTextState extends EditableTextState {
@override
TextSpan buildTextSpan() {
return TextSpan(
style: const TextStyle(fontStyle: FontStyle.italic),
text: widget.controller.value.text,
);
}
}