// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// reduced-test-set:
//   This file is run as part of a reduced test set in CI on Mac and Windows
//   machines.
@Tags(<String>['reduced-test-set'])
library;

import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Color;

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior, PointerDeviceKind, kDoubleTapTimeout, kLongPressTimeout, kSecondaryMouseButton;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

import '../widgets/clipboard_utils.dart';
import '../widgets/editable_text_utils.dart' show OverflowWidgetTextEditingController, isContextMenuProvidedByPlatform;
import '../widgets/live_text_utils.dart';
import '../widgets/semantics_tester.dart';
import '../widgets/text_selection_toolbar_utils.dart';

class MockTextSelectionControls extends TextSelectionControls {
  @override
  Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]) {
    throw UnimplementedError();
  }

  @override
  Widget buildToolbar(
    BuildContext context,
    Rect globalEditableRegion,
    double textLineHeight,
    Offset position,
    List<TextSelectionPoint> endpoints,
    TextSelectionDelegate delegate,
    ValueListenable<ClipboardStatus>? clipboardStatus,
    Offset? lastSecondaryTapDownPosition,
  ) {
    throw UnimplementedError();
  }

  @override
  Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
    throw UnimplementedError();
  }

  @override
  Size getHandleSize(double textLineHeight) {
    throw UnimplementedError();
  }
}

class PathBoundsMatcher extends Matcher {
  const PathBoundsMatcher({
    this.rectMatcher,
    this.topMatcher,
    this.leftMatcher,
    this.rightMatcher,
    this.bottomMatcher,
  }) : super();

  final Matcher? rectMatcher;
  final Matcher? topMatcher;
  final Matcher? leftMatcher;
  final Matcher? rightMatcher;
  final Matcher? bottomMatcher;

  @override
  bool matches(covariant Path item, Map<dynamic, dynamic> matchState) {
    final Rect bounds = item.getBounds();

    final List<Matcher?> matchers = <Matcher?> [rectMatcher, topMatcher, leftMatcher, rightMatcher, bottomMatcher];
    final List<dynamic> values = <dynamic> [bounds, bounds.top, bounds.left, bounds.right, bounds.bottom];
    final Map<Matcher, dynamic> failedMatcher = <Matcher, dynamic> {};

    for (int idx = 0; idx < matchers.length; idx++) {
      if (!(matchers[idx]?.matches(values[idx], matchState) ?? true)) {
        failedMatcher[matchers[idx]!] = values[idx];
      }
    }

    matchState['failedMatcher'] = failedMatcher;
    return failedMatcher.isEmpty;
  }

  @override
  Description describe(Description description) => description.add('The actual Rect does not match');

  @override
  Description describeMismatch(covariant Path item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
    final Description description = super.describeMismatch(item, mismatchDescription, matchState, verbose);
    final Map<Matcher, dynamic> map = matchState['failedMatcher'] as Map<Matcher, dynamic>;
    final Iterable<String> descriptions = map.entries
      .map<String>(
        (MapEntry<Matcher, dynamic> entry) => entry.key.describeMismatch(entry.value, StringDescription(), matchState, verbose).toString(),
      );

    // description is guaranteed to be non-null.
    return description
        ..add('mismatch Rect: ${item.getBounds()}')
        .addAll(': ', ', ', '. ', descriptions);
  }
}

class PathPointsMatcher extends Matcher {
  const PathPointsMatcher({
    this.includes = const <Offset>[],
    this.excludes = const <Offset>[],
  }) : super();

  final Iterable<Offset> includes;
  final Iterable<Offset> excludes;

  @override
  bool matches(covariant Path item, Map<dynamic, dynamic> matchState) {
    final Offset? notIncluded = includes.cast<Offset?>().firstWhere((Offset? offset) => !item.contains(offset!), orElse: () => null);
    final Offset? notExcluded = excludes.cast<Offset?>().firstWhere((Offset? offset) => item.contains(offset!), orElse: () => null);

    matchState['notIncluded'] = notIncluded;
    matchState['notExcluded'] = notExcluded;
    return (notIncluded ?? notExcluded) == null;
  }

  @override
  Description describe(Description description) => description.add('must include these points $includes and must not include $excludes');

