text_selection_test.dart 43 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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

10 11
import 'dart:ui' as ui show BoxHeightStyle;

12
import 'package:flutter/cupertino.dart';
13 14
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
15
import 'package:flutter/rendering.dart';
16
import 'package:flutter/services.dart';
17
import 'package:flutter_test/flutter_test.dart';
18
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
19

20
import '../widgets/clipboard_utils.dart';
21
import '../widgets/editable_text_utils.dart' show findRenderEditable, textOffsetToPosition;
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57

class _LongCupertinoLocalizationsDelegate extends LocalizationsDelegate<CupertinoLocalizations> {
  const _LongCupertinoLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) => locale.languageCode == 'en';

  @override
  Future<_LongCupertinoLocalizations> load(Locale locale) => _LongCupertinoLocalizations.load(locale);

  @override
  bool shouldReload(_LongCupertinoLocalizationsDelegate old) => false;

  @override
  String toString() => '_LongCupertinoLocalizations.delegate(en_US)';
}

class _LongCupertinoLocalizations extends DefaultCupertinoLocalizations {
  const _LongCupertinoLocalizations();

  @override
  String get cutButtonLabel => 'Cutttttttttttttttttttttttttttttttttttttttttttt';
  @override
  String get copyButtonLabel => 'Copyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy';
  @override
  String get pasteButtonLabel => 'Pasteeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
  @override
  String get selectAllButtonLabel => 'Select Allllllllllllllllllllllllllllllll';

  static Future<_LongCupertinoLocalizations> load(Locale locale) {
    return SynchronousFuture<_LongCupertinoLocalizations>(const _LongCupertinoLocalizations());
  }

  static const LocalizationsDelegate<CupertinoLocalizations> delegate = _LongCupertinoLocalizationsDelegate();
}

58
const _LongCupertinoLocalizations _longLocalizations = _LongCupertinoLocalizations();
59 60

void main() {
61 62
  TestWidgetsFlutterBinding.ensureInitialized();
  final MockClipboard mockClipboard = MockClipboard();
63

64 65 66 67 68 69 70 71 72
  List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) {
    return points.map<TextSelectionPoint>((TextSelectionPoint point) {
      return TextSelectionPoint(
        box.localToGlobal(point.point),
        point.direction,
      );
    }).toList();
  }

73
  setUp(() async {
74
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
75 76 77 78 79 80 81 82 83
      SystemChannels.platform,
      mockClipboard.handleMethodCall,
    );
    // Fill the clipboard so that the Paste option is available in the text
    // selection menu.
    await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
  });

  tearDown(() {
84
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
85 86 87 88 89
      SystemChannels.platform,
      null,
    );
  });

90 91
  group('canSelectAll', () {
    Widget createEditableText({
92 93 94
      Key? key,
      String? text,
      TextSelection? selection,
95 96 97
    }) {
      final TextEditingController controller = TextEditingController(text: text)
        ..selection = selection ?? const TextSelection.collapsed(offset: -1);
98 99 100 101
      addTearDown(controller.dispose);
      final FocusNode focusNode = FocusNode();
      addTearDown(focusNode.dispose);

102 103 104 105
      return CupertinoApp(
        home: EditableText(
          key: key,
          controller: controller,
106
          focusNode: focusNode,
107 108 109
          style: const TextStyle(),
          cursorColor: const Color.fromARGB(0, 0, 0, 0),
          backgroundCursorColor: const Color.fromARGB(0, 0, 0, 0),
110
        ),
111 112 113
      );
    }

114
    testWidgetsWithLeakTracking('should return false when there is no text', (WidgetTester tester) async {
115 116
      final GlobalKey<EditableTextState> key = GlobalKey();
      await tester.pumpWidget(createEditableText(key: key));
117
      expect(cupertinoTextSelectionControls.canSelectAll(key.currentState!), false);
118 119
    });

120
    testWidgetsWithLeakTracking('should return true when there is text and collapsed selection', (WidgetTester tester) async {
121 122 123 124 125
      final GlobalKey<EditableTextState> key = GlobalKey();
      await tester.pumpWidget(createEditableText(
        key: key,
        text: '123',
      ));
126
      expect(cupertinoTextSelectionControls.canSelectAll(key.currentState!), true);
127 128
    });

129
    testWidgetsWithLeakTracking('should return false when there is text and partial uncollapsed selection', (WidgetTester tester) async {
130 131 132 133 134 135
      final GlobalKey<EditableTextState> key = GlobalKey();
      await tester.pumpWidget(createEditableText(
        key: key,
        text: '123',
        selection: const TextSelection(baseOffset: 1, extentOffset: 2),
      ));
136
      expect(cupertinoTextSelectionControls.canSelectAll(key.currentState!), false);
137 138
    });

139
    testWidgetsWithLeakTracking('should return false when there is text and full selection', (WidgetTester tester) async {
140 141 142 143 144 145
      final GlobalKey<EditableTextState> key = GlobalKey();
      await tester.pumpWidget(createEditableText(
        key: key,
        text: '123',
        selection: const TextSelection(baseOffset: 0, extentOffset: 3),
      ));
146
      expect(cupertinoTextSelectionControls.canSelectAll(key.currentState!), false);
147 148
    });
  });
