From 73e534545fdaeee80ee89fac54cbc89d59547c22 Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:30:59 -0400 Subject: [PATCH 01/15] provide a way to globally override the context menu in the text selection theme --- .../material_ui/lib/src/search_anchor.dart | 30 +- .../material_ui/lib/src/selectable_text.dart | 24 +- packages/material_ui/lib/src/text_field.dart | 30 +- .../material_ui/lib/src/text_form_field.dart | 8 +- .../lib/src/text_selection_theme.dart | 51 +- .../material_ui/test/search_anchor_test.dart | 211 +- .../test/selectable_text_test.dart | 1216 ++++--- .../material_ui/test/text_field_test.dart | 3141 +++++++++-------- .../test/text_form_field_test.dart | 116 + .../test/text_selection_theme_test.dart | 105 +- 10 files changed, 2873 insertions(+), 2059 deletions(-) diff --git a/packages/material_ui/lib/src/search_anchor.dart b/packages/material_ui/lib/src/search_anchor.dart index 845de4d3ddb1..eefb478a859e 100644 --- a/packages/material_ui/lib/src/search_anchor.dart +++ b/packages/material_ui/lib/src/search_anchor.dart @@ -11,6 +11,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/src/material/text_selection_theme.dart'; import 'package:flutter/widgets.dart'; import 'adaptive_text_selection_toolbar.dart'; @@ -1309,7 +1310,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor { super.textInputAction, super.keyboardType, EdgeInsets scrollPadding = const EdgeInsets.all(20.0), - EditableTextContextMenuBuilder contextMenuBuilder = SearchBar._defaultContextMenuBuilder, + EditableTextContextMenuBuilder contextMenuBuilder = _SearchBarState._defaultContextMenuBuilder, super.enabled, super.smartDashesType, super.smartQuotesType, @@ -1486,7 +1487,7 @@ class SearchBar extends StatefulWidget { this.textInputAction, this.keyboardType, this.scrollPadding = const EdgeInsets.all(20.0), - this.contextMenuBuilder = _defaultContextMenuBuilder, + this.contextMenuBuilder, this.readOnly = false, this.smartDashesType, this.smartQuotesType, @@ -1678,16 +1679,6 @@ class SearchBar extends StatefulWidget { /// configuration option on a standalone [TextField]. final SmartQuotesType? smartQuotesType; - static Widget _defaultContextMenuBuilder( - BuildContext context, - EditableTextState editableTextState, - ) { - if (SystemContextMenu.isSupportedByField(editableTextState)) { - return SystemContextMenu.editableText(editableTextState: editableTextState); - } - return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); - } - @override State createState() => _SearchBarState(); } @@ -1713,6 +1704,16 @@ class _SearchBarState extends State { super.dispose(); } + static Widget _defaultContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + if (SystemContextMenu.isSupportedByField(editableTextState)) { + return SystemContextMenu.editableText(editableTextState: editableTextState); + } + return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); + } + @override Widget build(BuildContext context) { final TextDirection textDirection = Directionality.of(context); @@ -1875,7 +1876,10 @@ class _SearchBarState extends State { textInputAction: widget.textInputAction, keyboardType: widget.keyboardType, scrollPadding: widget.scrollPadding, - contextMenuBuilder: widget.contextMenuBuilder, + contextMenuBuilder: + widget.contextMenuBuilder ?? + TextSelectionTheme.of(context).contextMenuBuilder ?? + _defaultContextMenuBuilder, smartDashesType: widget.smartDashesType, smartQuotesType: widget.smartQuotesType, ), diff --git a/packages/material_ui/lib/src/selectable_text.dart b/packages/material_ui/lib/src/selectable_text.dart index 171b9b83547e..b46924e38829 100644 --- a/packages/material_ui/lib/src/selectable_text.dart +++ b/packages/material_ui/lib/src/selectable_text.dart @@ -15,6 +15,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/src/material/text_selection_theme.dart'; import 'adaptive_text_selection_toolbar.dart'; import 'desktop_text_selection.dart'; @@ -204,7 +205,7 @@ class SelectableText extends StatefulWidget { this.textHeightBehavior, this.textWidthBasis, this.onSelectionChanged, - this.contextMenuBuilder = _defaultContextMenuBuilder, + this.contextMenuBuilder, this.magnifierConfiguration, }) : assert(maxLines == null || maxLines > 0), assert(minLines == null || minLines > 0), @@ -263,7 +264,7 @@ class SelectableText extends StatefulWidget { this.textHeightBehavior, this.textWidthBasis, this.onSelectionChanged, - this.contextMenuBuilder = _defaultContextMenuBuilder, + this.contextMenuBuilder, this.magnifierConfiguration, }) : assert(maxLines == null || maxLines > 0), assert(minLines == null || minLines > 0), @@ -451,13 +452,6 @@ class SelectableText extends StatefulWidget { /// {@macro flutter.widgets.EditableText.contextMenuBuilder} final EditableTextContextMenuBuilder? contextMenuBuilder; - static Widget _defaultContextMenuBuilder( - BuildContext context, - EditableTextState editableTextState, - ) { - return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); - } - /// The configuration for the magnifier used when the text is selected. /// /// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] @@ -674,6 +668,13 @@ class _SelectableTextState extends State return false; } + static Widget _defaultContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); + } + @override Widget build(BuildContext context) { // TODO(garyq): Assert to block WidgetSpans from being used here are removed, @@ -805,7 +806,10 @@ class _SelectableTextState extends State scrollPhysics: widget.scrollPhysics, scrollBehavior: widget.scrollBehavior, autofillHints: null, - contextMenuBuilder: widget.contextMenuBuilder, + contextMenuBuilder: + widget.contextMenuBuilder ?? + TextSelectionTheme.of(context).contextMenuBuilder ?? + _defaultContextMenuBuilder, ), ); diff --git a/packages/material_ui/lib/src/text_field.dart b/packages/material_ui/lib/src/text_field.dart index 7e3ff9146ac6..063a213697a7 100644 --- a/packages/material_ui/lib/src/text_field.dart +++ b/packages/material_ui/lib/src/text_field.dart @@ -16,6 +16,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/src/material/text_selection_theme.dart'; import 'adaptive_text_selection_toolbar.dart'; import 'color_scheme.dart'; @@ -342,7 +343,7 @@ class TextField extends StatefulWidget { this.stylusHandwritingEnabled = EditableText.defaultStylusHandwritingEnabled, this.enableIMEPersonalizedLearning = true, this.enableInlinePrediction, - this.contextMenuBuilder = _defaultContextMenuBuilder, + this.contextMenuBuilder, this.canRequestFocus = true, this.spellCheckConfiguration, this.magnifierConfiguration, @@ -888,6 +889,8 @@ class TextField extends StatefulWidget { /// * [AdaptiveTextSelectionToolbar], which is built by default. /// * [BrowserContextMenu], which allows the browser's context menu on web to /// be disabled and Flutter-rendered context menus to appear. + /// * [ThemeData.textSelectionTheme], which provides a way to override the + /// default context menu. final EditableTextContextMenuBuilder? contextMenuBuilder; /// Determine whether this text field can request the primary focus. @@ -903,16 +906,6 @@ class TextField extends StatefulWidget { /// {@macro flutter.services.TextInputConfiguration.hintLocales} final List? hintLocales; - static Widget _defaultContextMenuBuilder( - BuildContext context, - EditableTextState editableTextState, - ) { - if (SystemContextMenu.isSupportedByField(editableTextState)) { - return SystemContextMenu.editableText(editableTextState: editableTextState); - } - return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); - } - /// {@macro flutter.widgets.EditableText.spellCheckConfiguration} /// /// If [SpellCheckConfiguration.misspelledTextStyle] is not specified in this @@ -1541,6 +1534,16 @@ class _TextFieldState extends State return providedStyle.merge(stateStyle); } + static Widget _defaultContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + if (SystemContextMenu.isSupportedByField(editableTextState)) { + return SystemContextMenu.editableText(editableTextState: editableTextState); + } + return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); + } + @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); @@ -1767,7 +1770,10 @@ class _TextFieldState extends State enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, enableInlinePrediction: widget.enableInlinePrediction, contentInsertionConfiguration: widget.contentInsertionConfiguration, - contextMenuBuilder: widget.contextMenuBuilder, + contextMenuBuilder: + widget.contextMenuBuilder ?? + TextSelectionTheme.of(context).contextMenuBuilder ?? + _defaultContextMenuBuilder, spellCheckConfiguration: spellCheckConfiguration, magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration, diff --git a/packages/material_ui/lib/src/text_form_field.dart b/packages/material_ui/lib/src/text_form_field.dart index 43139398487c..4b6b9ca53976 100644 --- a/packages/material_ui/lib/src/text_form_field.dart +++ b/packages/material_ui/lib/src/text_form_field.dart @@ -12,6 +12,7 @@ import 'adaptive_text_selection_toolbar.dart'; import 'input_decorator.dart'; import 'material_state.dart'; import 'text_field.dart'; +import 'text_selection_theme.dart'; export 'package:flutter/services.dart' show SmartDashesType, SmartQuotesType; @@ -187,7 +188,7 @@ class TextFormField extends FormField { super.restorationId, bool enableIMEPersonalizedLearning = true, MouseCursor? mouseCursor, - EditableTextContextMenuBuilder? contextMenuBuilder = _defaultContextMenuBuilder, + EditableTextContextMenuBuilder? contextMenuBuilder, SpellCheckConfiguration? spellCheckConfiguration, TextMagnifierConfiguration? magnifierConfiguration, UndoHistoryController? undoController, @@ -309,7 +310,10 @@ class TextFormField extends FormField { scrollController: scrollController, enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, mouseCursor: mouseCursor, - contextMenuBuilder: contextMenuBuilder, + contextMenuBuilder: + contextMenuBuilder ?? + TextSelectionTheme.of(state.context).contextMenuBuilder ?? + _defaultContextMenuBuilder, spellCheckConfiguration: spellCheckConfiguration, magnifierConfiguration: magnifierConfiguration, undoController: undoController, diff --git a/packages/material_ui/lib/src/text_selection_theme.dart b/packages/material_ui/lib/src/text_selection_theme.dart index a90d96eba5d7..253b10e99bf8 100644 --- a/packages/material_ui/lib/src/text_selection_theme.dart +++ b/packages/material_ui/lib/src/text_selection_theme.dart @@ -36,7 +36,12 @@ import 'theme.dart'; @immutable class TextSelectionThemeData with Diagnosticable { /// Creates the set of properties used to configure [TextField]s. - const TextSelectionThemeData({this.cursorColor, this.selectionColor, this.selectionHandleColor}); + const TextSelectionThemeData({ + this.cursorColor, + this.selectionColor, + this.selectionHandleColor, + this.contextMenuBuilder, + }); /// The color of the cursor in the text field. /// @@ -58,17 +63,31 @@ class TextSelectionThemeData with Diagnosticable { /// containing your [TextField] or [SelectableText] with a [CupertinoTheme]. final Color? selectionHandleColor; + /// {@macro flutter.widgets.EditableText.contextMenuBuilder} + /// + /// Provides a way to globally override the default Flutter context menu for + /// text fields and selectable text. + /// + /// See also: + /// + /// * [AdaptiveTextSelectionToolbar], which is built by default. + /// * [BrowserContextMenu], which allows the browser's context menu on web to + /// be disabled and Flutter-rendered context menus to appear. + final EditableTextContextMenuBuilder? contextMenuBuilder; + /// Creates a copy of this object with the given fields replaced with the /// specified values. TextSelectionThemeData copyWith({ Color? cursorColor, Color? selectionColor, Color? selectionHandleColor, + EditableTextContextMenuBuilder? contextMenuBuilder, }) { return TextSelectionThemeData( cursorColor: cursorColor ?? this.cursorColor, selectionColor: selectionColor ?? this.selectionColor, selectionHandleColor: selectionHandleColor ?? this.selectionHandleColor, + contextMenuBuilder: contextMenuBuilder ?? this.contextMenuBuilder, ); } @@ -89,11 +108,29 @@ class TextSelectionThemeData with Diagnosticable { cursorColor: Color.lerp(a?.cursorColor, b?.cursorColor, t), selectionColor: Color.lerp(a?.selectionColor, b?.selectionColor, t), selectionHandleColor: Color.lerp(a?.selectionHandleColor, b?.selectionHandleColor, t), + contextMenuBuilder: _lerpContextMenuBuilder(a, b, t), ); } + /// Linear interpolation doesn't make much sense between two context menu builders, + /// so instead of interpolating, choose one based on the value of 't' if both are non-null. + /// If one is null but not the other return the non-null one. If both are null, return null. + static EditableTextContextMenuBuilder? _lerpContextMenuBuilder( + TextSelectionThemeData? a, + TextSelectionThemeData? b, + double t, + ) { + final first = a?.contextMenuBuilder; + final second = b?.contextMenuBuilder; + + if (first == null) return second; + if (second == null) return first; + return t < 0.5 ? first : second; + } + @override - int get hashCode => Object.hash(cursorColor, selectionColor, selectionHandleColor); + int get hashCode => + Object.hash(cursorColor, selectionColor, selectionHandleColor, contextMenuBuilder); @override bool operator ==(Object other) { @@ -106,7 +143,8 @@ class TextSelectionThemeData with Diagnosticable { return other is TextSelectionThemeData && other.cursorColor == cursorColor && other.selectionColor == selectionColor && - other.selectionHandleColor == selectionHandleColor; + other.selectionHandleColor == selectionHandleColor && + other.contextMenuBuilder == contextMenuBuilder; } @override @@ -115,6 +153,13 @@ class TextSelectionThemeData with Diagnosticable { properties.add(ColorProperty('cursorColor', cursorColor, defaultValue: null)); properties.add(ColorProperty('selectionColor', selectionColor, defaultValue: null)); properties.add(ColorProperty('selectionHandleColor', selectionHandleColor, defaultValue: null)); + properties.add( + DiagnosticsProperty( + 'contextMenuBuilder', + contextMenuBuilder, + defaultValue: null, + ), + ); } } diff --git a/packages/material_ui/test/search_anchor_test.dart b/packages/material_ui/test/search_anchor_test.dart index d810d74dce07..4a8726faa848 100644 --- a/packages/material_ui/test/search_anchor_test.dart +++ b/packages/material_ui/test/search_anchor_test.dart @@ -3535,49 +3535,51 @@ void main() { expect(box.size.height, 32); }); - testWidgets('Tapping outside searchbar should unfocus the searchbar on mobile', ( - WidgetTester tester, - ) async { - final focusNode = FocusNode(debugLabel: 'Test Node'); - addTearDown(focusNode.dispose); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SearchAnchor( - builder: (BuildContext context, SearchController controller) { - return SearchBar( - controller: controller, - onTap: () { - controller.openView(); - }, - onTapOutside: (PointerDownEvent event) { - focusNode.unfocus(); - }, - onChanged: (_) { - controller.openView(); - }, - autoFocus: true, - focusNode: focusNode, - ); - }, - suggestionsBuilder: (BuildContext context, SearchController controller) { - return List.generate(5, (int index) { - final item = 'item $index'; - return ListTile(title: Text(item)); - }); - }, + testWidgets( + 'Tapping outside searchbar should unfocus the searchbar on mobile', + (WidgetTester tester) async { + final focusNode = FocusNode(debugLabel: 'Test Node'); + addTearDown(focusNode.dispose); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SearchAnchor( + builder: (BuildContext context, SearchController controller) { + return SearchBar( + controller: controller, + onTap: () { + controller.openView(); + }, + onTapOutside: (PointerDownEvent event) { + focusNode.unfocus(); + }, + onChanged: (_) { + controller.openView(); + }, + autoFocus: true, + focusNode: focusNode, + ); + }, + suggestionsBuilder: (BuildContext context, SearchController controller) { + return List.generate(5, (int index) { + final item = 'item $index'; + return ListTile(title: Text(item)); + }); + }, + ), ), ), - ), - ); - await tester.pump(); - expect(focusNode.hasPrimaryFocus, isTrue); + ); + await tester.pump(); + expect(focusNode.hasPrimaryFocus, isTrue); - await tester.tapAt(const Offset(50, 50)); - await tester.pump(); + await tester.tapAt(const Offset(50, 50)); + await tester.pump(); - expect(focusNode.hasPrimaryFocus, isFalse); - }, variant: TargetPlatformVariant.mobile()); + expect(focusNode.hasPrimaryFocus, isFalse); + }, + variant: TargetPlatformVariant.mobile(), + ); testWidgets('The default clear button only shows when text input is not empty ' 'on the search view', (WidgetTester tester) async { @@ -3824,6 +3826,137 @@ void main() { ); }); + testWidgets('SearchBar uses ThemeData.textSelectionTheme contextMenuBuilder', ( + WidgetTester tester, + ) async { + Widget themeDataContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Icon(Icons.search); + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: TextSelectionThemeData( + contextMenuBuilder: themeDataContextMenuBuilder, + ), + ), + home: const Material(child: SearchBar()), + ), + ); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final BuildContext searchBarContext = tester.element(find.byType(SearchBar)); + final Widget contextMenu = editableTextState.widget.contextMenuBuilder!( + searchBarContext, + editableTextState, + ); + + expect(contextMenu, isA()); + }); + + testWidgets('SearchBar prefers local TextSelectionTheme contextMenuBuilder', ( + WidgetTester tester, + ) async { + Widget themeDataContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Icon(Icons.search); + } + + Widget localTextSelectionThemeContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Placeholder(); + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: TextSelectionThemeData( + contextMenuBuilder: themeDataContextMenuBuilder, + ), + ), + home: Material( + child: TextSelectionTheme( + data: TextSelectionThemeData( + contextMenuBuilder: localTextSelectionThemeContextMenuBuilder, + ), + child: const SearchBar(), + ), + ), + ), + ); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final BuildContext searchBarContext = tester.element(find.byType(SearchBar)); + final Widget contextMenu = editableTextState.widget.contextMenuBuilder!( + searchBarContext, + editableTextState, + ); + + expect(contextMenu, isA()); + expect(contextMenu, isNot(isA())); + }); + + testWidgets('SearchBar.contextMenuBuilder overrides TextSelectionTheme and ThemeData', ( + WidgetTester tester, + ) async { + Widget themeDataContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Icon(Icons.search); + } + + Widget localTextSelectionThemeContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Placeholder(); + } + + Widget searchBarContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const SizedBox(); + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: TextSelectionThemeData( + contextMenuBuilder: themeDataContextMenuBuilder, + ), + ), + home: Material( + child: TextSelectionTheme( + data: TextSelectionThemeData( + contextMenuBuilder: localTextSelectionThemeContextMenuBuilder, + ), + child: SearchBar(contextMenuBuilder: searchBarContextMenuBuilder), + ), + ), + ), + ); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final BuildContext searchBarContext = tester.element(find.byType(SearchBar)); + final Widget contextMenu = editableTextState.widget.contextMenuBuilder!( + searchBarContext, + editableTextState, + ); + + expect(contextMenu, isA()); + expect(contextMenu, isNot(isA())); + expect(contextMenu, isNot(isA())); + }); + testWidgets('SearchAnchor.bar.contextMenuBuilder is passed through to EditableText', ( WidgetTester tester, ) async { diff --git a/packages/material_ui/test/selectable_text_test.dart b/packages/material_ui/test/selectable_text_test.dart index 1f80b02a937e..edc39923ff97 100644 --- a/packages/material_ui/test/selectable_text_test.dart +++ b/packages/material_ui/test/selectable_text_test.dart @@ -2928,108 +2928,132 @@ void main() { expect(tester.takeException(), isNotNull); }); - testWidgets('tap moves cursor to the edge of the word it tapped', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), - ), - ); + testWidgets( + 'tap moves cursor to the edge of the word it tapped', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; - // We moved the cursor. - expect( - controller.selection, - const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), - ); + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; + // We moved the cursor. + expect( + controller.selection, + const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), + ); - // But don't trigger the toolbar. - expect(find.byType(CupertinoButton), findsNothing); - }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); + // But don't trigger the toolbar. + expect(find.byType(CupertinoButton), findsNothing); + }, + variant: TargetPlatformVariant.only(TargetPlatform.iOS), + ); - testWidgets('tap moves cursor to the position tapped (Android)', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), - ), - ); + testWidgets( + 'tap moves cursor to the position tapped (Android)', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // We moved the cursor. - expect( - controller.selection, - const TextSelection.collapsed(offset: 4, affinity: TextAffinity.upstream), - ); + // We moved the cursor. + expect( + controller.selection, + const TextSelection.collapsed(offset: 4, affinity: TextAffinity.upstream), + ); - // But don't trigger the toolbar. - expect(find.byType(TextButton), findsNothing); - }, variant: TargetPlatformVariant.all(excluding: const {TargetPlatform.iOS})); + // But don't trigger the toolbar. + expect(find.byType(TextButton), findsNothing); + }, + variant: TargetPlatformVariant.all(excluding: const {TargetPlatform.iOS}), + ); - testWidgets('two slow taps do not trigger a word selection on iOS', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), - ), - ); + testWidgets( + 'two slow taps do not trigger a word selection on iOS', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(const Duration(milliseconds: 500)); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // Plain collapsed selection. - expect( - controller.selection, - const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), - ); + // Plain collapsed selection. + expect( + controller.selection, + const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), + ); - // No toolbar. - expect(find.byType(CupertinoButton), findsNothing); - }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); + // No toolbar. + expect(find.byType(CupertinoButton), findsNothing); + }, + variant: TargetPlatformVariant.only(TargetPlatform.iOS), + ); - testWidgets('two slow taps do not trigger a word selection', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), - ), - ); + testWidgets( + 'two slow taps do not trigger a word selection', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(const Duration(milliseconds: 500)); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // Plain collapsed selection. - expect( - controller.selection, - const TextSelection.collapsed(offset: 4, affinity: TextAffinity.upstream), - ); + // Plain collapsed selection. + expect( + controller.selection, + const TextSelection.collapsed(offset: 4, affinity: TextAffinity.upstream), + ); - // No toolbar. - expect(find.byType(CupertinoButton), findsNothing); - }, variant: TargetPlatformVariant.all(excluding: {TargetPlatform.iOS})); + // No toolbar. + expect(find.byType(CupertinoButton), findsNothing); + }, + variant: TargetPlatformVariant.all(excluding: {TargetPlatform.iOS}), + ); testWidgets( 'double tap selects word and first tap of double tap moves cursor', @@ -3227,74 +3251,86 @@ void main() { }), ); - testWidgets('tap after a double tap select is not affected (iOS)', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), - ), - ); + testWidgets( + 'tap after a double tap select is not affected (iOS)', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // First tap moved the cursor. - expect( - controller.selection, - const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream), - ); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 500)); + // First tap moved the cursor. + expect( + controller.selection, + const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream), + ); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(selectableTextStart + const Offset(100.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(100.0, 5.0)); + await tester.pump(); - // Plain collapsed selection at the edge of first word. 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, const TextSelection.collapsed(offset: 7)); + // Plain collapsed selection at the edge of first word. 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, const TextSelection.collapsed(offset: 7)); - // No toolbar. - expect(find.byType(CupertinoButton), findsNothing); - }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); + // No toolbar. + expect(find.byType(CupertinoButton), findsNothing); + }, + variant: TargetPlatformVariant.only(TargetPlatform.iOS), + ); - testWidgets('tap after a double tap select is not affected (macOS)', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), - ), - ); + testWidgets( + 'tap after a double tap select is not affected (macOS)', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // First tap moved the cursor. - expect( - controller.selection, - const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream), - ); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 500)); + // First tap moved the cursor. + expect( + controller.selection, + const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream), + ); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(selectableTextStart + const Offset(100.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(100.0, 5.0)); + await tester.pump(); - // Collapse selection. - expect(controller.selection, const TextSelection.collapsed(offset: 7)); + // Collapse selection. + expect(controller.selection, const TextSelection.collapsed(offset: 7)); - // No toolbar. - expect(find.byType(CupertinoButton), findsNothing); - }, variant: TargetPlatformVariant.only(TargetPlatform.macOS)); + // No toolbar. + expect(find.byType(CupertinoButton), findsNothing); + }, + variant: TargetPlatformVariant.only(TargetPlatform.macOS), + ); testWidgets( 'long press selects word and shows toolbar (iOS)', @@ -3346,69 +3382,73 @@ void main() { expectMaterialSelectionToolbar(); }); - testWidgets('long press selects word and shows custom toolbar (Cupertino)', ( - WidgetTester tester, - ) async { - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Center( - child: SelectableText( - 'Atwater Peel Sherbrooke Bonaventure', - selectionControls: cupertinoTextSelectionControls, + testWidgets( + 'long press selects word and shows custom toolbar (Cupertino)', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: SelectableText( + 'Atwater Peel Sherbrooke Bonaventure', + selectionControls: cupertinoTextSelectionControls, + ), ), ), ), - ), - ); + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // The long pressed word is selected. - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); + // The long pressed word is selected. + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); - // Toolbar shows one button (copy). - expect(find.byType(CupertinoButton), findsNWidgets(1)); - expect(find.text('Copy'), findsOneWidget); - }, variant: TargetPlatformVariant.all()); + // Toolbar shows one button (copy). + expect(find.byType(CupertinoButton), findsNWidgets(1)); + expect(find.text('Copy'), findsOneWidget); + }, + variant: TargetPlatformVariant.all(), + ); - testWidgets('long press selects word and shows custom toolbar (Material)', ( - WidgetTester tester, - ) async { - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Center( - child: SelectableText( - 'Atwater Peel Sherbrooke Bonaventure', - selectionControls: materialTextSelectionControls, + testWidgets( + 'long press selects word and shows custom toolbar (Material)', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: SelectableText( + 'Atwater Peel Sherbrooke Bonaventure', + selectionControls: materialTextSelectionControls, + ), ), ), ), - ), - ); + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); - // Collapsed toolbar shows 2 buttons: copy, select all - expect(find.byType(TextButton), findsNWidgets(2)); - expect(find.text('Copy'), findsOneWidget); - expect(find.text('Select all'), findsOneWidget); - }, variant: TargetPlatformVariant.all()); + // Collapsed toolbar shows 2 buttons: copy, select all + expect(find.byType(TextButton), findsNWidgets(2)); + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Select all'), findsOneWidget); + }, + variant: TargetPlatformVariant.all(), + ); testWidgets('textSelectionControls is passed to EditableText', (WidgetTester tester) async { await tester.pumpWidget( @@ -3428,129 +3468,145 @@ void main() { expect(widget.selectionControls, equals(materialTextSelectionControls)); }); - testWidgets('PageView beats SelectableText drag gestures (iOS)', (WidgetTester tester) async { - // This is a regression test for - // https://github.com/flutter/flutter/issues/130198. - final pageController = PageController(); - addTearDown(pageController.dispose); - const testValue = 'abc def ghi jkl mno pqr stu vwx yz'; + testWidgets( + 'PageView beats SelectableText drag gestures (iOS)', + (WidgetTester tester) async { + // This is a regression test for + // https://github.com/flutter/flutter/issues/130198. + final pageController = PageController(); + addTearDown(pageController.dispose); + const testValue = 'abc def ghi jkl mno pqr stu vwx yz'; - await tester.pumpWidget( - MaterialApp( - home: Material( - child: PageView( - controller: pageController, - children: const [ - Center(child: SelectableText(testValue)), - SizedBox(height: 200.0, child: Center(child: Text('Page 2'))), - ], + await tester.pumpWidget( + MaterialApp( + home: Material( + child: PageView( + controller: pageController, + children: const [ + Center(child: SelectableText(testValue)), + SizedBox(height: 200.0, child: Center(child: Text('Page 2'))), + ], + ), ), ), - ), - ); + ); - await skipPastScrollingAnimation(tester); + await skipPastScrollingAnimation(tester); - final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); - final Offset pPos = textOffsetToPosition(tester, testValue.indexOf('p')); + final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); + final Offset pPos = textOffsetToPosition(tester, testValue.indexOf('p')); - // A double tap + drag should take precedence over parent drags. - final TestGesture gesture = await tester.startGesture(gPos); - await tester.pump(); - await gesture.up(); - await tester.pump(); - await gesture.down(gPos); - await tester.pumpAndSettle(); - await gesture.moveTo(pPos); - await tester.pump(); - await gesture.up(); - await tester.pumpAndSettle(); - final TextEditingValue currentValue = tester - .state(find.byType(EditableText)) - .textEditingValue; - expect( - currentValue.selection, - TextSelection(baseOffset: testValue.indexOf('g'), extentOffset: testValue.indexOf('p') + 3), - ); + // A double tap + drag should take precedence over parent drags. + final TestGesture gesture = await tester.startGesture(gPos); + await tester.pump(); + await gesture.up(); + await tester.pump(); + await gesture.down(gPos); + await tester.pumpAndSettle(); + await gesture.moveTo(pPos); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + final TextEditingValue currentValue = tester + .state(find.byType(EditableText)) + .textEditingValue; + expect( + currentValue.selection, + TextSelection(baseOffset: testValue.indexOf('g'), extentOffset: testValue.indexOf('p') + 3), + ); - expect(pageController.page, isNotNull); - expect(pageController.page, 0.0); - // A horizontal drag directly on the SelectableText should move the page - // view to the next page. - final Rect selectableTextRect = tester.getRect(find.byType(SelectableText)); - await tester.dragFrom( - selectableTextRect.centerRight - const Offset(0.1, 0.0), - const Offset(-500.0, 0.0), - ); - await tester.pumpAndSettle(); - expect(pageController.page, isNotNull); - expect(pageController.page, 1.0); - }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); + expect(pageController.page, isNotNull); + expect(pageController.page, 0.0); + // A horizontal drag directly on the SelectableText should move the page + // view to the next page. + final Rect selectableTextRect = tester.getRect(find.byType(SelectableText)); + await tester.dragFrom( + selectableTextRect.centerRight - const Offset(0.1, 0.0), + const Offset(-500.0, 0.0), + ); + await tester.pumpAndSettle(); + expect(pageController.page, isNotNull); + expect(pageController.page, 1.0); + }, + variant: const TargetPlatformVariant({TargetPlatform.iOS}), + ); - testWidgets('long press tap cannot initiate a double tap on macOS', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), - ), - ); + testWidgets( + 'long press tap cannot initiate a double tap on macOS', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(const Duration(milliseconds: 50)); + await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); - // Hide the toolbar so it doesn't interfere with taps on the text. - final EditableTextState editableTextState = tester.state( - find.byType(EditableText), - ); - editableTextState.hideToolbar(); - await tester.pumpAndSettle(); + // Hide the toolbar so it doesn't interfere with taps on the text. + final EditableTextState editableTextState = tester.state( + find.byType(EditableText), + ); + editableTextState.hideToolbar(); + await tester.pumpAndSettle(); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // Move the cursor to the tapped position. - expect( - controller.selection, - const TextSelection.collapsed(offset: 4, affinity: TextAffinity.upstream), - ); + // Move the cursor to the tapped position. + expect( + controller.selection, + const TextSelection.collapsed(offset: 4, affinity: TextAffinity.upstream), + ); - expect(find.byType(CupertinoButton), findsNothing); - }, variant: TargetPlatformVariant.only(TargetPlatform.macOS)); + expect(find.byType(CupertinoButton), findsNothing); + }, + variant: TargetPlatformVariant.only(TargetPlatform.macOS), + ); - testWidgets('long press tap cannot initiate a double tap on iOS', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), - ), - ); + testWidgets( + 'long press tap cannot initiate a double tap on iOS', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(const Duration(milliseconds: 50)); + await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); - // Hide the toolbar so it doesn't interfere with taps on the text. - final EditableTextState editableTextState = tester.state( - find.byType(EditableText), - ); - editableTextState.hideToolbar(); - await tester.pumpAndSettle(); + // Hide the toolbar so it doesn't interfere with taps on the text. + final EditableTextState editableTextState = tester.state( + find.byType(EditableText), + ); + editableTextState.hideToolbar(); + await tester.pumpAndSettle(); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // Move the cursor to the edge of the same word and toggle the toolbar. - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); + // Move the cursor to the edge of the same word and toggle the toolbar. + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); - expect(find.byType(CupertinoButton), findsNWidgets(4)); - }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); + expect(find.byType(CupertinoButton), findsNWidgets(4)); + }, + variant: TargetPlatformVariant.only(TargetPlatform.iOS), + ); testWidgets( 'long press drag extends the selection to the word under the drag and shows toolbar on lift on non-Apple platforms', @@ -3671,54 +3727,58 @@ void main() { variant: const TargetPlatformVariant({TargetPlatform.iOS}), ); - testWidgets('long press drag moves the cursor under the drag and shows toolbar on lift (macOS)', ( - WidgetTester tester, - ) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), - ), - ); + testWidgets( + 'long press drag moves the cursor under the drag and shows toolbar on lift (macOS)', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - final TestGesture gesture = await tester.startGesture( - selectableTextStart + const Offset(50.0, 5.0), - ); - await tester.pump(const Duration(milliseconds: 500)); + final TestGesture gesture = await tester.startGesture( + selectableTextStart + const Offset(50.0, 5.0), + ); + await tester.pump(const Duration(milliseconds: 500)); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // The long pressed word is selected. - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); - // Cursor move doesn't trigger a toolbar initially. - expect(find.byType(CupertinoButton), findsNothing); + // The long pressed word is selected. + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); + // Cursor move doesn't trigger a toolbar initially. + expect(find.byType(CupertinoButton), findsNothing); - await gesture.moveBy(const Offset(50, 0)); - await tester.pump(); + await gesture.moveBy(const Offset(50, 0)); + await tester.pump(); - // The selection position is now moved with the drag. - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 8)); - // Still no toolbar. - expect(find.byType(CupertinoButton), findsNothing); + // The selection position is now moved with the drag. + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 8)); + // Still no toolbar. + expect(find.byType(CupertinoButton), findsNothing); - await gesture.moveBy(const Offset(50, 0)); - await tester.pump(); + await gesture.moveBy(const Offset(50, 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. - expect(find.byType(CupertinoButton), findsNothing); + // The selection position is now moved with the drag. + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 12)); + // Still no toolbar. + expect(find.byType(CupertinoButton), findsNothing); - await gesture.up(); - await tester.pump(); + await gesture.up(); + await tester.pump(); - // The selection isn't affected by the gesture lift. - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 12)); - // The toolbar now shows up. - expectCupertinoSelectionToolbar(); - }, variant: const TargetPlatformVariant({TargetPlatform.macOS})); + // The selection isn't affected by the gesture lift. + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 12)); + // The toolbar now shows up. + expectCupertinoSelectionToolbar(); + }, + variant: const TargetPlatformVariant({TargetPlatform.macOS}), + ); testWidgets( 'long press drag can edge scroll when inside a scrollable', @@ -3794,67 +3854,69 @@ void main() { variant: const TargetPlatformVariant({TargetPlatform.iOS}), ); - testWidgets('Desktop mouse drag can edge scroll when inside a horizontal scrollable', ( - WidgetTester tester, - ) async { - // This is a regression test for https://github.com/flutter/flutter/issues/129590. - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Center( - child: SizedBox( - width: 300.0, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SelectableText( - 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges ' * 2, - maxLines: 1, + testWidgets( + 'Desktop mouse drag can edge scroll when inside a horizontal scrollable', + (WidgetTester tester) async { + // This is a regression test for https://github.com/flutter/flutter/issues/129590. + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: SizedBox( + width: 300.0, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SelectableText( + 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges ' * 2, + maxLines: 1, + ), ), ), ), ), ), - ), - ); + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - final TestGesture gesture = await tester.startGesture( - selectableTextStart + const Offset(200.0, 0.0), - ); - await tester.pump(); + final TestGesture gesture = await tester.startGesture( + selectableTextStart + const Offset(200.0, 0.0), + ); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - await gesture.moveBy(const Offset(100, 0)); - // To the edge of the screen basically. - await tester.pump(); - expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 21)); - // Keep moving out. - await gesture.moveBy(const Offset(100, 0)); - await tester.pump(); - expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 28)); - await gesture.moveBy(const Offset(1600, 0)); - await tester.pump(); - expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 134)); + await gesture.moveBy(const Offset(100, 0)); + // To the edge of the screen basically. + await tester.pump(); + expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 21)); + // Keep moving out. + await gesture.moveBy(const Offset(100, 0)); + await tester.pump(); + expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 28)); + await gesture.moveBy(const Offset(1600, 0)); + await tester.pump(); + expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 134)); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // The selection isn't affected by the gesture lift. - expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 134)); + // The selection isn't affected by the gesture lift. + expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 134)); - final RenderEditable renderEditable = findRenderEditable(tester); - final List endpoints = globalize( - renderEditable.getEndpointsForSelection(controller.selection), - renderEditable, - ); - expect(endpoints.isNotEmpty, isTrue); - expect(endpoints.length, 2); - expect(endpoints[0].point.dx, isNegative); - expect(endpoints[1].point.dx, isPositive); - }, variant: TargetPlatformVariant.desktop()); + final RenderEditable renderEditable = findRenderEditable(tester); + final List endpoints = globalize( + renderEditable.getEndpointsForSelection(controller.selection), + renderEditable, + ); + expect(endpoints.isNotEmpty, isTrue); + expect(endpoints.length, 2); + expect(endpoints[0].point.dx, isNegative); + expect(endpoints[1].point.dx, isPositive); + }, + variant: TargetPlatformVariant.desktop(), + ); testWidgets( 'long press drag can edge scroll', @@ -3940,76 +4002,84 @@ void main() { }), ); - testWidgets('long tap still selects after a double tap select (iOS)', ( - WidgetTester tester, - ) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), - ), - ); + testWidgets( + 'long tap still selects after a double tap select (iOS)', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // First tap moved the cursor to the beginning of the second word. - expect( - controller.selection, - const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream), - ); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 500)); + // First tap moved the cursor to the beginning of the second word. + expect( + controller.selection, + const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream), + ); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 500)); - await tester.longPressAt(selectableTextStart + const Offset(100.0, 5.0)); - await tester.pump(); + await tester.longPressAt(selectableTextStart + const Offset(100.0, 5.0)); + await tester.pump(); - // Selected the "word" where the tap happened, which is the first space. - // Because the "word" is a whitespace, the selection will shift to the - // previous "word" that is not a whitespace. - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); + // Selected the "word" where the tap happened, which is the first space. + // Because the "word" is a whitespace, the selection will shift to the + // previous "word" that is not a whitespace. + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); - expectCupertinoSelectionToolbar(); - }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); + expectCupertinoSelectionToolbar(); + }, + variant: const TargetPlatformVariant({TargetPlatform.iOS}), + ); - testWidgets('long tap still selects after a double tap select (macOS)', ( - WidgetTester tester, - ) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), - ), - ); + testWidgets( + 'long tap still selects after a double tap select (macOS)', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // First tap moves the cursor to the tapped position. - expect( - controller.selection, - const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream), - ); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 500)); + // First tap moves the cursor to the tapped position. + expect( + controller.selection, + const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream), + ); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 500)); - await tester.longPressAt(selectableTextStart + const Offset(100.0, 5.0)); - await tester.pump(); + await tester.longPressAt(selectableTextStart + const Offset(100.0, 5.0)); + await tester.pump(); - // Selected the "word" where the tap happened, which is the first space. - expect(controller.selection, const TextSelection(baseOffset: 7, extentOffset: 8)); + // Selected the "word" where the tap happened, which is the first space. + expect(controller.selection, const TextSelection(baseOffset: 7, extentOffset: 8)); - // The toolbar shows up. - expectCupertinoSelectionToolbar(); - }, variant: const TargetPlatformVariant({TargetPlatform.macOS})); + // The toolbar shows up. + expectCupertinoSelectionToolbar(); + }, + variant: const TargetPlatformVariant({TargetPlatform.macOS}), + ); //convert testWidgets( 'double tap after a long tap is not affected', @@ -4182,101 +4252,109 @@ void main() { expect(find.byType(TextButton), findsNothing); }); - testWidgets('force press selects word', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ); + testWidgets( + 'force press selects word', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - final int pointerValue = tester.nextPointer; - final Offset offset = selectableTextStart + const Offset(150.0, 5.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, - ), - ); + final int pointerValue = tester.nextPointer; + final Offset offset = selectableTextStart + const Offset(150.0, 5.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: selectableTextStart + const Offset(150.0, 5.0), - pressure: 0.5, - pressureMin: 0, - ), - ); + await gesture.updateWithCustomEvent( + PointerMoveEvent( + pointer: pointerValue, + position: selectableTextStart + const Offset(150.0, 5.0), + pressure: 0.5, + pressureMin: 0, + ), + ); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // We expect the force press to select a word at the given location. - expect(controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12)); + // 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.pump(); - expectCupertinoSelectionToolbar(); - }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); + await gesture.up(); + await tester.pump(); + expectCupertinoSelectionToolbar(); + }, + variant: const TargetPlatformVariant({TargetPlatform.iOS}), + ); - testWidgets('tap on non-force-press-supported devices work', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ); + testWidgets( + 'tap on non-force-press-supported devices work', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - final int pointerValue = tester.nextPointer; - final Offset offset = selectableTextStart + const Offset(150.0, 5.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, - ), - ); + final int pointerValue = tester.nextPointer; + final Offset offset = selectableTextStart + const Offset(150.0, 5.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: selectableTextStart + const Offset(150.0, 5.0), - pressure: 0.5, - pressureMin: 0, - ), - ); - await gesture.up(); + await gesture.updateWithCustomEvent( + PointerMoveEvent( + pointer: pointerValue, + position: selectableTextStart + const Offset(150.0, 5.0), + pressure: 0.5, + pressureMin: 0, + ), + ); + await gesture.up(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // 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), - ); + // 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. - expect(find.byType(CupertinoButton), findsNothing); + await tester.pump(); + // Single taps shouldn't trigger the toolbar. + expect(find.byType(CupertinoButton), findsNothing); - // 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)); + // 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 SelectableText debugFillProperties', (WidgetTester tester) async { final builder = DiagnosticPropertiesBuilder(); @@ -5604,6 +5682,140 @@ void main() { ); group('context menu', () { + testWidgets('SelectableText uses ThemeData.textSelectionTheme contextMenuBuilder', ( + WidgetTester tester, + ) async { + Widget themeDataContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Placeholder(); + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: TextSelectionThemeData( + contextMenuBuilder: themeDataContextMenuBuilder, + ), + ), + home: const Material(child: SelectableText('one two three')), + ), + ); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final BuildContext selectableTextContext = tester.element(find.byType(SelectableText)); + final Widget contextMenu = editableTextState.widget.contextMenuBuilder!( + selectableTextContext, + editableTextState, + ); + + expect(contextMenu, isA()); + }); + + testWidgets('SelectableText prefers local TextSelectionTheme contextMenuBuilder', ( + WidgetTester tester, + ) async { + Widget themeDataContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Placeholder(); + } + + Widget localTextSelectionThemeContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Icon(Icons.search); + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: TextSelectionThemeData( + contextMenuBuilder: themeDataContextMenuBuilder, + ), + ), + home: Material( + child: TextSelectionTheme( + data: TextSelectionThemeData( + contextMenuBuilder: localTextSelectionThemeContextMenuBuilder, + ), + child: const SelectableText('one two three'), + ), + ), + ), + ); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final BuildContext selectableTextContext = tester.element(find.byType(SelectableText)); + final Widget contextMenu = editableTextState.widget.contextMenuBuilder!( + selectableTextContext, + editableTextState, + ); + + expect(contextMenu, isA()); + expect(contextMenu, isNot(isA())); + }); + + testWidgets('SelectableText.contextMenuBuilder overrides TextSelectionTheme and ThemeData', ( + WidgetTester tester, + ) async { + Widget themeDataContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Placeholder(); + } + + Widget localTextSelectionThemeContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Icon(Icons.search); + } + + Widget selectableTextContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const SizedBox(); + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: TextSelectionThemeData( + contextMenuBuilder: themeDataContextMenuBuilder, + ), + ), + home: Material( + child: TextSelectionTheme( + data: TextSelectionThemeData( + contextMenuBuilder: localTextSelectionThemeContextMenuBuilder, + ), + child: SelectableText( + 'one two three', + contextMenuBuilder: selectableTextContextMenuBuilder, + ), + ), + ), + ), + ); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final BuildContext selectableTextContext = tester.element(find.byType(SelectableText)); + final Widget contextMenu = editableTextState.widget.contextMenuBuilder!( + selectableTextContext, + editableTextState, + ); + + expect(contextMenu, isA()); + expect(contextMenu, isNot(isA())); + expect(contextMenu, isNot(isA())); + }); + // Regression test for https://github.com/flutter/flutter/issues/169001. testWidgets( 'iOS does not use the system context menu by default even when supported', diff --git a/packages/material_ui/test/text_field_test.dart b/packages/material_ui/test/text_field_test.dart index 7b2e24e5b8ea..ef54845ffb08 100644 --- a/packages/material_ui/test/text_field_test.dart +++ b/packages/material_ui/test/text_field_test.dart @@ -1636,47 +1636,51 @@ void main() { 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'), + 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)); + ); + await tester.showKeyboard(find.byType(TextField)); - const testValue = 'ABC'; - tester.testTextInput.updateEditingValue( - const TextEditingValue( - text: testValue, - selection: TextSelection.collapsed(offset: testValue.length), - ), - ); + const testValue = 'ABC'; + tester.testTextInput.updateEditingValue( + const TextEditingValue( + text: testValue, + selection: TextSelection.collapsed(offset: testValue.length), + ), + ); - await tester.pump(); + 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), - ), - ); + // 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(); + await tester.pump(); - String editText = (findRenderEditable(tester).text! as TextSpan).text!; - expect(editText.substring(editText.length - 1), newChar); + String editText = (findRenderEditable(tester).text! as TextSpan).text!; + expect(editText.substring(editText.length - 1), newChar); - await tester.pump(const Duration(seconds: 2)); + 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.android})); + editText = (findRenderEditable(tester).text! as TextSpan).text!; + expect(editText.substring(editText.length - 1), '\u2022'); + }, + variant: const TargetPlatformVariant({TargetPlatform.android}), + ); testWidgets( 'desktop obscureText control test', @@ -2473,54 +2477,56 @@ void main() { 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(); + 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), + 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); + 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')); + 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); + // 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); + 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(); + // 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')); + 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.iOS})); + // End gesture and skip the magnifier hide animation, so it can release + // resources. + await gesture.up(); + await tester.pumpAndSettle(); + }, + variant: const TargetPlatformVariant({TargetPlatform.iOS}), + ); testWidgets( 'Cursor should not move on a quick touch drag when touch does not begin on previous selection (iOS)', @@ -3468,65 +3474,69 @@ void main() { skip: kIsWeb, // [intended] on web only one selection handle can be dragged at a time. ); - 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); + 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), + 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); + // 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 endpoints = globalize( - renderEditable.getEndpointsForSelection(controller.selection), - renderEditable, - ); - expect(endpoints.length, 2); + final RenderEditable renderEditable = findRenderEditable(tester); + final List 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); + // 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(); + // 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)); + // 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', @@ -4813,53 +4823,55 @@ void main() { 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: [ - PopupMenuButton( - itemBuilder: (BuildContext context) { - return {'About'}.map((String value) { - return PopupMenuItem(value: value, child: Text(value)); - }).toList(); - }, - ), - ], + 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: [ + PopupMenuButton( + itemBuilder: (BuildContext context) { + return {'About'}.map((String value) { + return PopupMenuItem(value: value, child: Text(value)); + }).toList(); + }, + ), + ], + ), + body: const TextField(), ), - body: const TextField(), - ), - ); + ); - await tester.pumpWidget(testWidget); + 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 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)); - await tester.pumpAndSettle(); + // Tap on 3 dot menu. + await tester.tap(find.byType(PopupMenuButton)); + 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. + 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(); + // Tap on TextField again. + await tester.tapAt(textFieldStart); + await tester.pumpAndSettle(); - expect(tester.takeException(), isNull); - }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); + expect(tester.takeException(), isNull); + }, + variant: const TargetPlatformVariant({TargetPlatform.iOS}), + ); testWidgets('TextField height with minLines unset', (WidgetTester tester) async { await tester.pumpWidget(textFieldBuilder()); @@ -10101,35 +10113,39 @@ void main() { 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.iOS})); - testWidgets( - 'tap with a mouse does not move cursor to the edge of the word', + '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.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', @@ -10235,191 +10251,199 @@ void main() { }), ); - 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)), + 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(); + 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'. - 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(); + // 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)); - // 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(); + 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(); - // 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(); + 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 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 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(); - // 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.iOS})); + // 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(); - 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)), + // 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.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'. + 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)); + // 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)); + 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)); + // Second tap selects the word around the cursor. + expect(controller.selection, const TextSelection(baseOffset: 24, extentOffset: 35)); - // The toolbar shows up. - _expectCupertinoToolbarForPartialSelection(); + // 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 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 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.iOS})); + // 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.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)), + 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'. + 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)); + // 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(); + 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)); + // 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.iOS})); + // The toolbar shows up. + _expectCupertinoToolbarForPartialSelection(); + }, + variant: const TargetPlatformVariant({TargetPlatform.iOS}), + ); testWidgets('iOS selectWordEdge works correctly', (WidgetTester tester) async { final TextEditingController controller = _textEditingController(text: 'blah1 blah2'); @@ -10788,66 +10812,11 @@ void main() { }), ); - 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', + 'Can triple tap to select a paragraph on mobile platforms', (WidgetTester tester) async { - // Regression test for https://github.com/flutter/flutter/issues/132126 final TextEditingController controller = _textEditingController(); + final isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( MaterialApp( @@ -10865,27 +10834,84 @@ void main() { await skipPastScrollingAnimation(tester); expect(controller.value.text, testValueB); - final Offset thirdLinePos = textOffsetToPosition(tester, 38); + final Offset firstLinePos = + tester.getTopLeft(find.byType(TextField)) + const Offset(50.0, 9.0); - // Click on text field to gain focus, and move the selection. - final TestGesture gesture = await tester.startGesture( - thirdLinePos, - kind: PointerDeviceKind.mouse, - ); + // 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, 38); + expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3); - // 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); + // 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, 38); - expect(controller.selection.extentOffset, 40); + 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. @@ -10901,62 +10927,64 @@ void main() { variant: TargetPlatformVariant.all(excluding: {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(); + 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.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); + await tester.enterText(find.byType(TextField), testValueB); + await skipPastScrollingAnimation(tester); + expect(controller.value.text, testValueB); - final Offset endOfTextPos = textOffsetToPosition(tester, 74); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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.linux})); + expect(controller.selection.baseOffset, 57); + expect(controller.selection.extentOffset, 74); + }, + variant: TargetPlatformVariant.all(excluding: {TargetPlatform.linux}), + ); testWidgets( 'triple tap chains work on Non-Apple mobile platforms', @@ -11030,71 +11058,75 @@ void main() { }), ); - 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)), + 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)); + 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.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(); + 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(); + // 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(); + 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.iOS})); + // 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.iOS}), + ); testWidgets('triple click chains work', (WidgetTester tester) async { final TextEditingController controller = _textEditingController(text: testValueA); @@ -11238,409 +11270,425 @@ void main() { ); }, 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)), - ), - ); + 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; - final Offset firstLinePos = - tester.getTopLeft(find.byType(TextField)) + const Offset(50.0, 9.0); + await tester.pumpWidget( + MaterialApp( + home: Material(child: TextField(controller: controller)), + ), + ); - // 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(); + final Offset firstLinePos = + tester.getTopLeft(find.byType(TextField)) + const Offset(50.0, 9.0); - expect(controller.selection.isCollapsed, true); - expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3); + // 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(); - // 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.isCollapsed, true); + expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3); - expect(controller.selection.baseOffset, 0); - expect(controller.selection.extentOffset, 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(); - // 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, 5); - expect(controller.selection.baseOffset, 0); - expect(controller.selection.extentOffset, 74); - }, variant: TargetPlatformVariant.mobile()); + // 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(); - testWidgets('Can triple click to select all on a single-line textfield on desktop platforms', ( - WidgetTester tester, - ) async { - final TextEditingController controller = _textEditingController(text: testValueA); + expect(controller.selection.baseOffset, 0); + expect(controller.selection.extentOffset, 74); + }, + variant: TargetPlatformVariant.mobile(), + ); - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller), + 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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()); + 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(); + 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.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); + await tester.enterText(find.byType(TextField), testValueA); + await skipPastScrollingAnimation(tester); + expect(controller.value.text, testValueA); - final Offset firstLinePos = textOffsetToPosition(tester, 5); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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)); + 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(); + 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.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); + await tester.enterText(find.byType(TextField), testValueA); + await skipPastScrollingAnimation(tester); + expect(controller.value.text, testValueA); - final Offset firstLinePos = textOffsetToPosition(tester, 5); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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.linux})); + expect(controller.selection.baseOffset, 0); + expect(controller.selection.extentOffset, 20); + }, + variant: TargetPlatformVariant.all(excluding: {TargetPlatform.linux}), + ); - testWidgets('Can triple click + drag to select line by line on Linux', ( - WidgetTester tester, - ) async { - final TextEditingController controller = _textEditingController(); + 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.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); + 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; + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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)); + 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(); + 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.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); + 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; + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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.linux})); + expect(controller.selection.baseOffset, 0); + expect(controller.selection.extentOffset, 20); + }, + variant: TargetPlatformVariant.all(excluding: {TargetPlatform.linux}), + ); testWidgets( 'Going past triple click retains the selection on Apple platforms', @@ -11812,87 +11860,91 @@ void main() { }), ); - 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)), + 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)); + 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); + // 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)); + // 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)); + // 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)); + // 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.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.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(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.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)); + 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( @@ -12284,67 +12336,71 @@ void main() { }), ); - 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)), + 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(find.byType(EditableText)); - final RenderEditable renderEditable = state.renderEditable; + final EditableTextState state = tester.state(find.byType(EditableText)); + final RenderEditable renderEditable = state.renderEditable; - final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); - await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0)); - await tester.pumpAndSettle(); + 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)); + // 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; + 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( + 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 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); + // 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)); + // 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', @@ -13583,60 +13639,64 @@ void main() { _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)), + 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)); + 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(); + 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(); + // 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(); + // 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.iOS})); + 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.iOS}), + ); testWidgets( 'double click chains work', @@ -13712,71 +13772,73 @@ void main() { }), ); - 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)), + 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); - 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); - // 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); + // 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); + // 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); + // 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.iOS})); + // 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.iOS}), + ); testWidgets( 'selecting a space selects the space on non-iOS platforms', @@ -13988,119 +14050,127 @@ void main() { await gesture.updateWithCustomEvent( PointerMoveEvent( pointer: pointerValue, - position: offset + const Offset(150.0, 9.0), + 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.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.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(); - // We don't want this gesture to select any word on Android. - expect(controller.selection, const TextSelection.collapsed(offset: 9)); - _expectNoMaterialToolbar(); + // 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: const TargetPlatformVariant({ - TargetPlatform.linux, - TargetPlatform.windows, - }), + variant: TargetPlatformVariant.only(TargetPlatform.iOS), ); - 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.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(); @@ -14507,41 +14577,43 @@ void main() { }), ); - testWidgets('iPad Scribble selection change shows selection handles', ( - WidgetTester tester, - ) async { - const testText = 'lorem ipsum'; - final TextEditingController controller = _textEditingController(text: testText); + 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.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(); + 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 transitions = find - .byType(FadeTransition) - .evaluate() - .map((Element e) => e.widget) - .cast() - .toList(); - expect(transitions.length, 2); - final FadeTransition left = transitions[0]; - final FadeTransition right = transitions[1]; + final List transitions = find + .byType(FadeTransition) + .evaluate() + .map((Element e) => e.widget) + .cast() + .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.iOS})); + expect(left.opacity.value, equals(1.0)); + expect(right.opacity.value, equals(1.0)); + }, + variant: const TargetPlatformVariant({TargetPlatform.iOS}), + ); testWidgets('Tap shows handles but not toolbar', (WidgetTester tester) async { final TextEditingController controller = _textEditingController(text: 'abc def ghi'); @@ -17219,6 +17291,137 @@ void main() { skip: kIsWeb, // [intended] on web the browser handles the context menu. ); + testWidgets('TextField uses ThemeData.textSelectionTheme contextMenuBuilder', ( + WidgetTester tester, + ) async { + Widget themeDataContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Placeholder(); + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: TextSelectionThemeData( + contextMenuBuilder: themeDataContextMenuBuilder, + ), + ), + home: const Material(child: TextField()), + ), + ); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final BuildContext textFieldContext = tester.element(find.byType(TextField)); + final Widget contextMenu = editableTextState.widget.contextMenuBuilder!( + textFieldContext, + editableTextState, + ); + + expect(contextMenu, isA()); + }); + + testWidgets('TextField prefers local TextSelectionTheme contextMenuBuilder', ( + WidgetTester tester, + ) async { + Widget themeDataContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Placeholder(); + } + + Widget localTextSelectionThemeContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Icon(Icons.search); + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: TextSelectionThemeData( + contextMenuBuilder: themeDataContextMenuBuilder, + ), + ), + home: Material( + child: TextSelectionTheme( + data: TextSelectionThemeData( + contextMenuBuilder: localTextSelectionThemeContextMenuBuilder, + ), + child: const TextField(), + ), + ), + ), + ); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final BuildContext textFieldContext = tester.element(find.byType(TextField)); + final Widget contextMenu = editableTextState.widget.contextMenuBuilder!( + textFieldContext, + editableTextState, + ); + + expect(contextMenu, isA()); + expect(contextMenu, isNot(isA())); + }); + + testWidgets('TextField.contextMenuBuilder overrides TextSelectionTheme and ThemeData', ( + WidgetTester tester, + ) async { + Widget themeDataContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Placeholder(); + } + + Widget localTextSelectionThemeContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Icon(Icons.search); + } + + Widget textFieldContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const SizedBox(); + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: TextSelectionThemeData( + contextMenuBuilder: themeDataContextMenuBuilder, + ), + ), + home: Material( + child: TextSelectionTheme( + data: TextSelectionThemeData( + contextMenuBuilder: localTextSelectionThemeContextMenuBuilder, + ), + child: TextField(contextMenuBuilder: textFieldContextMenuBuilder), + ), + ), + ), + ); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final BuildContext textFieldContext = tester.element(find.byType(TextField)); + final Widget contextMenu = editableTextState.widget.contextMenuBuilder!( + textFieldContext, + editableTextState, + ); + + expect(contextMenu, isA()); + expect(contextMenu, isNot(isA())); + expect(contextMenu, isNot(isA())); + }); + testWidgets( 'iOS uses the system context menu by default if supported', (WidgetTester tester) async { @@ -17358,41 +17561,49 @@ void main() { }); group('defaults', () { - testWidgets('should build Magnifier on Android', (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: Scaffold(body: TextField()))); + 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.empty); - addTearDown(magnifierInfo.dispose); + final BuildContext context = tester.firstElement(find.byType(TextField)); + final EditableText editableText = tester.widget(find.byType(EditableText)); + final magnifierInfo = ValueNotifier(MagnifierInfo.empty); + addTearDown(magnifierInfo.dispose); - expect( - editableText.magnifierConfiguration.magnifierBuilder( - context, - MagnifierController(), - magnifierInfo, - ), - isA(), - ); - }, variant: TargetPlatformVariant.only(TargetPlatform.android)); + expect( + editableText.magnifierConfiguration.magnifierBuilder( + context, + MagnifierController(), + magnifierInfo, + ), + isA(), + ); + }, + variant: TargetPlatformVariant.only(TargetPlatform.android), + ); - testWidgets('should build CupertinoMagnifier on iOS', (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: Scaffold(body: TextField()))); + 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.empty); - addTearDown(magnifierInfo.dispose); + final BuildContext context = tester.firstElement(find.byType(TextField)); + final EditableText editableText = tester.widget(find.byType(EditableText)); + final magnifierInfo = ValueNotifier(MagnifierInfo.empty); + addTearDown(magnifierInfo.dispose); - expect( - editableText.magnifierConfiguration.magnifierBuilder( - context, - MagnifierController(), - magnifierInfo, - ), - isA(), - ); - }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); + expect( + editableText.magnifierConfiguration.magnifierBuilder( + context, + MagnifierController(), + magnifierInfo, + ), + isA(), + ); + }, + variant: TargetPlatformVariant.only(TargetPlatform.iOS), + ); testWidgets( 'should build nothing on all platforms but iOS and Android', @@ -17671,108 +17882,110 @@ void main() { }), ); - 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 localMagnifierInfo, - ) { - magnifierController = controller; - return TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder( - context, - controller, - localMagnifierInfo, - ); - }, + 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 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); + const testValue = 'one two three four five six seven'; + await tester.enterText(find.byType(TextField), testValue); + await skipPastScrollingAnimation(tester); - // 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); + // 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); - // 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(); + // 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); - expect(controller.selection.isCollapsed, false); - expect(controller.selection.baseOffset, 0); - expect(controller.selection.extentOffset, 18); - expect(magnifierController, isNotNull); - expect(magnifierController!.shown, true); + // 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(); - // 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); + expect(controller.selection.isCollapsed, false); + expect(controller.selection.baseOffset, 0); + expect(controller.selection.extentOffset, 18); + expect(magnifierController, isNotNull); + expect(magnifierController!.shown, true); - // Keep dragging 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); + // 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); - // 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); + // Keep dragging 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); - // 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)); + // 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', @@ -18640,60 +18853,66 @@ void main() { 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), + 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 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)); - } + final EditableTextState state = tester.state(find.byType(EditableText)); + Future 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(); + // 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)); + 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, @@ -18767,103 +18986,107 @@ void main() { 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)), + 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( - id: 1, - textDirection: TextDirection.ltr, - children: [ - TestSemantics( - id: 2, - children: [ - TestSemantics( - id: 3, - flags: [SemanticsFlag.scopesRoute], - children: [ - TestSemantics( - id: 4, - inputType: ui.SemanticsInputType.text, - currentValueLength: 0, - flags: [ - SemanticsFlag.isTextField, - SemanticsFlag.isFocusable, - SemanticsFlag.hasEnabledState, - SemanticsFlag.isReadOnly, - ], - actions: [ - if (defaultTargetPlatform == TargetPlatform.windows || - defaultTargetPlatform == TargetPlatform.macOS || - defaultTargetPlatform == TargetPlatform.linux) - SemanticsAction.didGainAccessibilityFocus, - if (defaultTargetPlatform == TargetPlatform.windows || - defaultTargetPlatform == TargetPlatform.macOS || - defaultTargetPlatform == TargetPlatform.linux) - SemanticsAction.didLoseAccessibilityFocus, - ], - ), - ], - ), - ], - ), - ], - ), - ], + ); + expect( + semantics, + hasSemantics( + TestSemantics.root( + children: [ + TestSemantics( + id: 1, + textDirection: TextDirection.ltr, + children: [ + TestSemantics( + id: 2, + children: [ + TestSemantics( + id: 3, + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + id: 4, + inputType: ui.SemanticsInputType.text, + currentValueLength: 0, + flags: [ + SemanticsFlag.isTextField, + SemanticsFlag.isFocusable, + SemanticsFlag.hasEnabledState, + SemanticsFlag.isReadOnly, + ], + actions: [ + 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, ), - 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()); + 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)), + 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(); + ); + 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'); + 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()); + semantics.dispose(); + }, + variant: TargetPlatformVariant.all(), + ); testWidgets( 'when receives SemanticsAction.focus while focused but read-only, does not show keyboard', diff --git a/packages/material_ui/test/text_form_field_test.dart b/packages/material_ui/test/text_form_field_test.dart index b4b553e43b45..685451363e96 100644 --- a/packages/material_ui/test/text_form_field_test.dart +++ b/packages/material_ui/test/text_form_field_test.dart @@ -1739,6 +1739,122 @@ void main() { ); group('context menu', () { + testWidgets('TextFormField uses ThemeData.textSelectionTheme contextMenuBuilder', ( + WidgetTester tester, + ) async { + Widget themeDataContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Placeholder(); + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: TextSelectionThemeData( + contextMenuBuilder: themeDataContextMenuBuilder, + ), + ), + home: Material(child: TextFormField()), + ), + ); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final BuildContext textFormFieldContext = tester.element(find.byType(TextFormField)); + final Widget contextMenu = editableTextState.widget.contextMenuBuilder!( + textFormFieldContext, + editableTextState, + ); + + expect(contextMenu, isA()); + }); + + testWidgets('TextFormField prefers local TextSelectionTheme contextMenuBuilder', ( + WidgetTester tester, + ) async { + Widget themeDataContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Placeholder(); + } + + Widget localTextSelectionThemeContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Icon(Icons.search); + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: TextSelectionThemeData( + contextMenuBuilder: themeDataContextMenuBuilder, + ), + ), + home: Material( + child: TextSelectionTheme( + data: TextSelectionThemeData( + contextMenuBuilder: localTextSelectionThemeContextMenuBuilder, + ), + child: TextFormField(), + ), + ), + ), + ); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final BuildContext textFormFieldContext = tester.element(find.byType(TextFormField)); + final Widget contextMenu = editableTextState.widget.contextMenuBuilder!( + textFormFieldContext, + editableTextState, + ); + + expect(contextMenu, isA()); + expect(contextMenu, isNot(isA())); + }); + + testWidgets('TextFormField.contextMenuBuilder overrides ThemeData.textSelectionTheme', ( + WidgetTester tester, + ) async { + Widget themeDataContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Placeholder(); + } + + Widget textFormFieldContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const SizedBox(); + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: TextSelectionThemeData( + contextMenuBuilder: themeDataContextMenuBuilder, + ), + ), + home: Material(child: TextFormField(contextMenuBuilder: textFormFieldContextMenuBuilder)), + ), + ); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final BuildContext textFormFieldContext = tester.element(find.byType(TextFormField)); + final Widget contextMenu = editableTextState.widget.contextMenuBuilder!( + textFormFieldContext, + editableTextState, + ); + + expect(contextMenu, isA()); + expect(contextMenu, isNot(isA())); + }); + testWidgets( 'iOS uses the system context menu by default if supported', (WidgetTester tester) async { diff --git a/packages/material_ui/test/text_selection_theme_test.dart b/packages/material_ui/test/text_selection_theme_test.dart index 7531109d743c..f223d7a04bd1 100644 --- a/packages/material_ui/test/text_selection_theme_test.dart +++ b/packages/material_ui/test/text_selection_theme_test.dart @@ -6,7 +6,32 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class CustomContextMenu extends AdaptiveTextSelectionToolbar { + const CustomContextMenu.buttonItems({ + super.key, + required super.anchors, + required super.buttonItems, + }) : super.buttonItems(); +} + void main() { + CustomContextMenu defaultContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return CustomContextMenu.buttonItems( + anchors: editableTextState.contextMenuAnchors, + buttonItems: [ + ContextMenuButtonItem( + onPressed: () { + ContextMenuController.removeAny(); + }, + label: 'Context Button Item', + ), + ], + ); + } + test('TextSelectionThemeData copyWith, ==, hashCode basics', () { expect(const TextSelectionThemeData(), const TextSelectionThemeData().copyWith()); expect( @@ -26,6 +51,7 @@ void main() { expect(theme.cursorColor, null); expect(theme.selectionColor, null); expect(theme.selectionHandleColor, null); + expect(theme.contextMenuBuilder, null); }); testWidgets('Default TextSelectionThemeData debugFillProperties', (WidgetTester tester) async { @@ -42,10 +68,11 @@ void main() { testWidgets('TextSelectionThemeData implements debugFillProperties', (WidgetTester tester) async { final builder = DiagnosticPropertiesBuilder(); - const TextSelectionThemeData( - cursorColor: Color(0xffeeffaa), - selectionColor: Color(0x88888888), - selectionHandleColor: Color(0xaabbccdd), + TextSelectionThemeData( + cursorColor: const Color(0xffeeffaa), + selectionColor: const Color(0x88888888), + selectionHandleColor: const Color(0xaabbccdd), + contextMenuBuilder: defaultContextMenuBuilder, ).debugFillProperties(builder); final List description = builder.properties @@ -57,6 +84,7 @@ void main() { 'cursorColor: ${const Color(0xffeeffaa)}', 'selectionColor: ${const Color(0x88888888)}', 'selectionHandleColor: ${const Color(0xaabbccdd)}', + 'contextMenuBuilder: Closure: (BuildContext, EditableTextState) => CustomContextMenu', ]); }); @@ -87,6 +115,12 @@ void main() { expect(renderEditable.cursorColor, defaultCursorColor); expect(renderEditable.selectionColor, defaultSelectionColor); + final BuildContext textFieldContext = tester.element(find.byType(TextField)); + final EditableTextContextMenuBuilder? themeContextMenuBuilder = TextSelectionTheme.of( + textFieldContext, + ).contextMenuBuilder; + expect(themeContextMenuBuilder, null); + // Test the selection handle color. await tester.pumpWidget( MaterialApp( @@ -136,6 +170,12 @@ void main() { expect(renderEditable.cursorColor, defaultCursorColor); expect(renderEditable.selectionColor, defaultSelectionColor); + final BuildContext textFieldContext = tester.element(find.byType(TextField)); + final EditableTextContextMenuBuilder? themeContextMenuBuilder = TextSelectionTheme.of( + textFieldContext, + ).contextMenuBuilder; + expect(themeContextMenuBuilder, null); + // Test the selection handle color. await tester.pumpWidget( MaterialApp( @@ -159,10 +199,11 @@ void main() { }); testWidgets('ThemeData.textSelectionTheme will be used if provided', (WidgetTester tester) async { - const textSelectionTheme = TextSelectionThemeData( - cursorColor: Color(0xffaabbcc), - selectionColor: Color(0x88888888), - selectionHandleColor: Color(0x00ccbbaa), + final textSelectionTheme = TextSelectionThemeData( + cursorColor: const Color(0xffaabbcc), + selectionColor: const Color(0x88888888), + selectionHandleColor: const Color(0x00ccbbaa), + contextMenuBuilder: defaultContextMenuBuilder, ); final ThemeData theme = ThemeData.fallback().copyWith(textSelectionTheme: textSelectionTheme); @@ -184,6 +225,11 @@ void main() { final RenderEditable renderEditable = editableTextState.renderEditable; expect(renderEditable.cursorColor, textSelectionTheme.cursorColor); expect(renderEditable.selectionColor, textSelectionTheme.selectionColor); + final BuildContext textFieldContext = tester.element(find.byType(TextField)); + final EditableTextContextMenuBuilder? themeContextMenuBuilder = TextSelectionTheme.of( + textFieldContext, + ).contextMenuBuilder; + expect(themeContextMenuBuilder, textSelectionTheme.contextMenuBuilder); // Test the selection handle color. await tester.pumpWidget( @@ -218,10 +264,11 @@ void main() { final ThemeData theme = ThemeData.fallback().copyWith( textSelectionTheme: defaultTextSelectionTheme, ); - const widgetTextSelectionTheme = TextSelectionThemeData( - cursorColor: Color(0xffddeeff), - selectionColor: Color(0x44444444), - selectionHandleColor: Color(0x00ffeedd), + final widgetTextSelectionTheme = TextSelectionThemeData( + cursorColor: const Color(0xffddeeff), + selectionColor: const Color(0x44444444), + selectionHandleColor: const Color(0x00ffeedd), + contextMenuBuilder: defaultContextMenuBuilder, ); EditableText.debugDeterministicCursor = true; @@ -232,10 +279,10 @@ void main() { await tester.pumpWidget( MaterialApp( theme: theme, - home: const Material( + home: Material( child: TextSelectionTheme( data: widgetTextSelectionTheme, - child: TextField(autofocus: true), + child: const TextField(autofocus: true), ), ), ), @@ -246,6 +293,12 @@ void main() { expect(renderEditable.cursorColor, widgetTextSelectionTheme.cursorColor); expect(renderEditable.selectionColor, widgetTextSelectionTheme.selectionColor); + final BuildContext textFieldContext = tester.element(find.byType(TextField)); + final EditableTextContextMenuBuilder? themeContextMenuBuilder = TextSelectionTheme.of( + textFieldContext, + ).contextMenuBuilder; + expect(themeContextMenuBuilder, widgetTextSelectionTheme.contextMenuBuilder); + // Test the selection handle color. await tester.pumpWidget( MaterialApp( @@ -289,10 +342,13 @@ void main() { await tester.pumpWidget( MaterialApp( theme: theme, - home: const Material( + home: Material( child: TextSelectionTheme( data: widgetTextSelectionTheme, - child: TextField(cursorColor: cursorColor), + child: TextField( + cursorColor: cursorColor, + contextMenuBuilder: defaultContextMenuBuilder, + ), ), ), ), @@ -301,15 +357,20 @@ void main() { final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); final RenderEditable renderEditable = editableTextState.renderEditable; expect(renderEditable.cursorColor, cursorColor.withAlpha(0)); + expect(editableTextState.widget.contextMenuBuilder, defaultContextMenuBuilder); // Test SelectableText's cursor color. await tester.pumpWidget( MaterialApp( theme: theme, - home: const Material( + home: Material( child: TextSelectionTheme( data: widgetTextSelectionTheme, - child: SelectableText('foobar', cursorColor: cursorColor), + child: SelectableText( + 'foobar', + cursorColor: cursorColor, + contextMenuBuilder: defaultContextMenuBuilder, + ), ), ), ), @@ -318,6 +379,7 @@ void main() { final EditableTextState selectableTextState = tester.firstState(find.byType(EditableText)); final RenderEditable renderSelectable = selectableTextState.renderEditable; expect(renderSelectable.cursorColor, cursorColor.withAlpha(0)); + expect(selectableTextState.widget.contextMenuBuilder, defaultContextMenuBuilder); }); testWidgets('TextSelectionThem overrides DefaultSelectionStyle', (WidgetTester tester) async { @@ -336,9 +398,10 @@ void main() { child: Container( key: defaultSelectionStyle, child: TextSelectionTheme( - data: const TextSelectionThemeData( + data: TextSelectionThemeData( selectionColor: themeSelectionColor, cursorColor: themeCursorColor, + contextMenuBuilder: defaultContextMenuBuilder, ), child: Placeholder(key: themeStyle), ), @@ -352,10 +415,14 @@ void main() { DefaultSelectionStyle style = DefaultSelectionStyle.of(defaultSelectionStyleContext); expect(style.selectionColor, defaultSelectionColor); expect(style.cursorColor, defaultCursorColor); + TextSelectionThemeData textSelectionTheme = TextSelectionTheme.of(defaultSelectionStyleContext); + expect(textSelectionTheme.contextMenuBuilder, null); final BuildContext themeStyleContext = tester.element(find.byKey(themeStyle)); style = DefaultSelectionStyle.of(themeStyleContext); expect(style.selectionColor, themeSelectionColor); expect(style.cursorColor, themeCursorColor); + textSelectionTheme = TextSelectionTheme.of(themeStyleContext); + expect(textSelectionTheme.contextMenuBuilder, defaultContextMenuBuilder); }); } From a1ed54a2204201a1786f1c50a0a0ae0c27b207b9 Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:47:29 -0400 Subject: [PATCH 02/15] update to use the material_ui package's text selection theme instead of the platform's --- packages/material_ui/lib/src/search_anchor.dart | 2 +- packages/material_ui/lib/src/selectable_text.dart | 2 +- packages/material_ui/lib/src/text_field.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/material_ui/lib/src/search_anchor.dart b/packages/material_ui/lib/src/search_anchor.dart index eefb478a859e..1de46e566fdb 100644 --- a/packages/material_ui/lib/src/search_anchor.dart +++ b/packages/material_ui/lib/src/search_anchor.dart @@ -11,7 +11,6 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/src/material/text_selection_theme.dart'; import 'package:flutter/widgets.dart'; import 'adaptive_text_selection_toolbar.dart'; @@ -33,6 +32,7 @@ import 'material_state.dart'; import 'search_bar_theme.dart'; import 'search_view_theme.dart'; import 'text_field.dart'; +import 'text_selection_theme.dart'; import 'text_theme.dart'; import 'theme.dart'; diff --git a/packages/material_ui/lib/src/selectable_text.dart b/packages/material_ui/lib/src/selectable_text.dart index b46924e38829..fc3bcee19022 100644 --- a/packages/material_ui/lib/src/selectable_text.dart +++ b/packages/material_ui/lib/src/selectable_text.dart @@ -15,12 +15,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/src/material/text_selection_theme.dart'; import 'adaptive_text_selection_toolbar.dart'; import 'desktop_text_selection.dart'; import 'magnifier.dart'; import 'text_selection.dart'; +import 'text_selection_theme.dart'; import 'theme.dart'; // Examples can assume: diff --git a/packages/material_ui/lib/src/text_field.dart b/packages/material_ui/lib/src/text_field.dart index ecea6907f6bb..9ee562011994 100644 --- a/packages/material_ui/lib/src/text_field.dart +++ b/packages/material_ui/lib/src/text_field.dart @@ -16,7 +16,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/src/material/text_selection_theme.dart'; import 'adaptive_text_selection_toolbar.dart'; import 'color_scheme.dart'; @@ -30,6 +29,7 @@ import 'material_state.dart'; import 'selectable_text.dart' show iOSHorizontalOffset; import 'spell_check_suggestions_toolbar.dart'; import 'text_selection.dart'; +import 'text_selection_theme.dart'; import 'theme.dart'; export 'package:flutter/services.dart' From 7462883aa9543a0f949ae00658985a17c68d0d5b Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:49:04 -0400 Subject: [PATCH 03/15] address Gemini's PR feedback by simplifying the override logic in serach_anchor and text_form_field and adding a test to make sure that SearchAnchor.bar uses the global theme override for the context menu --- .../material_ui/lib/src/search_anchor.dart | 7 +--- .../material_ui/lib/src/text_form_field.dart | 15 +------- .../material_ui/test/search_anchor_test.dart | 37 +++++++++++++++++++ 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/packages/material_ui/lib/src/search_anchor.dart b/packages/material_ui/lib/src/search_anchor.dart index 1de46e566fdb..b2441de64dac 100644 --- a/packages/material_ui/lib/src/search_anchor.dart +++ b/packages/material_ui/lib/src/search_anchor.dart @@ -1310,7 +1310,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor { super.textInputAction, super.keyboardType, EdgeInsets scrollPadding = const EdgeInsets.all(20.0), - EditableTextContextMenuBuilder contextMenuBuilder = _SearchBarState._defaultContextMenuBuilder, + EditableTextContextMenuBuilder? contextMenuBuilder, super.enabled, super.smartDashesType, super.smartQuotesType, @@ -1876,10 +1876,7 @@ class _SearchBarState extends State { textInputAction: widget.textInputAction, keyboardType: widget.keyboardType, scrollPadding: widget.scrollPadding, - contextMenuBuilder: - widget.contextMenuBuilder ?? - TextSelectionTheme.of(context).contextMenuBuilder ?? - _defaultContextMenuBuilder, + contextMenuBuilder: widget.contextMenuBuilder, smartDashesType: widget.smartDashesType, smartQuotesType: widget.smartQuotesType, ), diff --git a/packages/material_ui/lib/src/text_form_field.dart b/packages/material_ui/lib/src/text_form_field.dart index 4b6b9ca53976..3a358b49a88e 100644 --- a/packages/material_ui/lib/src/text_form_field.dart +++ b/packages/material_ui/lib/src/text_form_field.dart @@ -310,10 +310,7 @@ class TextFormField extends FormField { scrollController: scrollController, enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, mouseCursor: mouseCursor, - contextMenuBuilder: - contextMenuBuilder ?? - TextSelectionTheme.of(state.context).contextMenuBuilder ?? - _defaultContextMenuBuilder, + contextMenuBuilder: contextMenuBuilder, spellCheckConfiguration: spellCheckConfiguration, magnifierConfiguration: magnifierConfiguration, undoController: undoController, @@ -346,16 +343,6 @@ class TextFormField extends FormField { /// {@macro cupertino_ui.TextFormField.onChanged} final ValueChanged? onChanged; - static Widget _defaultContextMenuBuilder( - BuildContext context, - EditableTextState editableTextState, - ) { - if (SystemContextMenu.isSupportedByField(editableTextState)) { - return SystemContextMenu.editableText(editableTextState: editableTextState); - } - return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); - } - @override FormFieldState createState() => _TextFormFieldState(); } diff --git a/packages/material_ui/test/search_anchor_test.dart b/packages/material_ui/test/search_anchor_test.dart index 4a8726faa848..f2de3c487d09 100644 --- a/packages/material_ui/test/search_anchor_test.dart +++ b/packages/material_ui/test/search_anchor_test.dart @@ -3989,6 +3989,43 @@ void main() { expect(find.byType(Placeholder), findsOneWidget); }); + testWidgets('SearchAnchor.bar uses ThemeData.textSelectionTheme contextMenuBuilder', ( + WidgetTester tester, + ) async { + Widget themeDataContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return const Icon(Icons.search); + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: TextSelectionThemeData( + contextMenuBuilder: themeDataContextMenuBuilder, + ), + ), + home: Material( + child: SearchAnchor.bar( + suggestionsBuilder: (BuildContext context, SearchController controller) { + return []; + }, + ), + ), + ), + ); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final BuildContext searchAnchorContext = tester.element(find.byType(SearchBar)); + final Widget contextMenu = editableTextState.widget.contextMenuBuilder!( + searchAnchorContext, + editableTextState, + ); + + expect(contextMenu, isA()); + }); + testWidgets( 'iOS uses the system context menu by default if supported', (WidgetTester tester) async { From 8f51f46f54a0a5274d84fa8ca2300e30788d838d Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:09:08 -0400 Subject: [PATCH 04/15] simplify context menu builder lerp --- packages/material_ui/lib/src/text_selection_theme.dart | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/material_ui/lib/src/text_selection_theme.dart b/packages/material_ui/lib/src/text_selection_theme.dart index 253b10e99bf8..d456ac900c2b 100644 --- a/packages/material_ui/lib/src/text_selection_theme.dart +++ b/packages/material_ui/lib/src/text_selection_theme.dart @@ -120,12 +120,7 @@ class TextSelectionThemeData with Diagnosticable { TextSelectionThemeData? b, double t, ) { - final first = a?.contextMenuBuilder; - final second = b?.contextMenuBuilder; - - if (first == null) return second; - if (second == null) return first; - return t < 0.5 ? first : second; + return t < 0.5 ? a?.contextMenuBuilder : b?.contextMenuBuilder; } @override From f8d4b3ac38f1c87536f9af490dd2bbf02b6e11e5 Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:12:26 -0400 Subject: [PATCH 05/15] update the doc comment and add a test for _lerpContextMenuBuilder --- .../lib/src/text_selection_theme.dart | 3 +- .../test/text_selection_theme_test.dart | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/material_ui/lib/src/text_selection_theme.dart b/packages/material_ui/lib/src/text_selection_theme.dart index d456ac900c2b..6c690035f3b2 100644 --- a/packages/material_ui/lib/src/text_selection_theme.dart +++ b/packages/material_ui/lib/src/text_selection_theme.dart @@ -113,8 +113,7 @@ class TextSelectionThemeData with Diagnosticable { } /// Linear interpolation doesn't make much sense between two context menu builders, - /// so instead of interpolating, choose one based on the value of 't' if both are non-null. - /// If one is null but not the other return the non-null one. If both are null, return null. + /// so instead of interpolating, choose one based on the value of 't'. static EditableTextContextMenuBuilder? _lerpContextMenuBuilder( TextSelectionThemeData? a, TextSelectionThemeData? b, diff --git a/packages/material_ui/test/text_selection_theme_test.dart b/packages/material_ui/test/text_selection_theme_test.dart index f223d7a04bd1..e554593d2127 100644 --- a/packages/material_ui/test/text_selection_theme_test.dart +++ b/packages/material_ui/test/text_selection_theme_test.dart @@ -46,6 +46,58 @@ void main() { expect(identical(TextSelectionThemeData.lerp(data, data, 0.5), data), true); }); + group('TextSelectionThemeData lerp contextMenuBuilder', () { + Widget aBuilder(BuildContext context, EditableTextState editableTextState) { + return const Placeholder(); + } + + Widget bBuilder(BuildContext context, EditableTextState editableTextState) { + return const Icon(Icons.search); + } + + const withoutBuilder = TextSelectionThemeData(); + final withABuilder = TextSelectionThemeData(contextMenuBuilder: aBuilder); + final withBBuilder = TextSelectionThemeData(contextMenuBuilder: bBuilder); + + test('returns null when both are null', () { + expect(TextSelectionThemeData.lerp(null, null, 0.5)!.contextMenuBuilder, null); + expect( + TextSelectionThemeData.lerp(withoutBuilder, withoutBuilder, 0.5)!.contextMenuBuilder, + null, + ); + }); + + test('returns a below 0.5 and b at or above 0.5', () { + expect( + TextSelectionThemeData.lerp(withABuilder, withBBuilder, 0)!.contextMenuBuilder, + aBuilder, + ); + expect( + TextSelectionThemeData.lerp(withABuilder, withBBuilder, 0.49)!.contextMenuBuilder, + aBuilder, + ); + expect( + TextSelectionThemeData.lerp(withABuilder, withBBuilder, 0.5)!.contextMenuBuilder, + bBuilder, + ); + expect( + TextSelectionThemeData.lerp(withABuilder, withBBuilder, 1)!.contextMenuBuilder, + bBuilder, + ); + }); + + test('can produce a null result when the chosen side is null', () { + expect( + TextSelectionThemeData.lerp(withABuilder, withoutBuilder, 0.5)!.contextMenuBuilder, + null, + ); + expect( + TextSelectionThemeData.lerp(withoutBuilder, withBBuilder, 0)!.contextMenuBuilder, + null, + ); + }); + }); + test('TextSelectionThemeData null fields by default', () { const theme = TextSelectionThemeData(); expect(theme.cursorColor, null); From a2d12655ddf7e6c96293f3a90780450d31c7d0c8 Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:19:05 -0400 Subject: [PATCH 06/15] address comments - remove the now unused _defaultContextMenuBuilder function from seach_anchor, revert formatting changes to selectable_text_test and text_field_test --- .../material_ui/lib/src/search_anchor.dart | 10 - .../test/selectable_text_test.dart | 1156 +++---- .../material_ui/test/text_field_test.dart | 2970 ++++++++--------- 3 files changed, 1978 insertions(+), 2158 deletions(-) diff --git a/packages/material_ui/lib/src/search_anchor.dart b/packages/material_ui/lib/src/search_anchor.dart index b2441de64dac..6c6fdd5514e0 100644 --- a/packages/material_ui/lib/src/search_anchor.dart +++ b/packages/material_ui/lib/src/search_anchor.dart @@ -1704,16 +1704,6 @@ class _SearchBarState extends State { super.dispose(); } - static Widget _defaultContextMenuBuilder( - BuildContext context, - EditableTextState editableTextState, - ) { - if (SystemContextMenu.isSupportedByField(editableTextState)) { - return SystemContextMenu.editableText(editableTextState: editableTextState); - } - return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); - } - @override Widget build(BuildContext context) { final TextDirection textDirection = Directionality.of(context); diff --git a/packages/material_ui/test/selectable_text_test.dart b/packages/material_ui/test/selectable_text_test.dart index edc39923ff97..56071af746e7 100644 --- a/packages/material_ui/test/selectable_text_test.dart +++ b/packages/material_ui/test/selectable_text_test.dart @@ -2928,132 +2928,108 @@ void main() { expect(tester.takeException(), isNotNull); }); - testWidgets( - 'tap moves cursor to the edge of the word it tapped', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material( - child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ), - ); + testWidgets('tap moves cursor to the edge of the word it tapped', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; - // We moved the cursor. - expect( - controller.selection, - const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), - ); + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; + // We moved the cursor. + expect( + controller.selection, + const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), + ); - // But don't trigger the toolbar. - expect(find.byType(CupertinoButton), findsNothing); - }, - variant: TargetPlatformVariant.only(TargetPlatform.iOS), - ); + // But don't trigger the toolbar. + expect(find.byType(CupertinoButton), findsNothing); + }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); - testWidgets( - 'tap moves cursor to the position tapped (Android)', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material( - child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ), - ); + testWidgets('tap moves cursor to the position tapped (Android)', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // We moved the cursor. - expect( - controller.selection, - const TextSelection.collapsed(offset: 4, affinity: TextAffinity.upstream), - ); + // We moved the cursor. + expect( + controller.selection, + const TextSelection.collapsed(offset: 4, affinity: TextAffinity.upstream), + ); - // But don't trigger the toolbar. - expect(find.byType(TextButton), findsNothing); - }, - variant: TargetPlatformVariant.all(excluding: const {TargetPlatform.iOS}), - ); + // But don't trigger the toolbar. + expect(find.byType(TextButton), findsNothing); + }, variant: TargetPlatformVariant.all(excluding: const {TargetPlatform.iOS})); - testWidgets( - 'two slow taps do not trigger a word selection on iOS', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material( - child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ), - ); + testWidgets('two slow taps do not trigger a word selection on iOS', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(const Duration(milliseconds: 500)); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // Plain collapsed selection. - expect( - controller.selection, - const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), - ); + // Plain collapsed selection. + expect( + controller.selection, + const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), + ); - // No toolbar. - expect(find.byType(CupertinoButton), findsNothing); - }, - variant: TargetPlatformVariant.only(TargetPlatform.iOS), - ); + // No toolbar. + expect(find.byType(CupertinoButton), findsNothing); + }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); - testWidgets( - 'two slow taps do not trigger a word selection', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material( - child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ), - ); + testWidgets('two slow taps do not trigger a word selection', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(const Duration(milliseconds: 500)); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // Plain collapsed selection. - expect( - controller.selection, - const TextSelection.collapsed(offset: 4, affinity: TextAffinity.upstream), - ); + // Plain collapsed selection. + expect( + controller.selection, + const TextSelection.collapsed(offset: 4, affinity: TextAffinity.upstream), + ); - // No toolbar. - expect(find.byType(CupertinoButton), findsNothing); - }, - variant: TargetPlatformVariant.all(excluding: {TargetPlatform.iOS}), - ); + // No toolbar. + expect(find.byType(CupertinoButton), findsNothing); + }, variant: TargetPlatformVariant.all(excluding: {TargetPlatform.iOS})); testWidgets( 'double tap selects word and first tap of double tap moves cursor', @@ -3251,86 +3227,74 @@ void main() { }), ); - testWidgets( - 'tap after a double tap select is not affected (iOS)', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material( - child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ), - ); + testWidgets('tap after a double tap select is not affected (iOS)', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // First tap moved the cursor. - expect( - controller.selection, - const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream), - ); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 500)); + // First tap moved the cursor. + expect( + controller.selection, + const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream), + ); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(selectableTextStart + const Offset(100.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(100.0, 5.0)); + await tester.pump(); - // Plain collapsed selection at the edge of first word. 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, const TextSelection.collapsed(offset: 7)); + // Plain collapsed selection at the edge of first word. 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, const TextSelection.collapsed(offset: 7)); - // No toolbar. - expect(find.byType(CupertinoButton), findsNothing); - }, - variant: TargetPlatformVariant.only(TargetPlatform.iOS), - ); + // No toolbar. + expect(find.byType(CupertinoButton), findsNothing); + }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); - testWidgets( - 'tap after a double tap select is not affected (macOS)', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material( - child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ), - ); + testWidgets('tap after a double tap select is not affected (macOS)', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // First tap moved the cursor. - expect( - controller.selection, - const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream), - ); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 500)); + // First tap moved the cursor. + expect( + controller.selection, + const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream), + ); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(selectableTextStart + const Offset(100.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(100.0, 5.0)); + await tester.pump(); - // Collapse selection. - expect(controller.selection, const TextSelection.collapsed(offset: 7)); + // Collapse selection. + expect(controller.selection, const TextSelection.collapsed(offset: 7)); - // No toolbar. - expect(find.byType(CupertinoButton), findsNothing); - }, - variant: TargetPlatformVariant.only(TargetPlatform.macOS), - ); + // No toolbar. + expect(find.byType(CupertinoButton), findsNothing); + }, variant: TargetPlatformVariant.only(TargetPlatform.macOS)); testWidgets( 'long press selects word and shows toolbar (iOS)', @@ -3382,73 +3346,69 @@ void main() { expectMaterialSelectionToolbar(); }); - testWidgets( - 'long press selects word and shows custom toolbar (Cupertino)', - (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Center( - child: SelectableText( - 'Atwater Peel Sherbrooke Bonaventure', - selectionControls: cupertinoTextSelectionControls, - ), + testWidgets('long press selects word and shows custom toolbar (Cupertino)', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: SelectableText( + 'Atwater Peel Sherbrooke Bonaventure', + selectionControls: cupertinoTextSelectionControls, ), ), ), - ); + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // The long pressed word is selected. - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); + // The long pressed word is selected. + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); - // Toolbar shows one button (copy). - expect(find.byType(CupertinoButton), findsNWidgets(1)); - expect(find.text('Copy'), findsOneWidget); - }, - variant: TargetPlatformVariant.all(), - ); + // Toolbar shows one button (copy). + expect(find.byType(CupertinoButton), findsNWidgets(1)); + expect(find.text('Copy'), findsOneWidget); + }, variant: TargetPlatformVariant.all()); - testWidgets( - 'long press selects word and shows custom toolbar (Material)', - (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Center( - child: SelectableText( - 'Atwater Peel Sherbrooke Bonaventure', - selectionControls: materialTextSelectionControls, - ), + testWidgets('long press selects word and shows custom toolbar (Material)', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: SelectableText( + 'Atwater Peel Sherbrooke Bonaventure', + selectionControls: materialTextSelectionControls, ), ), ), - ); + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); - // Collapsed toolbar shows 2 buttons: copy, select all - expect(find.byType(TextButton), findsNWidgets(2)); - expect(find.text('Copy'), findsOneWidget); - expect(find.text('Select all'), findsOneWidget); - }, - variant: TargetPlatformVariant.all(), - ); + // Collapsed toolbar shows 2 buttons: copy, select all + expect(find.byType(TextButton), findsNWidgets(2)); + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Select all'), findsOneWidget); + }, variant: TargetPlatformVariant.all()); testWidgets('textSelectionControls is passed to EditableText', (WidgetTester tester) async { await tester.pumpWidget( @@ -3468,145 +3428,129 @@ void main() { expect(widget.selectionControls, equals(materialTextSelectionControls)); }); - testWidgets( - 'PageView beats SelectableText drag gestures (iOS)', - (WidgetTester tester) async { - // This is a regression test for - // https://github.com/flutter/flutter/issues/130198. - final pageController = PageController(); - addTearDown(pageController.dispose); - const testValue = 'abc def ghi jkl mno pqr stu vwx yz'; + testWidgets('PageView beats SelectableText drag gestures (iOS)', (WidgetTester tester) async { + // This is a regression test for + // https://github.com/flutter/flutter/issues/130198. + final pageController = PageController(); + addTearDown(pageController.dispose); + const testValue = 'abc def ghi jkl mno pqr stu vwx yz'; - await tester.pumpWidget( - MaterialApp( - home: Material( - child: PageView( - controller: pageController, - children: const [ - Center(child: SelectableText(testValue)), - SizedBox(height: 200.0, child: Center(child: Text('Page 2'))), - ], - ), + await tester.pumpWidget( + MaterialApp( + home: Material( + child: PageView( + controller: pageController, + children: const [ + Center(child: SelectableText(testValue)), + SizedBox(height: 200.0, child: Center(child: Text('Page 2'))), + ], ), ), - ); + ), + ); - await skipPastScrollingAnimation(tester); + await skipPastScrollingAnimation(tester); - final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); - final Offset pPos = textOffsetToPosition(tester, testValue.indexOf('p')); + final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); + final Offset pPos = textOffsetToPosition(tester, testValue.indexOf('p')); - // A double tap + drag should take precedence over parent drags. - final TestGesture gesture = await tester.startGesture(gPos); - await tester.pump(); - await gesture.up(); - await tester.pump(); - await gesture.down(gPos); - await tester.pumpAndSettle(); - await gesture.moveTo(pPos); - await tester.pump(); - await gesture.up(); - await tester.pumpAndSettle(); - final TextEditingValue currentValue = tester - .state(find.byType(EditableText)) - .textEditingValue; - expect( - currentValue.selection, - TextSelection(baseOffset: testValue.indexOf('g'), extentOffset: testValue.indexOf('p') + 3), - ); + // A double tap + drag should take precedence over parent drags. + final TestGesture gesture = await tester.startGesture(gPos); + await tester.pump(); + await gesture.up(); + await tester.pump(); + await gesture.down(gPos); + await tester.pumpAndSettle(); + await gesture.moveTo(pPos); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + final TextEditingValue currentValue = tester + .state(find.byType(EditableText)) + .textEditingValue; + expect( + currentValue.selection, + TextSelection(baseOffset: testValue.indexOf('g'), extentOffset: testValue.indexOf('p') + 3), + ); - expect(pageController.page, isNotNull); - expect(pageController.page, 0.0); - // A horizontal drag directly on the SelectableText should move the page - // view to the next page. - final Rect selectableTextRect = tester.getRect(find.byType(SelectableText)); - await tester.dragFrom( - selectableTextRect.centerRight - const Offset(0.1, 0.0), - const Offset(-500.0, 0.0), - ); - await tester.pumpAndSettle(); - expect(pageController.page, isNotNull); - expect(pageController.page, 1.0); - }, - variant: const TargetPlatformVariant({TargetPlatform.iOS}), - ); + expect(pageController.page, isNotNull); + expect(pageController.page, 0.0); + // A horizontal drag directly on the SelectableText should move the page + // view to the next page. + final Rect selectableTextRect = tester.getRect(find.byType(SelectableText)); + await tester.dragFrom( + selectableTextRect.centerRight - const Offset(0.1, 0.0), + const Offset(-500.0, 0.0), + ); + await tester.pumpAndSettle(); + expect(pageController.page, isNotNull); + expect(pageController.page, 1.0); + }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); - testWidgets( - 'long press tap cannot initiate a double tap on macOS', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material( - child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ), - ); + testWidgets('long press tap cannot initiate a double tap on macOS', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(const Duration(milliseconds: 50)); + await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); - // Hide the toolbar so it doesn't interfere with taps on the text. - final EditableTextState editableTextState = tester.state( - find.byType(EditableText), - ); - editableTextState.hideToolbar(); - await tester.pumpAndSettle(); + // Hide the toolbar so it doesn't interfere with taps on the text. + final EditableTextState editableTextState = tester.state( + find.byType(EditableText), + ); + editableTextState.hideToolbar(); + await tester.pumpAndSettle(); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // Move the cursor to the tapped position. - expect( - controller.selection, - const TextSelection.collapsed(offset: 4, affinity: TextAffinity.upstream), - ); + // Move the cursor to the tapped position. + expect( + controller.selection, + const TextSelection.collapsed(offset: 4, affinity: TextAffinity.upstream), + ); - expect(find.byType(CupertinoButton), findsNothing); - }, - variant: TargetPlatformVariant.only(TargetPlatform.macOS), - ); + expect(find.byType(CupertinoButton), findsNothing); + }, variant: TargetPlatformVariant.only(TargetPlatform.macOS)); - testWidgets( - 'long press tap cannot initiate a double tap on iOS', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material( - child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ), - ); + testWidgets('long press tap cannot initiate a double tap on iOS', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(const Duration(milliseconds: 50)); + await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); - // Hide the toolbar so it doesn't interfere with taps on the text. - final EditableTextState editableTextState = tester.state( - find.byType(EditableText), - ); - editableTextState.hideToolbar(); - await tester.pumpAndSettle(); + // Hide the toolbar so it doesn't interfere with taps on the text. + final EditableTextState editableTextState = tester.state( + find.byType(EditableText), + ); + editableTextState.hideToolbar(); + await tester.pumpAndSettle(); - await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); - await tester.pump(); + await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); + await tester.pump(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // Move the cursor to the edge of the same word and toggle the toolbar. - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); + // Move the cursor to the edge of the same word and toggle the toolbar. + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); - expect(find.byType(CupertinoButton), findsNWidgets(4)); - }, - variant: TargetPlatformVariant.only(TargetPlatform.iOS), - ); + expect(find.byType(CupertinoButton), findsNWidgets(4)); + }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); testWidgets( 'long press drag extends the selection to the word under the drag and shows toolbar on lift on non-Apple platforms', @@ -3727,135 +3671,57 @@ void main() { variant: const TargetPlatformVariant({TargetPlatform.iOS}), ); - testWidgets( - 'long press drag moves the cursor under the drag and shows toolbar on lift (macOS)', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material( - child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ), - ); - - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - - final TestGesture gesture = await tester.startGesture( - selectableTextStart + const Offset(50.0, 5.0), - ); - await tester.pump(const Duration(milliseconds: 500)); - - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; - - // The long pressed word is selected. - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); - // Cursor move doesn't trigger a toolbar initially. - expect(find.byType(CupertinoButton), findsNothing); - - await gesture.moveBy(const Offset(50, 0)); - await tester.pump(); - - // The selection position is now moved with the drag. - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 8)); - // Still no toolbar. - expect(find.byType(CupertinoButton), findsNothing); - - await gesture.moveBy(const Offset(50, 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. - expect(find.byType(CupertinoButton), findsNothing); - - await gesture.up(); - await tester.pump(); - - // The selection isn't affected by the gesture lift. - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 12)); - // The toolbar now shows up. - expectCupertinoSelectionToolbar(); - }, - variant: const TargetPlatformVariant({TargetPlatform.macOS}), - ); - - testWidgets( - 'long press drag can edge scroll when inside a scrollable', - (WidgetTester tester) async { - // This is a regression test for https://github.com/flutter/flutter/issues/129590. - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Center( - child: SizedBox( - width: 300.0, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SelectableText( - 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges ' * 2, - maxLines: 1, - ), - ), - ), - ), - ), - ), - ); - - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - - final TestGesture gesture = await tester.startGesture( - selectableTextStart + const Offset(200.0, 0.0), - ); - await tester.pump(kLongPressTimeout); - - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; - - expect(controller.selection, const TextSelection(baseOffset: 13, extentOffset: 23)); - - await gesture.moveBy(const Offset(100, 0)); - // To the edge of the screen basically. - await tester.pump(); - expect(controller.selection, const TextSelection(baseOffset: 13, extentOffset: 23)); - // Keep moving out. - await gesture.moveBy(const Offset(100, 0)); - await tester.pump(); - expect(controller.selection, const TextSelection(baseOffset: 13, extentOffset: 35)); - await gesture.moveBy(const Offset(1600, 0)); - await tester.pump(); - expect(controller.selection, const TextSelection(baseOffset: 13, extentOffset: 134)); + testWidgets('long press drag moves the cursor under the drag and shows toolbar on lift (macOS)', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), + ), + ); - await gesture.up(); - await tester.pumpAndSettle(); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - // The selection isn't affected by the gesture lift. - expect(controller.selection, const TextSelection(baseOffset: 13, extentOffset: 134)); - // The toolbar shows up. - if (defaultTargetPlatform == TargetPlatform.iOS) { - expectCupertinoSelectionToolbar(); - } else { - expectMaterialSelectionToolbar(); - } + final TestGesture gesture = await tester.startGesture( + selectableTextStart + const Offset(50.0, 5.0), + ); + await tester.pump(const Duration(milliseconds: 500)); - final RenderEditable renderEditable = findRenderEditable(tester); - final List endpoints = globalize( - renderEditable.getEndpointsForSelection(controller.selection), - renderEditable, - ); - expect(endpoints.isNotEmpty, isTrue); - expect(endpoints.length, 2); - expect(endpoints[0].point.dx, isNegative); - expect(endpoints[1].point.dx, isPositive); - }, - // TODO(Renzo-Olivares): Add in TargetPlatform.android in the line below when - // we fix edge scrolling in a Scrollable https://github.com/flutter/flutter/issues/64059. - variant: const TargetPlatformVariant({TargetPlatform.iOS}), - ); + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; + + // The long pressed word is selected. + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); + // Cursor move doesn't trigger a toolbar initially. + expect(find.byType(CupertinoButton), findsNothing); + + await gesture.moveBy(const Offset(50, 0)); + await tester.pump(); + + // The selection position is now moved with the drag. + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 8)); + // Still no toolbar. + expect(find.byType(CupertinoButton), findsNothing); + + await gesture.moveBy(const Offset(50, 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. + expect(find.byType(CupertinoButton), findsNothing); + + await gesture.up(); + await tester.pump(); + + // The selection isn't affected by the gesture lift. + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 12)); + // The toolbar now shows up. + expectCupertinoSelectionToolbar(); + }, variant: const TargetPlatformVariant({TargetPlatform.macOS})); testWidgets( - 'Desktop mouse drag can edge scroll when inside a horizontal scrollable', + 'long press drag can edge scroll when inside a scrollable', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/129590. await tester.pumpWidget( @@ -3882,28 +3748,36 @@ void main() { final TestGesture gesture = await tester.startGesture( selectableTextStart + const Offset(200.0, 0.0), ); - await tester.pump(); + await tester.pump(kLongPressTimeout); final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); final TextEditingController controller = editableTextWidget.controller; + expect(controller.selection, const TextSelection(baseOffset: 13, extentOffset: 23)); + await gesture.moveBy(const Offset(100, 0)); // To the edge of the screen basically. await tester.pump(); - expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 21)); + expect(controller.selection, const TextSelection(baseOffset: 13, extentOffset: 23)); // Keep moving out. await gesture.moveBy(const Offset(100, 0)); await tester.pump(); - expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 28)); + expect(controller.selection, const TextSelection(baseOffset: 13, extentOffset: 35)); await gesture.moveBy(const Offset(1600, 0)); await tester.pump(); - expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 134)); + expect(controller.selection, const TextSelection(baseOffset: 13, extentOffset: 134)); await gesture.up(); await tester.pumpAndSettle(); // The selection isn't affected by the gesture lift. - expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 134)); + expect(controller.selection, const TextSelection(baseOffset: 13, extentOffset: 134)); + // The toolbar shows up. + if (defaultTargetPlatform == TargetPlatform.iOS) { + expectCupertinoSelectionToolbar(); + } else { + expectMaterialSelectionToolbar(); + } final RenderEditable renderEditable = findRenderEditable(tester); final List endpoints = globalize( @@ -3915,9 +3789,73 @@ void main() { expect(endpoints[0].point.dx, isNegative); expect(endpoints[1].point.dx, isPositive); }, - variant: TargetPlatformVariant.desktop(), + // TODO(Renzo-Olivares): Add in TargetPlatform.android in the line below when + // we fix edge scrolling in a Scrollable https://github.com/flutter/flutter/issues/64059. + variant: const TargetPlatformVariant({TargetPlatform.iOS}), ); + testWidgets('Desktop mouse drag can edge scroll when inside a horizontal scrollable', ( + WidgetTester tester, + ) async { + // This is a regression test for https://github.com/flutter/flutter/issues/129590. + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: SizedBox( + width: 300.0, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SelectableText( + 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges ' * 2, + maxLines: 1, + ), + ), + ), + ), + ), + ), + ); + + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + + final TestGesture gesture = await tester.startGesture( + selectableTextStart + const Offset(200.0, 0.0), + ); + await tester.pump(); + + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; + + await gesture.moveBy(const Offset(100, 0)); + // To the edge of the screen basically. + await tester.pump(); + expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 21)); + // Keep moving out. + await gesture.moveBy(const Offset(100, 0)); + await tester.pump(); + expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 28)); + await gesture.moveBy(const Offset(1600, 0)); + await tester.pump(); + expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 134)); + + await gesture.up(); + await tester.pumpAndSettle(); + + // The selection isn't affected by the gesture lift. + expect(controller.selection, const TextSelection(baseOffset: 14, extentOffset: 134)); + + final RenderEditable renderEditable = findRenderEditable(tester); + final List endpoints = globalize( + renderEditable.getEndpointsForSelection(controller.selection), + renderEditable, + ); + expect(endpoints.isNotEmpty, isTrue); + expect(endpoints.length, 2); + expect(endpoints[0].point.dx, isNegative); + expect(endpoints[1].point.dx, isPositive); + }, variant: TargetPlatformVariant.desktop()); + testWidgets( 'long press drag can edge scroll', (WidgetTester tester) async { @@ -4002,84 +3940,76 @@ void main() { }), ); - testWidgets( - 'long tap still selects after a double tap select (iOS)', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material( - child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ), - ); + testWidgets('long tap still selects after a double tap select (iOS)', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // First tap moved the cursor to the beginning of the second word. - expect( - controller.selection, - const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream), - ); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 500)); + // First tap moved the cursor to the beginning of the second word. + expect( + controller.selection, + const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream), + ); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 500)); - await tester.longPressAt(selectableTextStart + const Offset(100.0, 5.0)); - await tester.pump(); + await tester.longPressAt(selectableTextStart + const Offset(100.0, 5.0)); + await tester.pump(); - // Selected the "word" where the tap happened, which is the first space. - // Because the "word" is a whitespace, the selection will shift to the - // previous "word" that is not a whitespace. - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); + // Selected the "word" where the tap happened, which is the first space. + // Because the "word" is a whitespace, the selection will shift to the + // previous "word" that is not a whitespace. + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7)); - expectCupertinoSelectionToolbar(); - }, - variant: const TargetPlatformVariant({TargetPlatform.iOS}), - ); + expectCupertinoSelectionToolbar(); + }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); - testWidgets( - 'long tap still selects after a double tap select (macOS)', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material( - child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ), - ); + testWidgets('long tap still selects after a double tap select (macOS)', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: Center(child: SelectableText('Atwater Peel Sherbrooke Bonaventure'))), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // First tap moves the cursor to the tapped position. - expect( - controller.selection, - const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream), - ); - await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); - await tester.pump(const Duration(milliseconds: 500)); + // First tap moves the cursor to the tapped position. + expect( + controller.selection, + const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream), + ); + await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 500)); - await tester.longPressAt(selectableTextStart + const Offset(100.0, 5.0)); - await tester.pump(); + await tester.longPressAt(selectableTextStart + const Offset(100.0, 5.0)); + await tester.pump(); - // Selected the "word" where the tap happened, which is the first space. - expect(controller.selection, const TextSelection(baseOffset: 7, extentOffset: 8)); + // Selected the "word" where the tap happened, which is the first space. + expect(controller.selection, const TextSelection(baseOffset: 7, extentOffset: 8)); - // The toolbar shows up. - expectCupertinoSelectionToolbar(); - }, - variant: const TargetPlatformVariant({TargetPlatform.macOS}), - ); + // The toolbar shows up. + expectCupertinoSelectionToolbar(); + }, variant: const TargetPlatformVariant({TargetPlatform.macOS})); //convert testWidgets( 'double tap after a long tap is not affected', @@ -4252,109 +4182,101 @@ void main() { expect(find.byType(TextButton), findsNothing); }); - testWidgets( - 'force press selects word', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ); + testWidgets('force press selects word', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - final int pointerValue = tester.nextPointer; - final Offset offset = selectableTextStart + const Offset(150.0, 5.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, - ), - ); + final int pointerValue = tester.nextPointer; + final Offset offset = selectableTextStart + const Offset(150.0, 5.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: selectableTextStart + const Offset(150.0, 5.0), - pressure: 0.5, - pressureMin: 0, - ), - ); + await gesture.updateWithCustomEvent( + PointerMoveEvent( + pointer: pointerValue, + position: selectableTextStart + const Offset(150.0, 5.0), + pressure: 0.5, + pressureMin: 0, + ), + ); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // We expect the force press to select a word at the given location. - expect(controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12)); + // 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.pump(); - expectCupertinoSelectionToolbar(); - }, - variant: const TargetPlatformVariant({TargetPlatform.iOS}), - ); + await gesture.up(); + await tester.pump(); + expectCupertinoSelectionToolbar(); + }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); - testWidgets( - 'tap on non-force-press-supported devices work', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), - ), - ); + testWidgets('tap on non-force-press-supported devices work', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material(child: SelectableText('Atwater Peel Sherbrooke Bonaventure')), + ), + ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); - final int pointerValue = tester.nextPointer; - final Offset offset = selectableTextStart + const Offset(150.0, 5.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, - ), - ); + final int pointerValue = tester.nextPointer; + final Offset offset = selectableTextStart + const Offset(150.0, 5.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: selectableTextStart + const Offset(150.0, 5.0), - pressure: 0.5, - pressureMin: 0, - ), - ); - await gesture.up(); + await gesture.updateWithCustomEvent( + PointerMoveEvent( + pointer: pointerValue, + position: selectableTextStart + const Offset(150.0, 5.0), + pressure: 0.5, + pressureMin: 0, + ), + ); + await gesture.up(); - final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); - final TextEditingController controller = editableTextWidget.controller; + final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); + final TextEditingController controller = editableTextWidget.controller; - // 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), - ); + // 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. - expect(find.byType(CupertinoButton), findsNothing); + await tester.pump(); + // Single taps shouldn't trigger the toolbar. + expect(find.byType(CupertinoButton), findsNothing); - // 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), - ); + // 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 SelectableText debugFillProperties', (WidgetTester tester) async { final builder = DiagnosticPropertiesBuilder(); diff --git a/packages/material_ui/test/text_field_test.dart b/packages/material_ui/test/text_field_test.dart index ef54845ffb08..c75c89198695 100644 --- a/packages/material_ui/test/text_field_test.dart +++ b/packages/material_ui/test/text_field_test.dart @@ -1636,51 +1636,47 @@ void main() { 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'), - ), + 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)); + ), + ); + await tester.showKeyboard(find.byType(TextField)); - const testValue = 'ABC'; - tester.testTextInput.updateEditingValue( - const TextEditingValue( - text: testValue, - selection: TextSelection.collapsed(offset: testValue.length), - ), - ); + const testValue = 'ABC'; + tester.testTextInput.updateEditingValue( + const TextEditingValue( + text: testValue, + selection: TextSelection.collapsed(offset: testValue.length), + ), + ); - await tester.pump(); + 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), - ), - ); + // 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(); + await tester.pump(); - String editText = (findRenderEditable(tester).text! as TextSpan).text!; - expect(editText.substring(editText.length - 1), newChar); + String editText = (findRenderEditable(tester).text! as TextSpan).text!; + expect(editText.substring(editText.length - 1), newChar); - await tester.pump(const Duration(seconds: 2)); + 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.android}), - ); + editText = (findRenderEditable(tester).text! as TextSpan).text!; + expect(editText.substring(editText.length - 1), '\u2022'); + }, variant: const TargetPlatformVariant({TargetPlatform.android})); testWidgets( 'desktop obscureText control test', @@ -2477,56 +2473,54 @@ void main() { 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(); + 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), - ), + 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); + 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')); + 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); + // 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); + 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(); + // 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')); + 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.iOS}), - ); + // End gesture and skip the magnifier hide animation, so it can release + // resources. + await gesture.up(); + await tester.pumpAndSettle(); + }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); testWidgets( 'Cursor should not move on a quick touch drag when touch does not begin on previous selection (iOS)', @@ -3474,69 +3468,65 @@ void main() { skip: kIsWeb, // [intended] on web only one selection handle can be dragged at a time. ); - 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); + 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), - ), + 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); + // 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 endpoints = globalize( - renderEditable.getEndpointsForSelection(controller.selection), - renderEditable, - ); - expect(endpoints.length, 2); + final RenderEditable renderEditable = findRenderEditable(tester); + final List 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); + // 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(); + // 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), - ); + // 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', @@ -4823,55 +4813,53 @@ void main() { 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: [ - PopupMenuButton( - itemBuilder: (BuildContext context) { - return {'About'}.map((String value) { - return PopupMenuItem(value: value, child: Text(value)); - }).toList(); - }, - ), - ], - ), - body: const TextField(), + 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: [ + PopupMenuButton( + itemBuilder: (BuildContext context) { + return {'About'}.map((String value) { + return PopupMenuItem(value: value, child: Text(value)); + }).toList(); + }, + ), + ], ), - ); + body: const TextField(), + ), + ); - await tester.pumpWidget(testWidget); + 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 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)); - await tester.pumpAndSettle(); + // Tap on 3 dot menu. + await tester.tap(find.byType(PopupMenuButton)); + 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. + 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(); + // Tap on TextField again. + await tester.tapAt(textFieldStart); + await tester.pumpAndSettle(); - expect(tester.takeException(), isNull); - }, - variant: const TargetPlatformVariant({TargetPlatform.iOS}), - ); + expect(tester.takeException(), isNull); + }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); testWidgets('TextField height with minLines unset', (WidgetTester tester) async { await tester.pumpWidget(textFieldBuilder()); @@ -10113,39 +10101,35 @@ void main() { 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.iOS})); + 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.iOS}), - ); - - testWidgets( - 'tap with a mouse does not move cursor to the edge of the word', + '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', @@ -10251,199 +10235,191 @@ void main() { }), ); - 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)), - ), + 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(); + 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(); + 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(); + // 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(); + // 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 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.iOS}), - ); + // 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.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)), - ), + 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'. + 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)); + // 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)); + 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)); + // Second tap selects the word around the cursor. + expect(controller.selection, const TextSelection(baseOffset: 24, extentOffset: 35)); - // The toolbar shows up. - _expectCupertinoToolbarForPartialSelection(); + // 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 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 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.iOS}), - ); + // 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.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)), - ), + 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'. + 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)); + // 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(); + 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)); + // 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.iOS}), - ); + // The toolbar shows up. + _expectCupertinoToolbarForPartialSelection(); + }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); testWidgets('iOS selectWordEdge works correctly', (WidgetTester tester) async { final TextEditingController controller = _textEditingController(text: 'blah1 blah2'); @@ -10812,11 +10788,66 @@ void main() { }), ); + 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( - 'Can triple tap to select a paragraph on mobile platforms', + '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(); - final isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( MaterialApp( @@ -10834,80 +10865,23 @@ void main() { await skipPastScrollingAnimation(tester); expect(controller.value.text, testValueB); - final Offset firstLinePos = - tester.getTopLeft(find.byType(TextField)) + const Offset(50.0, 9.0); + final Offset thirdLinePos = textOffsetToPosition(tester, 38); - // Tap on text field to gain focus, and move the selection. - final TestGesture gesture = await tester.startGesture(firstLinePos); + // 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, isTargetPlatformApple ? 5 : 3); + expect(controller.selection.baseOffset, 38); - // 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); + // 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); @@ -10927,64 +10901,62 @@ void main() { variant: TargetPlatformVariant.all(excluding: {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(); + 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.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); + await tester.enterText(find.byType(TextField), testValueB); + await skipPastScrollingAnimation(tester); + expect(controller.value.text, testValueB); - final Offset endOfTextPos = textOffsetToPosition(tester, 74); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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.linux}), - ); + expect(controller.selection.baseOffset, 57); + expect(controller.selection.extentOffset, 74); + }, variant: TargetPlatformVariant.all(excluding: {TargetPlatform.linux})); testWidgets( 'triple tap chains work on Non-Apple mobile platforms', @@ -11058,75 +11030,71 @@ void main() { }), ); - 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)), - ), + 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)); + 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.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(); + 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(); + // 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(); + 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.iOS}), - ); + // 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.iOS})); testWidgets('triple click chains work', (WidgetTester tester) async { final TextEditingController controller = _textEditingController(text: testValueA); @@ -11270,425 +11238,409 @@ void main() { ); }, 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; + 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)), - ), - ); + await tester.pumpWidget( + MaterialApp( + home: Material(child: TextField(controller: controller)), + ), + ); - final Offset firstLinePos = - tester.getTopLeft(find.byType(TextField)) + const Offset(50.0, 9.0); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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(), - ); + 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); + 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), - ), + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField(dragStartBehavior: DragStartBehavior.down, controller: controller), ), - ); + ), + ); - final Offset firstLinePos = textOffsetToPosition(tester, 5); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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(), - ); + 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(); + 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.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); + await tester.enterText(find.byType(TextField), testValueA); + await skipPastScrollingAnimation(tester); + expect(controller.value.text, testValueA); - final Offset firstLinePos = textOffsetToPosition(tester, 5); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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), - ); + 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(); + 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.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); + await tester.enterText(find.byType(TextField), testValueA); + await skipPastScrollingAnimation(tester); + expect(controller.value.text, testValueA); - final Offset firstLinePos = textOffsetToPosition(tester, 5); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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.linux}), - ); + expect(controller.selection.baseOffset, 0); + expect(controller.selection.extentOffset, 20); + }, variant: TargetPlatformVariant.all(excluding: {TargetPlatform.linux})); - testWidgets( - 'Can triple click + drag to select line by line on Linux', - (WidgetTester tester) async { - final TextEditingController controller = _textEditingController(); + 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.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); + 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; + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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), - ); + 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(); + 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.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); + 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; + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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); + 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(); + // 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.linux}), - ); + expect(controller.selection.baseOffset, 0); + expect(controller.selection.extentOffset, 20); + }, variant: TargetPlatformVariant.all(excluding: {TargetPlatform.linux})); testWidgets( 'Going past triple click retains the selection on Apple platforms', @@ -11860,91 +11812,87 @@ void main() { }), ); - 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)), - ), + 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); + final Offset textFieldStart = tester.getTopLeft(find.byType(TextField)); - // 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)); + // 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); - // 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)); + // 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)); - // 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)); + // 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)); - 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)); + // 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.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(); + // 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(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.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(); - // 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.pumpAndSettle(); - // Clicking again selects the paragraph. - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 20)); - }, - variant: TargetPlatformVariant.only(TargetPlatform.windows), - ); + 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( @@ -12336,71 +12284,67 @@ void main() { }), ); - 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)), - ), + 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(find.byType(EditableText)); - final RenderEditable renderEditable = state.renderEditable; + final EditableTextState state = tester.state(find.byType(EditableText)); + final RenderEditable renderEditable = state.renderEditable; - final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); - await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0)); - await tester.pumpAndSettle(); + 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)); + // 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; + 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( + 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 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); + // 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), - ); + // 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', @@ -13639,64 +13583,60 @@ void main() { _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)), - ), + 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)); + 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(); + 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(); + // 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(); + // 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.iOS}), - ); + 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.iOS})); testWidgets( 'double click chains work', @@ -13772,73 +13712,71 @@ void main() { }), ); - 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)), - ), + 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); + expect(controller.value.selection, isNotNull); + expect(controller.value.selection.baseOffset, -1); + expect(controller.value.selection.extentOffset, -1); - // 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 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); + // 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.iOS}), - ); + // 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.iOS})); testWidgets( 'selecting a space selects the space on non-iOS platforms', @@ -14069,107 +14007,99 @@ void main() { }), ); - 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)); + 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 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, - ), - ); + final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); - 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)); + 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.up(); - await tester.pumpAndSettle(); - _expectCupertinoToolbarForPartialSelection(); - }, - variant: const TargetPlatformVariant({TargetPlatform.iOS}), - ); + 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)); - 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)), - ), - ); + await gesture.up(); + await tester.pumpAndSettle(); + _expectCupertinoToolbarForPartialSelection(); + }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); - final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + 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 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, - ), - ); + 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 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(); + 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), - ); + // 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(); @@ -14577,43 +14507,41 @@ void main() { }), ); - testWidgets( - 'iPad Scribble selection change shows selection handles', - (WidgetTester tester) async { - const testText = 'lorem ipsum'; - final TextEditingController controller = _textEditingController(text: testText); + 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.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(); + 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 transitions = find - .byType(FadeTransition) - .evaluate() - .map((Element e) => e.widget) - .cast() - .toList(); - expect(transitions.length, 2); - final FadeTransition left = transitions[0]; - final FadeTransition right = transitions[1]; + final List transitions = find + .byType(FadeTransition) + .evaluate() + .map((Element e) => e.widget) + .cast() + .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.iOS}), - ); + expect(left.opacity.value, equals(1.0)); + expect(right.opacity.value, equals(1.0)); + }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); testWidgets('Tap shows handles but not toolbar', (WidgetTester tester) async { final TextEditingController controller = _textEditingController(text: 'abc def ghi'); @@ -17561,49 +17489,41 @@ void main() { }); group('defaults', () { - testWidgets( - 'should build Magnifier on Android', - (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: Scaffold(body: TextField()))); + 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.empty); - addTearDown(magnifierInfo.dispose); + final BuildContext context = tester.firstElement(find.byType(TextField)); + final EditableText editableText = tester.widget(find.byType(EditableText)); + final magnifierInfo = ValueNotifier(MagnifierInfo.empty); + addTearDown(magnifierInfo.dispose); - expect( - editableText.magnifierConfiguration.magnifierBuilder( - context, - MagnifierController(), - magnifierInfo, - ), - isA(), - ); - }, - variant: TargetPlatformVariant.only(TargetPlatform.android), - ); + expect( + editableText.magnifierConfiguration.magnifierBuilder( + context, + MagnifierController(), + magnifierInfo, + ), + isA(), + ); + }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets( - 'should build CupertinoMagnifier on iOS', - (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: Scaffold(body: TextField()))); + 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.empty); - addTearDown(magnifierInfo.dispose); + final BuildContext context = tester.firstElement(find.byType(TextField)); + final EditableText editableText = tester.widget(find.byType(EditableText)); + final magnifierInfo = ValueNotifier(MagnifierInfo.empty); + addTearDown(magnifierInfo.dispose); - expect( - editableText.magnifierConfiguration.magnifierBuilder( - context, - MagnifierController(), - magnifierInfo, - ), - isA(), - ); - }, - variant: TargetPlatformVariant.only(TargetPlatform.iOS), - ); + expect( + editableText.magnifierConfiguration.magnifierBuilder( + context, + MagnifierController(), + magnifierInfo, + ), + isA(), + ); + }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); testWidgets( 'should build nothing on all platforms but iOS and Android', @@ -17882,110 +17802,108 @@ void main() { }), ); - 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 localMagnifierInfo, - ) { - magnifierController = controller; - return TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder( - context, - controller, - localMagnifierInfo, - ); - }, - ), + 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 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); + const testValue = 'one two three four five six seven'; + await tester.enterText(find.byType(TextField), testValue); + await skipPastScrollingAnimation(tester); - // 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); + // 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); - // 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(); + // 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); - expect(controller.selection.isCollapsed, false); - expect(controller.selection.baseOffset, 0); - expect(controller.selection.extentOffset, 18); - expect(magnifierController, isNotNull); - expect(magnifierController!.shown, true); + // 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(); - // 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); + expect(controller.selection.isCollapsed, false); + expect(controller.selection.baseOffset, 0); + expect(controller.selection.extentOffset, 18); + expect(magnifierController, isNotNull); + expect(magnifierController!.shown, true); - // Keep dragging 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); + // 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); - // 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); + // Keep dragging 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); - // 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), - ); + // 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', @@ -18853,66 +18771,60 @@ void main() { 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, - ), - ), + 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 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)); + ), + ); + final EditableTextState state = tester.state(find.byType(EditableText)); + Future 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(); + // 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), - ); + 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, @@ -18986,107 +18898,103 @@ void main() { 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)), - ), + 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( - id: 1, - textDirection: TextDirection.ltr, - children: [ - TestSemantics( - id: 2, - children: [ - TestSemantics( - id: 3, - flags: [SemanticsFlag.scopesRoute], - children: [ - TestSemantics( - id: 4, - inputType: ui.SemanticsInputType.text, - currentValueLength: 0, - flags: [ - SemanticsFlag.isTextField, - SemanticsFlag.isFocusable, - SemanticsFlag.hasEnabledState, - SemanticsFlag.isReadOnly, - ], - actions: [ - 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( + semantics, + hasSemantics( + TestSemantics.root( + children: [ + TestSemantics( + id: 1, + textDirection: TextDirection.ltr, + children: [ + TestSemantics( + id: 2, + children: [ + TestSemantics( + id: 3, + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + id: 4, + inputType: ui.SemanticsInputType.text, + currentValueLength: 0, + flags: [ + SemanticsFlag.isTextField, + SemanticsFlag.isFocusable, + SemanticsFlag.hasEnabledState, + SemanticsFlag.isReadOnly, + ], + actions: [ + 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(), - ); + 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)), - ), + 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(); + ), + ); + 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'); + 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(), - ); + semantics.dispose(); + }, variant: TargetPlatformVariant.all()); testWidgets( 'when receives SemanticsAction.focus while focused but read-only, does not show keyboard', From cd43d25aeacf961ea23323c9b5359e175c42ef74 Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:29:10 -0400 Subject: [PATCH 07/15] fix test --- packages/material_ui/test/text_selection_theme_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/material_ui/test/text_selection_theme_test.dart b/packages/material_ui/test/text_selection_theme_test.dart index e554593d2127..4335d5b52747 100644 --- a/packages/material_ui/test/text_selection_theme_test.dart +++ b/packages/material_ui/test/text_selection_theme_test.dart @@ -60,9 +60,9 @@ void main() { final withBBuilder = TextSelectionThemeData(contextMenuBuilder: bBuilder); test('returns null when both are null', () { - expect(TextSelectionThemeData.lerp(null, null, 0.5)!.contextMenuBuilder, null); + expect(TextSelectionThemeData.lerp(null, null, 0.5)?.contextMenuBuilder, null); expect( - TextSelectionThemeData.lerp(withoutBuilder, withoutBuilder, 0.5)!.contextMenuBuilder, + TextSelectionThemeData.lerp(withoutBuilder, withoutBuilder, 0.5)?.contextMenuBuilder, null, ); }); From 5e03c82b092ecb5c3df95336e2e96a026c178fa7 Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Fri, 11 Sep 2026 09:39:45 -0400 Subject: [PATCH 08/15] add change log pending entry --- .../pending_changelogs/change_2026_09_11_1789133831528.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml diff --git a/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml b/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml new file mode 100644 index 000000000000..f7c52c5429d4 --- /dev/null +++ b/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml @@ -0,0 +1,3 @@ +changelog: | + - Adds global context menu builder override to TextSelectionThemeData +version: patch From d18c36c0de727a3a6d81698669e7ede298efc808 Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Fri, 11 Sep 2026 09:40:44 -0400 Subject: [PATCH 09/15] add a . to the end --- .../pending_changelogs/change_2026_09_11_1789133831528.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml b/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml index f7c52c5429d4..68a522b40dbb 100644 --- a/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml +++ b/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml @@ -1,3 +1,3 @@ changelog: | - - Adds global context menu builder override to TextSelectionThemeData + - Adds global context menu builder override to TextSelectionThemeData. version: patch From b4c1130446b05049a85f7f28688dcf1d154f1b20 Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Fri, 11 Sep 2026 09:41:17 -0400 Subject: [PATCH 10/15] grammar --- .../pending_changelogs/change_2026_09_11_1789133831528.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml b/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml index 68a522b40dbb..23bac6354337 100644 --- a/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml +++ b/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml @@ -1,3 +1,3 @@ changelog: | - - Adds global context menu builder override to TextSelectionThemeData. + - Adds a global context menu builder override to TextSelectionThemeData. version: patch From 5df9d9c54cfad1ca424a3b3070346cd7916e462b Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:07:16 -0400 Subject: [PATCH 11/15] fix static analysis failures - unused imports and redudant args --- packages/material_ui/lib/src/search_anchor.dart | 1 - packages/material_ui/lib/src/text_form_field.dart | 2 -- packages/material_ui/test/text_field_test.dart | 4 ++-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/material_ui/lib/src/search_anchor.dart b/packages/material_ui/lib/src/search_anchor.dart index 6c6fdd5514e0..402d857e1e5f 100644 --- a/packages/material_ui/lib/src/search_anchor.dart +++ b/packages/material_ui/lib/src/search_anchor.dart @@ -32,7 +32,6 @@ import 'material_state.dart'; import 'search_bar_theme.dart'; import 'search_view_theme.dart'; import 'text_field.dart'; -import 'text_selection_theme.dart'; import 'text_theme.dart'; import 'theme.dart'; diff --git a/packages/material_ui/lib/src/text_form_field.dart b/packages/material_ui/lib/src/text_form_field.dart index 3a358b49a88e..19d3cdbf8eda 100644 --- a/packages/material_ui/lib/src/text_form_field.dart +++ b/packages/material_ui/lib/src/text_form_field.dart @@ -8,11 +8,9 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'adaptive_text_selection_toolbar.dart'; import 'input_decorator.dart'; import 'material_state.dart'; import 'text_field.dart'; -import 'text_selection_theme.dart'; export 'package:flutter/services.dart' show SmartDashesType, SmartQuotesType; diff --git a/packages/material_ui/test/text_field_test.dart b/packages/material_ui/test/text_field_test.dart index c75c89198695..10ec464f180d 100644 --- a/packages/material_ui/test/text_field_test.dart +++ b/packages/material_ui/test/text_field_test.dart @@ -17199,7 +17199,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: Material( - child: TextField(key: key, controller: controller, contextMenuBuilder: null), + child: TextField(key: key, controller: controller), ), ), ); @@ -17210,7 +17210,7 @@ void main() { home: Material( child: Padding( padding: EdgeInsets.zero, - child: TextField(key: key, controller: controller, contextMenuBuilder: null), + child: TextField(key: key, controller: controller), ), ), ), From 13948b1c763bb8e7e6e4c0f3cdc7eb18aafc9fa1 Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:23:12 -0400 Subject: [PATCH 12/15] update the version type to minor, attempt to format test without touching anything else --- .../change_2026_09_11_1789133831528.yaml | 2 +- .../material_ui/test/search_anchor_test.dart | 80 +++++++++---------- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml b/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml index 23bac6354337..cec96e5f323a 100644 --- a/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml +++ b/packages/material_ui/pending_changelogs/change_2026_09_11_1789133831528.yaml @@ -1,3 +1,3 @@ changelog: | - Adds a global context menu builder override to TextSelectionThemeData. -version: patch +version: minor diff --git a/packages/material_ui/test/search_anchor_test.dart b/packages/material_ui/test/search_anchor_test.dart index f2de3c487d09..c0b8cb882dcd 100644 --- a/packages/material_ui/test/search_anchor_test.dart +++ b/packages/material_ui/test/search_anchor_test.dart @@ -3535,51 +3535,49 @@ void main() { expect(box.size.height, 32); }); - testWidgets( - 'Tapping outside searchbar should unfocus the searchbar on mobile', - (WidgetTester tester) async { - final focusNode = FocusNode(debugLabel: 'Test Node'); - addTearDown(focusNode.dispose); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SearchAnchor( - builder: (BuildContext context, SearchController controller) { - return SearchBar( - controller: controller, - onTap: () { - controller.openView(); - }, - onTapOutside: (PointerDownEvent event) { - focusNode.unfocus(); - }, - onChanged: (_) { - controller.openView(); - }, - autoFocus: true, - focusNode: focusNode, - ); - }, - suggestionsBuilder: (BuildContext context, SearchController controller) { - return List.generate(5, (int index) { - final item = 'item $index'; - return ListTile(title: Text(item)); - }); - }, - ), + testWidgets('Tapping outside searchbar should unfocus the searchbar on mobile', ( + WidgetTester tester, + ) async { + final focusNode = FocusNode(debugLabel: 'Test Node'); + addTearDown(focusNode.dispose); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SearchAnchor( + builder: (BuildContext context, SearchController controller) { + return SearchBar( + controller: controller, + onTap: () { + controller.openView(); + }, + onTapOutside: (PointerDownEvent event) { + focusNode.unfocus(); + }, + onChanged: (_) { + controller.openView(); + }, + autoFocus: true, + focusNode: focusNode, + ); + }, + suggestionsBuilder: (BuildContext context, SearchController controller) { + return List.generate(5, (int index) { + final item = 'item $index'; + return ListTile(title: Text(item)); + }); + }, ), ), - ); - await tester.pump(); - expect(focusNode.hasPrimaryFocus, isTrue); + ), + ); + await tester.pump(); + expect(focusNode.hasPrimaryFocus, isTrue); - await tester.tapAt(const Offset(50, 50)); - await tester.pump(); + await tester.tapAt(const Offset(50, 50)); + await tester.pump(); - expect(focusNode.hasPrimaryFocus, isFalse); - }, - variant: TargetPlatformVariant.mobile(), - ); + expect(focusNode.hasPrimaryFocus, isFalse); + }, variant: TargetPlatformVariant.mobile()); testWidgets('The default clear button only shows when text input is not empty ' 'on the search view', (WidgetTester tester) async { From ddd102f83d61839a90d3d69d96ca39d00d630830 Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:11:59 -0400 Subject: [PATCH 13/15] fix test failure when running on chrome platform --- .../test/text_selection_theme_test.dart | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/material_ui/test/text_selection_theme_test.dart b/packages/material_ui/test/text_selection_theme_test.dart index 4335d5b52747..7647e594e940 100644 --- a/packages/material_ui/test/text_selection_theme_test.dart +++ b/packages/material_ui/test/text_selection_theme_test.dart @@ -127,8 +127,15 @@ void main() { contextMenuBuilder: defaultContextMenuBuilder, ).debugFillProperties(builder); - final List description = builder.properties + // The contextMenuBuilder property is checked separately below: its + // Function.toString() representation is compiler-dependent (VM vs + // dart2js/DDC), so it can't be compared as an exact string. + final List properties = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .toList(); + + final List description = properties + .where((DiagnosticsNode node) => node.name != 'contextMenuBuilder') .map((DiagnosticsNode node) => node.toString()) .toList(); @@ -136,8 +143,12 @@ void main() { 'cursorColor: ${const Color(0xffeeffaa)}', 'selectionColor: ${const Color(0x88888888)}', 'selectionHandleColor: ${const Color(0xaabbccdd)}', - 'contextMenuBuilder: Closure: (BuildContext, EditableTextState) => CustomContextMenu', ]); + + final DiagnosticsNode contextMenuBuilderNode = properties.singleWhere( + (DiagnosticsNode node) => node.name == 'contextMenuBuilder', + ); + expect(contextMenuBuilderNode.value, defaultContextMenuBuilder); }); testWidgets('Material2 - Empty textSelectionTheme will use defaults', ( From 072e1e45c550e53730a2c1b03554cf88984be593 Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:56:28 -0400 Subject: [PATCH 14/15] Default to the built-in default context menu but allow an explicit null to be passed in to override it, add some tests to cover this case when they don't already exist --- .../material_ui/lib/src/search_anchor.dart | 21 ++++++- .../material_ui/lib/src/selectable_text.dart | 26 ++++---- packages/material_ui/lib/src/text_field.dart | 30 +++++----- .../material_ui/lib/src/text_form_field.dart | 19 +++++- .../material_ui/test/search_anchor_test.dart | 53 +++++++++++++++++ .../test/selectable_text_test.dart | 59 +++++++++++++++++++ .../material_ui/test/text_field_test.dart | 16 ++++- .../test/text_form_field_test.dart | 52 ++++++++++++++++ 8 files changed, 240 insertions(+), 36 deletions(-) diff --git a/packages/material_ui/lib/src/search_anchor.dart b/packages/material_ui/lib/src/search_anchor.dart index 402d857e1e5f..16a38da82f7e 100644 --- a/packages/material_ui/lib/src/search_anchor.dart +++ b/packages/material_ui/lib/src/search_anchor.dart @@ -32,6 +32,7 @@ import 'material_state.dart'; import 'search_bar_theme.dart'; import 'search_view_theme.dart'; import 'text_field.dart'; +import 'text_selection_theme.dart'; import 'text_theme.dart'; import 'theme.dart'; @@ -1309,7 +1310,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor { super.textInputAction, super.keyboardType, EdgeInsets scrollPadding = const EdgeInsets.all(20.0), - EditableTextContextMenuBuilder? contextMenuBuilder, + EditableTextContextMenuBuilder? contextMenuBuilder = SearchBar._defaultContextMenuBuilder, super.enabled, super.smartDashesType, super.smartQuotesType, @@ -1486,7 +1487,7 @@ class SearchBar extends StatefulWidget { this.textInputAction, this.keyboardType, this.scrollPadding = const EdgeInsets.all(20.0), - this.contextMenuBuilder, + this.contextMenuBuilder = _defaultContextMenuBuilder, this.readOnly = false, this.smartDashesType, this.smartQuotesType, @@ -1678,6 +1679,16 @@ class SearchBar extends StatefulWidget { /// configuration option on a standalone [TextField]. final SmartQuotesType? smartQuotesType; + static Widget _defaultContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + if (SystemContextMenu.isSupportedByField(editableTextState)) { + return SystemContextMenu.editableText(editableTextState: editableTextState); + } + return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); + } + @override State createState() => _SearchBarState(); } @@ -1865,7 +1876,11 @@ class _SearchBarState extends State { textInputAction: widget.textInputAction, keyboardType: widget.keyboardType, scrollPadding: widget.scrollPadding, - contextMenuBuilder: widget.contextMenuBuilder, + contextMenuBuilder: + widget.contextMenuBuilder == SearchBar._defaultContextMenuBuilder + ? (TextSelectionTheme.of(context).contextMenuBuilder ?? + SearchBar._defaultContextMenuBuilder) + : widget.contextMenuBuilder, smartDashesType: widget.smartDashesType, smartQuotesType: widget.smartQuotesType, ), diff --git a/packages/material_ui/lib/src/selectable_text.dart b/packages/material_ui/lib/src/selectable_text.dart index fc3bcee19022..1920c395a292 100644 --- a/packages/material_ui/lib/src/selectable_text.dart +++ b/packages/material_ui/lib/src/selectable_text.dart @@ -205,7 +205,7 @@ class SelectableText extends StatefulWidget { this.textHeightBehavior, this.textWidthBasis, this.onSelectionChanged, - this.contextMenuBuilder, + this.contextMenuBuilder = _defaultContextMenuBuilder, this.magnifierConfiguration, }) : assert(maxLines == null || maxLines > 0), assert(minLines == null || minLines > 0), @@ -264,7 +264,7 @@ class SelectableText extends StatefulWidget { this.textHeightBehavior, this.textWidthBasis, this.onSelectionChanged, - this.contextMenuBuilder, + this.contextMenuBuilder = _defaultContextMenuBuilder, this.magnifierConfiguration, }) : assert(maxLines == null || maxLines > 0), assert(minLines == null || minLines > 0), @@ -452,6 +452,13 @@ class SelectableText extends StatefulWidget { /// {@macro flutter.widgets.EditableText.contextMenuBuilder} final EditableTextContextMenuBuilder? contextMenuBuilder; + static Widget _defaultContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); + } + /// The configuration for the magnifier used when the text is selected. /// /// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] @@ -668,13 +675,6 @@ class _SelectableTextState extends State return false; } - static Widget _defaultContextMenuBuilder( - BuildContext context, - EditableTextState editableTextState, - ) { - return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); - } - @override Widget build(BuildContext context) { // TODO(garyq): Assert to block WidgetSpans from being used here are removed, @@ -806,10 +806,10 @@ class _SelectableTextState extends State scrollPhysics: widget.scrollPhysics, scrollBehavior: widget.scrollBehavior, autofillHints: null, - contextMenuBuilder: - widget.contextMenuBuilder ?? - TextSelectionTheme.of(context).contextMenuBuilder ?? - _defaultContextMenuBuilder, + contextMenuBuilder: widget.contextMenuBuilder == SelectableText._defaultContextMenuBuilder + ? (TextSelectionTheme.of(context).contextMenuBuilder ?? + SelectableText._defaultContextMenuBuilder) + : widget.contextMenuBuilder, ), ); diff --git a/packages/material_ui/lib/src/text_field.dart b/packages/material_ui/lib/src/text_field.dart index 9ee562011994..b6b5a906023f 100644 --- a/packages/material_ui/lib/src/text_field.dart +++ b/packages/material_ui/lib/src/text_field.dart @@ -343,7 +343,7 @@ class TextField extends StatefulWidget { this.stylusHandwritingEnabled = EditableText.defaultStylusHandwritingEnabled, this.enableIMEPersonalizedLearning = true, this.enableInlinePrediction, - this.contextMenuBuilder, + this.contextMenuBuilder = _defaultContextMenuBuilder, this.canRequestFocus = true, this.spellCheckConfiguration, this.magnifierConfiguration, @@ -906,6 +906,16 @@ class TextField extends StatefulWidget { /// {@macro flutter.services.TextInputConfiguration.hintLocales} final List? hintLocales; + static Widget _defaultContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + if (SystemContextMenu.isSupportedByField(editableTextState)) { + return SystemContextMenu.editableText(editableTextState: editableTextState); + } + return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); + } + /// {@macro flutter.widgets.EditableText.spellCheckConfiguration} /// /// If [SpellCheckConfiguration.misspelledTextStyle] is not specified in this @@ -1534,16 +1544,6 @@ class _TextFieldState extends State return providedStyle.merge(stateStyle); } - static Widget _defaultContextMenuBuilder( - BuildContext context, - EditableTextState editableTextState, - ) { - if (SystemContextMenu.isSupportedByField(editableTextState)) { - return SystemContextMenu.editableText(editableTextState: editableTextState); - } - return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); - } - @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); @@ -1770,10 +1770,10 @@ class _TextFieldState extends State enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, enableInlinePrediction: widget.enableInlinePrediction, contentInsertionConfiguration: widget.contentInsertionConfiguration, - contextMenuBuilder: - widget.contextMenuBuilder ?? - TextSelectionTheme.of(context).contextMenuBuilder ?? - _defaultContextMenuBuilder, + contextMenuBuilder: widget.contextMenuBuilder == TextField._defaultContextMenuBuilder + ? (TextSelectionTheme.of(context).contextMenuBuilder ?? + TextField._defaultContextMenuBuilder) + : widget.contextMenuBuilder, spellCheckConfiguration: spellCheckConfiguration, magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration, diff --git a/packages/material_ui/lib/src/text_form_field.dart b/packages/material_ui/lib/src/text_form_field.dart index 19d3cdbf8eda..e2db72329979 100644 --- a/packages/material_ui/lib/src/text_form_field.dart +++ b/packages/material_ui/lib/src/text_form_field.dart @@ -8,9 +8,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'adaptive_text_selection_toolbar.dart'; import 'input_decorator.dart'; import 'material_state.dart'; import 'text_field.dart'; +import 'text_selection_theme.dart'; export 'package:flutter/services.dart' show SmartDashesType, SmartQuotesType; @@ -186,7 +188,7 @@ class TextFormField extends FormField { super.restorationId, bool enableIMEPersonalizedLearning = true, MouseCursor? mouseCursor, - EditableTextContextMenuBuilder? contextMenuBuilder, + EditableTextContextMenuBuilder? contextMenuBuilder = _defaultContextMenuBuilder, SpellCheckConfiguration? spellCheckConfiguration, TextMagnifierConfiguration? magnifierConfiguration, UndoHistoryController? undoController, @@ -308,7 +310,10 @@ class TextFormField extends FormField { scrollController: scrollController, enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, mouseCursor: mouseCursor, - contextMenuBuilder: contextMenuBuilder, + contextMenuBuilder: contextMenuBuilder == _defaultContextMenuBuilder + ? (TextSelectionTheme.of(field.context).contextMenuBuilder ?? + _defaultContextMenuBuilder) + : contextMenuBuilder, spellCheckConfiguration: spellCheckConfiguration, magnifierConfiguration: magnifierConfiguration, undoController: undoController, @@ -341,6 +346,16 @@ class TextFormField extends FormField { /// {@macro cupertino_ui.TextFormField.onChanged} final ValueChanged? onChanged; + static Widget _defaultContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + if (SystemContextMenu.isSupportedByField(editableTextState)) { + return SystemContextMenu.editableText(editableTextState: editableTextState); + } + return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); + } + @override FormFieldState createState() => _TextFormFieldState(); } diff --git a/packages/material_ui/test/search_anchor_test.dart b/packages/material_ui/test/search_anchor_test.dart index c0b8cb882dcd..72cacd3067f9 100644 --- a/packages/material_ui/test/search_anchor_test.dart +++ b/packages/material_ui/test/search_anchor_test.dart @@ -4024,6 +4024,59 @@ void main() { expect(contextMenu, isA()); }); + testWidgets( + 'contextMenuBuilder changes from default to null', + (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: Material(child: SearchBar(key: key)), + ), + ); + + await tester.pump(); // Wait for autofocus to take effect. + + // Long-press to bring up the context menu. + Finder textFinder = find.byType(EditableText); + await tester.longPress(textFinder); + tester.state(textFinder).showToolbar(); + await tester.pump(); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); + + // Set contextMenuBuilder to null. + await tester.pumpWidget( + MaterialApp( + home: Material(child: SearchBar(key: key, contextMenuBuilder: null)), + ), + ); + + // Trigger build one more time... + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Padding( + padding: EdgeInsets.zero, + child: SearchBar(key: key, contextMenuBuilder: null), + ), + ), + ), + ); + + await tester.pump(); // Wait for autofocus to take effect. + + // Long-press to bring up the context menu. + textFinder = find.byType(EditableText); + await tester.longPress(textFinder); + tester.state(textFinder).showToolbar(); + await tester.pump(); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); + }, + 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 { diff --git a/packages/material_ui/test/selectable_text_test.dart b/packages/material_ui/test/selectable_text_test.dart index 56071af746e7..2f9c6404954d 100644 --- a/packages/material_ui/test/selectable_text_test.dart +++ b/packages/material_ui/test/selectable_text_test.dart @@ -5738,6 +5738,65 @@ void main() { expect(contextMenu, isNot(isA())); }); + testWidgets( + 'contextMenuBuilder changes from default to null', + (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + const data = 'one two three'; + + await tester.pumpWidget( + MaterialApp( + home: Material(child: SelectableText(data, key: key)), + ), + ); + + await tester.pump(); // Wait for autofocus to take effect. + + // Long-press to bring up the context menu. + Finder textFinder = find.byType(SelectableText); + Finder editableTextFinder = find.descendant( + of: textFinder, + matching: find.byType(EditableText), + ); + await tester.longPress(editableTextFinder); + tester.state(editableTextFinder).showToolbar(); + await tester.pump(); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); + + // Set contextMenuBuilder to null. + await tester.pumpWidget( + MaterialApp( + home: Material(child: SelectableText(data, key: key, contextMenuBuilder: null)), + ), + ); + + // Trigger build one more time... + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Padding( + padding: EdgeInsets.zero, + child: SelectableText(data, key: key, contextMenuBuilder: null), + ), + ), + ), + ); + + await tester.pump(); // Wait for autofocus to take effect. + + // Long-press to bring up the context menu. + textFinder = find.byType(SelectableText); + editableTextFinder = find.descendant(of: textFinder, matching: find.byType(EditableText)); + await tester.longPress(editableTextFinder); + tester.state(editableTextFinder).showToolbar(); + await tester.pump(); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); + }, + skip: kIsWeb, // [intended] on web the browser handles the context menu. + ); + // Regression test for https://github.com/flutter/flutter/issues/169001. testWidgets( 'iOS does not use the system context menu by default even when supported', diff --git a/packages/material_ui/test/text_field_test.dart b/packages/material_ui/test/text_field_test.dart index 10ec464f180d..a516117fe854 100644 --- a/packages/material_ui/test/text_field_test.dart +++ b/packages/material_ui/test/text_field_test.dart @@ -17188,7 +17188,7 @@ void main() { await tester.pump(); // Wait for autofocus to take effect. // Long-press to bring up the context menu. - final Finder textFinder = find.byType(EditableText); + Finder textFinder = find.byType(EditableText); await tester.longPress(textFinder); tester.state(textFinder).showToolbar(); await tester.pump(); @@ -17199,7 +17199,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: Material( - child: TextField(key: key, controller: controller), + child: TextField(key: key, controller: controller, contextMenuBuilder: null), ), ), ); @@ -17210,11 +17210,21 @@ void main() { home: Material( child: Padding( padding: EdgeInsets.zero, - child: TextField(key: key, controller: controller), + child: TextField(key: key, controller: controller, contextMenuBuilder: null), ), ), ), ); + + await tester.pump(); // Wait for autofocus to take effect. + + // Long-press to bring up the context menu. + textFinder = find.byType(EditableText); + await tester.longPress(textFinder); + tester.state(textFinder).showToolbar(); + await tester.pump(); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); }, skip: kIsWeb, // [intended] on web the browser handles the context menu. ); diff --git a/packages/material_ui/test/text_form_field_test.dart b/packages/material_ui/test/text_form_field_test.dart index 685451363e96..dab3157dd40f 100644 --- a/packages/material_ui/test/text_form_field_test.dart +++ b/packages/material_ui/test/text_form_field_test.dart @@ -1855,6 +1855,58 @@ void main() { expect(contextMenu, isNot(isA())); }); + testWidgets( + 'contextMenuBuilder changes from default to null', + (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + MaterialApp( + home: Material(child: TextFormField(key: key)), + ), + ); + + await tester.pump(); // Wait for autofocus to take effect. + + // Long-press to bring up the context menu. + Finder textFinder = find.byType(EditableText); + await tester.longPress(textFinder); + tester.state(textFinder).showToolbar(); + await tester.pump(); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); + + // Set contextMenuBuilder to null. + await tester.pumpWidget( + MaterialApp( + home: Material(child: TextFormField(key: key, contextMenuBuilder: null)), + ), + ); + + // Trigger build one more time... + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Padding( + padding: EdgeInsets.zero, + child: TextFormField(key: key, contextMenuBuilder: null), + ), + ), + ), + ); + + await tester.pump(); // Wait for autofocus to take effect. + + // Long-press to bring up the context menu. + textFinder = find.byType(EditableText); + await tester.longPress(textFinder); + tester.state(textFinder).showToolbar(); + await tester.pump(); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); + }, + 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 { From e0fce8dd4b34dc825005ca9d125c36ba6dedecd3 Mon Sep 17 00:00:00 2001 From: matthewhendrix <29866911+matthewhendrix@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:42:30 -0400 Subject: [PATCH 15/15] make SearchAnchor.bar's contextMenuBuilder nullable and a test for this, pull the logic for resolving which context menu builder to use out of the constructor and in to a separate var --- .../material_ui/lib/src/search_anchor.dart | 14 ++-- .../material_ui/lib/src/selectable_text.dart | 13 ++-- packages/material_ui/lib/src/text_field.dart | 11 ++-- .../material_ui/lib/src/text_form_field.dart | 11 ++-- .../material_ui/test/search_anchor_test.dart | 66 +++++++++++++++++++ 5 files changed, 97 insertions(+), 18 deletions(-) diff --git a/packages/material_ui/lib/src/search_anchor.dart b/packages/material_ui/lib/src/search_anchor.dart index 16a38da82f7e..e4bb5454518c 100644 --- a/packages/material_ui/lib/src/search_anchor.dart +++ b/packages/material_ui/lib/src/search_anchor.dart @@ -242,7 +242,7 @@ class SearchAnchor extends StatefulWidget { TextInputAction? textInputAction, TextInputType? keyboardType, EdgeInsets scrollPadding, - EditableTextContextMenuBuilder contextMenuBuilder, + EditableTextContextMenuBuilder? contextMenuBuilder, bool enabled, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, @@ -1813,6 +1813,12 @@ class _SearchBarState extends State { ) .toList(); + final EditableTextContextMenuBuilder? resolvedContextMenuBuilder = + widget.contextMenuBuilder == SearchBar._defaultContextMenuBuilder + ? (TextSelectionTheme.of(context).contextMenuBuilder ?? + SearchBar._defaultContextMenuBuilder) + : widget.contextMenuBuilder; + return ConstrainedBox( constraints: widget.constraints ?? searchBarTheme.constraints ?? defaults.constraints!, child: Opacity( @@ -1876,11 +1882,7 @@ class _SearchBarState extends State { textInputAction: widget.textInputAction, keyboardType: widget.keyboardType, scrollPadding: widget.scrollPadding, - contextMenuBuilder: - widget.contextMenuBuilder == SearchBar._defaultContextMenuBuilder - ? (TextSelectionTheme.of(context).contextMenuBuilder ?? - SearchBar._defaultContextMenuBuilder) - : widget.contextMenuBuilder, + contextMenuBuilder: resolvedContextMenuBuilder, smartDashesType: widget.smartDashesType, smartQuotesType: widget.smartQuotesType, ), diff --git a/packages/material_ui/lib/src/selectable_text.dart b/packages/material_ui/lib/src/selectable_text.dart index 1920c395a292..7cc1456f0110 100644 --- a/packages/material_ui/lib/src/selectable_text.dart +++ b/packages/material_ui/lib/src/selectable_text.dart @@ -758,12 +758,20 @@ class _SelectableTextState extends State widget.style ?? _controller._textSpan.style, ); } + final TextScaler? effectiveScaler = widget.textScaler ?? switch (widget.textScaleFactor) { null => null, final double textScaleFactor => TextScaler.linear(textScaleFactor), }; + + final EditableTextContextMenuBuilder? resolvedContextMenuBuilder = + widget.contextMenuBuilder == SelectableText._defaultContextMenuBuilder + ? (TextSelectionTheme.of(context).contextMenuBuilder ?? + SelectableText._defaultContextMenuBuilder) + : widget.contextMenuBuilder; + final Widget child = RepaintBoundary( child: EditableText( key: editableTextKey, @@ -806,10 +814,7 @@ class _SelectableTextState extends State scrollPhysics: widget.scrollPhysics, scrollBehavior: widget.scrollBehavior, autofillHints: null, - contextMenuBuilder: widget.contextMenuBuilder == SelectableText._defaultContextMenuBuilder - ? (TextSelectionTheme.of(context).contextMenuBuilder ?? - SelectableText._defaultContextMenuBuilder) - : widget.contextMenuBuilder, + contextMenuBuilder: resolvedContextMenuBuilder, ), ); diff --git a/packages/material_ui/lib/src/text_field.dart b/packages/material_ui/lib/src/text_field.dart index b6b5a906023f..6cf6ee031bcb 100644 --- a/packages/material_ui/lib/src/text_field.dart +++ b/packages/material_ui/lib/src/text_field.dart @@ -1699,6 +1699,12 @@ class _TextFieldState extends State }; } + final EditableTextContextMenuBuilder? resolvedContextMenuBuilder = + widget.contextMenuBuilder == TextField._defaultContextMenuBuilder + ? (TextSelectionTheme.of(context).contextMenuBuilder ?? + TextField._defaultContextMenuBuilder) + : widget.contextMenuBuilder; + Widget child = RepaintBoundary( child: UnmanagedRestorationScope( bucket: bucket, @@ -1770,10 +1776,7 @@ class _TextFieldState extends State enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, enableInlinePrediction: widget.enableInlinePrediction, contentInsertionConfiguration: widget.contentInsertionConfiguration, - contextMenuBuilder: widget.contextMenuBuilder == TextField._defaultContextMenuBuilder - ? (TextSelectionTheme.of(context).contextMenuBuilder ?? - TextField._defaultContextMenuBuilder) - : widget.contextMenuBuilder, + contextMenuBuilder: resolvedContextMenuBuilder, spellCheckConfiguration: spellCheckConfiguration, magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration, diff --git a/packages/material_ui/lib/src/text_form_field.dart b/packages/material_ui/lib/src/text_form_field.dart index e2db72329979..dd8824910564 100644 --- a/packages/material_ui/lib/src/text_form_field.dart +++ b/packages/material_ui/lib/src/text_form_field.dart @@ -247,6 +247,12 @@ class TextFormField extends FormField { onChanged?.call(value); } + final EditableTextContextMenuBuilder? resolvedContextMenuBuilder = + contextMenuBuilder == _defaultContextMenuBuilder + ? (TextSelectionTheme.of(field.context).contextMenuBuilder ?? + _defaultContextMenuBuilder) + : contextMenuBuilder; + return UnmanagedRestorationScope( bucket: field.bucket, child: TextField( @@ -310,10 +316,7 @@ class TextFormField extends FormField { scrollController: scrollController, enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, mouseCursor: mouseCursor, - contextMenuBuilder: contextMenuBuilder == _defaultContextMenuBuilder - ? (TextSelectionTheme.of(field.context).contextMenuBuilder ?? - _defaultContextMenuBuilder) - : contextMenuBuilder, + contextMenuBuilder: resolvedContextMenuBuilder, spellCheckConfiguration: spellCheckConfiguration, magnifierConfiguration: magnifierConfiguration, undoController: undoController, diff --git a/packages/material_ui/test/search_anchor_test.dart b/packages/material_ui/test/search_anchor_test.dart index 72cacd3067f9..716c8904082e 100644 --- a/packages/material_ui/test/search_anchor_test.dart +++ b/packages/material_ui/test/search_anchor_test.dart @@ -4077,6 +4077,72 @@ void main() { skip: kIsWeb, // [intended] on web the browser handles the context menu. ); + testWidgets( + 'SearchAnchor.bar contextMenuBuilder changes from default to null', + (WidgetTester tester) async { + FutureOr> suggestionsBuilder( + BuildContext context, + SearchController controller, + ) { + return []; + } + + await tester.pumpWidget( + MaterialApp( + home: Material(child: SearchAnchor.bar(suggestionsBuilder: suggestionsBuilder)), + ), + ); + + await tester.pump(); // Wait for autofocus to take effect. + + // Long-press to bring up the context menu. + Finder textFinder = find.byType(EditableText); + await tester.longPress(textFinder); + tester.state(textFinder).showToolbar(); + await tester.pump(); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); + + // Set contextMenuBuilder to null. + await tester.pumpWidget( + MaterialApp( + home: Material( + child: SearchAnchor.bar( + suggestionsBuilder: suggestionsBuilder, + contextMenuBuilder: null, + ), + ), + ), + ); + + // Trigger build one more time... + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Padding( + padding: EdgeInsets.zero, + child: SearchAnchor.bar( + suggestionsBuilder: suggestionsBuilder, + contextMenuBuilder: null, + ), + ), + ), + ), + ); + + await tester.pump(); // Wait for autofocus to take effect. + + // Long-press to bring up the context menu. + textFinder = find.byType(EditableText); + await tester.longPress(textFinder); + tester.state(textFinder).showToolbar(); + await tester.pump(); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); + }, + 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 {