mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
This PR introduces the convenience widget `TestTextField` in the framework widget tests. This new widget should be used in place of convenient `TextField` or `CupertinoTextField` usages in the widgets library tests. part of: #177415 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Renzo Olivares <roliv@google.com>
299 lines
9.5 KiB
Dart
299 lines
9.5 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.
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
/// On web, the context menu (aka toolbar) is provided by the browser.
|
|
const bool isContextMenuProvidedByPlatform = isBrowser;
|
|
|
|
// Returns the RenderEditable at the given index, or the first if not given.
|
|
RenderEditable findRenderEditable(WidgetTester tester, {int index = 0}) {
|
|
final RenderObject root = tester.renderObject(find.byType(EditableText).at(index));
|
|
expect(root, isNotNull);
|
|
|
|
late RenderEditable renderEditable;
|
|
void recursiveFinder(RenderObject child) {
|
|
if (child is RenderEditable) {
|
|
renderEditable = child;
|
|
return;
|
|
}
|
|
child.visitChildren(recursiveFinder);
|
|
}
|
|
|
|
root.visitChildren(recursiveFinder);
|
|
expect(renderEditable, isNotNull);
|
|
return renderEditable;
|
|
}
|
|
|
|
List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) {
|
|
return points.map<TextSelectionPoint>((TextSelectionPoint point) {
|
|
return TextSelectionPoint(box.localToGlobal(point.point), point.direction);
|
|
}).toList();
|
|
}
|
|
|
|
Offset textOffsetToPosition(WidgetTester tester, int offset, {int index = 0}) {
|
|
final RenderEditable renderEditable = findRenderEditable(tester, index: index);
|
|
final List<TextSelectionPoint> endpoints = globalize(
|
|
renderEditable.getEndpointsForSelection(TextSelection.collapsed(offset: offset)),
|
|
renderEditable,
|
|
);
|
|
expect(endpoints.length, 1);
|
|
return endpoints[0].point + const Offset(kIsWeb ? 1.0 : 0.0, -2.0);
|
|
}
|
|
|
|
/// Mimic key press events by sending key down and key up events via the [tester].
|
|
Future<void> sendKeys(
|
|
WidgetTester tester,
|
|
List<LogicalKeyboardKey> keys, {
|
|
bool shift = false,
|
|
bool wordModifier = false,
|
|
bool lineModifier = false,
|
|
bool shortcutModifier = false,
|
|
required TargetPlatform targetPlatform,
|
|
}) async {
|
|
final targetPlatformString = targetPlatform.toString();
|
|
final String platform = targetPlatformString
|
|
.substring(targetPlatformString.indexOf('.') + 1)
|
|
.toLowerCase();
|
|
if (shift) {
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft, platform: platform);
|
|
}
|
|
if (shortcutModifier) {
|
|
await tester.sendKeyDownEvent(
|
|
platform == 'macos' || platform == 'ios'
|
|
? LogicalKeyboardKey.metaLeft
|
|
: LogicalKeyboardKey.controlLeft,
|
|
platform: platform,
|
|
);
|
|
}
|
|
if (wordModifier) {
|
|
await tester.sendKeyDownEvent(
|
|
platform == 'macos' || platform == 'ios'
|
|
? LogicalKeyboardKey.altLeft
|
|
: LogicalKeyboardKey.controlLeft,
|
|
platform: platform,
|
|
);
|
|
}
|
|
if (lineModifier) {
|
|
await tester.sendKeyDownEvent(
|
|
platform == 'macos' || platform == 'ios'
|
|
? LogicalKeyboardKey.metaLeft
|
|
: LogicalKeyboardKey.altLeft,
|
|
platform: platform,
|
|
);
|
|
}
|
|
for (final key in keys) {
|
|
await tester.sendKeyEvent(key, platform: platform);
|
|
await tester.pump();
|
|
}
|
|
if (lineModifier) {
|
|
await tester.sendKeyUpEvent(
|
|
platform == 'macos' || platform == 'ios'
|
|
? LogicalKeyboardKey.metaLeft
|
|
: LogicalKeyboardKey.altLeft,
|
|
platform: platform,
|
|
);
|
|
}
|
|
if (wordModifier) {
|
|
await tester.sendKeyUpEvent(
|
|
platform == 'macos' || platform == 'ios'
|
|
? LogicalKeyboardKey.altLeft
|
|
: LogicalKeyboardKey.controlLeft,
|
|
platform: platform,
|
|
);
|
|
}
|
|
if (shortcutModifier) {
|
|
await tester.sendKeyUpEvent(
|
|
platform == 'macos' || platform == 'ios'
|
|
? LogicalKeyboardKey.metaLeft
|
|
: LogicalKeyboardKey.controlLeft,
|
|
platform: platform,
|
|
);
|
|
}
|
|
if (shift) {
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft, platform: platform);
|
|
}
|
|
if (shift || wordModifier || lineModifier) {
|
|
await tester.pump();
|
|
}
|
|
}
|
|
|
|
// Simple controller that builds a WidgetSpan with 100 height.
|
|
class OverflowWidgetTextEditingController extends TextEditingController {
|
|
@override
|
|
TextSpan buildTextSpan({
|
|
required BuildContext context,
|
|
TextStyle? style,
|
|
required bool withComposing,
|
|
}) {
|
|
return TextSpan(
|
|
style: style,
|
|
children: <InlineSpan>[
|
|
const TextSpan(text: 'Hi'),
|
|
WidgetSpan(child: Container(color: const Color(0xffff0000), height: 100.0)),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A minimal wrapper around [EditableText] for use in widget tests.
|
|
///
|
|
/// This widget provides robust text selection gestures through
|
|
/// [TextSelectionGestureDetector].
|
|
///
|
|
/// This widget does not provide text selection context menus out of the box.
|
|
/// They can be added by providing [contextMenuBuilder].
|
|
///
|
|
/// This widget does not provide a concrete implementation of text selection handles
|
|
/// out of the box, and instead uses [testTextSelectionHandleControls] as a default.
|
|
/// A more robust implementation of text selection handles can be provided by
|
|
/// setting [selectionControls].
|
|
///
|
|
/// This input field manages its own internal [TextEditingController]
|
|
/// and [FocusNode] unless provided one. This field also provides defaults
|
|
/// for required [EditableText] members [EditableText.cursorColor],
|
|
/// [EditableText.backgroundCursorColor], and [EditableText.style].
|
|
class TestTextField extends StatefulWidget {
|
|
const TestTextField({
|
|
super.key,
|
|
this.autofillHints = const <String>[],
|
|
this.autofocus = false,
|
|
this.contextMenuBuilder,
|
|
this.cursorColor,
|
|
this.cursorOpacityAnimates = false,
|
|
this.focusNode,
|
|
this.groupId = EditableText,
|
|
this.maxLines = 1,
|
|
this.onChanged,
|
|
this.readOnly = false,
|
|
this.selectionControls,
|
|
this.showCursor,
|
|
this.style,
|
|
this.controller,
|
|
});
|
|
|
|
final Iterable<String>? autofillHints;
|
|
final bool autofocus;
|
|
final EditableTextContextMenuBuilder? contextMenuBuilder;
|
|
final Color? cursorColor;
|
|
final bool cursorOpacityAnimates;
|
|
final FocusNode? focusNode;
|
|
final Object groupId;
|
|
final int? maxLines;
|
|
final ValueChanged<String>? onChanged;
|
|
final bool readOnly;
|
|
final TextSelectionControls? selectionControls;
|
|
final bool? showCursor;
|
|
final TextStyle? style;
|
|
final TextEditingController? controller;
|
|
|
|
@override
|
|
State<TestTextField> createState() => _TestTextFieldState();
|
|
}
|
|
|
|
class _TestTextFieldState extends State<TestTextField>
|
|
implements TextSelectionGestureDetectorBuilderDelegate {
|
|
TextEditingController? _controller;
|
|
TextEditingController get _effectiveController =>
|
|
widget.controller ?? (_controller ??= TextEditingController());
|
|
FocusNode? _focusNode;
|
|
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
|
|
|
|
late _TestTextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
|
|
|
|
// API for TextSelectionGestureDetectorBuilderDelegate.
|
|
@override
|
|
bool get forcePressEnabled => false;
|
|
|
|
@override
|
|
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
|
|
|
|
@override
|
|
bool get selectionEnabled => true;
|
|
// End of API for TextSelectionGestureDetectorBuilderDelegate.
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectionGestureDetectorBuilder = _TestTextFieldSelectionGestureDetectorBuilder(state: this);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_focusNode?.dispose();
|
|
_controller?.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
static const Color _red = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return _selectionGestureDetectorBuilder.buildGestureDetector(
|
|
behavior: HitTestBehavior.translucent,
|
|
child: EditableText(
|
|
key: editableTextKey,
|
|
autofillHints: widget.autofillHints,
|
|
autofocus: widget.autofocus,
|
|
backgroundCursorColor: _red, // Colors.red, required by editable text.
|
|
contextMenuBuilder: widget.contextMenuBuilder,
|
|
cursorColor: widget.cursorColor ?? _red, // Colors.red, required by editable text.
|
|
cursorOpacityAnimates: widget.cursorOpacityAnimates,
|
|
focusNode: _effectiveFocusNode, // required by editable text.
|
|
groupId: widget.groupId,
|
|
maxLines: widget.maxLines,
|
|
onChanged: widget.onChanged,
|
|
readOnly: widget.readOnly,
|
|
rendererIgnoresPointer: true, // gestures are provided by text selection gesture detector.
|
|
selectionControls: widget.selectionControls ?? testTextSelectionHandleControls,
|
|
showCursor: widget.showCursor,
|
|
style: widget.style ?? const TextStyle(), // required by editable text.
|
|
controller: _effectiveController, // required by editable text.
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TestTextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
|
|
_TestTextFieldSelectionGestureDetectorBuilder({required _TestTextFieldState state})
|
|
: super(delegate: state);
|
|
|
|
@override
|
|
void onUserTap() {
|
|
// BasicTestTextField does not have an onTap callback.
|
|
}
|
|
|
|
@override
|
|
bool get onUserTapAlwaysCalled => false;
|
|
}
|
|
|
|
class TestTextSelectionHandleControls extends TextSelectionControls
|
|
with TextSelectionHandleControls {
|
|
@override
|
|
Widget buildHandle(
|
|
BuildContext context,
|
|
TextSelectionHandleType type,
|
|
double textLineHeight, [
|
|
VoidCallback? onTap,
|
|
]) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
@override
|
|
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
|
|
return Offset.zero;
|
|
}
|
|
|
|
@override
|
|
Size getHandleSize(double textLineHeight) {
|
|
return Size.zero;
|
|
}
|
|
}
|
|
|
|
final TextSelectionControls testTextSelectionHandleControls = TestTextSelectionHandleControls();
|