149 150

  group('cupertino handles', () {
151
    testWidgetsWithLeakTracking('draws transparent handle correctly', (WidgetTester tester) async {
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
      await tester.pumpWidget(RepaintBoundary(
        child: CupertinoTheme(
          data: const CupertinoThemeData(
            primaryColor: Color(0x550000AA),
          ),
          child: Builder(
            builder: (BuildContext context) {
              return Container(
                color: CupertinoColors.white,
                height: 800,
                width: 800,
                child: Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 250),
                  child: FittedBox(
                    child: cupertinoTextSelectionControls.buildHandle(
                      context,
                      TextSelectionHandleType.right,
                      10.0,
                    ),
                  ),
                ),
              );
            },
          ),
        ),
      ));

      await expectLater(
        find.byType(RepaintBoundary),
        matchesGoldenFile('text_selection.handle.transparent.png'),
      );
    });
  });
185 186

  group('Text selection menu overflow (iOS)', () {
187 188
    Finder findOverflowNextButton() {
      return find.byWidgetPredicate((Widget widget) =>
189
      widget is CustomPaint &&
190 191 192 193
          '${widget.painter?.runtimeType}' == '_RightCupertinoChevronPainter',
      );
    }

194 195 196 197 198
    Finder findOverflowBackButton() => find.byWidgetPredicate((Widget widget) =>
      widget is CustomPaint &&
      '${widget.painter?.runtimeType}' == '_LeftCupertinoChevronPainter',
    );

199
    testWidgetsWithLeakTracking('All menu items show when they fit.', (WidgetTester tester) async {
200
      final TextEditingController controller = TextEditingController(text: 'abc def ghi');
201
      addTearDown(controller.dispose);
202 203 204 205 206 207 208
      await tester.pumpWidget(CupertinoApp(
        home: Directionality(
            textDirection: TextDirection.ltr,
            child: MediaQuery(
              data: const MediaQueryData(size: Size(800.0, 600.0)),
              child: Center(
                child: CupertinoTextField(
209
                  autofocus: true,
210 211 212 213 214 215 216
                  controller: controller,
                ),
              ),
            ),
          ),
      ));

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

220 221 222 223 224
      // Initially, the menu isn't shown at all.
      expect(find.text('Cut'), findsNothing);
      expect(find.text('Copy'), findsNothing);
      expect(find.text('Paste'), findsNothing);
      expect(find.text('Select All'), findsNothing);
225 226
      expect(findOverflowBackButton(), findsNothing);
      expect(findOverflowNextButton(), findsNothing);
227 228 229

      // Long press on an empty space to show the selection menu.
      await tester.longPressAt(textOffsetToPosition(tester, 4));
230
      await tester.pumpAndSettle();
231 232 233 234
      expect(find.text('Cut'), findsNothing);
      expect(find.text('Copy'), findsNothing);
      expect(find.text('Paste'), findsOneWidget);
      expect(find.text('Select All'), findsOneWidget);
235 236
      expect(findOverflowBackButton(), findsNothing);
      expect(findOverflowNextButton(), findsNothing);
237 238 239 240 241 242 243 244 245 246 247 248 249

      // Double tap to select a word and show the full selection menu.
      final Offset textOffset = textOffsetToPosition(tester, 1);
      await tester.tapAt(textOffset);
      await tester.pump(const Duration(milliseconds: 200));
      await tester.tapAt(textOffset);
      await tester.pumpAndSettle();

      // The full menu is shown without the navigation buttons.
      expect(find.text('Cut'), findsOneWidget);
      expect(find.text('Copy'), findsOneWidget);
      expect(find.text('Paste'), findsOneWidget);
      expect(find.text('Select All'), findsNothing);
250 251
      expect(findOverflowBackButton(), findsNothing);
      expect(findOverflowNextButton(), findsNothing);
252
    },
253
      skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
254 255
      variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
    );
256

257
    testWidgetsWithLeakTracking("When a menu item doesn't fit, a second page is used.", (WidgetTester tester) async {
258
      // Set the screen size to more narrow, so that Paste can't fit.
259
      tester.view.physicalSize = const Size(1000, 800);
260
      addTearDown(tester.view.reset);
261 262

      final TextEditingController controller = TextEditingController(text: 'abc def ghi');
263
      addTearDown(controller.dispose);
264 265 266 267 268 269 270 271 272 273 274 275 276 277
      await tester.pumpWidget(CupertinoApp(
        home: Directionality(
            textDirection: TextDirection.ltr,
            child: MediaQuery(
              data: const MediaQueryData(size: Size(800.0, 600.0)),
              child: Center(
                child: CupertinoTextField(
                  controller: controller,
                ),
              ),
            ),
          ),
      ));

278 279 280 281 282 283 284 285 286 287
      Future<void> tapNextButton() async {
        await tester.tapAt(tester.getCenter(findOverflowNextButton()));
        await tester.pumpAndSettle();
      }

      Future<void> tapBackButton() async {
        await tester.tapAt(tester.getCenter(findOverflowBackButton()));
        await tester.pumpAndSettle();
      }

288 289 290 291 292
      // Initially, the menu isn't shown at all.
      expect(find.text('Cut'), findsNothing);
      expect(find.text('Copy'), findsNothing);
      expect(find.text('Paste'), findsNothing);
      expect(find.text('Select All'), findsNothing);
293
      expect(find.text('Look Up'), findsNothing);
294
      expect(find.text('Search Web'), findsNothing);
295 296
      expect(findOverflowBackButton(), findsNothing);
      expect(findOverflowNextButton(), findsNothing);
297 298 299 300 301 302 303 304 305 306 307 308 309

      // Double tap to select a word and show the selection menu.
      final Offset textOffset = textOffsetToPosition(tester, 1);
      await tester.tapAt(textOffset);
      await tester.pump(const Duration(milliseconds: 200));
      await tester.tapAt(textOffset);
      await tester.pumpAndSettle();

      // The last button is missing, and a next button is shown.
      expect(find.text('Cut'), findsOneWidget);
      expect(find.text('Copy'), findsOneWidget);
      expect(find.text('Paste'), findsNothing);
      expect(find.text('Select All'), findsNothing);
310
      expect(find.text('Look Up'), findsNothing);
311
      expect(find.text('Search Web'), findsNothing);
312 313
      expect(findOverflowBackButton(), findsNothing);
      expect(findOverflowNextButton(), findsOneWidget);
314

315
      // Tapping the next button shows both the overflow, back, and next buttons.
316
      await tapNextButton();
317 318 319 320 321
      expect(find.text('Cut'), findsNothing);
      expect(find.text('Copy'), findsNothing);
      expect(find.text('Paste'), findsOneWidget);
      expect(find.text('Select All'), findsNothing);
      expect(find.text('Look Up'), findsNothing);
322
      expect(find.text('Search Web'), findsNothing);
323 324 325
      expect(findOverflowBackButton(), findsOneWidget);
      expect(findOverflowNextButton(), findsOneWidget);

326 327
      // Tapping the next button shows the next, back, and Look Up button
      await tapNextButton();
328 329
      expect(find.text('Cut'), findsNothing);
      expect(find.text('Copy'), findsNothing);
330
      expect(find.text('Paste'), findsNothing);
331
      expect(find.text('Select All'), findsNothing);
332
      expect(find.text('Look Up'), findsOneWidget);
333 334 335 336 337 338 339 340 341 342 343 344
      expect(find.text('Search Web'), findsNothing);
      expect(findOverflowBackButton(), findsOneWidget);
      expect(findOverflowNextButton(), findsOneWidget);

      // Tapping the next button shows the back and Search Web button
      await tapNextButton();
      expect(find.text('Cut'), findsNothing);
      expect(find.text('Copy'), findsNothing);
      expect(find.text('Paste'), findsNothing);
      expect(find.text('Select All'), findsNothing);
      expect(find.text('Look Up'), findsNothing);
      expect(find.text('Search Web'), findsOneWidget);
345
      expect(findOverflowBackButton(), findsOneWidget);
346
      expect(findOverflowNextButton(), findsOneWidget);
347

348 349 350 351
      // Tapping the back button thrice shows the first page again with the next button.
      await tapBackButton();
      await tapBackButton();
      await tapBackButton();
352 353 354 355
      expect(find.text('Cut'), findsOneWidget);
      expect(find.text('Copy'), findsOneWidget);
      expect(find.text('Paste'), findsNothing);
      expect(find.text('Select All'), findsNothing);
356
      expect(find.text('Look Up'), findsNothing);
357
      expect(find.text('Search Web'), findsNothing);
358 359
      expect(findOverflowBackButton(), findsNothing);
      expect(findOverflowNextButton(), findsOneWidget);
360
    },
361
      skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
362 363
      variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
    );
364

365
    testWidgetsWithLeakTracking('A smaller menu puts each button on its own page.', (WidgetTester tester) async {
366 367
      // Set the screen size to more narrow, so that two buttons can't fit on
      // the same page.
368 369
      tester.view.physicalSize = const Size(640, 800);
      addTearDown(tester.view.reset);
370 371

      final TextEditingController controller = TextEditingController(text: 'abc def ghi');
372
      addTearDown(controller.dispose);
373 374 375 376 377 378 379 380 381 382 383 384 385 386
      await tester.pumpWidget(CupertinoApp(
        home: Directionality(
            textDirection: TextDirection.ltr,
            child: MediaQuery(
              data: const MediaQueryData(size: Size(800.0, 600.0)),
              child: Center(
                child: CupertinoTextField(
                  controller: controller,
                ),
              ),
            ),
          ),
      ));

387 388 389 390 391 392 393 394 395 396
      Future<void> tapNextButton() async {
        await tester.tapAt(tester.getCenter(findOverflowNextButton()));
        await tester.pumpAndSettle();
      }

      Future<void> tapBackButton() async {
        await tester.tapAt(tester.getCenter(findOverflowBackButton()));
        await tester.pumpAndSettle();
      }

397
      // Initially, the menu isn't shown at all.
398
      expect(find.byType(CupertinoTextSelectionToolbarButton), findsNothing);
399 400 401 402
      expect(find.text('Cut'), findsNothing);
      expect(find.text('Copy'), findsNothing);
      expect(find.text('Paste'), findsNothing);
      expect(find.text('Select All'), findsNothing);
403
      expect(find.text('Look Up'), findsNothing);
404
      expect(find.text('Search Web'), findsNothing);
405
      expect(find.text('Share...'), findsNothing);
406 407
      expect(findOverflowBackButton(), findsNothing);
      expect(findOverflowNextButton(), findsNothing);
408 409 410 411 412 413 414 415 416

      // Double tap to select a word and show the selection menu.
      final Offset textOffset = textOffsetToPosition(tester, 1);
      await tester.tapAt(textOffset);
      await tester.pump(const Duration(milliseconds: 200));
      await tester.tapAt(textOffset);
      await tester.pumpAndSettle();

      // Only the first button fits, and a next button is shown.
417
      expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(2));
418 419 420 421
      expect(find.text('Cut'), findsOneWidget);
      expect(find.text('Copy'), findsNothing);
      expect(find.text('Paste'), findsNothing);
      expect(find.text('Select All'), findsNothing);
422
      expect(find.text('Look Up'), findsNothing);
423
      expect(find.text('Search Web'), findsNothing);
424
      expect(find.text('Share...'), findsNothing);
425 426
      expect(findOverflowBackButton(), findsNothing);
      expect(findOverflowNextButton(), findsOneWidget);
427 428

      // Tapping the next button shows Copy.
429
      await tapNextButton();
430
      expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(3));
431 432 433 434
      expect(find.text('Cut'), findsNothing);
      expect(find.text('Copy'), findsOneWidget);
      expect(find.text('Paste'), findsNothing);
      expect(find.text('Select All'), findsNothing);
435
      expect(find.text('Look Up'), findsNothing);
436
      expect(find.text('Search Web'), findsNothing);
437
      expect(find.text('Share...'), findsNothing);
438 439
      expect(findOverflowBackButton(), findsOneWidget);
      expect(findOverflowNextButton(), findsOneWidget);
440

441
      // Tapping the next button again shows Paste
442
      await tapNextButton();
443
      expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(3));
444 445 446 447
      expect(find.text('Cut'), findsNothing);
      expect(find.text('Copy'), findsNothing);
      expect(find.text('Paste'), findsOneWidget);
      expect(find.text('Select All'), findsNothing);
448
      expect(find.text('Look Up'), findsNothing);
449
      expect(find.text('Search Web'), findsNothing);
450
      expect(find.text('Share...'), findsNothing);
451 452 453
      expect(findOverflowBackButton(), findsOneWidget);
      expect(findOverflowNextButton(), findsOneWidget);

454 455
      // Tapping the next button again shows the Look Up Button.
      await tapNextButton();
456 457 458 459 460
      expect(find.text('Cut'), findsNothing);
      expect(find.text('Copy'), findsNothing);
      expect(find.text('Paste'), findsNothing);
      expect(find.text('Select All'), findsNothing);
      expect(find.text('Look Up'), findsOneWidget);
461
      expect(find.text('Search Web'), findsNothing);
462
      expect(find.text('Share...'), findsNothing);
463 464 465
      expect(findOverflowBackButton(), findsOneWidget);
      expect(findOverflowNextButton(), findsOneWidget);

466
      // Tapping the next button again shows the Search Web Button.
467 468 469 470 471 472 473
      await tapNextButton();
      expect(find.text('Cut'), findsNothing);
      expect(find.text('Copy'), findsNothing);
      expect(find.text('Paste'), findsNothing);
      expect(find.text('Select All'), findsNothing);
      expect(find.text('Look Up'), findsNothing);
      expect(find.text('Search Web'), findsOneWidget);
474
      expect(find.text('Share...'), findsNothing);
475
      expect(findOverflowBackButton(), findsOneWidget);
476
      expect(findOverflowNextButton(), findsOneWidget);
477

478 479
      // Tapping the next button again shows the last page and the Share button
      await tapNextButton();
480
      expect(find.text('Cut'), findsNothing);
481
      expect(find.text('Copy'), findsNothing);
482 483
      expect(find.text('Paste'), findsNothing);
      expect(find.text('Select All'), findsNothing);
484
      expect(find.text('Look Up'), findsNothing);
485 486
      expect(find.text('Search Web'), findsNothing);
      expect(find.text('Share...'), findsOneWidget);
487
      expect(findOverflowBackButton(), findsOneWidget);
488
      expect(findOverflowNextButton(), findsNothing);
489

490 491 492 493 494
      // Tapping the back button 5 times shows the first page again.
      await tapBackButton();
      await tapBackButton();
      await tapBackButton();
      await tapBackButton();
495
      await tapBackButton();
496
      expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(2));