  @override
  Description describeMismatch(covariant Path item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
    final Offset? notIncluded = matchState['notIncluded'] as Offset?;
    final Offset? notExcluded = matchState['notExcluded'] as Offset?;
    final Description desc = super.describeMismatch(item, mismatchDescription, matchState, verbose);

    if ((notExcluded ?? notIncluded) != null) {
      desc.add('Within the bounds of the path ${item.getBounds()}: ');
    }

    if (notIncluded != null) {
      desc.add('$notIncluded is not included. ');
    }
    if (notExcluded != null) {
      desc.add('$notExcluded is not excluded. ');
    }
    return desc;
  }
}

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();
  final MockClipboard mockClipboard = MockClipboard();
  TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);

  // Returns the first RenderEditable.
  RenderEditable findRenderEditable(WidgetTester tester) {
    final RenderObject root = tester.renderObject(find.byType(EditableText));
    expect(root, isNotNull);

    RenderEditable? renderEditable;
    void recursiveFinder(RenderObject child) {
      if (child is RenderEditable) {
        renderEditable = child;
        return;
      }
      child.visitChildren(recursiveFinder);
    }
    root.visitChildren(recursiveFinder);
    expect(renderEditable, isNotNull);
    return renderEditable!;
  }

  List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) {
    return points.map<TextSelectionPoint>((TextSelectionPoint point) {
      return TextSelectionPoint(
        box.localToGlobal(point.point),
        point.direction,
      );
    }).toList();
  }

  Offset textOffsetToBottomLeftPosition(WidgetTester tester, int offset) {
    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(
        TextSelection.collapsed(offset: offset),
      ),
      renderEditable,
    );
    expect(endpoints.length, 1);
    return endpoints[0].point;
  }

  // Web has a less threshold for downstream/upstream text position.
  Offset textOffsetToPosition(WidgetTester tester, int offset) => textOffsetToBottomLeftPosition(tester, offset) + const Offset(kIsWeb ? 1 : 0, -2);

  setUp(() async {
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);

    EditableText.debugDeterministicCursor = false;
    // Fill the clipboard so that the Paste option is available in the text
    // selection menu.
    await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
  });

  testWidgets(
    'Live Text button shows and hides correctly when LiveTextStatus changes',
    (WidgetTester tester) async {
      final LiveTextInputTester liveTextInputTester = LiveTextInputTester();
      addTearDown(liveTextInputTester.dispose);

      final TextEditingController controller = TextEditingController(text: '');
      addTearDown(controller.dispose);
      const Key key = ValueKey<String>('TextField');
      final FocusNode focusNode = FocusNode();
      addTearDown(focusNode.dispose);
      final Widget app = MaterialApp(
        theme: ThemeData(platform: TargetPlatform.iOS),
        home: Scaffold(
          body: Center(
            child: CupertinoTextField(
              key: key,
              controller: controller,
              focusNode: focusNode,
            ),
          ),
        ),
      );

      liveTextInputTester.mockLiveTextInputEnabled = true;
      await tester.pumpWidget(app);
      focusNode.requestFocus();
      await tester.pumpAndSettle();

      final Finder textFinder = find.byType(EditableText);
      await tester.longPress(textFinder);
      await tester.pumpAndSettle();
      expect(
        findLiveTextButton(),
        kIsWeb ? findsNothing : findsOneWidget,
      );

      liveTextInputTester.mockLiveTextInputEnabled = false;
      await tester.longPress(textFinder);
      await tester.pumpAndSettle();
      expect(findLiveTextButton(), findsNothing);
    },
  );

  testWidgets('Look Up shows up on iOS only', (WidgetTester tester) async {
    String? lastLookUp;
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
      if (methodCall.method == 'LookUp.invoke') {
        expect(methodCall.arguments, isA<String>());
        lastLookUp = methodCall.arguments as String;
      }
      return null;
    });

      final TextEditingController controller = TextEditingController(
        text: 'Test',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      final bool isTargetPlatformiOS = defaultTargetPlatform == TargetPlatform.iOS;

      // Long press to put the cursor after the "s".
      const int index = 3;
      await tester.longPressAt(textOffsetToPosition(tester, index));
      await tester.pumpAndSettle();

      // Double tap on the same location to select the word around the cursor.
      await tester.tapAt(textOffsetToPosition(tester, index));
      await tester.pump(const Duration(milliseconds: 50));
      await tester.tapAt(textOffsetToPosition(tester, index));
      await tester.pumpAndSettle();

      expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4));
      expect(find.text('Look Up'), isTargetPlatformiOS? findsOneWidget : findsNothing);

      if (isTargetPlatformiOS) {
        await tester.tap(find.text('Look Up'));
        expect(lastLookUp, 'Test');
      }
    },
      variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }),
      skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
    );

  testWidgets('Search Web shows up on iOS only', (WidgetTester tester) async {
    String? lastSearch;
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
      if (methodCall.method == 'SearchWeb.invoke') {
        expect(methodCall.arguments, isA<String>());
        lastSearch = methodCall.arguments as String;
      }
      return null;
    });

    final TextEditingController controller = TextEditingController(
      text: 'Test',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    final bool isTargetPlatformiOS = defaultTargetPlatform == TargetPlatform.iOS;

    // Long press to put the cursor after the "s".
    const int index = 3;
    await tester.longPressAt(textOffsetToPosition(tester, index));
    await tester.pumpAndSettle();

    // Double tap on the same location to select the word around the cursor.
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pumpAndSettle();

    expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4));
    expect(find.text('Search Web'), isTargetPlatformiOS? findsOneWidget : findsNothing);

    if (isTargetPlatformiOS) {
      await tester.tap(find.text('Search Web'));
      expect(lastSearch, 'Test');
    }
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }),
    skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
  );

  testWidgets('Share shows up on iOS and Android', (WidgetTester tester) async {
    String? lastShare;
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
      if (methodCall.method == 'Share.invoke') {
        expect(methodCall.arguments, isA<String>());
        lastShare = methodCall.arguments as String;
      }
      return null;
    });

    final TextEditingController controller = TextEditingController(
      text: 'Test',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    // Long press to put the cursor after the "s".
    const int index = 3;
    await tester.longPressAt(textOffsetToPosition(tester, index));
    await tester.pumpAndSettle();

    // Double tap on the same location to select the word around the cursor.
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pumpAndSettle();

    expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4));
    expect(find.text('Share...'), findsOneWidget);

    await tester.tap(find.text('Share...'));
    expect(lastShare, 'Test');
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }),
    skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
  );

  testWidgets('can use the desktop cut/copy/paste buttons on Mac', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'blah1 blah2',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: ConstrainedBox(
            constraints: BoxConstraints.loose(const Size(400, 200)),
            child: CupertinoTextField(controller: controller),
          ),
        ),
      ),
    );

    // Initially, the menu is not shown and there is no selection.
    expect(find.byType(CupertinoButton), findsNothing);
    expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));

    final Offset midBlah1 = textOffsetToPosition(tester, 2);

    // Right clicking shows the menu.
    final TestGesture gesture = await tester.startGesture(
      midBlah1,
      kind: PointerDeviceKind.mouse,
      buttons: kSecondaryMouseButton,
    );
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle();
    expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
    expect(find.text('Copy'), findsOneWidget);
    expect(find.text('Cut'), findsOneWidget);
    expect(find.text('Paste'), findsOneWidget);

    // Copy the first word.
    await tester.tap(find.text('Copy'));
    await tester.pumpAndSettle();
    expect(controller.text, 'blah1 blah2');
    expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
    expect(find.byType(CupertinoButton), findsNothing);

    // Paste it at the end.
    await gesture.down(textOffsetToPosition(tester, controller.text.length));
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle();
    expect(controller.selection, const TextSelection(baseOffset: 11, extentOffset: 11, affinity: TextAffinity.upstream));
    expect(find.text('Cut'), findsNothing);
    expect(find.text('Copy'), findsNothing);
    expect(find.text('Paste'), findsOneWidget);
    await tester.tap(find.text('Paste'));
    await tester.pumpAndSettle();
    expect(controller.text, 'blah1 blah2blah1');
    expect(controller.selection, const TextSelection.collapsed(offset: 16));

    // Cut the first word.
    await gesture.down(midBlah1);
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle();
    expect(find.text('Cut'), findsOneWidget);
    expect(find.text('Copy'), findsOneWidget);
    expect(find.text('Paste'), findsOneWidget);
    expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
    await tester.tap(find.text('Cut'));
    await tester.pumpAndSettle();
    expect(controller.text, ' blah2blah1');
    expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0));
    expect(find.byType(CupertinoButton), findsNothing);
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }),
    skip: kIsWeb, // [intended] the web handles this on its own.
  );

  testWidgets('can get text selection color initially on desktop', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();
    addTearDown(focusNode.dispose);
    final TextEditingController controller = TextEditingController(
      text: 'blah1 blah2',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: RepaintBoundary(
            child: CupertinoTextField(
              key: const ValueKey<int>(1),
              controller: controller,
              focusNode: focusNode,
            ),
          ),
        ),
      ),
    );

    controller.selection = const TextSelection(baseOffset: 0, extentOffset: 11);
    focusNode.requestFocus();
    await tester.pump();

    expect(focusNode.hasFocus, true);
    await expectLater(
      find.byKey(const ValueKey<int>(1)),
      matchesGoldenFile('text_field_golden.text_selection_color.0.png'),
    );
  });

  testWidgets('Activates the text field when receives semantics focus on Mac, Windows', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
    final FocusNode focusNode = FocusNode();
    addTearDown(focusNode.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: CupertinoTextField(focusNode: focusNode),
      ),
    );
    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics(
            id: 1,
            textDirection: TextDirection.ltr,
            children: <TestSemantics>[
              TestSemantics(
                id: 2,
                children: <TestSemantics>[
                  TestSemantics(
                    id: 3,
                    flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                    children: <TestSemantics>[
                      TestSemantics(
                        id: 4,
                        flags: <SemanticsFlag>[
                          SemanticsFlag.isTextField,
                          SemanticsFlag.hasEnabledState,
                          SemanticsFlag.isEnabled,
                        ],
                        actions: <SemanticsAction>[SemanticsAction.tap,
                          SemanticsAction.didGainAccessibilityFocus,],
                        textDirection: TextDirection.ltr,
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
        ],
      ),
      ignoreRect: true,
      ignoreTransform: true,
    ));

    expect(focusNode.hasFocus, isFalse);
    semanticsOwner.performAction(4, SemanticsAction.didGainAccessibilityFocus);
    await tester.pumpAndSettle();
    expect(focusNode.hasFocus, isTrue);
    semantics.dispose();
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows }));

  testWidgets(
    'takes available space horizontally and takes intrinsic space vertically no-strut',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: ConstrainedBox(
              constraints: BoxConstraints.loose(const Size(200, 200)),
              child: const CupertinoTextField(strutStyle: StrutStyle.disabled),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(CupertinoTextField)),
        const Size(200, 31), // 31 is the height of the default font + padding etc.
      );
    },
  );

  testWidgets('sets cursorOpacityAnimates on EditableText correctly', (WidgetTester tester) async {

    // True

    await tester.pumpWidget(
      const CupertinoApp(
        home: CupertinoTextField(autofocus: true),
      ),
    );
    await tester.pump();
    EditableText editableText = tester.widget(find.byType(EditableText));
    expect(editableText.cursorOpacityAnimates, true);

    // False

    await tester.pumpWidget(
      const CupertinoApp(
        home: CupertinoTextField(autofocus: true, cursorOpacityAnimates: false),
      ),
    );
    await tester.pump();
    editableText = tester.widget(find.byType(EditableText));
    expect(editableText.cursorOpacityAnimates, false);
  });

  testWidgets(
    'takes available space horizontally and takes intrinsic space vertically',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: ConstrainedBox(
              constraints: BoxConstraints.loose(const Size(200, 200)),
              child: const CupertinoTextField(),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(CupertinoTextField)),
        const Size(200, 31), // 31 is the height of the default font (17) + decoration (12).
      );
    },
  );

  testWidgets('selection handles color respects CupertinoTheme', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/74890.
    const Color expectedSelectionHandleColor = Color.fromARGB(255, 10, 200, 255);

    final TextEditingController controller = TextEditingController(text: 'Some text.');
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        theme: const CupertinoThemeData(
          primaryColor: Colors.red,
        ),
        home: Center(
          child: CupertinoTheme(
            data: const CupertinoThemeData(
              primaryColor: expectedSelectionHandleColor,
            ),
            child: CupertinoTextField(controller: controller),
          ),
        ),
      ),
    );

    await tester.tapAt(textOffsetToPosition(tester, 0));
    await tester.pump();
    await tester.tapAt(textOffsetToPosition(tester, 0));
    await tester.pumpAndSettle();
    final Iterable<RenderBox> boxes = tester.renderObjectList<RenderBox>(
      find.descendant(
        of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
        matching: find.byType(CustomPaint),
      ),
    );
    expect(boxes.length, 2);

    for (final RenderBox box in boxes) {
      expect(box, paints..path(color: expectedSelectionHandleColor));
    }
  },
    variant: TargetPlatformVariant.only(TargetPlatform.iOS),
  );

  testWidgets(
    'uses DefaultSelectionStyle for selection and cursor colors if provided',
    (WidgetTester tester) async {
      const Color selectionColor = Colors.black;
      const Color cursorColor = Colors.white;

      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: DefaultSelectionStyle(
              selectionColor: selectionColor,
              cursorColor: cursorColor,
              child: CupertinoTextField(
                autofocus: true,
              )
            ),
          ),
        ),
      );
      await tester.pump();
      final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
      expect(state.widget.selectionColor, selectionColor);
      expect(state.widget.cursorColor, cursorColor);
    },
  );

  testWidgets('Text field drops selection color when losing focus', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/103341.
    final Key key1 = UniqueKey();
    final Key key2 = UniqueKey();
    final TextEditingController controller1 = TextEditingController();
    addTearDown(controller1.dispose);
    const Color selectionColor = Colors.orange;
    const Color cursorColor = Colors.red;

    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: DefaultSelectionStyle(
            selectionColor: selectionColor,
            cursorColor: cursorColor,
            child: Column(
              children: <Widget>[
                CupertinoTextField(
                  key: key1,
                  controller: controller1,
                ),
                CupertinoTextField(key: key2),
              ],
            ),
          ),
        ),
      ),
    );

    const TextSelection selection = TextSelection(baseOffset: 0, extentOffset: 4);
    final EditableTextState state1 = tester.state<EditableTextState>(find.byType(EditableText).first);
    final EditableTextState state2 = tester.state<EditableTextState>(find.byType(EditableText).last);

    await tester.tap(find.byKey(key1));
    await tester.enterText(find.byKey(key1), 'abcd');
    await tester.pump();

    await tester.tap(find.byKey(key2));
    await tester.enterText(find.byKey(key2), 'dcba');
    await tester.pumpAndSettle();

    // Focus and selection is active on first TextField, so the second TextFields
    // selectionColor should be dropped.
    await tester.tap(find.byKey(key1));
    controller1.selection = const TextSelection(baseOffset: 0, extentOffset: 4);
    await tester.pump();
    expect(controller1.selection, selection);
    expect(state1.widget.selectionColor, selectionColor);
    expect(state2.widget.selectionColor, null);

    // Focus and selection is active on second TextField, so the first TextField
    // selectionColor should be dropped.
    await tester.tap(find.byKey(key2));
    await tester.pump();
    expect(state1.widget.selectionColor, null);
    expect(state2.widget.selectionColor, selectionColor);
  });

  testWidgets(
    'multi-lined text fields are intrinsically taller no-strut',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: ConstrainedBox(
              constraints: BoxConstraints.loose(const Size(200, 200)),
              child: const CupertinoTextField(
                maxLines: 3,
                strutStyle: StrutStyle.disabled,
              ),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(CupertinoTextField)),
        const Size(200, 65), // 65 is the height of the default font (17) * maxlines (3) + decoration height (12).
      );
    },
  );

  testWidgets(
    'multi-lined text fields are intrinsically taller',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: ConstrainedBox(
              constraints: BoxConstraints.loose(const Size(200, 200)),
              child: const CupertinoTextField(maxLines: 3),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(CupertinoTextField)),
        const Size(200, 65),
      );
    },
  );

  testWidgets(
    'strut height override',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: ConstrainedBox(
              constraints: BoxConstraints.loose(const Size(200, 200)),
              child: const CupertinoTextField(
                maxLines: 3,
                strutStyle: StrutStyle(
                  fontSize: 8,
                  forceStrutHeight: true,
                ),
              ),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(CupertinoTextField)),
        const Size(200, 38),
      );
    },
    // TODO(mdebbar): Strut styles support.
    skip: isBrowser, // https://github.com/flutter/flutter/issues/32243
  );

  testWidgets(
    'strut forces field taller',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: ConstrainedBox(
              constraints: BoxConstraints.loose(const Size(200, 200)),
              child: const CupertinoTextField(
                maxLines: 3,
                style: TextStyle(fontSize: 10),
                strutStyle: StrutStyle(
                  fontSize: 18,
                  forceStrutHeight: true,
                ),
              ),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(CupertinoTextField)),
        const Size(200, 68),
      );
    },
    // TODO(mdebbar): Strut styles support.
    skip: isBrowser, // https://github.com/flutter/flutter/issues/32243
  );

  testWidgets(
    'default text field has a border',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(),
          ),
        ),
      );

      BoxDecoration decoration = tester.widget<DecoratedBox>(
        find.descendant(
          of: find.byType(CupertinoTextField),
          matching: find.byType(DecoratedBox),
        ),
      ).decoration as BoxDecoration;

      expect(
        decoration.borderRadius,
        const BorderRadius.all(Radius.circular(5)),
      );
      expect(
        decoration.border!.bottom.color.value,
        0x33000000,
      );

      // Dark mode.
      await tester.pumpWidget(
        const CupertinoApp(
          theme: CupertinoThemeData(brightness: Brightness.dark),
          home: Center(
            child: CupertinoTextField(),
          ),
        ),
      );

      decoration = tester.widget<DecoratedBox>(
        find.descendant(
          of: find.byType(CupertinoTextField),
          matching: find.byType(DecoratedBox),
        ),
      ).decoration as BoxDecoration;

      expect(
        decoration.borderRadius,
        const BorderRadius.all(Radius.circular(5)),
      );
      expect(
        decoration.border!.bottom.color.value,
        0x33FFFFFF,
      );
    },
  );

  testWidgets(
    'decoration can be overridden',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              decoration: null,
            ),
          ),
        ),
      );

      expect(
        find.descendant(
          of: find.byType(CupertinoTextField),
          matching: find.byType(DecoratedBox),
        ),
        findsNothing,
      );
    },
  );

  testWidgets(
    'text entries are padded by default',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(text: 'initial');
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      expect(
        tester.getTopLeft(find.text('initial')) - tester.getTopLeft(find.byType(CupertinoTextField)),
        const Offset(7.0, 7.0),
      );
    },
  );

  testWidgets('iOS cursor has offset', (WidgetTester tester) async {
    await tester.pumpWidget(
      const CupertinoApp(
        home: CupertinoTextField(),
      ),
    );

    final EditableText editableText = tester.firstWidget(find.byType(EditableText));
    expect(editableText.cursorOffset, const Offset(-2.0 / 3.0, 0));
  });

  testWidgets('Cursor radius is 2.0', (WidgetTester tester) async {
    await tester.pumpWidget(
      const CupertinoApp(
        home: CupertinoTextField(),
      ),
    );

    final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
    final RenderEditable renderEditable = editableTextState.renderEditable;

    expect(renderEditable.cursorRadius, const Radius.circular(2.0));
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));

  testWidgets('Cupertino cursor android golden', (WidgetTester tester) async {
    final Widget widget = CupertinoApp(
      home: Center(
        child: RepaintBoundary(
          key: const ValueKey<int>(1),
          child: ConstrainedBox(
            constraints: BoxConstraints.loose(const Size(400, 400)),
            child: const CupertinoTextField(),
          ),
        ),
      ),
    );
    await tester.pumpWidget(widget);

    const String testValue = 'A short phrase';
    await tester.enterText(find.byType(CupertinoTextField), testValue);
    await tester.pump();

    await tester.tapAt(textOffsetToPosition(tester, testValue.length));
    await tester.pumpAndSettle();

    await expectLater(
      find.byKey(const ValueKey<int>(1)),
      matchesGoldenFile('text_field_cursor_test.cupertino.0.png'),
    );
  });

  testWidgets('Cupertino cursor golden', (WidgetTester tester) async {
    final Widget widget = CupertinoApp(
      home: Center(
        child: RepaintBoundary(
          key: const ValueKey<int>(1),
          child: ConstrainedBox(
            constraints: BoxConstraints.loose(const Size(400, 400)),
            child: const CupertinoTextField(),
          ),
        ),
      ),
    );
    await tester.pumpWidget(widget);

    const String testValue = 'A short phrase';
    await tester.enterText(find.byType(CupertinoTextField), testValue);
    await tester.pump();

    await tester.tapAt(textOffsetToPosition(tester, testValue.length));
    await tester.pumpAndSettle();

    await expectLater(
      find.byKey(const ValueKey<int>(1)),
      matchesGoldenFile(
        'text_field_cursor_test.cupertino_${debugDefaultTargetPlatformOverride!.name.toLowerCase()}.1.png',
      ),
    );
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets(
    'can control text content via controller',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController();
      addTearDown(controller.dispose);

      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      controller.text = 'controller text';
      await tester.pump();

      expect(find.text('controller text'), findsOneWidget);

      controller.text = '';
      await tester.pump();

      expect(find.text('controller text'), findsNothing);
    },
  );

  testWidgets(
    'placeholder respects textAlign',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              placeholder: 'placeholder',
              textAlign: TextAlign.right,
            ),
          ),
        ),
      );

      final Text placeholder = tester.widget(find.text('placeholder'));
      expect(placeholder.textAlign, TextAlign.right);

      await tester.enterText(find.byType(CupertinoTextField), 'input');
      await tester.pump();

      final EditableText inputText = tester.widget(find.text('input'));
      expect(placeholder.textAlign, inputText.textAlign);
    },
  );

  testWidgets('placeholder dark mode', (WidgetTester tester) async {
    await tester.pumpWidget(
      const CupertinoApp(
        theme: CupertinoThemeData(brightness: Brightness.dark),
        home: Center(
          child: CupertinoTextField(
            placeholder: 'placeholder',
            textAlign: TextAlign.right,
          ),
        ),
      ),
    );

    final Text placeholder = tester.widget(find.text('placeholder'));
    expect(placeholder.style!.color!.value, CupertinoColors.placeholderText.darkColor.value);
  });

  testWidgets(
    'placeholders are lightly colored and disappears once typing starts',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              placeholder: 'placeholder',
            ),
          ),
        ),
      );

      final Text placeholder = tester.widget(find.text('placeholder'));
      expect(placeholder.style!.color!.value, CupertinoColors.placeholderText.color.value);

      await tester.enterText(find.byType(CupertinoTextField), 'input');
      await tester.pump();
      final Element element = tester.element(find.text('placeholder'));
      expect(Visibility.of(element), false);
    },
  );

  testWidgets(
    "placeholderStyle modifies placeholder's style and doesn't affect text's style",
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              placeholder: 'placeholder',
              style: TextStyle(
                color: Color(0x00FFFFFF),
                fontWeight: FontWeight.w300,
              ),
              placeholderStyle: TextStyle(
                color: Color(0xAAFFFFFF),
                fontWeight: FontWeight.w600,
              ),
            ),
          ),
        ),
      );

      final Text placeholder = tester.widget(find.text('placeholder'));
      expect(placeholder.style!.color, const Color(0xAAFFFFFF));
      expect(placeholder.style!.fontWeight, FontWeight.w600);

      await tester.enterText(find.byType(CupertinoTextField), 'input');
      await tester.pump();

      final EditableText inputText = tester.widget(find.text('input'));
      expect(inputText.style.color, const Color(0x00FFFFFF));
      expect(inputText.style.fontWeight, FontWeight.w300);
    },
  );

  testWidgets(
    'prefix widget is in front of the text',
    (WidgetTester tester) async {
      final FocusNode focusNode = FocusNode();
      addTearDown(focusNode.dispose);
      final TextEditingController controller = TextEditingController(text: 'input');
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              focusNode: focusNode,
              prefix: const Icon(CupertinoIcons.add),
              controller: controller,
            ),
          ),
        ),
      );

      expect(
        tester.getTopRight(find.byIcon(CupertinoIcons.add)).dx + 7.0, // 7px standard padding around input.
        tester.getTopLeft(find.byType(EditableText)).dx,
      );

      expect(
        tester.getTopLeft(find.byType(EditableText)).dx,
        tester.getTopLeft(find.byType(CupertinoTextField)).dx
            + tester.getSize(find.byIcon(CupertinoIcons.add)).width
            + 7.0,
      );
    },
  );

  testWidgets(
    'prefix widget respects visibility mode',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              prefix: Icon(CupertinoIcons.add),
              prefixMode: OverlayVisibilityMode.editing,
            ),
          ),
        ),
      );

      expect(find.byIcon(CupertinoIcons.add), findsNothing);
      // The position should just be the edge of the whole text field plus padding.
      expect(
        tester.getTopLeft(find.byType(EditableText)).dx,
        tester.getTopLeft(find.byType(CupertinoTextField)).dx + 7.0,
      );

      await tester.enterText(find.byType(CupertinoTextField), 'text input');
      await tester.pump();

      expect(find.text('text input'), findsOneWidget);
      expect(find.byIcon(CupertinoIcons.add), findsOneWidget);

      // Text is now moved to the right.
      expect(
        tester.getTopLeft(find.byType(EditableText)).dx,
        tester.getTopLeft(find.byType(CupertinoTextField)).dx
            + tester.getSize(find.byIcon(CupertinoIcons.add)).width
            + 7.0,
      );
    },
  );

  testWidgets(
    'suffix widget is after the text',
    (WidgetTester tester) async {
      final FocusNode focusNode = FocusNode();
      addTearDown(focusNode.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              focusNode: focusNode,
              suffix: const Icon(CupertinoIcons.add),
            ),
          ),
        ),
      );

      expect(
        tester.getTopRight(find.byType(EditableText)).dx + 7.0,
        tester.getTopLeft(find.byIcon(CupertinoIcons.add)).dx, // 7px standard padding around input.
      );

      expect(
        tester.getTopRight(find.byType(EditableText)).dx,
        tester.getTopRight(find.byType(CupertinoTextField)).dx
            - tester.getSize(find.byIcon(CupertinoIcons.add)).width
            - 7.0,
      );
    },
  );

  testWidgets(
    'suffix widget respects visibility mode',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              suffix: Icon(CupertinoIcons.add),
              suffixMode: OverlayVisibilityMode.notEditing,
            ),
          ),
        ),
      );

      expect(find.byIcon(CupertinoIcons.add), findsOneWidget);

      await tester.enterText(find.byType(CupertinoTextField), 'text input');
      await tester.pump();

      expect(find.text('text input'), findsOneWidget);
      expect(find.byIcon(CupertinoIcons.add), findsNothing);
    },
  );

  testWidgets(
    'can customize padding',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              padding: EdgeInsets.zero,
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(EditableText)),
        tester.getSize(find.byType(CupertinoTextField)),
      );
    },
  );

  testWidgets(
    'padding is in between prefix and suffix no-strut',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              padding: EdgeInsets.all(20.0),
              prefix: SizedBox(height: 100.0, width: 100.0),
              suffix: SizedBox(height: 50.0, width: 50.0),
              strutStyle: StrutStyle.disabled,
            ),
          ),
        ),
      );

      expect(
        tester.getTopLeft(find.byType(EditableText)).dx,
        // Size of prefix + padding.
        100.0 + 20.0,
      );

      expect(tester.getTopLeft(find.byType(EditableText)).dy, 291.5);

      expect(
        tester.getTopRight(find.byType(EditableText)).dx,
        800.0 - 50.0 - 20.0,
      );

      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              padding: EdgeInsets.all(30.0),
              prefix: SizedBox(height: 100.0, width: 100.0),
              suffix: SizedBox(height: 50.0, width: 50.0),
              strutStyle: StrutStyle.disabled,
            ),
          ),
        ),
      );

      expect(
        tester.getTopLeft(find.byType(EditableText)).dx,
        100.0 + 30.0,
      );

      // Since the highest component, the prefix box, is higher than
      // the text + paddings, the text's vertical position isn't affected.
      expect(tester.getTopLeft(find.byType(EditableText)).dy, 291.5);

      expect(
        tester.getTopRight(find.byType(EditableText)).dx,
        800.0 - 50.0 - 30.0,
      );
    },
  );

  testWidgets(
    'padding is in between prefix and suffix',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              padding: EdgeInsets.all(20.0),
              prefix: SizedBox(height: 100.0, width: 100.0),
              suffix: SizedBox(height: 50.0, width: 50.0),
            ),
          ),
        ),
      );

      expect(
        tester.getTopLeft(find.byType(EditableText)).dx,
        // Size of prefix + padding.
        100.0 + 20.0,
      );

      expect(tester.getTopLeft(find.byType(EditableText)).dy, 291.5);

      expect(
        tester.getTopRight(find.byType(EditableText)).dx,
        800.0 - 50.0 - 20.0,
      );

      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              padding: EdgeInsets.all(30.0),
              prefix: SizedBox(height: 100.0, width: 100.0),
              suffix: SizedBox(height: 50.0, width: 50.0),
            ),
          ),
        ),
      );

      expect(
        tester.getTopLeft(find.byType(EditableText)).dx,
        100.0 + 30.0,
      );

      // Since the highest component, the prefix box, is higher than
      // the text + paddings, the text's vertical position isn't affected.
      expect(tester.getTopLeft(find.byType(EditableText)).dy, 291.5);

      expect(
        tester.getTopRight(find.byType(EditableText)).dx,
        800.0 - 50.0 - 30.0,
      );
    },
  );

  testWidgets(
    'clear button shows with right visibility mode',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController();
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
              placeholder: 'placeholder does not affect clear button',
              clearButtonMode: OverlayVisibilityMode.always,
            ),
          ),
        ),
      );

      expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsOneWidget);

      expect(
        tester.getTopRight(find.byType(EditableText)).dx,
        800.0 - 30.0  /* size of button */ - 7.0 /* padding */,
      );

      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
              placeholder: 'placeholder does not affect clear button',
              clearButtonMode: OverlayVisibilityMode.editing,
            ),
          ),
        ),
      );

      expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsNothing);
      expect(
        tester.getTopRight(find.byType(EditableText)).dx,
        800.0 - 7.0 /* padding */,
      );

      await tester.enterText(find.byType(CupertinoTextField), 'text input');
      await tester.pump();

      expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsOneWidget);
      expect(find.text('text input'), findsOneWidget);
      expect(
        tester.getTopRight(find.byType(EditableText)).dx,
        800.0 - 30.0 - 7.0,
      );

      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
              placeholder: 'placeholder does not affect clear button',
              clearButtonMode: OverlayVisibilityMode.notEditing,
            ),
          ),
        ),
      );
      expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsNothing);

      controller.text = '';
      await tester.pump();

      expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsOneWidget);
    },
  );

  testWidgets(
    'clear button removes text',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController();
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
              placeholder: 'placeholder',
              clearButtonMode: OverlayVisibilityMode.editing,
            ),
          ),
        ),
      );

      controller.text = 'text entry';
      await tester.pump();

      await tester.tap(find.byIcon(CupertinoIcons.clear_thick_circled));
      await tester.pump();

      expect(controller.text, '');
      expect(find.text('placeholder'), findsOneWidget);
      expect(find.text('text entry'), findsNothing);
      expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsNothing);
    },
  );

  testWidgets(
    'tapping clear button also calls onChanged when text not empty',
    (WidgetTester tester) async {
      String value = 'text entry';
      final TextEditingController controller = TextEditingController();
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
              placeholder: 'placeholder',
              onChanged: (String newValue) => value = newValue,
              clearButtonMode: OverlayVisibilityMode.always,
            ),
          ),
        ),
      );

      controller.text = value;
      await tester.pump();

      await tester.tap(find.byIcon(CupertinoIcons.clear_thick_circled));
      await tester.pump();

      expect(controller.text, isEmpty);
      expect(find.text('text entry'), findsNothing);
      expect(value, isEmpty);
    },
  );

  testWidgets(
    'clear button yields precedence to suffix',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController();
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
              clearButtonMode: OverlayVisibilityMode.always,
              suffix: const Icon(CupertinoIcons.add_circled_solid),
              suffixMode: OverlayVisibilityMode.editing,
            ),
          ),
        ),
      );

      expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsOneWidget);
      expect(find.byIcon(CupertinoIcons.add_circled_solid), findsNothing);

      expect(
        tester.getTopRight(find.byType(EditableText)).dx,
        800.0 - 30.0  /* size of button */ - 7.0 /* padding */,
      );

      controller.text = 'non empty text';
      await tester.pump();

      expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsNothing);
      expect(find.byIcon(CupertinoIcons.add_circled_solid), findsOneWidget);

      // Still just takes the space of one widget.
      expect(
        tester.getTopRight(find.byType(EditableText)).dx,
        800.0 - 24.0  /* size of button */ - 7.0 /* padding */,
      );
    },
  );

  testWidgets(
    'font style controls intrinsic height no-strut',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              strutStyle: StrutStyle.disabled,
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(CupertinoTextField)).height,
        31.0,
      );

      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              style: TextStyle(
                // A larger font.
                fontSize: 50.0,
              ),
              strutStyle: StrutStyle.disabled,
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(CupertinoTextField)).height,
        64.0,
      );
    },
  );

  testWidgets(
    'font style controls intrinsic height',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(CupertinoTextField)).height,
        31.0,
      );

      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              style: TextStyle(
                // A larger font.
                fontSize: 50.0,
              ),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(CupertinoTextField)).height,
        64.0,
      );
    },
  );

  testWidgets(
    'RTL puts attachments to the right places',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Directionality(
            textDirection: TextDirection.rtl,
            child: Center(
              child: CupertinoTextField(
                padding: EdgeInsets.all(20.0),
                prefix: Icon(CupertinoIcons.book),
                clearButtonMode: OverlayVisibilityMode.always,
              ),
            ),
          ),
        ),
      );

      expect(
        tester.getTopLeft(find.byIcon(CupertinoIcons.book)).dx,
        800.0 - 24.0,
      );

      expect(
        tester.getTopRight(find.byIcon(CupertinoIcons.clear_thick_circled)).dx,
        24.0,
      );
    },
  );

  testWidgets(
    'text fields with no max lines can grow no-strut',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              maxLines: null,
              strutStyle: StrutStyle.disabled,
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(CupertinoTextField)).height,
        31.0, // Initially one line high.
      );

      await tester.enterText(find.byType(CupertinoTextField), '\n');
      await tester.pump();

      expect(
        tester.getSize(find.byType(CupertinoTextField)).height,
        48.0, // Initially one line high.
      );
    },
  );

  testWidgets(
    'text fields with no max lines can grow',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              maxLines: null,
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(CupertinoTextField)).height,
        31.0, // Initially one line high.
      );

      await tester.enterText(find.byType(CupertinoTextField), '\n');
      await tester.pump();

      expect(
        tester.getSize(find.byType(CupertinoTextField)).height,
        48.0, // Initially one line high.
      );
    },
  );

  testWidgets('cannot enter new lines onto single line TextField', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    await tester.enterText(find.byType(CupertinoTextField), 'abc\ndef');

    expect(controller.text, 'abcdef');
  });

  testWidgets('toolbar colors change with theme brightness, but nothing else', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: "j'aime la poutine",
    );
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: Column(
          children: <Widget>[
            CupertinoTextField(
              controller: controller,
            ),
          ],
        ),
      ),
    );

    await tester.longPressAt(
      tester.getTopRight(find.text("j'aime la poutine")),
    );
    await tester.pumpAndSettle();
    await tester.pump(const Duration(milliseconds: 200));

    Text text = tester.widget<Text>(find.text('Paste'));
    expect(text.style!.color!.value, CupertinoColors.black.value);
    expect(text.style!.fontSize, 15);
    expect(text.style!.letterSpacing, -0.15);
    expect(text.style!.fontWeight, FontWeight.w400);

    // Change the theme.
    await tester.pumpWidget(
      CupertinoApp(
        theme: const CupertinoThemeData(
          brightness: Brightness.dark,
          textTheme: CupertinoTextThemeData(
            textStyle: TextStyle(fontSize: 100, fontWeight: FontWeight.w800),
          ),
        ),
        home: Column(
          children: <Widget>[
            CupertinoTextField(
              controller: controller,
            ),
          ],
        ),
      ),
    );

    await tester.longPressAt(
      tester.getTopRight(find.text("j'aime la poutine")),
    );
    await tester.pumpAndSettle();
    await tester.pump(const Duration(milliseconds: 200));

    text = tester.widget<Text>(find.text('Paste'));
    // The toolbar buttons' text are still the same style.
    expect(text.style!.color!.value, CupertinoColors.white.value);
    expect(text.style!.fontSize, 15);
    expect(text.style!.letterSpacing, -0.15);
    expect(text.style!.fontWeight, FontWeight.w400);
  }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.

  testWidgets('text field toolbar options correctly changes options on Apple Platforms', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Column(
          children: <Widget>[
            CupertinoTextField(
              autofocus: true,
              controller: controller,
              toolbarOptions: const ToolbarOptions(copy: true),
            ),
          ],
        ),
      ),
    );

    // This extra pump is so autofocus can propagate to renderEditable.
    await tester.pump();

    // Long press to put the cursor after the "w".
    const int index = 3;
    await tester.longPressAt(textOffsetToPosition(tester, index));
    await tester.pumpAndSettle();
    expect(
      controller.selection,
      const TextSelection.collapsed(offset: index),
    );

    // Double tap on the same location to select the word around the cursor.
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pump();
    expect(
      controller.selection,
      const TextSelection(baseOffset: 0, extentOffset: 7),
    );

    // Selected text shows 'Copy'.
    expect(find.text('Paste'), findsNothing);
    expect(find.text('Copy'), findsOneWidget);
    expect(find.text('Cut'), findsNothing);
    expect(find.text('Select All'), findsNothing);
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
    skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
  );

  testWidgets('text field toolbar options correctly changes options on non-Apple platforms', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Column(
          children: <Widget>[
            CupertinoTextField(
              controller: controller,
              toolbarOptions: const ToolbarOptions(copy: true),
            ),
          ],
        ),
      ),
    );

    // Long press to select 'Atwater'
    const int index = 3;
    await tester.longPressAt(textOffsetToPosition(tester, index));
    await tester.pumpAndSettle();
    expect(
      controller.selection,
      const TextSelection(baseOffset: 0, extentOffset: 7),
    );

    // Tap elsewhere to hide the context menu so that subsequent taps don't
    // collide with it.
    await tester.tapAt(textOffsetToPosition(tester, controller.text.length));
    await tester.pump();
    expect(
      controller.selection,
      const TextSelection.collapsed(offset: 35, affinity: TextAffinity.upstream),
    );

    // Double tap on the same location to select the word around the cursor.
    await tester.tapAt(textOffsetToPosition(tester, 10));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, 10));
    await tester.pump();
    expect(
      controller.selection,
      const TextSelection(baseOffset: 8, extentOffset: 12),
    );

    // Selected text shows 'Copy'.
    expect(find.text('Paste'), findsNothing);
    expect(find.text('Copy'), findsOneWidget);
    expect(find.text('Cut'), findsNothing);
    expect(find.text('Select All'), findsNothing);
  },
    variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
    skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
  );

  testWidgets('Read only text field', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(text: 'readonly');
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: Column(
          children: <Widget>[
            CupertinoTextField(
              controller: controller,
              readOnly: true,
            ),
          ],
        ),
      ),
    );
    // Read only text field cannot open keyboard.
    await tester.showKeyboard(find.byType(CupertinoTextField));
    expect(tester.testTextInput.hasAnyClients, false);

    await tester.longPressAt(
      tester.getTopRight(find.text('readonly')),
    );

    await tester.pumpAndSettle();

    expect(find.text('Paste'), findsNothing);
    expect(find.text('Cut'), findsNothing);
    expect(find.text('Select All'), findsOneWidget);

    await tester.tap(find.text('Select All'));
    await tester.pump();

    expect(find.text('Copy'), findsOneWidget);
    expect(find.text('Paste'), findsNothing);
    expect(find.text('Cut'), findsNothing);
  }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.

  testWidgets('copy paste', (WidgetTester tester) async {
    await tester.pumpWidget(
      const CupertinoApp(
        home: Column(
          children: <Widget>[
            CupertinoTextField(
              placeholder: 'field 1',
            ),
            CupertinoTextField(
              placeholder: 'field 2',
            ),
          ],
        ),
      ),
    );

    await tester.enterText(
      find.widgetWithText(CupertinoTextField, 'field 1'),
      "j'aime la poutine",
    );
    await tester.pump();

    // Tap an area inside the EditableText but with no text.
    await tester.longPressAt(
      tester.getTopRight(find.text("j'aime la poutine")),
    );
    await tester.pumpAndSettle();
    await tester.pump(const Duration(milliseconds: 200));

    await tester.tap(find.text('Select All'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200));

    await tester.tap(find.text('Cut'));
    await tester.pump();

    // Placeholder 1 is back since the text is cut.
    expect(find.text('field 1'), findsOneWidget);
    expect(find.text('field 2'), findsOneWidget);

    await tester.longPress(find.text('field 2'), warnIfMissed: false); // can't actually hit placeholder
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200));

    await tester.tap(find.text('Paste'));
    await tester.pump();

    expect(find.text('field 1'), findsOneWidget);
    expect(find.text("j'aime la poutine"), findsOneWidget);

    final Element placeholder2Element = tester.element(find.text('field 2'));
    expect(Visibility.of(placeholder2Element), false);
  }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.

  testWidgets(
    'tap moves cursor to the edge of the word it tapped on',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

      await tester.tapAt(textFieldStart + const Offset(50.0, 5.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.
      expect(find.byType(CupertinoButton), findsNothing);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));

  testWidgets(
    'slow double tap does not trigger double tap',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
      // On macOS, we select the precise position of the tap.
      final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      final Offset pos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'.

      await tester.tapAt(pos);
      await tester.pump(const Duration(milliseconds: 500));
      await tester.tapAt(pos);
      await tester.pump();

      // Plain collapsed selection.
      expect(controller.selection.isCollapsed, isTrue);
      expect(controller.selection.baseOffset, isTargetPlatformIOS ? 7 : 6);

      // Toolbar shows on mobile.
      if (isTargetPlatformIOS) {
        expectCupertinoToolbarForCollapsedSelection();
      } else {
        // After a tap, macOS does not show a selection toolbar for a collapsed selection.
        expectNoCupertinoToolbar();
      }
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets(
    'Tapping on a collapsed selection toggles the toolbar',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neigse Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
      );
      addTearDown(controller.dispose);
      // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              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);
      expect(find.byType(CupertinoButton), findsNothing);

      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);
      expect(find.byType(CupertinoButton), findsNothing);

      // Long press at the end of the first line to move the cursor to the end of the first line
      // where the word wrap is. Since there is a word wrap here, and the direction of the text is LTR,
      // the TextAffinity will be upstream and against the natural direction. The toolbar is also
      // shown after a long press.
      await tester.longPressAt(endPos);
      await tester.pumpAndSettle(const Duration(milliseconds: 500));
      expect(controller.selection.isCollapsed, true);
      expect(controller.selection.baseOffset, 46);
      expect(controller.selection.affinity, TextAffinity.upstream);
      expectCupertinoToolbarForCollapsedSelection();

      // Tap at the same position to toggle the toolbar.
      await tester.tapAt(endPos);
      await tester.pumpAndSettle();
      expect(controller.selection.isCollapsed, true);
      expect(controller.selection.baseOffset, 46);
      expect(controller.selection.affinity, TextAffinity.upstream);
      expectNoCupertinoToolbar();

      // Tap at the beginning of the second line to move the cursor to the front of the first word on the
      // second line, where the word wrap is. Since there is a word wrap here, and the direction of the text is LTR,
      // the TextAffinity will be downstream and following the natural direction. The toolbar will be hidden after this tap.
      await tester.tapAt(begPos + Offset(0.0, lineHeight));
      await tester.pumpAndSettle();
      expect(controller.selection.isCollapsed, true);
      expect(controller.selection.baseOffset, 46);
      expect(controller.selection.affinity, TextAffinity.downstream);
      expectNoCupertinoToolbar();
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
  );

  testWidgets(
    'Tapping on a non-collapsed selection toggles the toolbar and retains the selection',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      final Offset vPos = textOffsetToPosition(tester, 29); // Index of 'Bonav|enture'.
      final Offset ePos = textOffsetToPosition(tester, 35) + const Offset(7.0, 0.0); // Index of 'Bonaventure|' + Offset(7.0,0), which taps slightly to the right of the end of the text.
      final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'.

      // This tap just puts the cursor somewhere different than where the double
      // tap will occur to test that the double tap moves the existing cursor first.
      await tester.tapAt(wPos);
      await tester.pump(const Duration(milliseconds: 500));

      await tester.tapAt(vPos);
      await tester.pump(const Duration(milliseconds: 50));
      // First tap moved the cursor.
      expect(controller.selection.isCollapsed, true);
      expect(
        controller.selection.baseOffset,
        35,
      );
      await tester.tapAt(vPos);
      await tester.pumpAndSettle(const Duration(milliseconds: 500));

      // Second tap selects the word around the cursor.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 24, extentOffset: 35),
      );

      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),
      );
      expect(find.byType(CupertinoButton), findsNothing);

      // 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);

      expect(find.byType(CupertinoButton), findsNothing);
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
  );

  testWidgets(
    'double tap selects word for non-Apple platforms',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      // Long press to select 'Atwater'.
      const int index = 3;
      await tester.longPressAt(textOffsetToPosition(tester, index));
      await tester.pumpAndSettle();
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 7),
      );

      // Tap elsewhere to hide the context menu so that subsequent taps don't
      // collide with it.
      await tester.tapAt(textOffsetToPosition(tester, controller.text.length));
      await tester.pump();
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 35, affinity: TextAffinity.upstream),
      );

      // Double tap in the middle of 'Peel' to select the word.
      await tester.tapAt(textOffsetToPosition(tester, 10));
      await tester.pump(const Duration(milliseconds: 50));
      await tester.tapAt(textOffsetToPosition(tester, 10));
      await tester.pumpAndSettle();
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );

      // The toolbar now shows up.
      expectCupertinoToolbarForPartialSelection();

      // Tap somewhere else to move the cursor.
      await tester.tapAt(textOffsetToPosition(tester, index));
      await tester.pumpAndSettle();
      expect(controller.selection, const TextSelection.collapsed(offset: index));
    },
    variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS}),
  );

  testWidgets(
    'double tap selects word for Apple platforms',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              autofocus: true,
              controller: controller,
            ),
          ),
        ),
      );

      // This extra pump is so autofocus can propagate to renderEditable.
      await tester.pump();

      // Long press to put the cursor after the "w".
      const int index = 3;
      await tester.longPressAt(textOffsetToPosition(tester, index));
      await tester.pumpAndSettle();
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: index),
      );

      // Double tap to select the word around the cursor. Move slightly left of
      // the previous tap in order to avoid hitting the text selection toolbar
      // on Mac.
      await tester.tapAt(textOffsetToPosition(tester, index) - const Offset(1.0, 0.0));
      await tester.pump(const Duration(milliseconds: 50));
      await tester.tapAt(textOffsetToPosition(tester, index));
      await tester.pumpAndSettle();
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 7),
      );

      expectCupertinoToolbarForPartialSelection();
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
  );

  testWidgets(
    'double tap does not select word on read-only obscured field',
        (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              readOnly: true,
              obscureText: true,
              controller: controller,
            ),
          ),
        ),
      );

      // Long press to put the cursor after the "w".
      const int index = 3;
      await tester.longPressAt(textOffsetToPosition(tester, index));
      await tester.pumpAndSettle();

      // Second tap doesn't select anything.
      await tester.tapAt(textOffsetToPosition(tester, index));
      await tester.pump(const Duration(milliseconds: 50));
      await tester.tapAt(textOffsetToPosition(tester, index));
      await tester.pumpAndSettle();
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 35),
      );

      // Selected text shows nothing.
      expect(find.byType(CupertinoButton), findsNothing);
    },
  );

  testWidgets(
    'Can double click + drag with a mouse to select word by word',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController();
      addTearDown(controller.dispose);

      await tester.pumpWidget(
        CupertinoApp(
          home: CupertinoPageScaffold(
            child: CupertinoTextField(
              dragStartBehavior: DragStartBehavior.down,
              controller: controller,
            ),
          ),
        ),
      );

      const String testValue = 'abc def ghi';
      await tester.enterText(find.byType(CupertinoTextField), testValue);
      await tester.pumpAndSettle(const Duration(milliseconds: 200));

      final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
      final Offset hPos = textOffsetToPosition(tester, testValue.indexOf('h'));

      // Tap on text field to gain focus, and set selection to '|e'.
      final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
      await tester.pump();
      await gesture.up();
      await tester.pump();
      expect(controller.selection.isCollapsed, true);
      expect(controller.selection.baseOffset, testValue.indexOf('e'));

      // Here we tap on '|e' again, to register a double tap. This will select
      // the word at the tapped position.
      await gesture.down(ePos);
      await tester.pump();

      expect(controller.selection.baseOffset, 4);
      expect(controller.selection.extentOffset, 7);

      // Drag, right after the double tap, to select word by word.
      // Moving to the position of 'h', will extend the selection to 'ghi'.
      await gesture.moveTo(hPos);
      await tester.pumpAndSettle();

      expect(controller.selection.baseOffset, testValue.indexOf('d'));
      expect(controller.selection.extentOffset, testValue.indexOf('i') + 1);
    },
  );

  testWidgets(
    'Can double tap + drag to select word by word',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController();
      addTearDown(controller.dispose);

      await tester.pumpWidget(
        CupertinoApp(
          home: CupertinoPageScaffold(
            child: CupertinoTextField(
              dragStartBehavior: DragStartBehavior.down,
              controller: controller,
            ),
          ),
        ),
      );

      const String testValue = 'abc def ghi';
      await tester.enterText(find.byType(CupertinoTextField), testValue);
      await tester.pumpAndSettle(const Duration(milliseconds: 200));

      final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
      final Offset hPos = textOffsetToPosition(tester, testValue.indexOf('h'));

      // Tap on text field to gain focus, and set selection to '|e'.
      final TestGesture gesture = await tester.startGesture(ePos);
      await tester.pump();
      await gesture.up();
      await tester.pump();

      expect(controller.selection.isCollapsed, true);
      expect(controller.selection.baseOffset, testValue.indexOf('e'));

      // Here we tap on '|e' again, to register a double tap. This will select
      // the word at the tapped position.
      await gesture.down(ePos);
      await tester.pumpAndSettle();

      expect(controller.selection.baseOffset, 4);
      expect(controller.selection.extentOffset, 7);

      // Drag, right after the double tap, to select word by word.
      // Moving to the position of 'h', will extend the selection to 'ghi'.
      await gesture.moveTo(hPos);
      await tester.pumpAndSettle();

      // Toolbar should be hidden during a drag.
      expect(find.byType(CupertinoButton), findsNothing);
      expect(controller.selection.baseOffset, testValue.indexOf('d'));
      expect(controller.selection.extentOffset, testValue.indexOf('i') + 1);

      // Toolbar should re-appear after a drag.
      await gesture.up();
      await tester.pump();
      expectCupertinoToolbarForPartialSelection();

      // Skip the magnifier hide animation, so it can release resources.
      await tester.pump(const Duration(milliseconds: 150));
    },
  );

  testWidgets('Readonly text field does not have tap action', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      const CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            maxLength: 10,
            readOnly: true,
          ),
        ),
      ),
    );

    expect(semantics, isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.tap])));

    semantics.dispose();
  });

  testWidgets(
    'double tap selects word and first tap of double tap moves cursor',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
      // On macOS, we select the precise position of the tap.
      final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'.
      final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'.


      await tester.tapAt(ePos);
      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.isCollapsed, isTrue);
      expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 9);

      await tester.tapAt(pPos);
      await tester.pumpAndSettle();

      // Second tap selects the word around the cursor.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );

      expectCupertinoToolbarForPartialSelection();
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets(
    'double tap hold selects word',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

      await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      final TestGesture gesture =
         await tester.startGesture(textFieldStart + const Offset(150.0, 5.0));
      // Hold the press.
      await tester.pumpAndSettle();

      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );

      expectCupertinoToolbarForPartialSelection();

      await gesture.up();
      await tester.pump();

      // Still selected.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );
      expectCupertinoToolbarForPartialSelection();
  }, variant: TargetPlatformVariant.all());

  testWidgets(
    'tap after a double tap select is not affected',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
      // On macOS, we select the precise position of the tap.
      final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'.
      final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'

      await tester.tapAt(pPos);
      await tester.pump(const Duration(milliseconds: 50));
      // First tap moved the cursor.
      expect(controller.selection.isCollapsed, isTrue);
      expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 9);

      await tester.tapAt(pPos);
      await tester.pump(const Duration(milliseconds: 500));

      await tester.tapAt(ePos);
      await tester.pump();

      // Plain collapsed selection at the edge of first word. In iOS 12, the
      // first tap after a double tap ends up putting the cursor at where
      // you tapped instead of the edge like every other single tap. This is
      // likely a bug in iOS 12 and not present in other versions.
      expect(controller.selection.isCollapsed, isTrue);
      expect(controller.selection.baseOffset, isTargetPlatformIOS ? 7 : 6);

      // No toolbar.
      expect(find.byType(CupertinoButton), findsNothing);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets('double tapping a space selects the previous word on iOS', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: ' blah blah  \n  blah',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
            maxLines: 2,
          ),
        ),
      ),
    );

    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 the second space selects the previous word.
    await tester.pump(const Duration(milliseconds: 500));
    await tester.tapAt(textOffsetToPosition(tester, 5));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, 5));
    await tester.pumpAndSettle();
    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, 1);
    expect(controller.value.selection.extentOffset, 5);

    // 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 first space selects the space.
    await tester.pump(const Duration(milliseconds: 500));
    await tester.tapAt(textOffsetToPosition(tester, 0));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, 0));
    await tester.pumpAndSettle();
    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, 0);
    expect(controller.value.selection.extentOffset, 1);

    // Put the cursor at the end of the field.
    await tester.tapAt(textOffsetToPosition(tester, 19));
    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, 19);
    expect(controller.value.selection.extentOffset, 19);

    // Double tapping the last space selects all previous contiguous spaces on
    // both lines and the previous word.
    await tester.pump(const Duration(milliseconds: 500));
    await tester.tapAt(textOffsetToPosition(tester, 14));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, 14));
    await tester.pumpAndSettle();
    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, 6);
    expect(controller.value.selection.extentOffset, 14);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));

  testWidgets('double tapping a space selects the space on Mac', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: ' blah blah',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, -1);
    expect(controller.value.selection.extentOffset, -1);

    // Put the cursor at the end of the field.
    await tester.tapAt(textOffsetToPosition(tester, 10));
    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, 10);
    expect(controller.value.selection.extentOffset, 10);

    // Double tapping the second space selects it.
    await tester.pump(const Duration(milliseconds: 500));
    await tester.tapAt(textOffsetToPosition(tester, 5));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, 5));
    await tester.pumpAndSettle();

    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, 5);
    expect(controller.value.selection.extentOffset, 6);

    // Tap at the end of the text to move the selection to the end. On some
    // platforms, the context menu "Cut" button blocks this tap, so move it out
    // of the way by an Offset.
    await tester.tapAt(textOffsetToPosition(tester, 10) + const Offset(200.0, 0.0));
    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, 10);
    expect(controller.value.selection.extentOffset, 10);

    // Double tapping the first space selects it.
    await tester.pump(const Duration(milliseconds: 500));
    await tester.tapAt(textOffsetToPosition(tester, 0));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, 0));
    await tester.pumpAndSettle();
    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, 0);
    expect(controller.value.selection.extentOffset, 1);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }));

  testWidgets('double clicking a space selects the space on Mac', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: ' blah blah',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, -1);
    expect(controller.value.selection.extentOffset, -1);

    // Put the cursor at the end of the field.
    final TestGesture gesture = await tester.startGesture(
      textOffsetToPosition(tester, 10),
      pointer: 7,
      kind: PointerDeviceKind.mouse,
    );
    await tester.pump();
    await gesture.up();
    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, 10);
    expect(controller.value.selection.extentOffset, 10);

    // Double tapping the second space selects it.
    await tester.pump(const Duration(milliseconds: 500));
    await gesture.down(textOffsetToPosition(tester, 5));
    await tester.pump();
    await gesture.up();
    await tester.pump(const Duration(milliseconds: 50));
    await gesture.down(textOffsetToPosition(tester, 5));
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle(kDoubleTapTimeout);
    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, 5);
    expect(controller.value.selection.extentOffset, 6);

    // Put the cursor at the end of the field.
    await gesture.down(textOffsetToPosition(tester, 10));
    await tester.pump();
    await gesture.up();
    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, 10);
    expect(controller.value.selection.extentOffset, 10);

    // Double tapping the first space selects it.
    await tester.pump(const Duration(milliseconds: 500));
    await gesture.down(textOffsetToPosition(tester, 0));
    await tester.pump();
    await gesture.up();
    await tester.pump(const Duration(milliseconds: 50));
    await gesture.down(textOffsetToPosition(tester, 0));
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle();
    expect(controller.value.selection, isNotNull);
    expect(controller.value.selection.baseOffset, 0);
    expect(controller.value.selection.extentOffset, 1);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }));

  testWidgets(
    'An obscured CupertinoTextField is not selectable when disabled',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
              obscureText: true,
              enableInteractiveSelection: false,
            ),
          ),
        ),
      );

      final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

      await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      final TestGesture gesture =
         await tester.startGesture(textFieldStart + const Offset(150.0, 5.0));
      // Hold the press.
      await tester.pump(const Duration(milliseconds: 500));

      // Nothing is selected despite the double tap long press gesture.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 35, extentOffset: 35),
      );

      // The selection menu is not present.
      expectNoCupertinoToolbar();

      await gesture.up();
      await tester.pump();

      // Still nothing selected and no selection menu.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 35, extentOffset: 35),
      );
      expectNoCupertinoToolbar();
    },
  );

  testWidgets(
    'A read-only obscured CupertinoTextField is not selectable',
        (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
              obscureText: true,
              readOnly: true,
            ),
          ),
        ),
      );

      final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

      await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      final TestGesture gesture =
          await tester.startGesture(textFieldStart + const Offset(150.0, 5.0));
      // Hold the press.
      await tester.pump(const Duration(milliseconds: 500));

      // Nothing is selected despite the double tap long press gesture.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 35, extentOffset: 35),
      );

      // The selection menu is not present.
      expectNoCupertinoToolbar();

      await gesture.up();
      await tester.pump();

      // Still nothing selected and no selection menu.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 35),
      );
      expectNoCupertinoToolbar();
    },
  );

  testWidgets(
    'An obscured CupertinoTextField is selectable by default',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
              obscureText: true,
            ),
          ),
        ),
      );

      final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

      await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      final TestGesture gesture =
         await tester.startGesture(textFieldStart + const Offset(150.0, 5.0));
      // Hold the press.
      await tester.pumpAndSettle();

      // The obscured text is treated as one word, should select all
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 35),
      );

      // Selected text shows paste toolbar button.
      expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(1));

      await gesture.up();
      await tester.pump();

      // Still selected.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 35),
      );

      expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(1));
    },
  );

  testWidgets('An obscured TextField has correct default context menu', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
            obscureText: true,
          ),
        ),
      ),
    );

    final Offset textFieldStart = tester.getCenter(find.byType(CupertinoTextField));

    await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.longPressAt(textFieldStart + const Offset(150.0, 5.0));
    await tester.pumpAndSettle();

    // Should only have paste option when whole obscure text is selected.
    expect(find.text('Paste'), findsOneWidget);
    expect(find.text('Copy'), findsNothing);
    expect(find.text('Cut'), findsNothing);
    expect(find.text('Select All'), findsNothing);

    // Tap to cancel selection.
    final Offset textFieldEnd = tester.getTopRight(find.byType(CupertinoTextField));
    await tester.tapAt(textFieldEnd + const Offset(-10.0, 5.0));
    await tester.pump(const Duration(milliseconds: 50));
    // Long tap at the end.
    await tester.longPressAt(textFieldEnd + const Offset(-10.0, 5.0));
    await tester.pumpAndSettle();

    // Should have paste and select all options when collapse.
    expect(find.text('Paste'), findsOneWidget);
    expect(find.text('Select All'), findsOneWidget);
    expect(find.text('Copy'), findsNothing);
    expect(find.text('Cut'), findsNothing);
  }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.

  testWidgets(
    'long press selects the word at the long press position and shows toolbar on non-Apple platforms',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

      await tester.longPressAt(textFieldStart + const Offset(50.0, 5.0));
      await tester.pumpAndSettle();

      // Select word, 'Atwater, on long press.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.upstream),
      );

      expectCupertinoToolbarForPartialSelection();
    },
    variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS}),
  );

  testWidgets(
    'long press moves cursor to the exact long press position and shows toolbar on Apple platforms',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              autofocus: true,
              controller: controller,
            ),
          ),
        ),
      );

      // This extra pump is so autofocus can propagate to renderEditable.
      await tester.pump();

      final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

      await tester.longPressAt(textFieldStart + const Offset(50.0, 5.0));
      await tester.pumpAndSettle();

      // Collapsed cursor for iOS long press.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 3, affinity: TextAffinity.upstream),
      );

      expectCupertinoToolbarForCollapsedSelection();
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
  );

  testWidgets(
    'long press tap cannot initiate a double tap',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              autofocus: true,
              controller: controller,
            ),
          ),
        ),
      );

      // This extra pump is so autofocus can propagate to renderEditable.
      await tester.pump();

      final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'

      await tester.longPressAt(ePos);
      await tester.pumpAndSettle(const Duration(milliseconds: 50));

      expectCupertinoToolbarForCollapsedSelection();

      expect(controller.selection.isCollapsed, isTrue);
      expect(controller.selection.baseOffset, 6);

      // Tap in a slightly different position to avoid hitting the context menu
      // on desktop.
      final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
      final Offset secondTapPos = isTargetPlatformIOS
          ? ePos
          : ePos + const Offset(-1.0, 0.0);
      await tester.tapAt(secondTapPos);
      await tester.pump();

      // The cursor does not move and the toolbar is toggled.
      expect(controller.selection.isCollapsed, isTrue);
      expect(controller.selection.baseOffset, 6);

      // The toolbar from the long press is now dismissed by the second tap.
      expectNoCupertinoToolbar();
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets(
    'long press drag selects word by word and shows toolbar on lift on non-Apple platforms',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

      final TestGesture gesture =
          await tester.startGesture(textFieldStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));

      // Long press on non-Apple platforms selects the word at the long press position.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.upstream),
      );
      // Toolbar only shows up on long press up.
      expectNoCupertinoToolbar();

      await gesture.moveBy(const Offset(100, 0));
      await tester.pump();

      // The selection is extended word by word to the drag position.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 12, affinity: TextAffinity.upstream),
      );
      expectNoCupertinoToolbar();

      await gesture.moveBy(const Offset(200, 0));
      await tester.pump();

      // The selection is extended word by word to the drag position.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 23, affinity: TextAffinity.upstream),
      );
      expectNoCupertinoToolbar();

      await gesture.up();
      await tester.pumpAndSettle();

      // The selection isn't affected by the gesture lift.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 23, affinity: TextAffinity.upstream),
      );

      // The toolbar now shows up.
      expectCupertinoToolbarForPartialSelection();
    },
    variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
  );

  testWidgets(
    'long press drag on a focused TextField moves the cursor under the drag and shows toolbar on lift on Apple platforms',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              autofocus: true,
              controller: controller,
            ),
          ),
        ),
      );

      // This extra pump is so autofocus can propagate to renderEditable.
      await tester.pump();

      final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

      final TestGesture gesture =
          await tester.startGesture(textFieldStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));

      // Long press on iOS shows collapsed selection cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 3, affinity: TextAffinity.upstream),
      );
      // Toolbar only shows up on long press up.
      expectNoCupertinoToolbar();

      await gesture.moveBy(const Offset(50, 0));
      await tester.pump();

      // The selection position is now moved with the drag.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 6, affinity: TextAffinity.upstream),
      );
      expectNoCupertinoToolbar();

      await gesture.moveBy(const Offset(50, 0));
      await tester.pump();

      // The selection position is now moved with the drag.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 9, affinity: TextAffinity.upstream),
      );
      expectNoCupertinoToolbar();

      await gesture.up();
      await tester.pumpAndSettle();

      // The selection isn't affected by the gesture lift.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 9, affinity: TextAffinity.upstream),
      );
      // The toolbar now shows up.
      expectCupertinoToolbarForCollapsedSelection();
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
  );

  testWidgets('long press drag can edge scroll on non-Apple platforms', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    final RenderEditable renderEditable = findRenderEditable(tester);

    List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection(
      const TextSelection.collapsed(offset: 66), // Last character's position.
    );

    expect(lastCharEndpoint.length, 1);
    // Just testing the test and making sure that the last character is off
    // the right side of the screen.
    expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(1094.73, epsilon: 0.25));

    final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

    final TestGesture gesture =
        await tester.startGesture(textfieldStart);
    await tester.pump(const Duration(milliseconds: 500));

    expect(
      controller.selection,
      const TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.upstream),
    );
    expect(find.byType(CupertinoButton), findsNothing);

    await gesture.moveBy(const Offset(950, 5));
    // To the edge of the screen basically.
    await tester.pump();
    expect(
      controller.selection,
      const TextSelection(baseOffset: 0, extentOffset: 59),
    );
    // Keep moving out.
    await gesture.moveBy(const Offset(1, 0));
    await tester.pump();
    expect(
      controller.selection,
      const TextSelection(baseOffset: 0, extentOffset: 66),
    );
    await gesture.moveBy(const Offset(1, 0));
    await tester.pump();
    expect(
      controller.selection,
      const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream),
    ); // We're at the edge now.
    expect(find.byType(CupertinoButton), findsNothing);

    await gesture.up();
    await tester.pumpAndSettle();

    // The selection isn't affected by the gesture lift.
    expect(
      controller.selection,
      const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream),
    );

    // The toolbar now shows up.
    expectCupertinoToolbarForFullSelection();

    lastCharEndpoint = renderEditable.getEndpointsForSelection(
      const TextSelection.collapsed(offset: 66), // Last character's position.
    );

    expect(lastCharEndpoint.length, 1);
    // The last character is now on screen near the right edge.
    expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(785.40, epsilon: 1));

    final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection(
      const TextSelection.collapsed(offset: 0), // First character's position.
    );
    expect(firstCharEndpoint.length, 1);
    // The first character is now offscreen to the left.
    expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-310.30, epsilon: 1));
  }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));

  testWidgets('long press drag can edge scroll on Apple platforms', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            autofocus: true,
            controller: controller,
          ),
        ),
      ),
    );

    // This extra pump is so autofocus can propagate to renderEditable.
    await tester.pump();

    final RenderEditable renderEditable = tester.renderObject<RenderEditable>(
      find.byElementPredicate((Element element) => element.renderObject is RenderEditable).last,
    );

    List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection(
      const TextSelection.collapsed(offset: 66), // Last character's position.
    );

    expect(lastCharEndpoint.length, 1);
    // Just testing the test and making sure that the last character is off
    // the right side of the screen.
    expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(1094.73, epsilon: 0.25));

    final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

    final TestGesture gesture =
        await tester.startGesture(textFieldStart + const Offset(300, 5));
    await tester.pump(const Duration(milliseconds: 500));

    expect(
      controller.selection,
      const TextSelection.collapsed(offset: 18, affinity: TextAffinity.upstream),
    );
    expect(find.byType(CupertinoButton), findsNothing);

    await gesture.moveBy(const Offset(600, 0));
    // To the edge of the screen basically.
    await tester.pump();
    expect(
      controller.selection,
      const TextSelection.collapsed(offset: 54, affinity: TextAffinity.upstream),
    );
    // Keep moving out.
    await gesture.moveBy(const Offset(1, 0));
    await tester.pump();
    expect(
      controller.selection,
      const TextSelection.collapsed(offset: 61, affinity: TextAffinity.upstream),
    );
    await gesture.moveBy(const Offset(1, 0));
    await tester.pump();
    expect(
      controller.selection,
      const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
    ); // We're at the edge now.
    expect(find.byType(CupertinoButton), findsNothing);

    await gesture.up();
    await tester.pumpAndSettle();

    // The selection isn't affected by the gesture lift.
    expect(
      controller.selection,
      const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
    );
    // The toolbar now shows up.
    expectCupertinoToolbarForCollapsedSelection();

    lastCharEndpoint = renderEditable.getEndpointsForSelection(
      const TextSelection.collapsed(offset: 66), // Last character's position.
    );

    expect(lastCharEndpoint.length, 1);
    // The last character is now on screen.
    expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(784.73, epsilon: 0.25));

    final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection(
      const TextSelection.collapsed(offset: 0), // First character's position.
    );
    expect(firstCharEndpoint.length, 1);
    // The first character is now offscreen to the left.
    expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-310.20, epsilon: 0.25));
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets(
    'long tap after a double tap select is not affected',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
      // On macOS, we select the precise position of the tap.
      final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
      final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'

      await tester.tapAt(pPos);
      await tester.pump(const Duration(milliseconds: 50));
      // First tap moved the cursor to the beginning of the second word.
      expect(controller.selection.isCollapsed, isTrue);
      expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 9);
      await tester.tapAt(pPos);
      await tester.pump(const Duration(milliseconds: 500));

      await tester.longPressAt(ePos);
      await tester.pumpAndSettle();

      // Plain collapsed selection at the exact tap position.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 6),
      );

      // Long press toolbar.
      expectCupertinoToolbarForCollapsedSelection();
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets(
    'double tap after a long tap is not affected',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
      // On macOS, we select the precise position of the tap.
      final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              autofocus: true,
              controller: controller,
            ),
          ),
        ),
      );

      // This extra pump is so autofocus can propagate to renderEditable.
      await tester.pump();

      // Use a position higher than wPos to avoid tapping the context menu on
      // desktop.
      final Offset pPos = textOffsetToPosition(tester, 9) + const Offset(0.0, -20.0); // Index of 'P|eel'
      final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'

      await tester.longPressAt(wPos);
      await tester.pumpAndSettle(const Duration(milliseconds: 50));

      expect(controller.selection.isCollapsed, isTrue);
      expect(controller.selection.baseOffset, 3);
      expectCupertinoToolbarForCollapsedSelection();

      await tester.tapAt(pPos);
      await tester.pump(const Duration(milliseconds: 50));

      // First tap moved the cursor.
      expect(find.byType(CupertinoButton), findsNothing);
      expect(controller.selection.isCollapsed, isTrue);
      expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 9);

      await tester.tapAt(pPos);
      await tester.pumpAndSettle();

      // Double tap selection.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );
      expectCupertinoToolbarForPartialSelection();
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets(
    'double tap chains work',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

      await tester.tapAt(textFieldStart + const Offset(50.0, 5.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, 5.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, 5.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),
      );
      expect(find.byType(CupertinoButton), findsNothing);
      // Second tap shows the toolbar, and retains the selection.
      await tester.tapAt(textFieldStart + const Offset(100.0, 5.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, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      // First tap moved the cursor and hides the toolbar.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
      );
      expect(find.byType(CupertinoButton), findsNothing);
      await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
      await tester.pumpAndSettle();
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );
      expectCupertinoToolbarForPartialSelection();
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));

  group('Triple tap/click', () {
    const String testValueA = 'Now is the time for\n' // 20
        'all good people\n'                         // 20 + 16 => 36
        'to come to the aid\n'                      // 36 + 19 => 55
        'of their country.';                        // 55 + 17 => 72
    const String testValueB = 'Today is the time for\n' // 22
        'all good people\n'                         // 22 + 16 => 38
        'to come to the aid\n'                      // 38 + 19 => 57
        'of their country.';                        // 57 + 17 => 74
    testWidgets(
      'Can triple tap to select a paragraph on mobile platforms when tapping at a word edge',
      (WidgetTester tester) async {
        // TODO(Renzo-Olivares): Enable, currently broken because selection overlay blocks the TextSelectionGestureDetector.
        final TextEditingController controller = TextEditingController();
        addTearDown(controller.dispose);
        final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;

        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: CupertinoTextField(
                dragStartBehavior: DragStartBehavior.down,
                controller: controller,
                maxLines: null,
              ),
            ),
          ),
        );

        await tester.enterText(find.byType(CupertinoTextField), testValueA);
        // Skip past scrolling animation.
        await tester.pump();
        await tester.pump(const Duration(milliseconds: 200));
        expect(controller.value.text, testValueA);

        final Offset firstLinePos = tester.getTopLeft(find.byType(CupertinoTextField)) + const Offset(110.0, 9.0);

        // Tap on text field to gain focus, and set selection to 'is|' on the first line.
        final TestGesture gesture = await tester.startGesture(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 6);

        // Here we tap on same position again, to register a double tap. This will select
        // the word at the tapped position. On iOS, tapping a whitespace selects the previous word.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 6);
        expect(controller.selection.extentOffset, isTargetPlatformApple ? 6 : 7);

        // Here we tap on same position again, to register a triple tap. This will select
        // the paragraph at the tapped position.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 20);
      },
      variant: TargetPlatformVariant.mobile(),
      skip: true, // https://github.com/flutter/flutter/issues/123415
    );

    testWidgets(
      'Can triple tap to select a paragraph on mobile platforms',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController();
        addTearDown(controller.dispose);
        final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;

        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: CupertinoTextField(
                dragStartBehavior: DragStartBehavior.down,
                controller: controller,
                maxLines: null,
              ),
            ),
          ),
        );

        await tester.enterText(find.byType(CupertinoTextField), testValueB);
        // Skip past scrolling animation.
        await tester.pump();
        await tester.pump(const Duration(milliseconds: 200));
        expect(controller.value.text, testValueB);

        final Offset firstLinePos = tester.getTopLeft(find.byType(CupertinoTextField)) + const Offset(50.0, 9.0);

        // Tap on text field to gain focus, and move the selection.
        final TestGesture gesture = await tester.startGesture(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3);

        // Here we tap on same position again, to register a double tap. This will select
        // the word at the tapped position.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 5);

        // Here we tap on same position again, to register a triple tap. This will select
        // the paragraph at the tapped position.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 22);
      },
      variant: TargetPlatformVariant.mobile(),
    );

    testWidgets(
      'Triple click at the beginning of a line should not select the previous paragraph',
      (WidgetTester tester) async {
        // Regression test for https://github.com/flutter/flutter/issues/132126
        final TextEditingController controller = TextEditingController();
        addTearDown(controller.dispose);

        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: CupertinoTextField(
                dragStartBehavior: DragStartBehavior.down,
                controller: controller,
                maxLines: null,
              ),
            ),
          ),
        );

        await tester.enterText(find.byType(CupertinoTextField), testValueB);
        // Skip past scrolling animation.
        await tester.pump();
        await tester.pump(const Duration(milliseconds: 200));
        expect(controller.value.text, testValueB);

        final Offset thirdLinePos = textOffsetToPosition(tester, 38);

        // Click on text field to gain focus, and move the selection.
        final TestGesture gesture = await tester.startGesture(thirdLinePos, kind: PointerDeviceKind.mouse);
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 38);

        // Here we click on same position again, to register a double click. This will select
        // the word at the clicked position.
        await gesture.down(thirdLinePos);
        await gesture.up();

        expect(controller.selection.baseOffset, 38);
        expect(controller.selection.extentOffset, 40);

        // Here we click on same position again, to register a triple click. This will select
        // the paragraph at the clicked position.
        await gesture.down(thirdLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pump();
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 38);
        expect(controller.selection.extentOffset, 57);
      },
      variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }),
    );

    testWidgets(
      'Triple click at the end of text should select the previous paragraph',
      (WidgetTester tester) async {
        // Regression test for https://github.com/flutter/flutter/issues/132126.
        final TextEditingController controller = TextEditingController();
        addTearDown(controller.dispose);

        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: CupertinoTextField(
                dragStartBehavior: DragStartBehavior.down,
                controller: controller,
                maxLines: null,
              ),
            ),
          ),
        );

        await tester.enterText(find.byType(CupertinoTextField), testValueB);
        // Skip past scrolling animation.
        await tester.pump();
        await tester.pump(const Duration(milliseconds: 200));
        expect(controller.value.text, testValueB);

        final Offset endOfTextPos = textOffsetToPosition(tester, 74);

        // Click on text field to gain focus, and move the selection.
        final TestGesture gesture = await tester.startGesture(endOfTextPos, kind: PointerDeviceKind.mouse);
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 74);

        // Here we click on same position again, to register a double click.
        await gesture.down(endOfTextPos);
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.baseOffset, 74);
        expect(controller.selection.extentOffset, 74);

        // Here we click on same position again, to register a triple click. This will select
        // the paragraph at the clicked position.
        await gesture.down(endOfTextPos);
        await tester.pump();
        await gesture.up();
        await tester.pump();
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 57);
        expect(controller.selection.extentOffset, 74);
      },
      variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }),
    );

    testWidgets(
      'triple tap chains work on Non-Apple mobile platforms',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController(
          text: 'Atwater Peel Sherbrooke Bonaventure',
        );
        addTearDown(controller.dispose);
        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: Center(
                child: CupertinoTextField(
                  controller: controller,
                ),
              ),
            ),
          ),
        );

        final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

        await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
        await tester.pump(const Duration(milliseconds: 50));
        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 3);
        await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
        await tester.pump();
        expect(
          controller.selection,
          const TextSelection(baseOffset: 0, extentOffset: 7),
        );
        expectCupertinoToolbarForPartialSelection();

        await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
        await tester.pumpAndSettle();
        expect(
          controller.selection,
          const TextSelection(baseOffset: 0, extentOffset: 35),
        );
        // Triple tap selecting the same paragraph somewhere else is fine.
        await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
        await tester.pump(const Duration(milliseconds: 50));
        // First tap hides the toolbar and moves the selection.
        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 6);
        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();
        expect(
          controller.selection,
          const TextSelection(baseOffset: 0, extentOffset: 35),
        );
        expectCupertinoToolbarForFullSelection();

        await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
        await tester.pump(const Duration(milliseconds: 50));
        // First tap moved the cursor and hid the toolbar.
        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 9);
        expect(find.byType(CupertinoButton), findsNothing);
        // Second tap selects the word.
        await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
        await tester.pump();
        expect(
          controller.selection,
          const TextSelection(baseOffset: 8, extentOffset: 12),
        );
        expectCupertinoToolbarForPartialSelection();

        // Third tap selects the paragraph and shows the toolbar.
        await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
        await tester.pumpAndSettle();
        expect(
          controller.selection,
          const TextSelection(baseOffset: 0, extentOffset: 35),
        );
        expectCupertinoToolbarForFullSelection();
      },
      variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }),
    );

    testWidgets(
      'triple tap chains work on Apple platforms',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController(
          text: 'Atwater Peel Sherbrooke Bonaventure\nThe fox jumped over the fence.',
        );
        addTearDown(controller.dispose);
        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: Center(
                child: CupertinoTextField(
                  controller: controller,
                  maxLines: null,
                ),
              ),
            ),
          ),
        );

        final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

        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),
        );
        expect(find.byType(CupertinoButton), findsNothing);

        // Second tap shows the toolbar and selects the word.
        await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
        await tester.pump();
        expect(
          controller.selection,
          const TextSelection(baseOffset: 0, extentOffset: 7),
        );
        expectCupertinoToolbarForPartialSelection();

        // Third tap shows the toolbar and selects the paragraph.
        await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
        await tester.pumpAndSettle(kDoubleTapTimeout);
        expect(
          controller.selection,
          const TextSelection(baseOffset: 0, extentOffset: 36),
        );
        expectCupertinoToolbarForPartialSelection();

        await tester.tapAt(textfieldStart + const Offset(150.0, 25.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),
        );
        expect(find.byType(CupertinoButton), findsNothing);

        // Second tap selects the word.
        await tester.tapAt(textfieldStart + const Offset(150.0, 25.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, 25.0));
        await tester.pumpAndSettle();
        expect(
          controller.selection,
          const TextSelection(baseOffset: 36, extentOffset: 66),
        );
        expectCupertinoToolbarForPartialSelection();
      },
      variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
    );

    testWidgets(
      'triple click chains work',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController(
          text: testValueA,
        );
        addTearDown(controller.dispose);
        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: Center(
                child: CupertinoTextField(
                  controller: controller,
                  maxLines: null,
                ),
              ),
            ),
          ),
        );

        final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
        final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;

        // First click moves the cursor to the point of the click, not the edge of
        // the clicked word.
        final TestGesture gesture = await tester.startGesture(
          textFieldStart + const Offset(200.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, 12);

        // Second click selects the word.
        await gesture.down(textFieldStart + const Offset(200.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(200.0, 9.0));
        await tester.pump();
        await gesture.up();
        // Wait for the consecutive tap timer to timeout so the next
        // tap is not detected as a triple tap.
        await tester.pumpAndSettle(kDoubleTapTimeout);
        expect(
          controller.selection,
          TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
        );

        // Triple click selecting the same paragraph somewhere else is fine.
        await gesture.down(textFieldStart + const Offset(100.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pump(const Duration(milliseconds: 50));
        // First click moved the cursor.
        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 6);
        await gesture.down(textFieldStart + const Offset(100.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pump();
        // Second click selected the word.
        expect(
          controller.selection,
          const TextSelection(baseOffset: 4, extentOffset: 6),
        );

        await gesture.down(textFieldStart + const Offset(100.0, 9.0));
        await tester.pump();
        await gesture.up();
        // Wait for the consecutive tap timer to timeout so the tap count
        // is reset.
        await tester.pumpAndSettle(kDoubleTapTimeout);
        // Third click selected the paragraph.
        expect(
          controller.selection,
          TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
        );

        await gesture.down(textFieldStart + const Offset(150.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pump(const Duration(milliseconds: 50));
        // First click moved the cursor.
        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 9);
        await gesture.down(textFieldStart + const Offset(150.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pump();
        // Second click selected the word.
        expect(
          controller.selection,
          const TextSelection(baseOffset: 7, extentOffset: 10),
        );

        await gesture.down(textFieldStart + const Offset(150.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle();
        // Third click selects the paragraph.
        expect(
          controller.selection,
          TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
        );
      },
      variant: TargetPlatformVariant.desktop(),
    );

    testWidgets(
      'triple click after a click on desktop platforms',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController(
          text: testValueA,
        );
        addTearDown(controller.dispose);
        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: Center(
                child: CupertinoTextField(
                  controller: controller,
                  maxLines: null,
                ),
              ),
            ),
          ),
        );

        final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
        final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;

        final TestGesture gesture = await tester.startGesture(
          textFieldStart + const Offset(50.0, 9.0),
          pointer: 7,
          kind: PointerDeviceKind.mouse,
        );
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle(kDoubleTapTimeout);
        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 3);
        // First click moves the selection.
        await gesture.down(textFieldStart + const Offset(150.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pump(const Duration(milliseconds: 50));
        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 9);

        // Double click selection to select a word.
        await gesture.down(textFieldStart + const Offset(150.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pump();
        expect(
          controller.selection,
          const TextSelection(baseOffset: 7, extentOffset: 10),
        );

        // Triple click selection to select a paragraph.
        await gesture.down(textFieldStart + const Offset(150.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle();
        expect(
          controller.selection,
          TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
        );

      },
      variant: TargetPlatformVariant.desktop(),
    );

    testWidgets(
      'Can triple tap to select all on a single-line textfield on mobile platforms',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController(
          text: testValueB,
        );
        addTearDown(controller.dispose);
        final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;

        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: CupertinoTextField(
                controller: controller,
              ),
            ),
          ),
        );

        final Offset firstLinePos = tester.getTopLeft(find.byType(CupertinoTextField)) + const Offset(50.0, 9.0);

        // Tap on text field to gain focus, and set selection somewhere on the first word.
        final TestGesture gesture = await tester.startGesture(
          firstLinePos,
          pointer: 7,
        );
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3);

        // Here we tap on same position again, to register a double tap. This will select
        // the word at the tapped position.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 5);

        // Here we tap on same position again, to register a triple tap. This will select
        // the entire text field if it is a single-line field.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 74);
      },
      variant: TargetPlatformVariant.mobile(),
    );

    testWidgets(
      'Can triple click to select all on a single-line textfield on desktop platforms',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController(
          text: testValueA,
        );
        addTearDown(controller.dispose);

        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: CupertinoTextField(
                dragStartBehavior: DragStartBehavior.down,
                controller: controller,
              ),
            ),
          ),
        );

        final Offset firstLinePos = textOffsetToPosition(tester, 5);

        // Tap on text field to gain focus, and set selection to 'i|s' on the first line.
        final TestGesture gesture = await tester.startGesture(
          firstLinePos,
          pointer: 7,
          kind: PointerDeviceKind.mouse,
        );
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 5);

        // Here we tap on same position again, to register a double tap. This will select
        // the word at the tapped position.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.baseOffset, 4);
        expect(controller.selection.extentOffset, 6);

        // Here we tap on same position again, to register a triple tap. This will select
        // the entire text field if it is a single-line field.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 72);
      },
      variant: TargetPlatformVariant.desktop(),
    );

    testWidgets(
      'Can triple click to select a line on Linux',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController();
        addTearDown(controller.dispose);

        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: CupertinoTextField(
                dragStartBehavior: DragStartBehavior.down,
                controller: controller,
                maxLines: null,
              ),
            ),
          ),
        );

        await tester.enterText(find.byType(CupertinoTextField), testValueA);
        // Skip past scrolling animation.
        await tester.pump();
        await tester.pump(const Duration(milliseconds: 200));
        expect(controller.value.text, testValueA);

        final Offset firstLinePos = textOffsetToPosition(tester, 5);

        // Tap on text field to gain focus, and set selection to 'i|s' on the first line.
        final TestGesture gesture = await tester.startGesture(
          firstLinePos,
          pointer: 7,
          kind: PointerDeviceKind.mouse,
        );
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 5);

        // Here we tap on same position again, to register a double tap. This will select
        // the word at the tapped position.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.baseOffset, 4);
        expect(controller.selection.extentOffset, 6);

        // Here we tap on same position again, to register a triple tap. This will select
        // the paragraph at the tapped position.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 19);
      },
      variant: TargetPlatformVariant.only(TargetPlatform.linux),
    );

    testWidgets(
      'Can triple click to select a paragraph',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController();
        addTearDown(controller.dispose);

        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: CupertinoTextField(
                dragStartBehavior: DragStartBehavior.down,
                controller: controller,
                maxLines: null,
              ),
            ),
          ),
        );

        await tester.enterText(find.byType(CupertinoTextField), testValueA);
        // Skip past scrolling animation.
        await tester.pump();
        await tester.pump(const Duration(milliseconds: 200));
        expect(controller.value.text, testValueA);

        final Offset firstLinePos = textOffsetToPosition(tester, 5);

        // Tap on text field to gain focus, and set selection to 'i|s' on the first line.
        final TestGesture gesture = await tester.startGesture(
          firstLinePos,
          pointer: 7,
          kind: PointerDeviceKind.mouse,
        );
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 5);

        // Here we tap on same position again, to register a double tap. This will select
        // the word at the tapped position.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.baseOffset, 4);
        expect(controller.selection.extentOffset, 6);

        // Here we tap on same position again, to register a triple tap. This will select
        // the paragraph at the tapped position.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 20);
      },
      variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }),
    );

    testWidgets(
      'Can triple click + drag to select line by line on Linux',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController();
        addTearDown(controller.dispose);

        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: CupertinoTextField(
                dragStartBehavior: DragStartBehavior.down,
                controller: controller,
                maxLines: null,
              ),
            ),
          ),
        );

        await tester.enterText(find.byType(CupertinoTextField), testValueA);
        // Skip past scrolling animation.
        await tester.pump();
        await tester.pump(const Duration(milliseconds: 200));
        expect(controller.value.text, testValueA);

        final Offset firstLinePos = textOffsetToPosition(tester, 5);

        // Tap on text field to gain focus, and set selection to 'i|s' on the first line.
        final TestGesture gesture = await tester.startGesture(
          firstLinePos,
          pointer: 7,
          kind: PointerDeviceKind.mouse,
        );
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 5);

        // Here we tap on same position again, to register a double tap. This will select
        // the word at the tapped position.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.baseOffset, 4);
        expect(controller.selection.extentOffset, 6);

        // Here we tap on the same position again, to register a triple tap. This will select
        // the line at the tapped position.
        await gesture.down(firstLinePos);
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 19);

        // Drag, down after the triple tap, to select line by line.
        // Moving down will extend the selection to the second line.
        await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 35);

        // Moving down will extend the selection to the third line.
        await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 54);

        // Moving down will extend the selection to the last line.
        await gesture.moveTo(firstLinePos + const Offset(0, 40.0));
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 72);

        // Moving up will extend the selection to the third line.
        await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 54);

        // Moving up will extend the selection to the second line.
        await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 35);

        // Moving up will extend the selection to the first line.
        await gesture.moveTo(firstLinePos);
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 19);
      },
      variant: TargetPlatformVariant.only(TargetPlatform.linux),
    );

    testWidgets(
      'Can triple click + drag to select paragraph by paragraph',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController();
        addTearDown(controller.dispose);

        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: CupertinoTextField(
                dragStartBehavior: DragStartBehavior.down,
                controller: controller,
                maxLines: null,
              ),
            ),
          ),
        );

        await tester.enterText(find.byType(CupertinoTextField), testValueA);
        // Skip past scrolling animation.
        await tester.pump();
        await tester.pump(const Duration(milliseconds: 200));
        expect(controller.value.text, testValueA);

        final Offset firstLinePos = textOffsetToPosition(tester, 5);

        // Tap on text field to gain focus, and set selection to 'i|s' on the first line.
        final TestGesture gesture = await tester.startGesture(
          firstLinePos,
          pointer: 7,
          kind: PointerDeviceKind.mouse,
        );
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 5);

        // Here we tap on same position again, to register a double tap. This will select
        // the word at the tapped position.
        await gesture.down(firstLinePos);
        await tester.pump();
        await gesture.up();
        await tester.pump();

        expect(controller.selection.baseOffset, 4);
        expect(controller.selection.extentOffset, 6);

        // Here we tap on the same position again, to register a triple tap. This will select
        // the paragraph at the tapped position.
        await gesture.down(firstLinePos);
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 20);

        // Drag, down after the triple tap, to select paragraph by paragraph.
        // Moving down will extend the selection to the second line.
        await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 36);

        // Moving down will extend the selection to the third line.
        await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 55);

        // Moving down will extend the selection to the last line.
        await gesture.moveTo(firstLinePos + const Offset(0, 40.0));
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 72);

        // Moving up will extend the selection to the third line.
        await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 55);

        // Moving up will extend the selection to the second line.
        await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 36);

        // Moving up will extend the selection to the first line.
        await gesture.moveTo(firstLinePos);
        await tester.pumpAndSettle();

        expect(controller.selection.baseOffset, 0);
        expect(controller.selection.extentOffset, 20);
      },
      variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }),
    );

    testWidgets(
      'Going past triple click retains the selection on Apple platforms',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController(
          text: testValueA,
        );
        addTearDown(controller.dispose);
        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: Center(
                child: CupertinoTextField(
                  controller: controller,
                  maxLines: null,
                ),
              ),
            ),
          ),
        );

        final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

        // 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(200.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, 12);

        // Second click selects the word.
        await gesture.down(textFieldStart + const Offset(200.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(200.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle();
        expect(
          controller.selection,
          const TextSelection(baseOffset: 0, extentOffset: 20),
        );

        // Clicking again retains the selection.
        await gesture.down(textFieldStart + const Offset(200.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pump(const Duration(milliseconds: 50));
        expect(
          controller.selection,
          const TextSelection(baseOffset: 0, extentOffset: 20),
        );

        await gesture.down(textFieldStart + const Offset(200.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pump();
        // Clicking again retains the selection.
        expect(
          controller.selection,
          const TextSelection(baseOffset: 0, extentOffset: 20),
        );

        await gesture.down(textFieldStart + const Offset(200.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle();
        // Clicking again retains the selection.
        expect(
          controller.selection,
          const TextSelection(baseOffset: 0, extentOffset: 20),
        );
      },
      variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
    );

    testWidgets(
      'Tap count resets when going past a triple tap on Android, Fuchsia, and Linux',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController(
          text: testValueA,
        );
        addTearDown(controller.dispose);
        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: Center(
                child: CupertinoTextField(
                  controller: controller,
                  maxLines: null,
                ),
              ),
            ),
          ),
        );

        final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
        final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;

        // First click moves the cursor to the point of the click, not the edge of
        // the clicked word.
        final TestGesture gesture = await tester.startGesture(
          textFieldStart + const Offset(200.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, 12);

        // Second click selects the word.
        await gesture.down(textFieldStart + const Offset(200.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(200.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle();
        expect(
          controller.selection,
          TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
        );

        // Clicking again moves the caret to the tapped position.
        await gesture.down(textFieldStart + const Offset(200.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pump(const Duration(milliseconds: 50));
        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 12);

        await gesture.down(textFieldStart + const Offset(200.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(200.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle();
        // Clicking again selects the paragraph.
        expect(
          controller.selection,
          TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
        );

        await gesture.down(textFieldStart + const Offset(200.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pump(const Duration(milliseconds: 50));
        // Clicking again moves the caret to the tapped position.
        expect(controller.selection.isCollapsed, true);
        expect(controller.selection.baseOffset, 12);

        await gesture.down(textFieldStart + const Offset(200.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(200.0, 9.0));
        await tester.pump();
        await gesture.up();
        await tester.pumpAndSettle();
        // Clicking again selects the paragraph.
        expect(
          controller.selection,
          TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
        );
      },
      variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux }),
    );

    testWidgets(
      'Double click and triple click alternate on Windows',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController(
          text: testValueA,
        );
        addTearDown(controller.dispose);
        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: Center(
                child: CupertinoTextField(
                  controller: controller,
                  maxLines: null,
                ),
              ),
            ),
          ),
        );

        final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

        // 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(200.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, 12);

        // Second click selects the word.
        await gesture.down(textFieldStart + const Offset(200.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(200.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(200.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(200.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(200.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(200.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(200.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(200.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('force press selects word', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

    final int pointerValue = tester.nextPointer;
    final TestGesture gesture = await tester.createGesture();
    await gesture.downWithCustomEvent(
      textFieldStart + const Offset(150.0, 5.0),
      PointerDownEvent(
        pointer: pointerValue,
        position: textFieldStart + const Offset(150.0, 5.0),
        pressure: 3.0,
        pressureMax: 6.0,
        pressureMin: 0.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();
    // Shows toolbar.
    expectCupertinoToolbarForPartialSelection();
  });

  testWidgets('force press on unsupported devices falls back to tap', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    addTearDown(controller.dispose);
    // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
    // On macOS, we select the precise position of the tap.
    final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'

    final int pointerValue = tester.nextPointer;
    final TestGesture gesture = await tester.createGesture();
    await gesture.downWithCustomEvent(
      pPos,
      PointerDownEvent(
        pointer: pointerValue,
        position: pPos,
        // iPhone 6 and below report 0 across the board.
        pressure: 0,
        pressureMax: 0,
        pressureMin: 0,
      ),
    );
    await gesture.up();
    // Fall back to a single tap which selects the edge of the word on iOS, and
    // a precise position on macOS.
    expect(controller.selection.isCollapsed, isTrue);
    expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 9);

    await tester.pump();
    // Falling back to a single tap doesn't trigger a toolbar.
    expect(find.byType(CupertinoButton), findsNothing);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'abc def ghi',
    );
    addTearDown(controller.dispose);
    // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
    // On macOS, we select the precise position of the tap.
    final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;

    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            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, isTargetPlatformIOS ? 7 : 5);
    await tester.tapAt(ePos, pointer: 7);
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 4);
    expect(controller.selection.extentOffset, 7);

    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    expect(endpoints.length, 2);

    // On Mac, the toolbar blocks the drag on the right handle, so hide it.
    final EditableTextState editableTextState = tester.state(find.byType(EditableText));
    editableTextState.hideToolbar(false);
    await tester.pumpAndSettle();

    // Drag the right handle until there's only 1 char selected.
    // We use a small offset because the endpoint is on the very corner
    // of the handle.
    final Offset handlePos = endpoints[1].point;
    Offset newHandlePos = textOffsetToPosition(tester, 5); // Position of 'e'.
    final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    expect(controller.selection.baseOffset, 4);
    expect(controller.selection.extentOffset, 5);

    newHandlePos = textOffsetToPosition(tester, 2); // Position of 'c'.
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(controller.selection.baseOffset, 4);
    // The selection doesn't move beyond the left handle. There's always at
    // least 1 char selected.
    expect(controller.selection.extentOffset, 5);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));

  testWidgets('Dragging between multiple lines keeps the contact point at the same place on the handle on Android', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      // 11 first line, 19 second line, 17 third line = length 49
      text: 'a big house\njumped over a mouse\nOne more line yay',
    );
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            dragStartBehavior: DragStartBehavior.down,
            controller: controller,
            maxLines: 3,
            minLines: 3,
          ),
        ),
      ),
    );

    // Double tap to select 'over'.
    final Offset pos = textOffsetToPosition(tester, controller.text.indexOf('v'));
    // The first tap.
    TestGesture gesture = await tester.startGesture(pos, pointer: 7);
    await tester.pump();
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero

    // The second tap.
    await gesture.down(pos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    final TextSelection selection = controller.selection;
    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 19,
        extentOffset: 23,
      ),
    );

    final RenderEditable renderEditable = findRenderEditable(tester);
    List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(selection),
      renderEditable,
    );
    expect(endpoints.length, 2);

    // Drag the right handle 4 letters to the right.
    // The adjustment moves the tap from the text position to the handle.
    const Offset endHandleAdjustment = Offset(1.0, 6.0);
    Offset handlePos = endpoints[1].point + endHandleAdjustment;
    Offset newHandlePos = textOffsetToPosition(tester, 27) + endHandleAdjustment;
    await tester.pump();
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 19,
        extentOffset: 27,
      ),
    );

    // Drag the right handle 1 line down.
    endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    handlePos = endpoints[1].point + endHandleAdjustment;
    final Offset toNextLine = Offset(
      0.0,
      findRenderEditable(tester).preferredLineHeight + 3.0,
    );
    newHandlePos = handlePos + toNextLine;
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 19,
        extentOffset: 47,
      ),
    );

    // Drag the right handle back up 1 line.
    endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    handlePos = endpoints[1].point + endHandleAdjustment;
    newHandlePos = handlePos - toNextLine;
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 19,
        extentOffset: 27,
      ),
    );

    // Drag the left handle 4 letters to the left.
    // The adjustment moves the tap from the text position to the handle.
    const Offset startHandleAdjustment = Offset(-1.0, 6.0);
    endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    handlePos = endpoints[0].point + startHandleAdjustment;
    newHandlePos = textOffsetToPosition(tester, 15) + startHandleAdjustment;
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 15,
        extentOffset: 27,
      ),
    );

    // Drag the left handle 1 line up.
    endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    handlePos = endpoints[0].point + startHandleAdjustment;
    // Move handle a sufficient global distance so it can be considered a drag
    // by the selection handle's [PanGestureRecognizer].
    newHandlePos = handlePos - (toNextLine * 2);
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 3,
        extentOffset: 27,
      ),
    );

    // Drag the left handle 1 line back down.
    endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    handlePos = endpoints[0].point + startHandleAdjustment;
    newHandlePos = handlePos + toNextLine;
    gesture = await tester.startGesture(handlePos, pointer: 7);
    // Move handle up a small amount before dragging it down so the total global
    // distance travelled can be accepted by the selection handle's [PanGestureRecognizer] as a drag.
    // This way it can declare itself the winner before the [TapAndDragGestureRecognizer] that
    // is on the selection overlay.
    await tester.pump();
    await gesture.moveTo(handlePos - toNextLine);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 15,
        extentOffset: 27,
      ),
    );
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
  );

  testWidgets('Dragging between multiple lines keeps the contact point at the same place on the handle on iOS', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      // 11 first line, 19 second line, 17 third line = length 49
      text: 'a big house\njumped over a mouse\nOne more line yay',
    );
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            dragStartBehavior: DragStartBehavior.down,
            controller: controller,
            maxLines: 3,
            minLines: 3,
          ),
        ),
      ),
    );

    // Double tap to select 'over'.
    final Offset pos = textOffsetToPosition(tester, controller.text.indexOf('v'));
    // The first tap.
    TestGesture gesture = await tester.startGesture(pos, pointer: 7);
    await tester.pump();
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero

    // The second tap.
    await gesture.down(pos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    final TextSelection selection = controller.selection;
    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 19,
        extentOffset: 23,
      ),
    );

    final RenderEditable renderEditable = findRenderEditable(tester);
    List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(selection),
      renderEditable,
    );
    expect(endpoints.length, 2);

    // Drag the right handle 4 letters to the right.
    // The adjustment moves the tap from the text position to the handle.
    const Offset endHandleAdjustment = Offset(1.0, 6.0);
    Offset handlePos = endpoints[1].point + endHandleAdjustment;
    Offset newHandlePos = textOffsetToPosition(tester, 27) + endHandleAdjustment;
    await tester.pump();
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 19,
        extentOffset: 27,
      ),
    );

    // Drag the right handle 1 line down.
    endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    handlePos = endpoints[1].point + endHandleAdjustment;
    final double lineHeight = findRenderEditable(tester).preferredLineHeight;
    final Offset toNextLine = Offset(0.0, lineHeight + 3.0);
    newHandlePos = handlePos + toNextLine;
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 19,
        extentOffset: 47,
      ),
    );

    // Drag the right handle back up 1 line.
    endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    handlePos = endpoints[1].point + endHandleAdjustment;
    newHandlePos = handlePos - toNextLine;
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 19,
        extentOffset: 27,
      ),
    );

    // Drag the left handle 4 letters to the left.
    // The adjustment moves the tap from the text position to the handle.
    final Offset startHandleAdjustment = Offset(-1.0, -lineHeight + 6.0);
    endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    handlePos = endpoints[0].point + startHandleAdjustment;
    newHandlePos = textOffsetToPosition(tester, 15) + startHandleAdjustment;
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    // On Apple platforms, dragging the base handle makes it the extent.
    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 27,
        extentOffset: 15,
      ),
    );

    // Drag the left handle 1 line up.
    endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    handlePos = endpoints[0].point + startHandleAdjustment;
    newHandlePos = handlePos - toNextLine;
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 27,
        extentOffset: 3,
      ),
    );

    // Drag the left handle 1 line back down.
    endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    handlePos = endpoints[0].point + startHandleAdjustment;
    newHandlePos = handlePos + toNextLine;
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 27,
        extentOffset: 15,
      ),
    );
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
  );

  testWidgets('Selection updates on tap down (Desktop platforms)', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(controller: controller),
        ),
      ),
    );

    const String testValue = 'abc def ghi';
    await tester.enterText(find.byType(CupertinoTextField), testValue);
    // Skip past scrolling animation.
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200));

    final Offset ePos = textOffsetToPosition(tester, 5);
    final Offset gPos = textOffsetToPosition(tester, 8);

    final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 5);
    expect(controller.selection.extentOffset, 5);

    await gesture.up();
    await tester.pumpAndSettle(kDoubleTapTimeout);

    await gesture.down(gPos);
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 8);

    // This should do nothing. The selection is set on tap down on desktop platforms.
    await gesture.up();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 8);
  },
      variant: TargetPlatformVariant.desktop(),
  );

  testWidgets('Selection updates on tap up (Mobile platforms)', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();
    addTearDown(controller.dispose);
    final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;

    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(controller: controller),
        ),
      ),
    );

    const String testValue = 'abc def ghi';
    await tester.enterText(find.byType(CupertinoTextField), testValue);
    // Skip past scrolling animation.
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200));

    final Offset ePos = textOffsetToPosition(tester, 5);
    final Offset gPos = textOffsetToPosition(tester, 8);

    final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
    await gesture.up();
    await tester.pumpAndSettle(kDoubleTapTimeout);

    await gesture.down(gPos);
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 5);
    expect(controller.selection.extentOffset, 5);

    await gesture.up();
    await tester.pumpAndSettle(kDoubleTapTimeout);
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 8);

    final TestGesture touchGesture = await tester.startGesture(ePos);
    await touchGesture.up();
    await tester.pumpAndSettle(kDoubleTapTimeout);
    // On iOS, a tap to select, selects the word edge instead of the exact tap position.
    expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5);
    expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5);

    // Selection should stay the same since it is set on tap up for mobile platforms.
    await touchGesture.down(gPos);
    await tester.pump();
    expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5);
    expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5);

    await touchGesture.up();
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 8);
  },
      variant: TargetPlatformVariant.mobile(),
  );

  testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            dragStartBehavior: DragStartBehavior.down,
            controller: controller,
            style: const TextStyle(fontSize: 10.0),
          ),
        ),
      ),
    );

    const String testValue = 'abc def ghi';
    await tester.enterText(find.byType(CupertinoTextField), testValue);
    // Skip past scrolling animation.
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200));

    final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
    final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));

    final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
    await tester.pump();
    await gesture.moveTo(gPos);
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle();

    expect(controller.selection.baseOffset, testValue.indexOf('e'));
    expect(controller.selection.extentOffset, testValue.indexOf('g'));
  });

  testWidgets('Cursor should not move on a quick touch drag when touch does not begin on previous selection (iOS)', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: CupertinoPageScaffold(
          child: CupertinoTextField(
            dragStartBehavior: DragStartBehavior.down,
            controller: controller,
          ),
        ),
      ),
    );

    const String testValue = 'abc def ghi';
    await tester.enterText(find.byType(CupertinoTextField), testValue);
    await tester.pumpAndSettle(const Duration(milliseconds: 200));

    final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
    final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));

    // Tap on text field to gain focus, and set selection to '|a'. On iOS
    // the selection is set to the word edge closest to the tap position.
    // We await for [kDoubleTapTimeout] after the up event, so our next down
    // event does not register as a double tap.
    final TestGesture gesture = await tester.startGesture(aPos);
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle(kDoubleTapTimeout);

    expect(controller.selection.isCollapsed, true);
    expect(controller.selection.baseOffset, 0);

    // The position we tap during a drag start is not on the collapsed selection,
    // so the cursor should not move.
    await gesture.down(textOffsetToPosition(tester, 7));
    await gesture.moveTo(iPos);
    await tester.pumpAndSettle();

    expect(controller.selection.isCollapsed, true);
    expect(controller.selection.baseOffset, 0);
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
  );

  testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS)', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: CupertinoPageScaffold(
          child: CupertinoTextField(
            dragStartBehavior: DragStartBehavior.down,
            controller: controller,
          ),
        ),
      ),
    );

    const String testValue = 'abc def ghi';
    await tester.enterText(find.byType(CupertinoTextField), testValue);
    await tester.pumpAndSettle(const Duration(milliseconds: 200));

    final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
    final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));

    // Tap on text field to gain focus, and set selection to '|g'. On iOS
    // the selection is set to the word edge closest to the tap position.
    // We await for [kDoubleTapTimeout] after the up event, so our next down
    // event does not register as a double tap.
    final TestGesture gesture = await tester.startGesture(ePos);
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle(kDoubleTapTimeout);

    expect(controller.selection.isCollapsed, true);
    expect(controller.selection.baseOffset, 7);

    // If the position we tap during a drag start is on the collapsed selection, then
    // we can move the cursor with a drag.
    // Here we tap on '|g', where our selection was previously, and move to '|i'.
    await gesture.down(textOffsetToPosition(tester, 7));
    await tester.pump();
    await gesture.moveTo(iPos);
    await tester.pumpAndSettle();

    expect(controller.selection.isCollapsed, true);
    expect(controller.selection.baseOffset, testValue.indexOf('i'));

    // End gesture and skip the magnifier hide animation, so it can release
    // resources.
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 150));
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
  );

  testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - multiline', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: CupertinoPageScaffold(
          child: CupertinoTextField(
            dragStartBehavior: DragStartBehavior.down,
            controller: controller,
            maxLines: null,
          ),
        ),
      ),
    );

    const String testValue = 'abc\ndef\nghi';
    await tester.enterText(find.byType(CupertinoTextField), testValue);
    await tester.pumpAndSettle(const Duration(milliseconds: 200));

    final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
    final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));

    // Tap on text field to gain focus, and set selection to '|a'. On iOS
    // the selection is set to the word edge closest to the tap position.
    // We await for kDoubleTapTimeout after the up event, so our next down event
    // does not register as a double tap.
    final TestGesture gesture = await tester.startGesture(aPos);
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle(kDoubleTapTimeout);

    expect(controller.selection.isCollapsed, true);
    expect(controller.selection.baseOffset, 0);

    // If the position we tap during a drag start is on the collapsed selection, then
    // we can move the cursor with a drag.
    // Here we tap on '|a', where our selection was previously, and move to '|i'.
    await gesture.down(aPos);
    await tester.pump();
    await gesture.moveTo(iPos);
    await tester.pumpAndSettle();

    expect(controller.selection.isCollapsed, true);
    expect(controller.selection.baseOffset, testValue.indexOf('i'));

    // End gesture and skip the magnifier hide animation, so it can release
    // resources.
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 150));
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
  );

  testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - ListView', (WidgetTester tester) async {
    // This is a regression test for
    // https://github.com/flutter/flutter/issues/122519
    final TextEditingController controller = TextEditingController();
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: CupertinoPageScaffold(
          child: CupertinoTextField(
            dragStartBehavior: DragStartBehavior.down,
            controller: controller,
            maxLines: null,
          ),
        ),
      ),
    );

    const String testValue = 'abc\ndef\nghi';
    await tester.enterText(find.byType(CupertinoTextField), testValue);
    await tester.pumpAndSettle(const Duration(milliseconds: 200));

    final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
    final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
    final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));

    // Tap on text field to gain focus, and set selection to '|a'. On iOS
    // the selection is set to the word edge closest to the tap position.
    // We await for kDoubleTapTimeout after the up event, so our next down event
    // does not register as a double tap.
    final TestGesture gesture = await tester.startGesture(aPos);
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle(kDoubleTapTimeout);

    expect(controller.selection.isCollapsed, true);
    expect(controller.selection.baseOffset, 0);

    // If the position we tap during a drag start is on the collapsed selection, then
    // we can move the cursor with a drag.
    // Here we tap on '|a', where our selection was previously, and attempt move
    // to '|g'. The cursor will not move because the `VerticalDragGestureRecognizer`
    // in the scrollable will beat the `TapAndHorizontalDragGestureRecognizer`
    // in the TextField. This is because moving from `|a` to `|g` is a completely
    // vertical movement.
    await gesture.down(aPos);
    await tester.pump();
    await gesture.moveTo(gPos);
    await tester.pumpAndSettle();

    expect(controller.selection.isCollapsed, true);
    expect(controller.selection.baseOffset, 0);

    // Release the pointer.
    await gesture.up();
    await tester.pumpAndSettle();

    // If the position we tap during a drag start is on the collapsed selection, then
    // we can move the cursor with a drag.
    // Here we tap on '|a', where our selection was previously, and move to '|i'.
    // Unlike our previous attempt to drag to `|g`, this works because moving
    // to `|i` includes a horizontal movement so the `TapAndHorizontalDragGestureRecognizer`
    // in TextField can beat the `VerticalDragGestureRecognizer` in the scrollable.
    await gesture.down(aPos);
    await tester.pump();
    await gesture.moveTo(iPos);
    await tester.pumpAndSettle();

    await gesture.up();
    await tester.pumpAndSettle();

    expect(controller.selection.isCollapsed, true);
    expect(controller.selection.baseOffset, testValue.indexOf('i'));
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
  );

  testWidgets('Can move cursor when dragging (Android)', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: CupertinoPageScaffold(
          child: CupertinoTextField(
            dragStartBehavior: DragStartBehavior.down,
            controller: controller,
          ),
        ),
      ),
    );

    const String testValue = 'abc def ghi';
    await tester.enterText(find.byType(CupertinoTextField), testValue);
    await tester.pumpAndSettle(const Duration(milliseconds: 200));

    final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
    final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));

    // Tap on text field to gain focus, and set selection to '|e'.
    // We await for [kDoubleTapTimeout] after the up event, so our
    // next down event does not register as a double tap.
    final TestGesture gesture = await tester.startGesture(ePos);
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle(kDoubleTapTimeout);

    expect(controller.selection.isCollapsed, true);
    expect(controller.selection.baseOffset, testValue.indexOf('e'));

    // Here we tap on '|d', and move to '|g'.
    await gesture.down(textOffsetToPosition(tester, testValue.indexOf('d')));
    await tester.pump();
    await gesture.moveTo(gPos);
    await tester.pumpAndSettle();

    expect(controller.selection.isCollapsed, true);
    expect(controller.selection.baseOffset, testValue.indexOf('g'));

    // End gesture and skip the magnifier hide animation, so it can release
    // resources.
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 150));
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }),
  );

  testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async {
    int selectionChangedCount = 0;
    const String testValue = 'abc def ghi';
    final TextEditingController controller = TextEditingController(text: testValue);
    addTearDown(controller.dispose);

    controller.addListener(() {
      selectionChangedCount++;
    });

    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            dragStartBehavior: DragStartBehavior.down,
            controller: controller,
            style: const TextStyle(fontSize: 10.0),
          ),
        ),
      ),
    );

    final Offset cPos = textOffsetToPosition(tester, 2); // Index of 'c'.
    final Offset gPos = textOffsetToPosition(tester, 8); // Index of 'g'.
    final Offset hPos = textOffsetToPosition(tester, 9); // Index of 'h'.

    // Drag from 'c' to 'g'.
    final TestGesture gesture = await tester.startGesture(cPos, kind: PointerDeviceKind.mouse);
    await tester.pump();
    await gesture.moveTo(gPos);
    await tester.pumpAndSettle();

    expect(selectionChangedCount, isNonZero);
    selectionChangedCount = 0;
    expect(controller.selection.baseOffset, 2);
    expect(controller.selection.extentOffset, 8);

    // Tiny movement shouldn't cause text selection to change.
    await gesture.moveTo(gPos + const Offset(2.0, 0.0));
    await tester.pumpAndSettle();
    expect(selectionChangedCount, 0);

    // Now a text selection change will occur after a significant movement.
    await gesture.moveTo(hPos);
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle();

    expect(selectionChangedCount, 1);
    expect(controller.selection.baseOffset, 2);
    expect(controller.selection.extentOffset, 9);
  });

  testWidgets('Tap does not show handles nor toolbar', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'abc def ghi',
    );
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(controller: controller),
        ),
      ),
    );

    // Tap to trigger the text field.
    await tester.tap(find.byType(CupertinoTextField));
    await tester.pump();

    final EditableTextState editableText = tester.state(find.byType(EditableText));
    expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
    expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
  });

  testWidgets('Long press shows toolbar but not handles', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'abc def ghi',
    );
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(controller: controller),
        ),
      ),
    );

    // Long press to trigger the text field.
    await tester.longPress(find.byType(CupertinoTextField));
    await tester.pump();
    // A long press in Cupertino should position the cursor without any selection.
    expect(controller.selection.isCollapsed, isTrue);

    final EditableTextState editableText = tester.state(find.byType(EditableText));
    expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
    expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue);
  });

  testWidgets(
    'Double tap shows handles and toolbar if selection is not collapsed',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'abc def ghi',
      );
      addTearDown(controller.dispose);

      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(controller: controller),
          ),
        ),
      );

      final Offset hPos = textOffsetToPosition(tester, 9); // Position of 'h'.

      // Double tap on 'h' to select 'ghi'.
      await tester.tapAt(hPos);
      await tester.pump(const Duration(milliseconds: 50));
      await tester.tapAt(hPos);
      await tester.pump();

      final EditableTextState editableText = tester.state(find.byType(EditableText));
      expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
      expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue);
    },
  );

  testWidgets(
    'Double tap shows toolbar but not handles if selection is collapsed',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'abc def ghi',
      );
      addTearDown(controller.dispose);

      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(controller: controller),
          ),
        ),
      );

      final Offset textEndPos = textOffsetToPosition(tester, 11); // Position at the end of text.

      // Double tap to place the cursor at the end.
      await tester.tapAt(textEndPos);
      await tester.pump(const Duration(milliseconds: 50));
      await tester.tapAt(textEndPos);
      await tester.pump();

      final EditableTextState editableText = tester.state(find.byType(EditableText));
      expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
      expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue);
    },
  );

  testWidgets(
    'Mouse long press does not show handles nor toolbar',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'abc def ghi',
      );
      addTearDown(controller.dispose);

      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(controller: controller),
          ),
        ),
      );

      // Long press to trigger the text field.
      final Offset textFieldPos = tester.getCenter(find.byType(CupertinoTextField));
      final TestGesture gesture = await tester.startGesture(
        textFieldPos,
        kind: PointerDeviceKind.mouse,
      );
      await tester.pump(const Duration(seconds: 2));
      await gesture.up();
      await tester.pump();

      final EditableTextState editableText = tester.state(find.byType(EditableText));
      expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
      expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
    },
  );

  testWidgets(
    'Mouse double tap does not show handles nor toolbar',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'abc def ghi',
      );
      addTearDown(controller.dispose);

      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(controller: controller),
          ),
        ),
      );

      final EditableTextState editableText = tester.state(find.byType(EditableText));

      // Double tap at the end of text.
      final Offset textEndPos = textOffsetToPosition(tester, 11); // Position at the end of text.
      final TestGesture gesture = await tester.startGesture(
        textEndPos,
        kind: PointerDeviceKind.mouse,
      );
      await tester.pump(const Duration(milliseconds: 50));
      await gesture.up();
      await tester.pump();
      await gesture.down(textEndPos);
      await tester.pump();
      await gesture.up();
      await tester.pump();

      expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
      expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);

      final Offset hPos = textOffsetToPosition(tester, 9); // Position of 'h'.

      // Double tap on 'h' to select 'ghi'.
      await gesture.down(hPos);
      await tester.pump(const Duration(milliseconds: 50));
      await gesture.up();
      await tester.pump();
      await gesture.down(hPos);
      await tester.pump();
      await gesture.up();
      await tester.pump();

      expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
      expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
    },
  );

  testWidgets('onTap is called upon tap', (WidgetTester tester) async {
    int tapCount = 0;
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            onTap: () => tapCount++,
          ),
        ),
      ),
    );

    expect(tapCount, 0);
    await tester.tap(find.byType(CupertinoTextField));
    await tester.pump();
    expect(tapCount, 1);

    // Wait out the double tap interval so the next tap doesn't end up being
    // recognized as a double tap.
    await tester.pump(const Duration(seconds: 1));

    // Double tap count as one single tap.
    await tester.tap(find.byType(CupertinoTextField));
    await tester.pump(const Duration(milliseconds: 100));
    await tester.tap(find.byType(CupertinoTextField));
    await tester.pump();
    expect(tapCount, 2);
  });

  testWidgets(
    'onTap does not work when the text field is disabled',
    (WidgetTester tester) async {
      int tapCount = 0;
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              enabled: false,
              onTap: () => tapCount++,
            ),
          ),
        ),
      );

      expect(tapCount, 0);
      await tester.tap(find.byType(CupertinoTextField), warnIfMissed: false); // disabled
      await tester.pump();
      expect(tapCount, 0);

      // Wait out the double tap interval so the next tap doesn't end up being
      // recognized as a double tap.
      await tester.pump(const Duration(seconds: 1));

      // Enabling the text field, now it should accept taps.
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              onTap: () => tapCount++,
            ),
          ),
        ),
      );

      await tester.tap(find.byType(CupertinoTextField));
      expect(tapCount, 1);

      await tester.pump(const Duration(seconds: 1));

      // Disable it again.
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              enabled: false,
              onTap: () => tapCount++,
            ),
          ),
        ),
      );
      await tester.tap(find.byType(CupertinoTextField), warnIfMissed: false); // disabled
      await tester.pump();
      expect(tapCount, 1);
    },
  );

  testWidgets('Focus test when the text field is disabled', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();
    addTearDown(focusNode.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            focusNode: focusNode,
          ),
        ),
      ),
    );

    expect(focusNode.hasFocus, false); // initial status

    // Should accept requestFocus.
    focusNode.requestFocus();
    await tester.pump();
    expect(focusNode.hasFocus, true);

    // Disable the text field, now it should not accept requestFocus.
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            enabled: false,
            focusNode: focusNode,
          ),
        ),
      ),
    );

    // Should not accept requestFocus.
    focusNode.requestFocus();
    await tester.pump();
    expect(focusNode.hasFocus, false);
  });

  testWidgets(
    'text field respects theme',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          theme: CupertinoThemeData(
            brightness: Brightness.dark,
          ),
          home: Center(
            child: CupertinoTextField(),
          ),
        ),
      );

      final BoxDecoration decoration = tester.widget<DecoratedBox>(
        find.descendant(
          of: find.byType(CupertinoTextField),
          matching: find.byType(DecoratedBox),
        ),
      ).decoration as BoxDecoration;

      expect(
        decoration.border!.bottom.color.value,
        0x33FFFFFF,
      );

      await tester.enterText(find.byType(CupertinoTextField), 'smoked meat');
      await tester.pump();

      expect(
        tester.renderObject<RenderEditable>(
          find.byElementPredicate((Element element) => element.renderObject is RenderEditable).last,
        ).text!.style!.color,
        isSameColorAs(CupertinoColors.white),
      );
    },
  );

  testWidgets(
    'Check the toolbar appears below the TextField when there is not enough space above the TextField to show it',
    (WidgetTester tester) async {
      // This is a regression test for
      // https://github.com/flutter/flutter/issues/29808
      const String testValue = 'abc def ghi';
      final TextEditingController controller = TextEditingController();
      addTearDown(controller.dispose);

      await tester.pumpWidget(
        CupertinoApp(
          home: Container(
            padding: const EdgeInsets.all(30),
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      await tester.enterText(find.byType(CupertinoTextField), testValue);
      // Tap the selection handle to bring up the "paste / select all" menu.
      await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
      RenderEditable renderEditable = findRenderEditable(tester);
      List<TextSelectionPoint> endpoints = globalize(
        renderEditable.getEndpointsForSelection(controller.selection),
        renderEditable,
      );
      await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 300)); // skip past the frame where the opacity is zero

      // Verify the selection toolbar position
      Offset toolbarTopLeft = tester.getTopLeft(find.text('Paste'));
      Offset textFieldTopLeft = tester.getTopLeft(find.byType(CupertinoTextField));
      expect(textFieldTopLeft.dy, lessThan(toolbarTopLeft.dy));

      await tester.pumpWidget(
        CupertinoApp(
          home: Container(
            padding: const EdgeInsets.all(150),
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      await tester.enterText(find.byType(CupertinoTextField), testValue);
      // Tap the selection handle to bring up the "paste / select all" menu.
      await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
      renderEditable = findRenderEditable(tester);
      endpoints = globalize(
        renderEditable.getEndpointsForSelection(controller.selection),
        renderEditable,
      );
      await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero

      // Verify the selection toolbar position
      toolbarTopLeft = tester.getTopLeft(find.text('Paste'));
      textFieldTopLeft = tester.getTopLeft(find.byType(CupertinoTextField));
      expect(toolbarTopLeft.dy, lessThan(textFieldTopLeft.dy));
    },
    skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
  );

  testWidgets('text field respects keyboardAppearance from theme', (WidgetTester tester) async {
    final List<MethodCall> log = <MethodCall>[];
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async {
      log.add(methodCall);
      return null;
    });

    await tester.pumpWidget(
      const CupertinoApp(
        theme: CupertinoThemeData(
          brightness: Brightness.dark,
        ),
        home: Center(
          child: CupertinoTextField(),
        ),
      ),
    );

    await tester.showKeyboard(find.byType(EditableText));
    final MethodCall setClient = log.first;
    expect(setClient.method, 'TextInput.setClient');
    expect(((setClient.arguments as List<dynamic>).last as Map<String, dynamic>)['keyboardAppearance'], 'Brightness.dark');
  });

  testWidgets('text field can override keyboardAppearance from theme', (WidgetTester tester) async {
    final List<MethodCall> log = <MethodCall>[];
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async {
      log.add(methodCall);
      return null;
    });

    await tester.pumpWidget(
      const CupertinoApp(
        theme: CupertinoThemeData(
          brightness: Brightness.dark,
        ),
        home: Center(
          child: CupertinoTextField(
            keyboardAppearance: Brightness.light,
          ),
        ),
      ),
    );

    await tester.showKeyboard(find.byType(EditableText));
    final MethodCall setClient = log.first;
    expect(setClient.method, 'TextInput.setClient');
    expect(((setClient.arguments as List<dynamic>).last as Map<String, dynamic>)['keyboardAppearance'], 'Brightness.light');
  });

  testWidgets('cursorColor respects theme', (WidgetTester tester) async {
    await tester.pumpWidget(
      const CupertinoApp(
        home: CupertinoTextField(),
      ),
    );

    final Finder textFinder = find.byType(CupertinoTextField);
    await tester.tap(textFinder);
    await tester.pump();

    final EditableTextState editableTextState =
    tester.firstState(find.byType(EditableText));
    final RenderEditable renderEditable = editableTextState.renderEditable;

    expect(renderEditable.cursorColor, CupertinoColors.activeBlue.color);

    await tester.pumpWidget(
      const CupertinoApp(
        home: CupertinoTextField(),
        theme: CupertinoThemeData(
          brightness: Brightness.dark,
        ),
      ),
    );

    await tester.pump();
    expect(renderEditable.cursorColor, CupertinoColors.activeBlue.darkColor);

    await tester.pumpWidget(
      const CupertinoApp(
        home: CupertinoTextField(),
        theme: CupertinoThemeData(
          primaryColor: Color(0xFFF44336),
        ),
      ),
    );

    await tester.pump();
    expect(renderEditable.cursorColor, const Color(0xFFF44336));
  });

  testWidgets('cursor can override color from theme', (WidgetTester tester) async {
    const CupertinoDynamicColor cursorColor = CupertinoDynamicColor.withBrightness(
      color: Color(0x12345678),
      darkColor: Color(0x87654321),
    );

    await tester.pumpWidget(
      const CupertinoApp(
        theme: CupertinoThemeData(),
        home: Center(
          child: CupertinoTextField(
            cursorColor: cursorColor,
          ),
        ),
      ),
    );

    EditableText editableText = tester.firstWidget(find.byType(EditableText));
    expect(editableText.cursorColor.value, 0x12345678);

    await tester.pumpWidget(
      const CupertinoApp(
        theme: CupertinoThemeData(brightness: Brightness.dark),
        home: Center(
          child: CupertinoTextField(
            cursorColor: cursorColor,
          ),
        ),
      ),
    );

    editableText = tester.firstWidget(find.byType(EditableText));
    expect(editableText.cursorColor.value, 0x87654321);
  });

  testWidgets('shows selection handles', (WidgetTester tester) async {
    const String testText = 'lorem ipsum';
    final TextEditingController controller = TextEditingController(text: testText);
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        theme: const CupertinoThemeData(),
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    final RenderEditable renderEditable =
      tester.state<EditableTextState>(find.byType(EditableText)).renderEditable;

    await tester.tapAt(textOffsetToPosition(tester, 5));
    renderEditable.selectWord(cause: SelectionChangedCause.longPress);
    await tester.pumpAndSettle();

    final List<Widget> transitions =
      find.byType(FadeTransition).evaluate().map((Element e) => e.widget).toList();
    expect(transitions.length, 2);
    final FadeTransition left = transitions[0] as FadeTransition;
    final FadeTransition right = transitions[1] as FadeTransition;

    expect(left.opacity.value, equals(1.0));
    expect(right.opacity.value, equals(1.0));
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));

  testWidgets('when CupertinoTextField would be blocked by keyboard, it is shown with enough space for the selection handle', (WidgetTester tester) async {
    final ScrollController scrollController = ScrollController();
    addTearDown(scrollController.dispose);
    final TextEditingController controller = TextEditingController();
    addTearDown(controller.dispose);

    await tester.pumpWidget(CupertinoApp(
      theme: const CupertinoThemeData(),
      home: Center(
        child: ListView(
          controller: scrollController,
          children: <Widget>[
            Container(height: 583), // Push field almost off screen.
            CupertinoTextField(controller: controller),
            Container(height: 1000),
          ],
        ),
      ),
    ));

    // Tap the TextField to put the cursor into it and bring it into view.
    expect(scrollController.offset, 0.0);
    await tester.tap(find.byType(CupertinoTextField));
    await tester.pumpAndSettle();

    // The ListView has scrolled to keep the TextField and cursor handle
    // visible.
    expect(scrollController.offset, 27.0);
  });

  testWidgets('disabled state golden', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(text: 'lorem');
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: DecoratedBox(
          decoration: const BoxDecoration(color: Color(0xFFFFFFFF)),
          child: Center(
            child: SizedBox(
              width: 200,
              height: 200,
              child: RepaintBoundary(
                key: const ValueKey<int>(1),
                child: CupertinoTextField(
                  controller: controller,
                  enabled: false,
                ),
              ),
            ),
          ),
        ),
      ),
    );

    await expectLater(
      find.byKey(const ValueKey<int>(1)),
      matchesGoldenFile('text_field_test.disabled.png'),
    );
  });

  testWidgets(
    'Can drag the left handle while the right handle remains off-screen',
    (WidgetTester tester) async {
      // Text is longer than textfield width.
      const String testValue = 'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb';
      final TextEditingController controller = TextEditingController(text: testValue);
      addTearDown(controller.dispose);
      final ScrollController scrollController = ScrollController();
      addTearDown(scrollController.dispose);


      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              dragStartBehavior: DragStartBehavior.down,
              controller: controller,
              scrollController: scrollController,
            ),
          ),
        ),
      );

      // Double tap 'b' to show handles.
      final Offset bPos = textOffsetToPosition(tester, testValue.indexOf('b'));
      await tester.tapAt(bPos);
      await tester.pump(kDoubleTapTimeout ~/ 2);
      await tester.tapAt(bPos);
      await tester.pumpAndSettle();

      final TextSelection selection = controller.selection;
      expect(selection.baseOffset, 28);
      expect(selection.extentOffset, testValue.length);

      // Move to the left edge.
      scrollController.jumpTo(0);
      await tester.pumpAndSettle();

      final RenderEditable renderEditable = findRenderEditable(tester);
      final List<TextSelectionPoint> endpoints = globalize(
        renderEditable.getEndpointsForSelection(selection),
        renderEditable,
      );
      expect(endpoints.length, 2);

      // Left handle should appear between textfield's left and right position.
      final Offset textFieldLeftPosition =
          tester.getTopLeft(find.byType(CupertinoTextField));
      expect(endpoints[0].point.dx - textFieldLeftPosition.dx, isPositive);
      final Offset textFieldRightPosition =
          tester.getTopRight(find.byType(CupertinoTextField));
      expect(textFieldRightPosition.dx - endpoints[0].point.dx, isPositive);
      // Right handle should remain off-screen.
      expect(endpoints[1].point.dx - textFieldRightPosition.dx, isPositive);

      // Drag the left handle to the right by 25 offset.
      const int toOffset = 25;
      final double beforeScrollOffset = scrollController.offset;
      final Offset handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
      final Offset newHandlePos = textOffsetToPosition(tester, toOffset);
      final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
      await tester.pump();
      await gesture.moveTo(newHandlePos);
      await tester.pump();
      await gesture.up();
      await tester.pump();

      switch (defaultTargetPlatform) {
        case TargetPlatform.iOS:
        case TargetPlatform.macOS:
          // On Apple platforms, dragging the base handle makes it the extent.
          expect(controller.selection.baseOffset, testValue.length);
          expect(controller.selection.extentOffset, toOffset);
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
        case TargetPlatform.linux:
        case TargetPlatform.windows:
          expect(controller.selection.baseOffset, toOffset);
          expect(controller.selection.extentOffset, testValue.length);
      }

      // The scroll area of text field should not move.
      expect(scrollController.offset, beforeScrollOffset);
    },
  );

  testWidgets(
    'Can drag the right handle while the left handle remains off-screen',
    (WidgetTester tester) async {
      // Text is longer than textfield width.
      const String testValue = 'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb';
      final TextEditingController controller = TextEditingController(text: testValue);
      addTearDown(controller.dispose);
      final ScrollController scrollController = ScrollController();
      addTearDown(scrollController.dispose);

      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              dragStartBehavior: DragStartBehavior.down,
              controller: controller,
              scrollController: scrollController,
            ),
          ),
        ),
      );

      // Double tap 'a' to show handles.
      final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
      await tester.tapAt(aPos);
      await tester.pump(kDoubleTapTimeout ~/ 2);
      await tester.tapAt(aPos);
      await tester.pumpAndSettle();

      final TextSelection selection = controller.selection;
      expect(selection.baseOffset, 0);
      expect(selection.extentOffset, 27);

      // Move to the right edge.
      scrollController.jumpTo(800);
      await tester.pumpAndSettle();

      final RenderEditable renderEditable = findRenderEditable(tester);
      final List<TextSelectionPoint> endpoints = globalize(
        renderEditable.getEndpointsForSelection(selection),
        renderEditable,
      );
      expect(endpoints.length, 2);

      // Right handle should appear between textfield's left and right position.
      final Offset textFieldLeftPosition =
          tester.getTopLeft(find.byType(CupertinoTextField));
      expect(endpoints[1].point.dx - textFieldLeftPosition.dx, isPositive);
      final Offset textFieldRightPosition =
          tester.getTopRight(find.byType(CupertinoTextField));
      expect(textFieldRightPosition.dx - endpoints[1].point.dx, isPositive);
      // Left handle should remain off-screen.
      expect(endpoints[0].point.dx, isNegative);

      // Drag the right handle to the left by 50 offset.
      const int toOffset = 50;
      final double beforeScrollOffset = scrollController.offset;
      final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
      final Offset newHandlePos = textOffsetToPosition(tester, toOffset);
      final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
      await tester.pump();
      await gesture.moveTo(newHandlePos);
      await tester.pump();
      await gesture.up();
      await tester.pump();

      expect(controller.selection.baseOffset, 0);
      expect(controller.selection.extentOffset, toOffset);

      // The scroll area of text field should not move.
      expect(scrollController.offset, beforeScrollOffset);
    },
  );

  group('Text selection toolbar', () {
    testWidgets('Collapsed selection works', (WidgetTester tester) async {
      tester.view.physicalSize = const Size(400, 400);
      tester.view.devicePixelRatio = 1;
      addTearDown(tester.view.reset);

      EditableText.debugDeterministicCursor = true;
      TextEditingController controller;
      EditableTextState state;
      Offset bottomLeftSelectionPosition;

      controller = TextEditingController(text: 'a');
      // Top left collapsed selection. The toolbar should flip vertically, and
      // the arrow should not point exactly to the caret because the caret is
      // too close to the left.
      await tester.pumpWidget(
        CupertinoApp(
          debugShowCheckedModeBanner: false,
          home: CupertinoPageScaffold(
            child: Align(
              alignment: Alignment.topLeft,
              child: SizedBox(
                width: 200,
                height: 200,
                child: CupertinoTextField(
                  controller: controller,
                  maxLines: null,
                ),
              ),
            ),
          ),
        ),
      );

      state = tester.state<EditableTextState>(find.byType(EditableText));
      final double lineHeight = state.renderEditable.preferredLineHeight;

      state.renderEditable.selectPositionAt(from: textOffsetToPosition(tester, 0), cause: SelectionChangedCause.tap);
      expect(state.showToolbar(), true);
      await tester.pumpAndSettle();

      bottomLeftSelectionPosition = textOffsetToBottomLeftPosition(tester, 0);
      expect(
        find.byType(CupertinoTextSelectionToolbar),
        paints..clipPath(
          pathMatcher: PathPointsMatcher(
            excludes: <Offset> [
              // Arrow should not point to the selection handle.
              bottomLeftSelectionPosition.translate(0, 8 + 0.1),
            ],
            includes: <Offset> [
              // Expected center of the arrow. The arrow should stay clear of
              // the edges of the selection toolbar.
              Offset(26.0, bottomLeftSelectionPosition.dy + 8.0 + 0.1),
            ],
          ),
        ),
      );

      expect(
        find.byType(CupertinoTextSelectionToolbar),
        paints..clipPath(
          pathMatcher: PathBoundsMatcher(
            topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01),
            leftMatcher: moreOrLessEquals(8),
            rightMatcher: lessThanOrEqualTo(400 - 8),
            bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 44, epsilon: 0.01),
          ),
        ),
      );

      // Top Right collapsed selection. The toolbar should flip vertically, and
      // the arrow should not point exactly to the caret because the caret is
      // too close to the right.
      controller.dispose();
      controller = TextEditingController(text: 'a' * 200);
      await tester.pumpWidget(
        CupertinoApp(
          debugShowCheckedModeBanner: false,
          home: CupertinoPageScaffold(
            child: Align(
              alignment: Alignment.topRight,
              child: SizedBox(
                width: 200,
                height: 200,
                child: CupertinoTextField(
                  controller: controller,
                  maxLines: null,
                ),
              ),
            ),
          ),
        ),
      );

      state = tester.state<EditableTextState>(find.byType(EditableText));
      state.renderEditable.selectPositionAt(
        from: tester.getTopRight(find.byType(CupertinoApp)),
        cause: SelectionChangedCause.tap,
      );
      await tester.pumpAndSettle();

      // -1 because we want to reach the end of the line, not the start of a new line.
      bottomLeftSelectionPosition = textOffsetToBottomLeftPosition(tester, state.renderEditable.selection!.baseOffset - 1);

      expect(
        find.byType(CupertinoTextSelectionToolbar),
        paints..clipPath(
          pathMatcher: PathPointsMatcher(
            excludes: <Offset> [
              // Arrow should not point to the selection handle.
              bottomLeftSelectionPosition.translate(0, 8 + 0.1),
            ],
            includes: <Offset> [
              // Expected center of the arrow.
              Offset(400 - 26.0, bottomLeftSelectionPosition.dy + 8 + 0.1),
            ],
          ),
        ),
      );

      expect(
        find.byType(CupertinoTextSelectionToolbar),
        paints..clipPath(
          pathMatcher: PathBoundsMatcher(
            topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01),
            rightMatcher: moreOrLessEquals(400.0 - 8),
            bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 44, epsilon: 0.01),
            leftMatcher: greaterThanOrEqualTo(8),
          ),
        ),
      );

      // Normal centered collapsed selection. The toolbar arrow should point down, and
      // it should point exactly to the caret.
      controller.dispose();
      controller = TextEditingController(text: 'a' * 200);
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          debugShowCheckedModeBanner: false,
          home: CupertinoPageScaffold(
            child: Align(
              child: SizedBox(
                width: 200,
                height: 200,
                child: CupertinoTextField(
                  controller: controller,
                  maxLines: null,
                ),
              ),
            ),
          ),
        ),
      );

      state = tester.state<EditableTextState>(find.byType(EditableText));
      state.renderEditable.selectPositionAt(
        from: tester.getCenter(find.byType(EditableText)),
        cause: SelectionChangedCause.tap,
      );
      await tester.pumpAndSettle();

      bottomLeftSelectionPosition = textOffsetToBottomLeftPosition(tester, state.renderEditable.selection!.baseOffset);

      expect(
        find.byType(CupertinoTextSelectionToolbar),
        paints..clipPath(
          pathMatcher: PathPointsMatcher(
            includes: <Offset> [
              // Expected center of the arrow.
              bottomLeftSelectionPosition.translate(0, -lineHeight - 8 - 0.1),
            ],
          ),
        ),
      );

      expect(
        find.byType(CupertinoTextSelectionToolbar),
        paints..clipPath(
          pathMatcher: PathBoundsMatcher(
            bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight, epsilon: 0.01),
            topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight - 44, epsilon: 0.01),
            rightMatcher: lessThanOrEqualTo(400 - 8),
            leftMatcher: greaterThanOrEqualTo(8),
          ),
        ),
      );
    });

    testWidgets('selecting multiple words works', (WidgetTester tester) async {
      tester.view.physicalSize = const Size(400, 400);
      tester.view.devicePixelRatio = 1;
      addTearDown(tester.view.reset);

      EditableText.debugDeterministicCursor = true;
      final TextEditingController controller;
      final EditableTextState state;

      // Normal multiword collapsed selection. The toolbar arrow should point down, and
      // it should point exactly to the caret.
      controller = TextEditingController(text: List<String>.filled(20, 'a').join('  '));
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          debugShowCheckedModeBanner: false,
          home: CupertinoPageScaffold(
            child: Align(
              child: SizedBox(
                width: 200,
                height: 200,
                child: CupertinoTextField(
                  controller: controller,
                  maxLines: null,
                ),
              ),
            ),
          ),
        ),
      );

      state = tester.state<EditableTextState>(find.byType(EditableText));
      final double lineHeight = state.renderEditable.preferredLineHeight;

      // Select the first 2 words.
      state.renderEditable.selectPositionAt(
        from: textOffsetToPosition(tester, 0),
        to: textOffsetToPosition(tester, 4),
        cause: SelectionChangedCause.tap,
      );
      expect(state.showToolbar(), true);
      await tester.pumpAndSettle();

      final Offset selectionPosition = (textOffsetToBottomLeftPosition(tester, 0) + textOffsetToBottomLeftPosition(tester, 4)) / 2;

      expect(
        find.byType(CupertinoTextSelectionToolbar),
        paints..clipPath(
          pathMatcher: PathPointsMatcher(
            includes: <Offset> [
              // Expected center of the arrow.
              selectionPosition.translate(0, -lineHeight - 8 - 0.1),
            ],
          ),
        ),
      );

      expect(
        find.byType(CupertinoTextSelectionToolbar),
        paints..clipPath(
          pathMatcher: PathBoundsMatcher(
            bottomMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight, epsilon: 0.01),
            topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 44, epsilon: 0.01),
            rightMatcher: lessThanOrEqualTo(400 - 8),
            leftMatcher: greaterThanOrEqualTo(8),
          ),
        ),
      );
    });

    testWidgets('selecting multiline works', (WidgetTester tester) async {
      tester.view.physicalSize = const Size(400, 400);
      tester.view.devicePixelRatio = 1;
      addTearDown(tester.view.reset);

      EditableText.debugDeterministicCursor = true;
      final TextEditingController controller;
      final EditableTextState state;

      // Normal multiline collapsed selection. The toolbar arrow should point down, and
      // it should point exactly to the horizontal center of the text field.
      controller = TextEditingController(text: List<String>.filled(20, 'a  a  ').join('\n'));
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          debugShowCheckedModeBanner: false,
          home: CupertinoPageScaffold(
            child: Align(
              child: SizedBox(
                width: 200,
                height: 200,
                child: CupertinoTextField(
                  controller: controller,
                  maxLines: null,
                ),
              ),
            ),
          ),
        ),
      );

      state = tester.state<EditableTextState>(find.byType(EditableText));
      final double lineHeight = state.renderEditable.preferredLineHeight;

      // Select the first 2 words.
      state.renderEditable.selectPositionAt(
        from: textOffsetToPosition(tester, 0),
        to: textOffsetToPosition(tester, 10),
        cause: SelectionChangedCause.tap,
      );
      expect(state.showToolbar(), true);
      await tester.pumpAndSettle();

      final Offset selectionPosition = Offset(
        // Toolbar should be centered.
        200,
        textOffsetToBottomLeftPosition(tester, 0).dy,
      );

      expect(
        find.byType(CupertinoTextSelectionToolbar),
        paints..clipPath(
          pathMatcher: PathPointsMatcher(
            includes: <Offset> [
              // Expected center of the arrow.
              selectionPosition.translate(0, -lineHeight - 8 - 0.1),
            ],
          ),
        ),
      );

      expect(
        find.byType(CupertinoTextSelectionToolbar),
        paints..clipPath(
          pathMatcher: PathBoundsMatcher(
            bottomMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight, epsilon: 0.01),
            topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 44, epsilon: 0.01),
            rightMatcher: lessThanOrEqualTo(400 - 8),
            leftMatcher: greaterThanOrEqualTo(8),
          ),
        ),
      );
    });

    // This is a regression test for
    // https://github.com/flutter/flutter/issues/37046.
    testWidgets('No exceptions when showing selection menu inside of nested Navigators', (WidgetTester tester) async {
      const String testValue = '123456';
      final TextEditingController controller = TextEditingController(
        text: testValue,
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: CupertinoPageScaffold(
            child: Center(
              child: Column(
                children: <Widget>[
                  Container(
                    height: 100,
                    color: CupertinoColors.black,
                  ),
                  Expanded(
                    child: Navigator(
                      onGenerateRoute: (_) => CupertinoPageRoute<void>(
                        builder: (_) => CupertinoTextField(
                          controller: controller,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      );

      // No text selection toolbar.
      expect(find.byType(CupertinoTextSelectionToolbar), findsNothing);

      // Double tap on the text in the input.
      await tester.pumpAndSettle();
      await tester.tapAt(textOffsetToPosition(tester, testValue.length ~/ 2));
      await tester.pump(const Duration(milliseconds: 100));
      await tester.tapAt(textOffsetToPosition(tester, testValue.length ~/ 2));
      await tester.pumpAndSettle();

      // Now the text selection toolbar is showing and there were no exceptions.
      expect(find.byType(CupertinoTextSelectionToolbar), findsOneWidget);
      expect(tester.takeException(), null);
    });

    testWidgets('Drag selection hides the selection menu', (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'blah1 blah2',
      );
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              controller: controller,
            ),
          ),
        ),
      );

      // Initially, the menu is not shown and there is no selection.
      expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
      final Offset midBlah1 = textOffsetToPosition(tester, 2);
      final Offset midBlah2 = textOffsetToPosition(tester, 8);

      // Right click the second word.
      final TestGesture gesture = await tester.startGesture(
        midBlah2,
        kind: PointerDeviceKind.mouse,
        buttons: kSecondaryMouseButton,
      );
      await tester.pump();
      await gesture.up();
      await tester.pumpAndSettle();

      // The toolbar is shown.
      expect(find.text('Paste'), findsOneWidget);

      // Drag the mouse to the first word.
      final TestGesture gesture2 = await tester.startGesture(
        midBlah1,
        kind: PointerDeviceKind.mouse,
      );
      await tester.pump();
      await gesture2.moveTo(midBlah2);
      await tester.pump();
      await gesture2.up();
      await tester.pumpAndSettle();

      // The toolbar is hidden.
      expect(find.text('Paste'), findsNothing);
    }, variant: TargetPlatformVariant.desktop());
  }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.

  group('textAlignVertical position', () {
    group('simple case', () {
      testWidgets('align top (default)', (WidgetTester tester) async {
        final FocusNode focusNode = FocusNode();
        addTearDown(focusNode.dispose);
        const Size size = Size(200.0, 200.0);
        await tester.pumpWidget(
          CupertinoApp(
            debugShowCheckedModeBanner: false,
            home: CupertinoPageScaffold(
              child: Align(
                child: SizedBox(
                  width: size.width,
                  height: size.height,
                  child: CupertinoTextField(
                    focusNode: focusNode,
                    expands: true,
                    maxLines: null,
                  ),
                ),
              ),
            ),
          ),
        );

        // Fills the whole container since expands is true.
        expect(tester.getSize(find.byType(CupertinoTextField)), size);

        // Tapping anywhere inside focuses it.
        expect(focusNode.hasFocus, false);
        await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, true);
        focusNode.unfocus();
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, false);
        final Offset justInside = tester
          .getBottomLeft(find.byType(CupertinoTextField))
          .translate(0.0, -1.0);
        await tester.tapAt(justInside);
        await tester.pumpAndSettle();
        await tester.pump(const Duration(milliseconds: 300));
        expect(focusNode.hasFocus, true);

        // The EditableText is at the top.
        expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
        expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(207.0, epsilon: .0001));
      });

      testWidgets('align center', (WidgetTester tester) async {
        final FocusNode focusNode = FocusNode();
        addTearDown(focusNode.dispose);
        const Size size = Size(200.0, 200.0);
        await tester.pumpWidget(
          CupertinoApp(
            debugShowCheckedModeBanner: false,
            home: CupertinoPageScaffold(
              child: Align(
                child: SizedBox(
                  width: size.width,
                  height: size.height,
                  child: CupertinoTextField(
                    textAlignVertical: TextAlignVertical.center,
                    focusNode: focusNode,
                    expands: true,
                    maxLines: null,
                  ),
                ),
              ),
            ),
          ),
        );

        // Fills the whole container since expands is true.
        expect(tester.getSize(find.byType(CupertinoTextField)), size);

        // Tapping anywhere inside focuses it.
        expect(focusNode.hasFocus, false);
        await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, true);
        focusNode.unfocus();
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, false);
        final Offset justInside = tester
          .getBottomLeft(find.byType(CupertinoTextField))
          .translate(0.0, -1.0);
        await tester.tapAt(justInside);
        await tester.pumpAndSettle();
        await tester.pump(const Duration(milliseconds: 300));
        expect(focusNode.hasFocus, true);

        // The EditableText is at the center.
        expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
        expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(291.5, epsilon: .0001));
      });

      testWidgets('align bottom', (WidgetTester tester) async {
        final FocusNode focusNode = FocusNode();
        addTearDown(focusNode.dispose);
        const Size size = Size(200.0, 200.0);
        await tester.pumpWidget(
          CupertinoApp(
            debugShowCheckedModeBanner: false,
            home: CupertinoPageScaffold(
              child: Align(
                child: SizedBox(
                  width: size.width,
                  height: size.height,
                  child: CupertinoTextField(
                    textAlignVertical: TextAlignVertical.bottom,
                    focusNode: focusNode,
                    expands: true,
                    maxLines: null,
                  ),
                ),
              ),
            ),
          ),
        );

        // Fills the whole container since expands is true.
        expect(tester.getSize(find.byType(CupertinoTextField)), size);

        // Tapping anywhere inside focuses it.
        expect(focusNode.hasFocus, false);
        await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, true);
        focusNode.unfocus();
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, false);
        final Offset justInside = tester
          .getBottomLeft(find.byType(CupertinoTextField))
          .translate(0.0, -1.0);
        await tester.tapAt(justInside);
        await tester.pumpAndSettle();
        await tester.pump(const Duration(milliseconds: 300));
        expect(focusNode.hasFocus, true);

        // The EditableText is at the bottom.
        expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
        expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(376.0, epsilon: .0001));
      });

      testWidgets('align as a double', (WidgetTester tester) async {
        final FocusNode focusNode = FocusNode();
        addTearDown(focusNode.dispose);
        const Size size = Size(200.0, 200.0);
        await tester.pumpWidget(
          CupertinoApp(
            debugShowCheckedModeBanner: false,
            home: CupertinoPageScaffold(
              child: Align(
                child: SizedBox(
                  width: size.width,
                  height: size.height,
                  child: CupertinoTextField(
                    textAlignVertical: const TextAlignVertical(y: 0.75),
                    focusNode: focusNode,
                    expands: true,
                    maxLines: null,
                  ),
                ),
              ),
            ),
          ),
        );

        // Fills the whole container since expands is true.
        expect(tester.getSize(find.byType(CupertinoTextField)), size);

        // Tapping anywhere inside focuses it.
        expect(focusNode.hasFocus, false);
        await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, true);
        focusNode.unfocus();
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, false);
        final Offset justInside = tester
          .getBottomLeft(find.byType(CupertinoTextField))
          .translate(0.0, -1.0);
        await tester.tapAt(justInside);
        await tester.pumpAndSettle();
        await tester.pump(const Duration(milliseconds: 300));
        expect(focusNode.hasFocus, true);

        // The EditableText is near the bottom.
        expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
        expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(354.875, epsilon: .0001));
      });
    });

    group('tall prefix', () {
      testWidgets('align center (default when prefix)', (WidgetTester tester) async {
        final FocusNode focusNode = FocusNode();
        addTearDown(focusNode.dispose);
        const Size size = Size(200.0, 200.0);
        await tester.pumpWidget(
          CupertinoApp(
            debugShowCheckedModeBanner: false,
            home: CupertinoPageScaffold(
              child: Align(
                child: SizedBox(
                  width: size.width,
                  height: size.height,
                  child: CupertinoTextField(
                    focusNode: focusNode,
                    expands: true,
                    maxLines: null,
                    prefix: const SizedBox(
                      height: 100,
                      width: 10,
                    ),
                  ),
                ),
              ),
            ),
          ),
        );

        // Fills the whole container since expands is true.
        expect(tester.getSize(find.byType(CupertinoTextField)), size);

        // Tapping anywhere inside focuses it. This includes tapping on the
        // prefix, because in this case it is transparent.
        expect(focusNode.hasFocus, false);
        await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, true);
        focusNode.unfocus();
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, false);
        final Offset justInside = tester
          .getBottomLeft(find.byType(CupertinoTextField))
          .translate(0.0, -1.0);
        await tester.tapAt(justInside);
        await tester.pumpAndSettle();
        await tester.pump(const Duration(milliseconds: 300));
        expect(focusNode.hasFocus, true);

        // The EditableText is at the center. Same as without prefix.
        expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
        expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(291.5, epsilon: .0001));
      });

      testWidgets('align top', (WidgetTester tester) async {
        final FocusNode focusNode = FocusNode();
        addTearDown(focusNode.dispose);
        const Size size = Size(200.0, 200.0);
        await tester.pumpWidget(
          CupertinoApp(
            debugShowCheckedModeBanner: false,
            home: CupertinoPageScaffold(
              child: Align(
                child: SizedBox(
                  width: size.width,
                  height: size.height,
                  child: CupertinoTextField(
                    textAlignVertical: TextAlignVertical.top,
                    focusNode: focusNode,
                    expands: true,
                    maxLines: null,
                    prefix: const SizedBox(
                      height: 100,
                      width: 10,
                    ),
                  ),
                ),
              ),
            ),
          ),
        );

        // Fills the whole container since expands is true.
        expect(tester.getSize(find.byType(CupertinoTextField)), size);

        // Tapping anywhere inside focuses it. This includes tapping on the
        // prefix, because in this case it is transparent.
        expect(focusNode.hasFocus, false);
        await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, true);
        focusNode.unfocus();
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, false);
        final Offset justInside = tester
          .getBottomLeft(find.byType(CupertinoTextField))
          .translate(0.0, -1.0);
        await tester.tapAt(justInside);
        await tester.pumpAndSettle();
        await tester.pump(const Duration(milliseconds: 300));
        expect(focusNode.hasFocus, true);

        // The prefix is at the top, and the EditableText is centered within its
        // height.
        expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
        expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(241.5, epsilon: .0001));
      });

      testWidgets('align bottom', (WidgetTester tester) async {
        final FocusNode focusNode = FocusNode();
        addTearDown(focusNode.dispose);
        const Size size = Size(200.0, 200.0);
        await tester.pumpWidget(
          CupertinoApp(
            debugShowCheckedModeBanner: false,
            home: CupertinoPageScaffold(
              child: Align(
                child: SizedBox(
                  width: size.width,
                  height: size.height,
                  child: CupertinoTextField(
                    textAlignVertical: TextAlignVertical.bottom,
                    focusNode: focusNode,
                    expands: true,
                    maxLines: null,
                    prefix: const SizedBox(
                      height: 100,
                      width: 10,
                    ),
                  ),
                ),
              ),
            ),
          ),
        );

        // Fills the whole container since expands is true.
        expect(tester.getSize(find.byType(CupertinoTextField)), size);

        // Tapping anywhere inside focuses it. This includes tapping on the
        // prefix, because in this case it is transparent.
        expect(focusNode.hasFocus, false);
        await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, true);
        focusNode.unfocus();
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, false);
        final Offset justInside = tester
          .getBottomLeft(find.byType(CupertinoTextField))
          .translate(0.0, -1.0);
        await tester.tapAt(justInside);
        await tester.pumpAndSettle();
        await tester.pump(const Duration(milliseconds: 300));
        expect(focusNode.hasFocus, true);

        // The prefix is at the bottom, and the EditableText is centered within
        // its height.
        expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
        expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(341.5, epsilon: .0001));
      });

      testWidgets('align as a double', (WidgetTester tester) async {
        final FocusNode focusNode = FocusNode();
        addTearDown(focusNode.dispose);
        const Size size = Size(200.0, 200.0);
        await tester.pumpWidget(
          CupertinoApp(
            debugShowCheckedModeBanner: false,
            home: CupertinoPageScaffold(
              child: Align(
                child: SizedBox(
                  width: size.width,
                  height: size.height,
                  child: CupertinoTextField(
                    textAlignVertical: const TextAlignVertical(y: 0.75),
                    focusNode: focusNode,
                    expands: true,
                    maxLines: null,
                    prefix: const SizedBox(
                      height: 100,
                      width: 10,
                    ),
                  ),
                ),
              ),
            ),
          ),
        );

        // Fills the whole container since expands is true.
        expect(tester.getSize(find.byType(CupertinoTextField)), size);

        // Tapping anywhere inside focuses it. This includes tapping on the
        // prefix, because in this case it is transparent.
        expect(focusNode.hasFocus, false);
        await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, true);
        focusNode.unfocus();
        await tester.pumpAndSettle();
        expect(focusNode.hasFocus, false);
        final Offset justInside = tester
          .getBottomLeft(find.byType(CupertinoTextField))
          .translate(0.0, -1.0);
        await tester.tapAt(justInside);
        await tester.pumpAndSettle();
        await tester.pump(const Duration(milliseconds: 300));
        expect(focusNode.hasFocus, true);

        // The EditableText is near the bottom.
        expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, moreOrLessEquals(size.height, epsilon: .0001));
        expect(tester.getTopLeft(find.byType(EditableText)).dy, moreOrLessEquals(329.0, epsilon: .0001));
      });
    });

    testWidgets(
      'Long press on an autofocused field shows the selection menu',
      (WidgetTester tester) async {
        await tester.pumpWidget(
          CupertinoApp(
            home: Center(
              child: ConstrainedBox(
                constraints: BoxConstraints.loose(const Size(200, 200)),
                child: const CupertinoTextField(
                  autofocus: true,
                ),
              ),
            ),
          ),
        );
        // This extra pump allows the selection set by autofocus to propagate to
        // the RenderEditable.
        await tester.pump();

        // Long press shows the selection menu.
        await tester.longPressAt(textOffsetToPosition(tester, 0));
        await tester.pumpAndSettle();
        expect(find.text('Paste'), isContextMenuProvidedByPlatform ? findsNothing : findsOneWidget);
      },
    );
  });

  testWidgets("Arrow keys don't move input focus", (WidgetTester tester) async {
    final TextEditingController controller1 = TextEditingController();
    final TextEditingController controller2 = TextEditingController();
    final TextEditingController controller3 = TextEditingController();
    final TextEditingController controller4 = TextEditingController();
    final TextEditingController controller5 = TextEditingController();
    final FocusNode focusNode1 = FocusNode(debugLabel: 'Field 1');
    final FocusNode focusNode2 = FocusNode(debugLabel: 'Field 2');
    final FocusNode focusNode3 = FocusNode(debugLabel: 'Field 3');
    final FocusNode focusNode4 = FocusNode(debugLabel: 'Field 4');
    final FocusNode focusNode5 = FocusNode(debugLabel: 'Field 5');
    addTearDown(focusNode1.dispose);
    addTearDown(focusNode2.dispose);
    addTearDown(focusNode3.dispose);
    addTearDown(focusNode4.dispose);
    addTearDown(focusNode5.dispose);
    addTearDown(controller1.dispose);
    addTearDown(controller2.dispose);
    addTearDown(controller3.dispose);
    addTearDown(controller4.dispose);
    addTearDown(controller5.dispose);

    // Lay out text fields in a "+" formation, and focus the center one.
    await tester.pumpWidget(CupertinoApp(
      home: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              SizedBox(
                width: 100.0,
                child: CupertinoTextField(
                  controller: controller1,
                  focusNode: focusNode1,
                ),
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  SizedBox(
                    width: 100.0,
                    child: CupertinoTextField(
                      controller: controller2,
                      focusNode: focusNode2,
                    ),
                  ),
                  SizedBox(
                    width: 100.0,
                    child: CupertinoTextField(
                      controller: controller3,
                      focusNode: focusNode3,
                    ),
                  ),
                  SizedBox(
                    width: 100.0,
                    child: CupertinoTextField(
                      controller: controller4,
                      focusNode: focusNode4,
                    ),
                  ),
                ],
              ),
              SizedBox(
                width: 100.0,
                child: CupertinoTextField(
                  controller: controller5,
                  focusNode: focusNode5,
                ),
              ),
            ],
          ),
        ),
      ),
    );

    focusNode3.requestFocus();
    await tester.pump();
    expect(focusNode3.hasPrimaryFocus, isTrue);

    await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
    await tester.pump();
    expect(focusNode3.hasPrimaryFocus, isTrue);

    await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
    await tester.pump();
    expect(focusNode3.hasPrimaryFocus, isTrue);

    await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
    await tester.pump();
    expect(focusNode3.hasPrimaryFocus, isTrue);

    await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
    await tester.pump();
    expect(focusNode3.hasPrimaryFocus, isTrue);
  }, variant: KeySimulatorTransitModeVariant.all());

  testWidgets('Scrolling shortcuts are disabled in text fields', (WidgetTester tester) async {
    bool scrollInvoked = false;
    await tester.pumpWidget(
      CupertinoApp(
        home: Actions(
          actions: <Type, Action<Intent>>{
            ScrollIntent: CallbackAction<ScrollIntent>(onInvoke: (Intent intent) {
              scrollInvoked = true;
              return null;
            }),
          },
          child: ListView(
            children: const <Widget>[
              Padding(padding: EdgeInsets.symmetric(vertical: 200)),
              CupertinoTextField(),
              Padding(padding: EdgeInsets.symmetric(vertical: 800)),
            ],
          ),
        ),
      ),
    );
    await tester.pump();
    expect(scrollInvoked, isFalse);

    // Set focus on the text field.
    await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField)));

    await tester.sendKeyEvent(LogicalKeyboardKey.space);
    expect(scrollInvoked, isFalse);

    await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
    expect(scrollInvoked, isFalse);
  }, variant: KeySimulatorTransitModeVariant.all());

  testWidgets('Cupertino text field semantics', (WidgetTester tester) async {
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: ConstrainedBox(
            constraints: BoxConstraints.loose(const Size(200, 200)),
            child: const CupertinoTextField(),
          ),
        ),
      ),
    );

    expect(
      tester.getSemantics(
        find.descendant(
          of: find.byType(CupertinoTextField),
          matching: find.byType(Semantics),
        ).first,
      ),
      matchesSemantics(
        isTextField: true,
        isEnabled: true,
        hasEnabledState: true,
        hasTapAction: true,
      ),
    );
  });

  testWidgets('Disabled Cupertino text field semantics', (WidgetTester tester) async {
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: ConstrainedBox(
            constraints: BoxConstraints.loose(const Size(200, 200)),
            child: const CupertinoTextField(
              enabled: false,
            ),
          ),
        ),
      ),
    );

    expect(
      tester.getSemantics(
        find.descendant(
          of: find.byType(CupertinoTextField),
          matching: find.byType(Semantics),
        ).first,
      ),
      matchesSemantics(
        hasEnabledState: true,
        isTextField: true,
        isReadOnly: true,
      ),
    );
  });

  testWidgets('Cupertino text field clear button semantics', (WidgetTester tester) async {
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: ConstrainedBox(
            constraints: BoxConstraints.loose(const Size(200, 200)),
            child: const CupertinoTextField(
              clearButtonMode: OverlayVisibilityMode.always,
            ),
          ),
        ),
      ),
    );

    expect(find.bySemanticsLabel('Clear'), findsOneWidget);

    expect(
      tester.getSemantics(
        find.bySemanticsLabel('Clear').first,
      ),
      matchesSemantics(
        isButton: true,
        hasTapAction: true,
        label: 'Clear'
      ),
    );
  });

  testWidgets('Cupertino text field clear semantic label', (WidgetTester tester) async {
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: ConstrainedBox(
            constraints: BoxConstraints.loose(const Size(200, 200)),
            child: const CupertinoTextField(
              clearButtonMode: OverlayVisibilityMode.always,
              clearButtonSemanticLabel: 'Delete Text'
            ),
          ),
        ),
      ),
    );

    expect(find.bySemanticsLabel('Clear'), findsNothing);

    expect(find.bySemanticsLabel('Delete Text'), findsOneWidget);

    expect(
      tester.getSemantics(
        find.bySemanticsLabel('Delete Text').first,
      ),
      matchesSemantics(
        isButton: true,
        hasTapAction: true,
        label: 'Delete Text'
      ),
    );
  });

  testWidgets('text selection style 1', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwassssup!',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
       CupertinoApp(
        home: Center(
          child: RepaintBoundary(
            child: Container(
              width: 650.0,
              height: 600.0,
              decoration: const BoxDecoration(
                color: Color(0xff00ff00),
              ),
              child: Column(
                children: <Widget>[
                  CupertinoTextField(
                    autofocus: true,
                    key: const Key('field0'),
                    controller: controller,
                    style: const TextStyle(height: 4, color: ui.Color.fromARGB(100, 0, 0, 0)),
                    toolbarOptions: const ToolbarOptions(selectAll: true),
                    selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingTop,
                    selectionWidthStyle: ui.BoxWidthStyle.max,
                    maxLines: 3,
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );

    // This extra pump is so autofocus can propagate to renderEditable.
    await tester.pump();

    final Offset textFieldStart = tester.getTopLeft(find.byKey(const Key('field0')));

    await tester.longPressAt(textFieldStart + const Offset(50.0, 2.0));
    await tester.pumpAndSettle(const Duration(milliseconds: 150));
    // Tap the Select All button.
    await tester.tapAt(textFieldStart + const Offset(20.0, 100.0));
    await tester.pump(const Duration(milliseconds: 300));

    await expectLater(
      find.byType(CupertinoApp),
      matchesGoldenFile('text_field_golden.TextSelectionStyle.1.png'),
    );
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
    skip: kIsWeb, // [intended] the web has its own Select All.
  );

  testWidgets('text selection style 2', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwassssup!',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: RepaintBoundary(
            child: Container(
              width: 650.0,
              height: 600.0,
              decoration: const BoxDecoration(
                color: Color(0xff00ff00),
              ),
              child: Column(
                children: <Widget>[
                  CupertinoTextField(
                    autofocus: true,
                    key: const Key('field0'),
                    controller: controller,
                    style: const TextStyle(height: 4, color: ui.Color.fromARGB(100, 0, 0, 0)),
                    toolbarOptions: const ToolbarOptions(selectAll: true),
                    selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingBottom,
                    maxLines: 3,
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );

    // This extra pump is so autofocus can propagate to renderEditable.
    await tester.pump();

    final Offset textFieldStart = tester.getTopLeft(find.byKey(const Key('field0')));

    await tester.longPressAt(textFieldStart + const Offset(50.0, 2.0));
    await tester.pumpAndSettle(const Duration(milliseconds: 150));
    // Tap the Select All button.
    await tester.tapAt(textFieldStart + const Offset(20.0, 100.0));
    await tester.pump(const Duration(milliseconds: 300));

    await expectLater(
      find.byType(CupertinoApp),
      matchesGoldenFile('text_field_golden.TextSelectionStyle.2.png'),
    );
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
    skip: kIsWeb, // [intended] the web has its own Select All.
  );

  testWidgets('textSelectionControls is passed to EditableText', (WidgetTester tester) async {
    final MockTextSelectionControls selectionControl = MockTextSelectionControls();
    await tester.pumpWidget(
       CupertinoApp(
        home: Center(
          child: CupertinoTextField(
              selectionControls: selectionControl,
            ),
          ),
        ),
      );

    final EditableText widget = tester.widget(find.byType(EditableText));
    expect(widget.selectionControls, equals(selectionControl));
  });

  testWidgets('Do not add LengthLimiting formatter to the user supplied list', (WidgetTester tester) async {
    final List<TextInputFormatter> formatters = <TextInputFormatter>[];

    await tester.pumpWidget(
      CupertinoApp(
        home: CupertinoTextField(maxLength: 5, inputFormatters: formatters),
      ),
    );

    expect(formatters.isEmpty, isTrue);
  });

  group('MaxLengthEnforcement', () {
    const int maxLength = 5;

    Future<void> setupWidget(
      WidgetTester tester,
      MaxLengthEnforcement? enforcement,
    ) async {

      final Widget widget = CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            maxLength: maxLength,
            maxLengthEnforcement: enforcement,
          ),
        ),
      );

      await tester.pumpWidget(widget);
      await tester.pumpAndSettle();
    }

    testWidgets('using none enforcement.', (WidgetTester tester) async {
      const MaxLengthEnforcement enforcement = MaxLengthEnforcement.none;

      await setupWidget(tester, enforcement);

      final EditableTextState state = tester.state(find.byType(EditableText));

      state.updateEditingValue(const TextEditingValue(text: 'abc'));
      expect(state.currentTextEditingValue.text, 'abc');
      expect(state.currentTextEditingValue.composing, TextRange.empty);

      state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6)));
      expect(state.currentTextEditingValue.text, 'abcdef');
      expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6));

      state.updateEditingValue(const TextEditingValue(text: 'abcdef'));
      expect(state.currentTextEditingValue.text, 'abcdef');
      expect(state.currentTextEditingValue.composing, TextRange.empty);
    });

    testWidgets('using enforced.', (WidgetTester tester) async {
      const MaxLengthEnforcement enforcement = MaxLengthEnforcement.enforced;

      await setupWidget(tester, enforcement);

      final EditableTextState state = tester.state(find.byType(EditableText));

      state.updateEditingValue(const TextEditingValue(text: 'abc'));
      expect(state.currentTextEditingValue.text, 'abc');
      expect(state.currentTextEditingValue.composing, TextRange.empty);

      state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5)));
      expect(state.currentTextEditingValue.text, 'abcde');
      expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));

      state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6)));
      expect(state.currentTextEditingValue.text, 'abcde');
      expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));

      state.updateEditingValue(const TextEditingValue(text: 'abcdef'));
      expect(state.currentTextEditingValue.text, 'abcde');
      expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
    });

    testWidgets('using truncateAfterCompositionEnds.', (WidgetTester tester) async {
      const MaxLengthEnforcement enforcement = MaxLengthEnforcement.truncateAfterCompositionEnds;

      await setupWidget(tester, enforcement);

      final EditableTextState state = tester.state(find.byType(EditableText));

      state.updateEditingValue(const TextEditingValue(text: 'abc'));
      expect(state.currentTextEditingValue.text, 'abc');
      expect(state.currentTextEditingValue.composing, TextRange.empty);

      state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5)));
      expect(state.currentTextEditingValue.text, 'abcde');
      expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));

      state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6)));
      expect(state.currentTextEditingValue.text, 'abcdef');
      expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6));

      state.updateEditingValue(const TextEditingValue(text: 'abcdef'));
      expect(state.currentTextEditingValue.text, 'abcde');
      expect(state.currentTextEditingValue.composing, TextRange.empty);
    });

    testWidgets('using default behavior for different platforms.', (WidgetTester tester) async {
      await setupWidget(tester, null);

      final EditableTextState state = tester.state(find.byType(EditableText));

      state.updateEditingValue(const TextEditingValue(text: '侬好啊'));
      expect(state.currentTextEditingValue.text, '侬好啊');
      expect(state.currentTextEditingValue.composing, TextRange.empty);

      state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友', composing: TextRange(start: 3, end: 5)));
      expect(state.currentTextEditingValue.text, '侬好啊旁友');
      expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));

      state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友们', composing: TextRange(start: 3, end: 6)));
      if (kIsWeb ||
        defaultTargetPlatform == TargetPlatform.iOS ||
        defaultTargetPlatform == TargetPlatform.macOS ||
        defaultTargetPlatform == TargetPlatform.linux ||
        defaultTargetPlatform == TargetPlatform.fuchsia
      ) {
        expect(state.currentTextEditingValue.text, '侬好啊旁友们');
        expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6));
      } else {
        expect(state.currentTextEditingValue.text, '侬好啊旁友');
        expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5));
      }

      state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友'));
      expect(state.currentTextEditingValue.text, '侬好啊旁友');
      expect(state.currentTextEditingValue.composing, TextRange.empty);
    });
  });

  testWidgets('disabled widget changes background color', (WidgetTester tester) async {
    await tester.pumpWidget(
      const CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            enabled: false,
          ),
        ),
      ),
    );

    BoxDecoration decoration = tester
        .widget<DecoratedBox>(
          find.descendant(
            of: find.byType(CupertinoTextField),
            matching: find.byType(DecoratedBox),
          ),
        )
        .decoration as BoxDecoration;

    expect(
      decoration.color!.value,
      0xFFFAFAFA,
    );

    await tester.pumpWidget(
      const CupertinoApp(
        home: Center(
          child: CupertinoTextField(),
        ),
      ),
    );

    decoration = tester
        .widget<DecoratedBox>(
          find.descendant(
            of: find.byType(CupertinoTextField),
            matching: find.byType(DecoratedBox),
          ),
        )
        .decoration as BoxDecoration;

    expect(
      decoration.color!.value,
      CupertinoColors.white.value,
    );

    await tester.pumpWidget(
      const CupertinoApp(
        theme: CupertinoThemeData(
          brightness: Brightness.dark,
        ),
        home: Center(
          child: CupertinoTextField(
            enabled: false,
          ),
        ),
      ),
    );

    decoration = tester
        .widget<DecoratedBox>(
          find.descendant(
            of: find.byType(CupertinoTextField),
            matching: find.byType(DecoratedBox),
          ),
        )
        .decoration as BoxDecoration;

    expect(
      decoration.color!.value,
      0xFF050505,
    );
  });

  // Regression test for https://github.com/flutter/flutter/issues/78097.
  testWidgets(
    'still gets disabled background color when decoration is null',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              decoration: null,
              enabled: false,
            ),
          ),
        ),
      );

      final Color disabledColor = tester.widget<ColoredBox>(
        find.descendant(
          of: find.byType(CupertinoTextField),
          matching: find.byType(ColoredBox),
        ),
      ).color;
      expect(disabledColor, isSameColorAs(const Color(0xFFFAFAFA)));
    },
  );

  testWidgets('autofill info has placeholder text', (WidgetTester tester) async {
    await tester.pumpWidget(
      const CupertinoApp(
        home: CupertinoTextField(
          placeholder: 'placeholder text',
        ),
      ),
    );
    await tester.tap(find.byType(CupertinoTextField));

    expect(
      tester.testTextInput.setClientArgs?['autofill'],
      containsPair('hintText', 'placeholder text'),
    );
  });

  testWidgets('textDirection is passed to EditableText', (WidgetTester tester) async {
    await tester.pumpWidget(
      const CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            textDirection: TextDirection.ltr,
          ),
        ),
      ),
    );

    final EditableText ltrWidget = tester.widget(find.byType(EditableText));
    expect(ltrWidget.textDirection, TextDirection.ltr);

    await tester.pumpWidget(
      const CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            textDirection: TextDirection.rtl,
          ),
        ),
      ),
    );

    final EditableText rtlWidget = tester.widget(find.byType(EditableText));
    expect(rtlWidget.textDirection, TextDirection.rtl);
  });

  testWidgets('clipBehavior has expected defaults', (WidgetTester tester) async {
    await tester.pumpWidget(
      const CupertinoApp(
        home: CupertinoTextField(
        ),
      ),
    );

    final CupertinoTextField textField = tester.firstWidget(find.byType(CupertinoTextField));
    expect(textField.clipBehavior, Clip.hardEdge);
  });

  testWidgets('Overflow clipBehavior none golden', (WidgetTester tester) async {
    final OverflowWidgetTextEditingController controller = OverflowWidgetTextEditingController();
    addTearDown(controller.dispose);
    final Widget widget = CupertinoApp(
      home: RepaintBoundary(
        key: const ValueKey<int>(1),
        child: SizedBox(
          height: 200.0,
          width: 200.0,
          child: Center(
            child: SizedBox(
              // Make sure the input field is not high enough for the WidgetSpan.
              height: 50,
              child: CupertinoTextField(
                controller: controller,
                clipBehavior: Clip.none,
              ),
            ),
          ),
        ),
      ),
    );
    await tester.pumpWidget(widget);

    final CupertinoTextField textField = tester.firstWidget(find.byType(CupertinoTextField));
    expect(textField.clipBehavior, Clip.none);

    final EditableText editableText = tester.firstWidget(find.byType(EditableText));
    expect(editableText.clipBehavior, Clip.none);

    await expectLater(
      find.byKey(const ValueKey<int>(1)),
      matchesGoldenFile('overflow_clipbehavior_none.cupertino.0.png'),
    );
  });

  testWidgets('can shift + tap to select with a keyboard (Apple platforms)', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    await tester.tapAt(textOffsetToPosition(tester, 13));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 13);
    expect(controller.selection.extentOffset, 13);

    await tester.pump(kDoubleTapTimeout);
    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    await tester.tapAt(textOffsetToPosition(tester, 20));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 13);
    expect(controller.selection.extentOffset, 20);

    await tester.pump(kDoubleTapTimeout);
    await tester.tapAt(textOffsetToPosition(tester, 23));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 13);
    expect(controller.selection.extentOffset, 23);

    await tester.pump(kDoubleTapTimeout);
    await tester.tapAt(textOffsetToPosition(tester, 4));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 4);

    await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 4);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets('can shift + tap to select with a keyboard (non-Apple platforms)', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    await tester.tapAt(textOffsetToPosition(tester, 13));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 13);
    expect(controller.selection.extentOffset, 13);

    await tester.pump(kDoubleTapTimeout);
    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    await tester.tapAt(textOffsetToPosition(tester, 20));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 13);
    expect(controller.selection.extentOffset, 20);

    await tester.pump(kDoubleTapTimeout);
    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    await tester.tapAt(textOffsetToPosition(tester, 23));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 13);
    expect(controller.selection.extentOffset, 23);

    await tester.pump(kDoubleTapTimeout);
    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    await tester.tapAt(textOffsetToPosition(tester, 4));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 13);
    expect(controller.selection.extentOffset, 4);

    await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
    expect(controller.selection.baseOffset, 13);
    expect(controller.selection.extentOffset, 4);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));

  testWidgets('shift tapping an unfocused field', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    addTearDown(controller.dispose);
    final FocusNode focusNode = FocusNode();
    addTearDown(focusNode.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
            focusNode: focusNode,
          ),
        ),
      ),
    );

    expect(focusNode.hasFocus, isFalse);

    // Put the cursor at the end of the field.
    await tester.tapAt(textOffsetToPosition(tester, controller.text.length));
    await tester.pump(kDoubleTapTimeout);
    await tester.pumpAndSettle();
    expect(focusNode.hasFocus, isTrue);
    expect(controller.selection.baseOffset, 35);
    expect(controller.selection.extentOffset, 35);

    // Unfocus the field, but the selection remains.
    focusNode.unfocus();
    await tester.pumpAndSettle();
    expect(focusNode.hasFocus, isFalse);
    expect(controller.selection.baseOffset, 35);
    expect(controller.selection.extentOffset, 35);

    // Shift tap in the middle of the field.
    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    await tester.tapAt(textOffsetToPosition(tester, 20));
    await tester.pumpAndSettle();
    expect(focusNode.hasFocus, isTrue);
    switch (defaultTargetPlatform) {
      // Apple platforms start the selection from 0.
      case TargetPlatform.iOS:
      case TargetPlatform.macOS:
        expect(controller.selection.baseOffset, 0);

      // Other platforms start from the previous selection.
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        expect(controller.selection.baseOffset, 35);
    }
    expect(controller.selection.extentOffset, 20);
  }, variant: TargetPlatformVariant.all());

  testWidgets('can shift + tap + drag to select with a keyboard (Apple platforms)', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    addTearDown(controller.dispose);
    final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    await tester.tapAt(textOffsetToPosition(tester, 8));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 8);

    await tester.pump(kDoubleTapTimeout);
    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    final TestGesture gesture =
        await tester.startGesture(
          textOffsetToPosition(tester, 23),
          pointer: 7,
          kind: PointerDeviceKind.mouse,
        );
    await tester.pumpAndSettle();
    if (isTargetPlatformIOS) {
      await gesture.up();
      // Not a double tap + drag.
      await tester.pumpAndSettle(kDoubleTapTimeout);
    }
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 23);

    // Expand the selection a bit.
    if (isTargetPlatformIOS) {
      await gesture.down(textOffsetToPosition(tester, 24));
      await tester.pumpAndSettle();
    }
    await gesture.moveTo(textOffsetToPosition(tester, 28));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 28);

    // Move back to the original selection.
    await gesture.moveTo(textOffsetToPosition(tester, 23));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 23);

    // Collapse the selection.
    await gesture.moveTo(textOffsetToPosition(tester, 8));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 8);

    // Invert the selection. The base jumps to the original extent.
    await gesture.moveTo(textOffsetToPosition(tester, 7));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 7);

    // Continuing to move in the inverted direction expands the selection.
    await gesture.moveTo(textOffsetToPosition(tester, 4));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 4);

    // Move back to the original base.
    await gesture.moveTo(textOffsetToPosition(tester, 8));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 8);

    // Continue to move past the original base, which will cause the selection
    // to invert back to the original orientation.
    await gesture.moveTo(textOffsetToPosition(tester, 9));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 9);

    // Continuing to select in this direction selects just like it did
    // originally.
    await gesture.moveTo(textOffsetToPosition(tester, 24));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 24);

    // Releasing the shift key has no effect; the selection continues as the
    // mouse continues to move.
    await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 24);
    await gesture.moveTo(textOffsetToPosition(tester, 26));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 26);

    await gesture.up();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 26);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets('can shift + tap + drag to select with a keyboard (non-Apple platforms)', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    addTearDown(controller.dispose);
    final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android
        || defaultTargetPlatform == TargetPlatform.fuchsia;
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    await tester.tapAt(textOffsetToPosition(tester, 8));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 8);

    await tester.pump(kDoubleTapTimeout);
    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    final TestGesture gesture =
        await tester.startGesture(
          textOffsetToPosition(tester, 23),
          pointer: 7,
          kind: PointerDeviceKind.mouse,
        );
    await tester.pumpAndSettle();
    if (isTargetPlatformMobile) {
      await gesture.up();
      // Not a double tap + drag.
      await tester.pumpAndSettle(kDoubleTapTimeout);
    }
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 23);

    // Expand the selection a bit.
    if (isTargetPlatformMobile) {
      await gesture.down(textOffsetToPosition(tester, 24));
      await tester.pumpAndSettle();
    }
    await gesture.moveTo(textOffsetToPosition(tester, 28));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 28);

    // Move back to the original selection.
    await gesture.moveTo(textOffsetToPosition(tester, 23));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 23);

    // Collapse the selection.
    await gesture.moveTo(textOffsetToPosition(tester, 8));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 8);

    // Invert the selection. The original selection is not restored like on iOS
    // and Mac.
    await gesture.moveTo(textOffsetToPosition(tester, 7));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 7);

    // Continuing to move in the inverted direction expands the selection.
    await gesture.moveTo(textOffsetToPosition(tester, 4));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 4);

    // Move back to the original base.
    await gesture.moveTo(textOffsetToPosition(tester, 8));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 8);

    // Continue to move past the original base.
    await gesture.moveTo(textOffsetToPosition(tester, 9));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 9);

    // Continuing to select in this direction selects just like it did
    // originally.
    await gesture.moveTo(textOffsetToPosition(tester, 24));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 24);

    // Releasing the shift key has no effect; the selection continues as the
    // mouse continues to move.
    await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 24);
    await gesture.moveTo(textOffsetToPosition(tester, 26));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 26);

    await gesture.up();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 26);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux,  TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));

  testWidgets('can shift + tap + drag to select with a keyboard, reversed (Apple platforms)', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    addTearDown(controller.dispose);
    final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS;
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    // Make a selection from right to left.
    await tester.tapAt(textOffsetToPosition(tester, 23));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 23);
    await tester.pump(kDoubleTapTimeout);
    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    final TestGesture gesture =
        await tester.startGesture(
          textOffsetToPosition(tester, 8),
          pointer: 7,
          kind: PointerDeviceKind.mouse,
        );
    await tester.pumpAndSettle();
    if (isTargetPlatformIOS) {
      await gesture.up();
      // Not a double tap + drag.
      await tester.pumpAndSettle(kDoubleTapTimeout);
    }
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 8);

    // Expand the selection a bit.
    if (isTargetPlatformIOS) {
      await gesture.down(textOffsetToPosition(tester, 7));
      await tester.pumpAndSettle();
    }
    await gesture.moveTo(textOffsetToPosition(tester, 5));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 5);

    // Move back to the original selection.
    await gesture.moveTo(textOffsetToPosition(tester, 8));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 8);

    // Collapse the selection.
    await gesture.moveTo(textOffsetToPosition(tester, 23));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 23);

    // Invert the selection. The base jumps to the original extent.
    await gesture.moveTo(textOffsetToPosition(tester, 24));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 24);

    // Continuing to move in the inverted direction expands the selection.
    await gesture.moveTo(textOffsetToPosition(tester, 27));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 27);

    // Move back to the original base.
    await gesture.moveTo(textOffsetToPosition(tester, 23));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 23);

    // Continue to move past the original base, which will cause the selection
    // to invert back to the original orientation.
    await gesture.moveTo(textOffsetToPosition(tester, 22));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 22);

    // Continuing to select in this direction selects just like it did
    // originally.
    await gesture.moveTo(textOffsetToPosition(tester, 16));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 16);

    // Releasing the shift key has no effect; the selection continues as the
    // mouse continues to move.
    await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 16);
    await gesture.moveTo(textOffsetToPosition(tester, 14));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 14);

    await gesture.up();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 14);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets('can shift + tap + drag to select with a keyboard, reversed (non-Apple platforms)', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    addTearDown(controller.dispose);
    final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android
        || defaultTargetPlatform == TargetPlatform.fuchsia;
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    // Make a selection from right to left.
    await tester.tapAt(textOffsetToPosition(tester, 23));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 23);
    await tester.pump(kDoubleTapTimeout);
    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    final TestGesture gesture =
        await tester.startGesture(
          textOffsetToPosition(tester, 8),
          pointer: 7,
          kind: PointerDeviceKind.mouse,
        );
    await tester.pumpAndSettle();
    if (isTargetPlatformMobile) {
      await gesture.up();
      // Not a double tap + drag.
      await tester.pumpAndSettle(kDoubleTapTimeout);
    }
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 8);

    // Expand the selection a bit.
    if (isTargetPlatformMobile) {
      await gesture.down(textOffsetToPosition(tester, 7));
      await tester.pumpAndSettle();
    }
    await gesture.moveTo(textOffsetToPosition(tester, 5));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 5);

    // Move back to the original selection.
    await gesture.moveTo(textOffsetToPosition(tester, 8));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 8);

    // Collapse the selection.
    await gesture.moveTo(textOffsetToPosition(tester, 23));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 23);

    // Invert the selection. The selection is not restored like it would be on
    // iOS and Mac.
    await gesture.moveTo(textOffsetToPosition(tester, 24));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 24);

    // Continuing to move in the inverted direction expands the selection.
    await gesture.moveTo(textOffsetToPosition(tester, 27));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 27);

    // Move back to the original base.
    await gesture.moveTo(textOffsetToPosition(tester, 23));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 23);

    // Continue to move past the original base.
    await gesture.moveTo(textOffsetToPosition(tester, 22));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 22);

    // Continuing to select in this direction selects just like it did
    // originally.
    await gesture.moveTo(textOffsetToPosition(tester, 16));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 16);

    // Releasing the shift key has no effect; the selection continues as the
    // mouse continues to move.
    await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 16);
    await gesture.moveTo(textOffsetToPosition(tester, 14));
    await tester.pumpAndSettle();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 14);

    await gesture.up();
    expect(controller.selection.baseOffset, 23);
    expect(controller.selection.extentOffset, 14);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux,  TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));

  // Regression test for https://github.com/flutter/flutter/issues/101587.
  testWidgets('Right clicking menu behavior', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'blah1 blah2',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    // Initially, the menu is not shown and there is no selection.
    expect(find.byType(CupertinoButton), findsNothing);
    expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));

    final Offset midBlah1 = textOffsetToPosition(tester, 2);
    final Offset midBlah2 = textOffsetToPosition(tester, 8);

    // Right click the second word.
    final TestGesture gesture = await tester.startGesture(
      midBlah2,
      kind: PointerDeviceKind.mouse,
      buttons: kSecondaryMouseButton,
    );
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle();

    switch (defaultTargetPlatform) {
      case TargetPlatform.iOS:
      case TargetPlatform.macOS:
        expect(controller.selection, const TextSelection(baseOffset: 6, extentOffset: 11));
        expect(find.text('Cut'), findsOneWidget);
        expect(find.text('Copy'), findsOneWidget);
        expect(find.text('Paste'), findsOneWidget);

      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        expect(controller.selection, const TextSelection.collapsed(offset: 8));
        expect(find.text('Cut'), findsNothing);
        expect(find.text('Copy'), findsNothing);
        expect(find.text('Paste'), findsOneWidget);
    }

    // Right click the first word.
    await gesture.down(midBlah1);
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle();

    switch (defaultTargetPlatform) {
      case TargetPlatform.iOS:
      case TargetPlatform.macOS:
        expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
        expect(find.text('Cut'), findsOneWidget);
        expect(find.text('Copy'), findsOneWidget);
        expect(find.text('Paste'), findsOneWidget);

      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        expect(controller.selection, const TextSelection.collapsed(offset: 8));
        expect(find.text('Cut'), findsNothing);
        expect(find.text('Copy'), findsNothing);
        expect(find.text('Paste'), findsNothing);
    }
  },
    variant: TargetPlatformVariant.all(),
    skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
  );

  group('Right click focus', () {
    testWidgets('Can right click to focus multiple times', (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/pull/103228
      final FocusNode focusNode1 = FocusNode();
      final FocusNode focusNode2 = FocusNode();
      addTearDown(focusNode1.dispose);
      addTearDown(focusNode2.dispose);
      final UniqueKey key1 = UniqueKey();
      final UniqueKey key2 = UniqueKey();
      await tester.pumpWidget(
        CupertinoApp(
          home: Column(
            children: <Widget>[
              CupertinoTextField(
                key: key1,
                focusNode: focusNode1,
              ),
              // This spacer prevents the context menu in one field from
              // overlapping with the other field.
              const SizedBox(height: 100.0),
              CupertinoTextField(
                key: key2,
                focusNode: focusNode2,
              ),
            ],
          ),
        ),
      );

      // Interact with the field to establish the input connection.
      await tester.tapAt(
        tester.getCenter(find.byKey(key1)),
        buttons: kSecondaryMouseButton,
      );
      await tester.pump();

      expect(focusNode1.hasFocus, isTrue);
      expect(focusNode2.hasFocus, isFalse);

      await tester.tapAt(
        tester.getCenter(find.byKey(key2)),
        buttons: kSecondaryMouseButton,
      );
      await tester.pump();

      expect(focusNode1.hasFocus, isFalse);
      expect(focusNode2.hasFocus, isTrue);

      await tester.tapAt(
        tester.getCenter(find.byKey(key1)),
        buttons: kSecondaryMouseButton,
      );
      await tester.pump();

      expect(focusNode1.hasFocus, isTrue);
      expect(focusNode2.hasFocus, isFalse);
    });

    testWidgets('Can right click to focus on previously selected word on Apple platforms', (WidgetTester tester) async {
      final FocusNode focusNode1 = FocusNode();
      final FocusNode focusNode2 = FocusNode();
      addTearDown(focusNode1.dispose);
      addTearDown(focusNode2.dispose);
      final TextEditingController controller = TextEditingController(
        text: 'first second',
      );
      addTearDown(controller.dispose);
      final UniqueKey key1 = UniqueKey();
      await tester.pumpWidget(
        CupertinoApp(
          home: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              CupertinoTextField(
                key: key1,
                controller: controller,
                focusNode: focusNode1,
              ),
              Focus(
                focusNode: focusNode2,
                child: const Text('focusable'),
              ),
            ],
          ),
        ),
      );

      // Interact with the field to establish the input connection.
      await tester.tapAt(
        tester.getCenter(find.byKey(key1)),
        buttons: kSecondaryMouseButton,
      );
      await tester.pump();

      expect(focusNode1.hasFocus, isTrue);
      expect(focusNode2.hasFocus, isFalse);

      // Select the second word.
      controller.selection = const TextSelection(
        baseOffset: 6,
        extentOffset: 12,
      );
      await tester.pump();

      expect(focusNode1.hasFocus, isTrue);
      expect(focusNode2.hasFocus, isFalse);
      expect(controller.selection.isCollapsed, isFalse);
      expect(controller.selection.baseOffset, 6);
      expect(controller.selection.extentOffset, 12);

      // Unfocus the first field.
      focusNode2.requestFocus();
      await tester.pumpAndSettle();

      expect(focusNode1.hasFocus, isFalse);
      expect(focusNode2.hasFocus, isTrue);

      // Right click the second word in the first field, which is still selected
      // even though the selection is not visible.
      await tester.tapAt(
        textOffsetToPosition(tester, 8),
        buttons: kSecondaryMouseButton,
      );
      await tester.pump();

      expect(focusNode1.hasFocus, isTrue);
      expect(focusNode2.hasFocus, isFalse);
      expect(controller.selection.baseOffset, 6);
      expect(controller.selection.extentOffset, 12);

      // Select everything.
      controller.selection = const TextSelection(
        baseOffset: 0,
        extentOffset: 12,
      );
      await tester.pump();

      expect(focusNode1.hasFocus, isTrue);
      expect(focusNode2.hasFocus, isFalse);
      expect(controller.selection.baseOffset, 0);
      expect(controller.selection.extentOffset, 12);

      // Unfocus the first field.
      focusNode2.requestFocus();
      await tester.pumpAndSettle();

      // Right click the first word in the first field.
      await tester.tapAt(
        textOffsetToPosition(tester, 2),
        buttons: kSecondaryMouseButton,
      );
      await tester.pump();

      expect(focusNode1.hasFocus, isTrue);
      expect(focusNode2.hasFocus, isFalse);
      expect(controller.selection.baseOffset, 0);
      expect(controller.selection.extentOffset, 5);
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
  });

  group('context menu', () {
    testWidgets('builds CupertinoAdaptiveTextSelectionToolbar by default', (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(text: '');
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              CupertinoTextField(
                controller: controller,
              ),
            ],
          ),
        ),
      );

      await tester.pump(); // Wait for autofocus to take effect.

      expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsNothing);

      // Long-press to bring up the context menu.
      final Finder textFinder = find.byType(EditableText);
      await tester.longPress(textFinder);
      tester.state<EditableTextState>(textFinder).showToolbar();
      await tester.pumpAndSettle();

      expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsOneWidget);
    },
      skip: kIsWeb, // [intended] on web the browser handles the context menu.
    );

    testWidgets('contextMenuBuilder is used in place of the default text selection toolbar', (WidgetTester tester) async {
      final GlobalKey key = GlobalKey();
      final TextEditingController controller = TextEditingController(text: '');
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              CupertinoTextField(
                controller: controller,
                contextMenuBuilder: (
                  BuildContext context,
                  EditableTextState editableTextState,
                ) {
                  return Placeholder(key: key);
                },
              ),
            ],
          ),
        ),
      );

      await tester.pump(); // Wait for autofocus to take effect.

      expect(find.byKey(key), findsNothing);

      // Long-press to bring up the context menu.
      final Finder textFinder = find.byType(EditableText);
      await tester.longPress(textFinder);
      tester.state<EditableTextState>(textFinder).showToolbar();
      await tester.pumpAndSettle();

      expect(find.byKey(key), findsOneWidget);
    },
      skip: kIsWeb, // [intended] on web the browser handles the context menu.
    );
  });

  group('magnifier', () {
    late ValueNotifier<MagnifierInfo> magnifierInfo;
    final Widget fakeMagnifier = Container(key: UniqueKey());

    group('magnifier builder', () {
      testWidgets('should build custom magnifier if given', (WidgetTester tester) async {
        final Widget customMagnifier = Container(
          key: UniqueKey(),
        );
        final CupertinoTextField defaultCupertinoTextField = CupertinoTextField(
          magnifierConfiguration: TextMagnifierConfiguration(magnifierBuilder: (_, __, ___) => customMagnifier),
        );

        await tester.pumpWidget(const CupertinoApp(
          home: Placeholder(),
        ));

        final BuildContext context =
            tester.firstElement(find.byType(Placeholder));

        final ValueNotifier<MagnifierInfo> magnifierInfo = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
        addTearDown(magnifierInfo.dispose);
        expect(
            defaultCupertinoTextField.magnifierConfiguration!.magnifierBuilder(
                context,
                MagnifierController(),
                magnifierInfo,
              ),
            isA<Widget>().having((Widget widget) => widget.key, 'key', equals(customMagnifier.key)));
      });

      group('defaults', () {
        testWidgets('should build CupertinoMagnifier on iOS and Android', (WidgetTester tester) async {
          await tester.pumpWidget(const CupertinoApp(
            home: CupertinoTextField(),
          ));

        final BuildContext context = tester.firstElement(find.byType(CupertinoTextField));
        final EditableText editableText = tester.widget(find.byType(EditableText));

          final ValueNotifier<MagnifierInfo> magnifierInfo = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
          addTearDown(magnifierInfo.dispose);
          expect(
              editableText.magnifierConfiguration.magnifierBuilder(
                  context,
                  MagnifierController(),
                  magnifierInfo,
                ),
              isA<CupertinoTextMagnifier>());
        },
            variant: const TargetPlatformVariant(
                <TargetPlatform>{TargetPlatform.iOS, TargetPlatform.android}));
      });

      testWidgets('should build nothing on all platforms but iOS and Android', (WidgetTester tester) async {
        await tester.pumpWidget(const CupertinoApp(
          home: CupertinoTextField(),
        ));

        final BuildContext context = tester.firstElement(find.byType(CupertinoTextField));
        final EditableText editableText = tester.widget(find.byType(EditableText));

        final ValueNotifier<MagnifierInfo> magnifierInfo = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
        addTearDown(magnifierInfo.dispose);
        expect(
            editableText.magnifierConfiguration.magnifierBuilder(
                context,
                MagnifierController(),
                magnifierInfo,
              ),
            isNull);
      },
          variant: TargetPlatformVariant.all(
              excluding: <TargetPlatform>{TargetPlatform.iOS, TargetPlatform.android}));
    });

    testWidgets('Can drag handles to show, unshow, and update magnifier',
        (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController();
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: CupertinoPageScaffold(
            child: Builder(
              builder: (BuildContext context) => CupertinoTextField(
                dragStartBehavior: DragStartBehavior.down,
                controller: controller,
                magnifierConfiguration: TextMagnifierConfiguration(
                    magnifierBuilder: (_,
                        MagnifierController controller,
                        ValueNotifier<MagnifierInfo>
                            localMagnifierInfo) {
                  magnifierInfo = localMagnifierInfo;
                  return fakeMagnifier;
                }),
              ),
            ),
          ),
        ),
      );

      const String testValue = 'abc def ghi';
      await tester.enterText(find.byType(CupertinoTextField), testValue);

      // Double tap the 'e' to select 'def'.
      await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
      await tester.pump(const Duration(milliseconds: 30));
      await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
      await tester.pump(const Duration(milliseconds: 30));

      final TextSelection selection = controller.selection;

      final RenderEditable renderEditable = findRenderEditable(tester);
      final List<TextSelectionPoint> endpoints = globalize(
        renderEditable.getEndpointsForSelection(selection),
        renderEditable,
      );

      // Drag the right handle 2 letters to the right.
      final Offset handlePos = endpoints.last.point + const Offset(1.0, 1.0);
      final TestGesture gesture =
          await tester.startGesture(handlePos, pointer: 7);

      Offset? firstDragGesturePosition;

      await gesture.moveTo(textOffsetToPosition(tester, testValue.length - 2));
      await tester.pump();

      expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
      firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;

      await gesture.moveTo(textOffsetToPosition(tester, testValue.length));
      await tester.pump();

      // Expect the position the magnifier gets to have moved.
      expect(firstDragGesturePosition,
          isNot(magnifierInfo.value.globalGesturePosition));

      await gesture.up();
      await tester.pump();

      expect(find.byKey(fakeMagnifier.key!), findsNothing);
    }, variant: TargetPlatformVariant.only(TargetPlatform.iOS));

    testWidgets('Can drag to show, unshow, and update magnifier', (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController();
      addTearDown(controller.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              dragStartBehavior: DragStartBehavior.down,
              controller: controller,
              magnifierConfiguration: TextMagnifierConfiguration(
                magnifierBuilder: (
                    _,
                    MagnifierController controller,
                    ValueNotifier<MagnifierInfo> localMagnifierInfo
                  ) {
                    magnifierInfo = localMagnifierInfo;
                    return fakeMagnifier;
                  },
              ),
            ),
          ),
        ),
      );

      const String testValue = 'abc def ghi';
      await tester.enterText(find.byType(CupertinoTextField), testValue);
      await tester.pumpAndSettle();

      // Tap at '|a' to move the selection to position 0.
      await tester.tapAt(textOffsetToPosition(tester, 0));
      await tester.pumpAndSettle(kDoubleTapTimeout);
      expect(controller.selection.isCollapsed, true);
      expect(controller.selection.baseOffset, 0);
      expect(find.byKey(fakeMagnifier.key!), findsNothing);

      // Start a drag gesture to move the selection to the dragged position, showing
      // the magnifier.
      final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, 0));
      await tester.pump();

      await gesture.moveTo(textOffsetToPosition(tester, 5));
      await tester.pump();
      expect(controller.selection.isCollapsed, true);
      expect(controller.selection.baseOffset, 5);
      expect(find.byKey(fakeMagnifier.key!), findsOneWidget);

      Offset firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;

      await gesture.moveTo(textOffsetToPosition(tester, 10));
      await tester.pump();
      expect(controller.selection.isCollapsed, true);
      expect(controller.selection.baseOffset, 10);
      expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
      // Expect the position the magnifier gets to have moved.
      expect(firstDragGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));

      // The magnifier should hide when the drag ends.
      await gesture.up();
      await tester.pump();
      expect(controller.selection.isCollapsed, true);
      expect(controller.selection.baseOffset, 10);
      expect(find.byKey(fakeMagnifier.key!), findsNothing);

      // Start a double-tap select the word at the tapped position.
      await gesture.down(textOffsetToPosition(tester, 1));
      await tester.pump();
      await gesture.up();
      await tester.pump();

      await gesture.down(textOffsetToPosition(tester, 1));
      await tester.pumpAndSettle();
      expect(controller.selection.baseOffset, 0);
      expect(controller.selection.extentOffset, 3);

      // Start a drag gesture to extend the selection word-by-word, showing the
      // magnifier.
      await gesture.moveTo(textOffsetToPosition(tester, 5));
      await tester.pump();
      expect(controller.selection.baseOffset, 0);
      expect(controller.selection.extentOffset, 7);
      expect(find.byKey(fakeMagnifier.key!), findsOneWidget);

      firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;

      await gesture.moveTo(textOffsetToPosition(tester, 10));
      await tester.pump();
      expect(controller.selection.baseOffset, 0);
      expect(controller.selection.extentOffset, 11);
      expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
      // Expect the position the magnifier gets to have moved.
      expect(firstDragGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));

      // The magnifier should hide when the drag ends.
      await gesture.up();
      await tester.pump();
      expect(controller.selection.baseOffset, 0);
      expect(controller.selection.extentOffset, 11);
      expect(find.byKey(fakeMagnifier.key!), findsNothing);
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }));

    testWidgets('Can long press to show, unshow, and update magnifier on non-Apple platforms', (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController();
      addTearDown(controller.dispose);
      final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              dragStartBehavior: DragStartBehavior.down,
              controller: controller,
              magnifierConfiguration: TextMagnifierConfiguration(
                magnifierBuilder: (
                    _,
                    MagnifierController controller,
                    ValueNotifier<MagnifierInfo> localMagnifierInfo
                  ) {
                    magnifierInfo = localMagnifierInfo;
                    return fakeMagnifier;
                  },
              ),
            ),
          ),
        ),
      );

      const String testValue = 'abc def ghi';
      await tester.enterText(find.byType(CupertinoTextField), testValue);
      await tester.pumpAndSettle();

      // Tap at 'e' to move the cursor before the 'e'.
      await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
      await tester.pumpAndSettle(const Duration(milliseconds: 300));
      expect(controller.selection.isCollapsed, true);
      expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 4);
      expect(find.byKey(fakeMagnifier.key!), findsNothing);

      // Long press the 'e' to select 'def' and show the magnifier.
      final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, testValue.indexOf('e')));
      await tester.pumpAndSettle(const Duration(milliseconds: 1000));
      expect(controller.selection.baseOffset, 4);
      expect(controller.selection.extentOffset, 7);
      expect(find.byKey(fakeMagnifier.key!), findsOneWidget);

      final Offset firstLongPressGesturePosition = magnifierInfo.value.globalGesturePosition;

      // Move the gesture to 'h' to extend the selection to 'ghi'.
      await gesture.moveTo(textOffsetToPosition(tester, testValue.indexOf('h')));
      await tester.pumpAndSettle();
      expect(controller.selection.baseOffset, 4);
      expect(controller.selection.extentOffset, 11);
      expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
      // Expect the position the magnifier gets to have moved.
      expect(firstLongPressGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));

      // End the long press to hide the magnifier.
      await gesture.up();
      await tester.pumpAndSettle();
      expect(find.byKey(fakeMagnifier.key!), findsNothing);
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }));

    testWidgets('Can long press to show, unshow, and update magnifier on iOS', (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController();
      addTearDown(controller.dispose);
      final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: CupertinoTextField(
              dragStartBehavior: DragStartBehavior.down,
              controller: controller,
              magnifierConfiguration: TextMagnifierConfiguration(
                magnifierBuilder: (
                    _,
                    MagnifierController controller,
                    ValueNotifier<MagnifierInfo> localMagnifierInfo
                  ) {
                    magnifierInfo = localMagnifierInfo;
                    return fakeMagnifier;
                  },
              ),
            ),
          ),
        ),
      );

      const String testValue = 'abc def ghi';
      await tester.enterText(find.byType(CupertinoTextField), testValue);
      await tester.pumpAndSettle();

      // Tap at 'e' to set the selection to position 5 on Android.
      // Tap at 'e' to set the selection to the closest word edge, which is position 4 on iOS.
      await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
      await tester.pumpAndSettle(const Duration(milliseconds: 300));
      expect(controller.selection.isCollapsed, true);
      expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 7);
      expect(find.byKey(fakeMagnifier.key!), findsNothing);

      // Long press the 'e' to move the cursor in front of the 'e' and show the magnifier.
      final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, testValue.indexOf('e')));
      await tester.pumpAndSettle(const Duration(milliseconds: 1000));
      expect(controller.selection.baseOffset, 5);
      expect(controller.selection.extentOffset, 5);
      expect(find.byKey(fakeMagnifier.key!), findsOneWidget);

      final Offset firstLongPressGesturePosition = magnifierInfo.value.globalGesturePosition;

      // Move the gesture to 'h' to update the magnifier and move the cursor to 'h'.
      await gesture.moveTo(textOffsetToPosition(tester, testValue.indexOf('h')));
      await tester.pumpAndSettle();
      expect(controller.selection.baseOffset, 9);
      expect(controller.selection.extentOffset, 9);
      expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
      // Expect the position the magnifier gets to have moved.
      expect(firstLongPressGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));

      // End the long press to hide the magnifier.
      await gesture.up();
      await tester.pumpAndSettle();
      expect(find.byKey(fakeMagnifier.key!), findsNothing);
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
  });

  group('TapRegion integration', () {
    testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async {
      final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
      addTearDown(focusNode.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: SizedBox(
              width: 100,
              height: 100,
              child: CupertinoTextField(
                autofocus: true,
                focusNode: focusNode,
              ),
            ),
          ),
        ),
      );
      await tester.pump();
      expect(focusNode.hasPrimaryFocus, isTrue);

      // Tap outside the border.
      await tester.tapAt(const Offset(10, 10));
      await tester.pump();

      expect(focusNode.hasPrimaryFocus, isFalse);
    }, variant: TargetPlatformVariant.desktop());

    testWidgets("Tapping outside doesn't lose focus on mobile", (WidgetTester tester) async {
      final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
      addTearDown(focusNode.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: SizedBox(
              width: 100,
              height: 100,
              child: CupertinoTextField(
                autofocus: true,
                focusNode: focusNode,
              ),
            ),
          ),
        ),
      );
      await tester.pump();
      expect(focusNode.hasPrimaryFocus, isTrue);

      // Tap just outside the border, but not inside the EditableText.
      await tester.tapAt(const Offset(10, 10));
      await tester.pump();

      // Focus is lost on mobile browsers, but not mobile apps.
      expect(focusNode.hasPrimaryFocus, kIsWeb ? isFalse : isTrue);
    }, variant: TargetPlatformVariant.mobile());

    testWidgets("tapping on toolbar doesn't lose focus", (WidgetTester tester) async {
      final TextEditingController controller;
      final EditableTextState state;

      controller = TextEditingController(text: 'A B C');
      addTearDown(controller.dispose);
      final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
      addTearDown(focusNode.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          debugShowCheckedModeBanner: false,
          home: CupertinoPageScaffold(
            child: Align(
              child: SizedBox(
                width: 200,
                height: 200,
                child: CupertinoTextField(
                  autofocus: true,
                  focusNode: focusNode,
                  controller: controller,
                ),
              ),
            ),
          ),
        ),
      );
      await tester.pump();
      expect(focusNode.hasPrimaryFocus, isTrue);

      state = tester.state<EditableTextState>(find.byType(EditableText));

      // Select the first 2 words.
      state.renderEditable.selectPositionAt(
        from: textOffsetToPosition(tester, 0),
        to: textOffsetToPosition(tester, 2),
        cause: SelectionChangedCause.tap,
      );

      final Offset midSelection = textOffsetToPosition(tester, 2);

      // Right click the selection.
      final TestGesture gesture = await tester.startGesture(
        midSelection,
        kind: PointerDeviceKind.mouse,
        buttons: kSecondaryMouseButton,
      );
      await tester.pump();
      await gesture.up();
      await tester.pumpAndSettle();

      expect(find.text('Copy'), findsOneWidget);

      // Copy the first word.
      await tester.tap(find.text('Copy'));
      await tester.pump();
      expect(focusNode.hasPrimaryFocus, isTrue);
    },
      variant: TargetPlatformVariant.all(),
      skip: kIsWeb, // [intended] The toolbar isn't rendered by Flutter on the web, it's rendered by the browser.
    );

    testWidgets("Tapping on border doesn't lose focus",
        (WidgetTester tester) async {
      final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
      addTearDown(focusNode.dispose);
      await tester.pumpWidget(
        CupertinoApp(
          home: Center(
            child: SizedBox(
              width: 100,
              height: 100,
              child: CupertinoTextField(
                autofocus: true,
                focusNode: focusNode,
              ),
            ),
          ),
        ),
      );
      await tester.pump();
      expect(focusNode.hasPrimaryFocus, isTrue);

      final Rect borderBox = tester.getRect(find.byType(CupertinoTextField));
      // Tap just inside the border, but not inside the EditableText.
      await tester.tapAt(borderBox.topLeft + const Offset(1, 1));
      await tester.pump();

      expect(focusNode.hasPrimaryFocus, isTrue);
    }, variant: TargetPlatformVariant.all());
  });

  testWidgets('Can drag handles to change selection correctly in multiline', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();
    addTearDown(controller.dispose);

    await tester.pumpWidget(
      CupertinoApp(
        debugShowCheckedModeBanner: false,
        home: CupertinoPageScaffold(
          child: CupertinoTextField(
            dragStartBehavior: DragStartBehavior.down,
            controller: controller,
            style: const TextStyle(color: Colors.black, fontSize: 34.0),
            maxLines: 3,
          ),
        ),
      ),
    );

    const String testValue =
        'First line of text is\n'
        'Second line goes until\n'
        'Third line of stuff';

    const String cutValue =
        'First line of text is\n'
        'Second until\n'
        'Third line of stuff';
    await tester.enterText(find.byType(CupertinoTextField), testValue);

    // Skip past scrolling animation.
    await tester.pump();
    await tester.pumpAndSettle(const Duration(milliseconds: 200));

    // Check that the text spans multiple lines.
    final Offset firstPos = textOffsetToPosition(tester, testValue.indexOf('First'));
    final Offset secondPos = textOffsetToPosition(tester, testValue.indexOf('Second'));
    final Offset thirdPos = textOffsetToPosition(tester, testValue.indexOf('Third'));
    expect(firstPos.dx, secondPos.dx);
    expect(firstPos.dx, thirdPos.dx);
    expect(firstPos.dy, lessThan(secondPos.dy));
    expect(secondPos.dy, lessThan(thirdPos.dy));

    // Double tap on the 'n' in 'until' to select the word.
    final Offset untilPos = textOffsetToPosition(tester, testValue.indexOf('until')+1);
    await tester.tapAt(untilPos);
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(untilPos);
    await tester.pumpAndSettle();

    // Skip past the frame where the opacity is zero.
    await tester.pump(const Duration(milliseconds: 200));

    expect(controller.selection.baseOffset, 39);
    expect(controller.selection.extentOffset, 44);

    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    expect(endpoints.length, 2);

    final Offset offsetFromEndPointToMiddlePoint = Offset(0.0, -renderEditable.preferredLineHeight / 2);

    // Drag the left handle to just after 'Second', still on the second line.
    Offset handlePos = endpoints[0].point + offsetFromEndPointToMiddlePoint;
    Offset newHandlePos = textOffsetToPosition(tester, testValue.indexOf('Second') + 6) + offsetFromEndPointToMiddlePoint;
    TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(controller.selection.baseOffset, 28);
    expect(controller.selection.extentOffset, 44);

    // Drag the right handle to just after 'goes', still on the second line.
    handlePos = endpoints[1].point + offsetFromEndPointToMiddlePoint;
    newHandlePos = textOffsetToPosition(tester, testValue.indexOf('goes') + 4) + offsetFromEndPointToMiddlePoint;
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(controller.selection.baseOffset, 28);
    expect(controller.selection.extentOffset, 38);

    if (!isContextMenuProvidedByPlatform) {
      await tester.tap(find.text('Cut'));
      await tester.pump();
      expect(controller.selection.isCollapsed, true);
      expect(controller.text, cutValue);
    }
  });

  testWidgets('placeholder style overflow works', (WidgetTester tester) async {
    final String placeholder = 'hint text' * 20;
    const TextStyle placeholderStyle = TextStyle(
      fontSize: 14.0,
      overflow: TextOverflow.fade,
    );

    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            placeholder: placeholder,
            placeholderStyle: placeholderStyle,
          ),
        ),
      ),
    );
    await tester.pumpAndSettle();
    final Finder placeholderFinder = find.text(placeholder);
    final Text placeholderWidget = tester.widget(placeholderFinder);
    expect(placeholderWidget.overflow, placeholderStyle.overflow);
    expect(placeholderWidget.style!.overflow, placeholderStyle.overflow);
  });

  testWidgets('tapping on a misspelled word on iOS hides the handles and shows red selection', (WidgetTester tester) async {
    tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue =
      true;
    // The default derived color for the iOS text selection highlight.
    const Color defaultSelectionColor = Color(0x33007aff);
    final TextEditingController controller = TextEditingController(
      text: 'test test testt',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
            spellCheckConfiguration:
              const SpellCheckConfiguration(
                misspelledTextStyle: CupertinoTextField.cupertinoMisspelledTextStyle,
                spellCheckSuggestionsToolbarBuilder: CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder,
            ),
          ),
        ),
      ),
    );

    final EditableTextState state =
        tester.state<EditableTextState>(find.byType(EditableText));
    state.spellCheckResults = SpellCheckResults(
      controller.value.text,
      const <SuggestionSpan>[
        SuggestionSpan(TextRange(start: 10, end: 15), <String>['test']),
      ]);

    // Double tapping a non-misspelled word shows the normal blue selection and
    // the selection handles.
    expect(state.selectionOverlay, isNull);
    await tester.tapAt(textOffsetToPosition(tester, 2));
    await tester.pump(const Duration(milliseconds: 50));
    expect(state.selectionOverlay!.handlesAreVisible, isFalse);
    await tester.tapAt(textOffsetToPosition(tester, 2));
    await tester.pumpAndSettle();
    expect(
      controller.selection,
      const TextSelection(baseOffset: 0, extentOffset: 4),
    );
    expect(state.selectionOverlay!.handlesAreVisible, isTrue);
    expect(state.renderEditable.selectionColor, defaultSelectionColor);

    // Single tapping a non-misspelled word shows a collapsed cursor.
    await tester.tapAt(textOffsetToPosition(tester, 7));
    await tester.pumpAndSettle();
    expect(
      controller.selection,
      const TextSelection.collapsed(offset: 9, affinity: TextAffinity.upstream),
    );
    expect(state.selectionOverlay!.handlesAreVisible, isFalse);
    expect(state.renderEditable.selectionColor, defaultSelectionColor);

    // Single tapping a misspelled word selects it in red with no handles.
    await tester.tapAt(textOffsetToPosition(tester, 13));
    await tester.pumpAndSettle();
    expect(
      controller.selection,
      const TextSelection(baseOffset: 10, extentOffset: 15),
    );
    expect(state.selectionOverlay!.handlesAreVisible, isFalse);
    expect(
      state.renderEditable.selectionColor,
      CupertinoTextField.kMisspelledSelectionColor,
    );
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
    skip: kIsWeb, // [intended]
  );

  testWidgets('text selection toolbar is hidden on tap down on desktop platforms', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
      text: 'blah1 blah2',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsNothing);

    TestGesture gesture = await tester.startGesture(
      textOffsetToPosition(tester, 8),
      kind: PointerDeviceKind.mouse,
      buttons: kSecondaryMouseButton,
    );
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle();

    expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsOneWidget);

    gesture = await tester.startGesture(
      textOffsetToPosition(tester, 2),
      kind: PointerDeviceKind.mouse,
    );
    await tester.pump();

    // After the gesture is down but not up, the toolbar is already gone.
    expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsNothing);

    await gesture.up();
    await tester.pumpAndSettle();

    expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsNothing);
  },
    skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
    variant: TargetPlatformVariant.all(excluding: TargetPlatformVariant.mobile().values),
  );

  testWidgets('Does not shrink in height when enters text when there is large single-line placeholder', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/133241.
    final TextEditingController controller = TextEditingController();
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Align(
          alignment: Alignment.topCenter,
          child: CupertinoTextField(
            placeholderStyle: const TextStyle(fontSize: 100),
            placeholder: 'p',
            controller: controller,
          ),
        ),
      ),
    );

    final Rect rectWithPlaceholder = tester.getRect(find.byType(CupertinoTextField));
    controller.value = const TextEditingValue(text: 'input');
    await tester.pump();

    final Rect rectWithText = tester.getRect(find.byType(CupertinoTextField));
    expect(rectWithPlaceholder, rectWithText);
  });

  testWidgets('Does not match the height of a multiline placeholder', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      CupertinoApp(
        home: Align(
          alignment: Alignment.topCenter,
          child: CupertinoTextField(
            placeholderStyle: const TextStyle(fontSize: 100),
            placeholder: 'p' * 50,
            maxLines: null,
            controller: controller,
          ),
        ),
      ),
    );

    final Rect rectWithPlaceholder = tester.getRect(find.byType(CupertinoTextField));
    controller.value = const TextEditingValue(text: 'input');
    await tester.pump();

    final Rect rectWithText = tester.getRect(find.byType(CupertinoTextField));
    // The text field is still top aligned.
    expect(rectWithPlaceholder.top, rectWithText.top);
    // But after entering text the text field should shrink since the
    // placeholder text is huge and multiline.
    expect(rectWithPlaceholder.height, greaterThan(rectWithText.height));
    // But still should be taller than or the same height of the first line of
    // placeholder.
    expect(rectWithText.height, greaterThan(100));
  });

  testWidgets('Start the floating cursor on long tap', (WidgetTester tester) async {
    EditableText.debugDeterministicCursor = true;
    final TextEditingController controller = TextEditingController(
      text: 'abcd',
    );
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: RepaintBoundary(
              key: const ValueKey<int>(1),
              child: CupertinoTextField(
                autofocus: true,
                controller: controller,
              ),
            )
          ),
        ),
      ),
    );
    // Wait for autofocus.
    await tester.pumpAndSettle();
    final Offset textFieldCenter = tester.getCenter(find.byType(CupertinoTextField));
    final TestGesture gesture = await tester.startGesture(textFieldCenter);
    await tester.pump(kLongPressTimeout);
    await expectLater(
      find.byKey(const ValueKey<int>(1)),
      matchesGoldenFile('text_field_floating_cursor.regular_and_floating_both.cupertino.0.png'),
    );
    await gesture.moveTo(Offset(10, textFieldCenter.dy));
    await tester.pump();
    await expectLater(
      find.byKey(const ValueKey<int>(1)),
      matchesGoldenFile('text_field_floating_cursor.only_floating_cursor.cupertino.0.png'),
    );
    await gesture.up();
    EditableText.debugDeterministicCursor = false;
  },
    variant: TargetPlatformVariant.only(TargetPlatform.iOS),
  );
}