497 498 499 500
      expect(find.text('Cut'), findsOneWidget);
      expect(find.text('Copy'), findsNothing);
      expect(find.text('Paste'), findsNothing);
      expect(find.text('Select All'), findsNothing);
501
      expect(find.text('Look Up'), findsNothing);
502 503
      expect(find.text('Search Web'), findsNothing);
      expect(find.text('Share...'), findsNothing);
504 505
      expect(findOverflowBackButton(), findsNothing);
      expect(findOverflowNextButton(), findsOneWidget);
506
    },
507
      skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
508 509
      variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
    );
510

511
    testWidgetsWithLeakTracking('Handles very long locale strings', (WidgetTester tester) async {
512
      final TextEditingController controller = TextEditingController(text: 'abc def ghi');
513
      addTearDown(controller.dispose);
514 515 516 517 518 519 520 521 522 523 524 525 526
      await tester.pumpWidget(CupertinoApp(
        locale: const Locale('en', 'us'),
        localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
          _LongCupertinoLocalizations.delegate,
          DefaultWidgetsLocalizations.delegate,
          DefaultMaterialLocalizations.delegate,
        ],
        home: Directionality(
            textDirection: TextDirection.ltr,
            child: MediaQuery(
              data: const MediaQueryData(size: Size(800.0, 600.0)),
              child: Center(
                child: CupertinoTextField(
527
                  autofocus: true,
528 529 530 531 532 533 534
                  controller: controller,
                ),
              ),
            ),
          ),
      ));

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

538
      // Initially, the menu isn't shown at all.
539 540 541 542
      expect(find.text(_longLocalizations.cutButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.copyButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
543 544
      expect(findOverflowBackButton(), findsNothing);
      expect(findOverflowNextButton(), findsNothing);
545 546 547 548

      // Long press on an empty space to show the selection menu, with only the
      // paste button visible.
      await tester.longPressAt(textOffsetToPosition(tester, 4));
549
      await tester.pumpAndSettle();
550 551 552 553
      expect(find.text(_longLocalizations.cutButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.copyButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.pasteButtonLabel), findsOneWidget);
      expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
554 555
      expect(findOverflowBackButton(), findsNothing);
      expect(findOverflowNextButton(), findsOneWidget);
556 557

      // Tap next to go to the second and final page.
558
      await tester.tapAt(tester.getCenter(findOverflowNextButton()));
559
      await tester.pumpAndSettle();
560 561 562 563
      expect(find.text(_longLocalizations.cutButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.copyButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.selectAllButtonLabel), findsOneWidget);
564 565
      expect(findOverflowBackButton(), findsOneWidget);
      expect(findOverflowNextButton(), findsNothing);
566 567

      // Tap select all to show the full selection menu.
568
      await tester.tap(find.text(_longLocalizations.selectAllButtonLabel));
569 570 571
      await tester.pumpAndSettle();

      // Only one button fits on each page.
572 573 574 575
      expect(find.text(_longLocalizations.cutButtonLabel), findsOneWidget);
      expect(find.text(_longLocalizations.copyButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
576 577
      expect(findOverflowBackButton(), findsNothing);
      expect(findOverflowNextButton(), findsOneWidget);
578 579

      // Tap next to go to the second page.
580
      await tester.tapAt(tester.getCenter(findOverflowNextButton()));
581
      await tester.pumpAndSettle();
582 583 584 585
      expect(find.text(_longLocalizations.cutButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.copyButtonLabel), findsOneWidget);
      expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
586 587
      expect(findOverflowBackButton(), findsOneWidget);
      expect(findOverflowNextButton(), findsOneWidget);
588

589
      // Tap twice to go to the third page.
590
      await tester.tapAt(tester.getCenter(findOverflowNextButton()));
591
      await tester.pumpAndSettle();
592 593 594 595
      expect(find.text(_longLocalizations.cutButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.copyButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.pasteButtonLabel), findsOneWidget);
      expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
596
      expect(findOverflowBackButton(), findsOneWidget);
597
      expect(findOverflowNextButton(), findsOneWidget);
598 599

      // Tap back to go to the second page again.
600
      await tester.tapAt(tester.getCenter(findOverflowBackButton()));
601
      await tester.pumpAndSettle();
602 603 604 605
      expect(find.text(_longLocalizations.cutButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.copyButtonLabel), findsOneWidget);
      expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
606 607
      expect(findOverflowBackButton(), findsOneWidget);
      expect(findOverflowNextButton(), findsOneWidget);
608 609

      // Tap back to go to the first page again.
610
      await tester.tapAt(tester.getCenter(findOverflowBackButton()));
611
      await tester.pumpAndSettle();
612 613 614 615
      expect(find.text(_longLocalizations.cutButtonLabel), findsOneWidget);
      expect(find.text(_longLocalizations.copyButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing);
      expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
616 617
      expect(findOverflowBackButton(), findsNothing);
      expect(findOverflowNextButton(), findsOneWidget);
618
    },
619
      skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
620 621
      variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
    );
622

623
    testWidgetsWithLeakTracking(
624 625 626
      'When selecting multiple lines over max lines',
      (WidgetTester tester) async {
        final TextEditingController controller = TextEditingController(text: 'abc\ndef\nghi\njkl\nmno\npqr');
627
        addTearDown(controller.dispose);
628 629 630 631 632 633 634
        await tester.pumpWidget(CupertinoApp(
          home: Directionality(
              textDirection: TextDirection.ltr,
              child: MediaQuery(
                data: const MediaQueryData(size: Size(800.0, 600.0)),
                child: Center(
                  child: CupertinoTextField(
635
                    autofocus: true,
636 637 638 639 640 641 642 643 644
                    padding: const EdgeInsets.all(8.0),
                    controller: controller,
                    maxLines: 2,
                  ),
                ),
              ),
            ),
        ));

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

648 649 650 651 652
        // Initially, the menu isn't shown at all.
        expect(find.text('Cut'), findsNothing);
        expect(find.text('Copy'), findsNothing);
        expect(find.text('Paste'), findsNothing);
        expect(find.text('Select All'), findsNothing);
653 654
        expect(findOverflowBackButton(), findsNothing);
        expect(findOverflowNextButton(), findsNothing);
655 656 657 658 659 660 661 662

        // Long press on an space to show the selection menu.
        await tester.longPressAt(textOffsetToPosition(tester, 1));
        await tester.pumpAndSettle();
        expect(find.text('Cut'), findsNothing);
        expect(find.text('Copy'), findsNothing);
        expect(find.text('Paste'), findsOneWidget);
        expect(find.text('Select All'), findsOneWidget);
663 664
        expect(findOverflowBackButton(), findsNothing);
        expect(findOverflowNextButton(), findsNothing);
665 666 667 668 669 670 671 672 673 674

        // Tap to select all.
        await tester.tap(find.text('Select All'));
        await tester.pumpAndSettle();

        // Only Cut, Copy, and Paste are shown.
        expect(find.text('Cut'), findsOneWidget);
        expect(find.text('Copy'), findsOneWidget);
        expect(find.text('Paste'), findsOneWidget);
        expect(find.text('Select All'), findsNothing);
675 676
        expect(findOverflowBackButton(), findsNothing);
        expect(findOverflowNextButton(), findsNothing);
677 678 679 680 681 682 683

        // The menu appears at the top of the visible selection.
        final Offset selectionOffset = tester
            .getTopLeft(find.byType(CupertinoTextSelectionToolbarButton).first);
        final Offset textFieldOffset =
            tester.getTopLeft(find.byType(CupertinoTextField));

684 685
        // 7.0 + 44.0 + 8.0 - 8.0 = _kToolbarArrowSize + text_button_height + _kToolbarContentDistance - padding
        expect(selectionOffset.dy + 7.0 + 44.0 + 8.0 - 8.0, equals(textFieldOffset.dy));
686 687 688 689
      },
      skip: isBrowser, // [intended] the selection menu isn't required by web
      variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
    );
690
  });
691

692
  testWidgetsWithLeakTracking('iOS selection handles scale with rich text (selection style 1)', (WidgetTester tester) async {
693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767
    await tester.pumpWidget(
      const CupertinoApp(
        home: Center(
          child: SelectableText.rich(
            TextSpan(
              children: <InlineSpan>[
                TextSpan(text: 'abc ', style: TextStyle(fontSize: 100.0)),
                TextSpan(text: 'def ', style: TextStyle(fontSize: 50.0)),
                TextSpan(text: 'hij', style: TextStyle(fontSize: 25.0)),
              ],
            ),
          ),
        ),
      ),
    );

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final EditableTextState editableTextState = tester.state(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;

    // Double tap to select the second word.
    const int index = 4;
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pumpAndSettle();
    expect(editableTextState.selectionOverlay!.handlesAreVisible, isTrue);
    expect(controller.selection.baseOffset, 4);
    expect(controller.selection.extentOffset, 7);

    // Drag the right handle 2 letters to the right. Placing the end handle on
    // the third word. We use a small offset because the endpoint is on the very
    // corner of the handle.
    final TextSelection selection = controller.selection;
    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(selection),
      renderEditable,
    );
    expect(endpoints.length, 2);

    final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
    final Offset newHandlePos = textOffsetToPosition(tester, 11);
    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, 4);
    expect(controller.selection.extentOffset, 11);

    // Find start and end handles and verify their sizes.
    expect(find.byType(Overlay), findsOneWidget);
    expect(find.descendant(
      of: find.byType(Overlay),
      matching: find.byType(CustomPaint),
    ), findsNWidgets(2));

    final Iterable<RenderBox> handles = tester.renderObjectList(find.descendant(
      of: find.byType(Overlay),
      matching: find.byType(CustomPaint),
    ));

    // The handle height is determined by the formula:
    // textLineHeight + _kSelectionHandleRadius * 2 - _kSelectionHandleOverlap .
    // The text line height will be the value of the fontSize.
    // The constant _kSelectionHandleRadius has the value of 6.
    // The constant _kSelectionHandleOverlap has the value of 1.5.
    // In the case of the start handle, which is located on the word 'def',
    // 50.0 + 6 * 2 - 1.5 = 60.5 .
    expect(handles.first.size.height, 60.5);
    expect(handles.last.size.height, 35.5);
  },
768
    skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
769 770 771
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
  );

772
  testWidgetsWithLeakTracking('iOS selection handles scale with rich text (selection style 2)', (WidgetTester tester) async {
773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851
    await tester.pumpWidget(
      const CupertinoApp(
        home: Center(
          child: SelectableText.rich(
            TextSpan(
              children: <InlineSpan>[
                TextSpan(text: 'abc ', style: TextStyle(fontSize: 100.0)),
                TextSpan(text: 'def ', style: TextStyle(fontSize: 50.0)),
                TextSpan(text: 'hij', style: TextStyle(fontSize: 25.0)),
              ],
            ),
            selectionHeightStyle: ui.BoxHeightStyle.max,
          ),
        ),
      ),
    );

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final EditableTextState editableTextState = tester.state(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;

    // Double tap to select the second word.
    const int index = 4;
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pumpAndSettle();
    expect(editableTextState.selectionOverlay!.handlesAreVisible, isTrue);
    expect(controller.selection.baseOffset, 4);
    expect(controller.selection.extentOffset, 7);

    // Drag the right handle 2 letters to the right. Placing the end handle on
    // the third word. We use a small offset because the endpoint is on the very
    // corner of the handle.
    final TextSelection selection = controller.selection;
    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(selection),
      renderEditable,
    );
    expect(endpoints.length, 2);

    final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
    final Offset newHandlePos = textOffsetToPosition(tester, 11);
    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, 4);
    expect(controller.selection.extentOffset, 11);

    // Find start and end handles and verify their sizes.
    expect(find.byType(Overlay), findsOneWidget);
    expect(find.descendant(
      of: find.byType(Overlay),
      matching: find.byType(CustomPaint),
    ), findsNWidgets(2));

    final Iterable<RenderBox> handles = tester.renderObjectList(find.descendant(
      of: find.byType(Overlay),
      matching: find.byType(CustomPaint),
    ));

    // The handle height is determined by the formula:
    // textLineHeight + _kSelectionHandleRadius * 2 - _kSelectionHandleOverlap .
    // The text line height will be the value of the fontSize, of the largest word on the line.
    // The constant _kSelectionHandleRadius has the value of 6.
    // The constant _kSelectionHandleOverlap has the value of 1.5.
    // In the case of the start handle, which is located on the word 'def',
    // 100 + 6 * 2 - 1.5 = 110.5 .
    // In this case both selection handles are the same size because the selection
    // height style is set to BoxHeightStyle.max which means that the height of
    // the selection highlight will be the height of the largest word on the line.
    expect(handles.first.size.height, 110.5);
    expect(handles.last.size.height, 110.5);
  },
852
    skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
853 854 855
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
  );

856
  testWidgetsWithLeakTracking('iOS selection handles scale with rich text (grapheme clusters)', (WidgetTester tester) async {
857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932
    await tester.pumpWidget(
      const CupertinoApp(
        home: Center(
          child: SelectableText.rich(
            TextSpan(
              children: <InlineSpan>[
                TextSpan(text: 'abc ', style: TextStyle(fontSize: 100.0)),
                TextSpan(text: 'def ', style: TextStyle(fontSize: 50.0)),
                TextSpan(text: '👨‍👩‍👦 ', style: TextStyle(fontSize: 35.0)),
                TextSpan(text: 'hij', style: TextStyle(fontSize: 25.0)),
              ],
            ),
          ),
        ),
      ),
    );

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final EditableTextState editableTextState = tester.state(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;

    // Double tap to select the second word.
    const int index = 4;
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pumpAndSettle();
    expect(editableTextState.selectionOverlay!.handlesAreVisible, isTrue);
    expect(controller.selection.baseOffset, 4);
    expect(controller.selection.extentOffset, 7);

    // Drag the right handle 2 letters to the right. Placing the end handle on
    // the third word. We use a small offset because the endpoint is on the very
    // corner of the handle.
    final TextSelection selection = controller.selection;
    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(selection),
      renderEditable,
    );
    expect(endpoints.length, 2);

    final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
    final Offset newHandlePos = textOffsetToPosition(tester, 16);
    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, 4);
    expect(controller.selection.extentOffset, 16);

    // Find start and end handles and verify their sizes.
    expect(find.byType(Overlay), findsOneWidget);
    expect(find.descendant(
      of: find.byType(Overlay),
      matching: find.byType(CustomPaint),
    ), findsNWidgets(2));

    final Iterable<RenderBox> handles = tester.renderObjectList(find.descendant(
      of: find.byType(Overlay),
      matching: find.byType(CustomPaint),
    ));

    // The handle height is determined by the formula:
    // textLineHeight + _kSelectionHandleRadius * 2 - _kSelectionHandleOverlap .
    // The text line height will be the value of the fontSize.
    // The constant _kSelectionHandleRadius has the value of 6.
    // The constant _kSelectionHandleOverlap has the value of 1.5.
    // In the case of the end handle, which is located on the grapheme cluster '👨‍👩‍👦',
    // 35.0 + 6 * 2 - 1.5 = 45.5 .
    expect(handles.first.size.height, 60.5);
    expect(handles.last.size.height, 45.5);
  },
933
    skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
934 935
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
  );
936

937 938
  testWidgetsWithLeakTracking(
    'iOS selection handles scaling falls back to preferredLineHeight when the current frame does not match the previous', (WidgetTester tester) async {
939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019
    await tester.pumpWidget(
      const CupertinoApp(
        home: Center(
          child: SelectableText.rich(
            TextSpan(
              children: <InlineSpan>[
                TextSpan(text: 'abc', style: TextStyle(fontSize: 40.0)),
                TextSpan(text: 'def', style: TextStyle(fontSize: 50.0)),
              ],
            ),
          ),
        ),
      ),
    );

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final EditableTextState editableTextState = tester.state(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;

    // Double tap to select the second word.
    const int index = 4;
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, index));
    await tester.pumpAndSettle();
    expect(editableTextState.selectionOverlay!.handlesAreVisible, isTrue);
    expect(controller.selection.baseOffset, 0);
    expect(controller.selection.extentOffset, 6);

    // Drag the right handle 2 letters to the right. Placing the end handle on
    // the third word. We use a small offset because the endpoint is on the very
    // corner of the handle.
    final TextSelection selection = controller.selection;
    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(selection),
      renderEditable,
    );
    expect(endpoints.length, 2);

    final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
    final Offset newHandlePos = textOffsetToPosition(tester, 3);
    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, 3);

    // Find start and end handles and verify their sizes.
    expect(find.byType(Overlay), findsOneWidget);
    expect(find.descendant(
      of: find.byType(Overlay),
      matching: find.byType(CustomPaint),
    ), findsNWidgets(2));

    final Iterable<RenderBox> handles = tester.renderObjectList(find.descendant(
      of: find.byType(Overlay),
      matching: find.byType(CustomPaint),
    ));

    // The handle height is determined by the formula:
    // textLineHeight + _kSelectionHandleRadius * 2 - _kSelectionHandleOverlap .
    // The text line height will be the value of the fontSize.
    // The constant _kSelectionHandleRadius has the value of 6.
    // The constant _kSelectionHandleOverlap has the value of 1.5.
    // In the case of the start handle, which is located on the word 'abc',
    // 40.0 + 6 * 2 - 1.5 = 50.5 .
    //
    // We are now using the current frames selection and text in order to
    // calculate the start and end handle heights (we fall back to preferredLineHeight
    // when the current frame differs from the previous frame), where previously
    // we would be using a mix of the previous and current frame. This could
    // result in the start and end handle heights being calculated inaccurately
    // if one of the handles falls between two varying text styles.
    expect(handles.first.size.height, 50.5);
    expect(handles.last.size.height, 50.5); // This is 60.5 with the previous frame.
  },
1020
    skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
1021 1022
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
  );
1023
}