| // 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. |
| |
| // This file is run as part of a reduced test set in CI on Mac and Windows |
| // machines. |
| @Tags(<String>['reduced-test-set']) |
| |
| import 'dart:ui' as ui show BoxHeightStyle; |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/foundation.dart'; |
| 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 findRenderEditable, textOffsetToPosition; |
| |
| 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(); |
| } |
| |
| const _LongCupertinoLocalizations _longLocalizations = _LongCupertinoLocalizations(); |
| |
| void main() { |
| TestWidgetsFlutterBinding.ensureInitialized(); |
| final MockClipboard mockClipboard = MockClipboard(); |
| |
| // Returns true iff the button is visually enabled. |
| bool appearsEnabled(WidgetTester tester, String text) { |
| final CupertinoButton button = tester.widget<CupertinoButton>( |
| find.ancestor( |
| of: find.text(text), |
| matching: find.byType(CupertinoButton), |
| ), |
| ); |
| // Disabled buttons have no opacity change when pressed. |
| return button.pressedOpacity! < 1.0; |
| } |
| |
| List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) { |
| return points.map<TextSelectionPoint>((TextSelectionPoint point) { |
| return TextSelectionPoint( |
| box.localToGlobal(point.point), |
| point.direction, |
| ); |
| }).toList(); |
| } |
| |
| setUp(() async { |
| TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler( |
| 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(() { |
| TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler( |
| SystemChannels.platform, |
| null, |
| ); |
| }); |
| |
| group('canSelectAll', () { |
| Widget createEditableText({ |
| Key? key, |
| String? text, |
| TextSelection? selection, |
| }) { |
| final TextEditingController controller = TextEditingController(text: text) |
| ..selection = selection ?? const TextSelection.collapsed(offset: -1); |
| return CupertinoApp( |
| home: EditableText( |
| key: key, |
| controller: controller, |
| focusNode: FocusNode(), |
| style: const TextStyle(), |
| cursorColor: const Color.fromARGB(0, 0, 0, 0), |
| backgroundCursorColor: const Color.fromARGB(0, 0, 0, 0), |
| ), |
| ); |
| } |
| |
| testWidgets('should return false when there is no text', (WidgetTester tester) async { |
| final GlobalKey<EditableTextState> key = GlobalKey(); |
| await tester.pumpWidget(createEditableText(key: key)); |
| expect(cupertinoTextSelectionControls.canSelectAll(key.currentState!), false); |
| }); |
| |
| testWidgets('should return true when there is text and collapsed selection', (WidgetTester tester) async { |
| final GlobalKey<EditableTextState> key = GlobalKey(); |
| await tester.pumpWidget(createEditableText( |
| key: key, |
| text: '123', |
| )); |
| expect(cupertinoTextSelectionControls.canSelectAll(key.currentState!), true); |
| }); |
| |
| testWidgets('should return false when there is text and partial uncollapsed selection', (WidgetTester tester) async { |
| final GlobalKey<EditableTextState> key = GlobalKey(); |
| await tester.pumpWidget(createEditableText( |
| key: key, |
| text: '123', |
| selection: const TextSelection(baseOffset: 1, extentOffset: 2), |
| )); |
| expect(cupertinoTextSelectionControls.canSelectAll(key.currentState!), false); |
| }); |
| |
| testWidgets('should return false when there is text and full selection', (WidgetTester tester) async { |
| final GlobalKey<EditableTextState> key = GlobalKey(); |
| await tester.pumpWidget(createEditableText( |
| key: key, |
| text: '123', |
| selection: const TextSelection(baseOffset: 0, extentOffset: 3), |
| )); |
| expect(cupertinoTextSelectionControls.canSelectAll(key.currentState!), false); |
| }); |
| }); |
| |
| group('cupertino handles', () { |
| testWidgets('draws transparent handle correctly', (WidgetTester tester) async { |
| 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'), |
| ); |
| }); |
| }); |
| |
| group('Text selection menu overflow (iOS)', () { |
| testWidgets('All menu items show when they fit.', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(text: 'abc def ghi'); |
| 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, |
| ), |
| ), |
| ), |
| ), |
| )); |
| |
| // 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); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsNothing); |
| |
| // Long press on an empty space to show the selection menu. |
| await tester.longPressAt(textOffsetToPosition(tester, 4)); |
| await tester.pumpAndSettle(); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), findsOneWidget); |
| expect(find.text('Select All'), findsOneWidget); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsNothing); |
| |
| // 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); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsNothing); |
| }, |
| skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets("When a menu item doesn't fit, a second page is used.", (WidgetTester tester) async { |
| // Set the screen size to more narrow, so that Paste can't fit. |
| tester.binding.window.physicalSizeTestValue = const Size(800, 800); |
| addTearDown(tester.binding.window.clearPhysicalSizeTestValue); |
| |
| final TextEditingController controller = TextEditingController(text: 'abc def ghi'); |
| 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, |
| ), |
| ), |
| ), |
| ), |
| )); |
| |
| // 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); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsNothing); |
| |
| // 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); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '▶'), true); |
| |
| // Tapping the next button shows the overflowing button. |
| await tester.tap(find.text('▶')); |
| await tester.pumpAndSettle(); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), findsOneWidget); |
| expect(find.text('Select All'), findsNothing); |
| expect(find.text('◀'), findsOneWidget); |
| expect(appearsEnabled(tester, '◀'), true); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '▶'), false); |
| |
| // Tapping the back button shows the first page again. |
| await tester.tap(find.text('◀')); |
| await tester.pumpAndSettle(); |
| expect(find.text('Cut'), findsOneWidget); |
| expect(find.text('Copy'), findsOneWidget); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Select All'), findsNothing); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '▶'), true); |
| }, |
| skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets('A smaller menu puts each button on its own page.', (WidgetTester tester) async { |
| // Set the screen size to more narrow, so that two buttons can't fit on |
| // the same page. |
| tester.binding.window.physicalSizeTestValue = const Size(640, 800); |
| addTearDown(tester.binding.window.clearPhysicalSizeTestValue); |
| |
| final TextEditingController controller = TextEditingController(text: 'abc def ghi'); |
| 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, |
| ), |
| ), |
| ), |
| ), |
| )); |
| |
| // Initially, the menu isn't shown at all. |
| expect(find.byType(CupertinoButton), findsNothing); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Select All'), findsNothing); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsNothing); |
| |
| // 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. |
| expect(find.byType(CupertinoButton), findsNWidgets(2)); |
| expect(find.text('Cut'), findsOneWidget); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Select All'), findsNothing); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '▶'), true); |
| |
| // Tapping the next button shows Copy. |
| await tester.tap(find.text('▶')); |
| await tester.pumpAndSettle(); |
| expect(find.byType(CupertinoButton), findsNWidgets(3)); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Copy'), findsOneWidget); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Select All'), findsNothing); |
| expect(find.text('◀'), findsOneWidget); |
| expect(appearsEnabled(tester, '◀'), true); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '▶'), true); |
| |
| // Tapping the next button again shows Paste. |
| await tester.tap(find.text('▶')); |
| await tester.pumpAndSettle(); |
| expect(find.byType(CupertinoButton), findsNWidgets(3)); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), findsOneWidget); |
| expect(find.text('Select All'), findsNothing); |
| expect(find.text('◀'), findsOneWidget); |
| expect(appearsEnabled(tester, '◀'), true); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '▶'), false); |
| |
| // Tapping the back button shows the second page again. |
| await tester.tap(find.text('◀')); |
| await tester.pumpAndSettle(); |
| expect(find.byType(CupertinoButton), findsNWidgets(3)); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Copy'), findsOneWidget); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Select All'), findsNothing); |
| expect(find.text('◀'), findsOneWidget); |
| expect(appearsEnabled(tester, '◀'), true); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '▶'), true); |
| |
| // Tapping the back button again shows the first page again. |
| await tester.tap(find.text('◀')); |
| await tester.pumpAndSettle(); |
| expect(find.byType(CupertinoButton), findsNWidgets(2)); |
| expect(find.text('Cut'), findsOneWidget); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Select All'), findsNothing); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '▶'), true); |
| }, |
| skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets('Handles very long locale strings', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(text: 'abc def ghi'); |
| 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( |
| controller: controller, |
| ), |
| ), |
| ), |
| ), |
| )); |
| |
| // Initially, the menu isn't shown at all. |
| expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsNothing); |
| |
| // Long press on an empty space to show the selection menu, with only the |
| // paste button visible. |
| await tester.longPressAt(textOffsetToPosition(tester, 4)); |
| await tester.pumpAndSettle(); |
| expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.pasteButtonLabel), findsOneWidget); |
| expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '▶'), true); |
| |
| // Tap next to go to the second and final page. |
| await tester.tap(find.text('▶')); |
| await tester.pumpAndSettle(); |
| expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.selectAllButtonLabel), findsOneWidget); |
| expect(find.text('◀'), findsOneWidget); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '◀'), true); |
| expect(appearsEnabled(tester, '▶'), false); |
| |
| // Tap select all to show the full selection menu. |
| await tester.tap(find.text(_longLocalizations.selectAllButtonLabel)); |
| await tester.pumpAndSettle(); |
| |
| // Only one button fits on each page. |
| expect(find.text(_longLocalizations.cutButtonLabel), findsOneWidget); |
| expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '▶'), true); |
| |
| // Tap next to go to the second page. |
| await tester.tap(find.text('▶')); |
| await tester.pumpAndSettle(); |
| expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.copyButtonLabel), findsOneWidget); |
| expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); |
| expect(find.text('◀'), findsOneWidget); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '◀'), true); |
| expect(appearsEnabled(tester, '▶'), true); |
| |
| // Tap next to go to the third and final page. |
| await tester.tap(find.text('▶')); |
| await tester.pumpAndSettle(); |
| expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.pasteButtonLabel), findsOneWidget); |
| expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); |
| expect(find.text('◀'), findsOneWidget); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '◀'), true); |
| expect(appearsEnabled(tester, '▶'), false); |
| |
| // Tap back to go to the second page again. |
| await tester.tap(find.text('◀')); |
| await tester.pumpAndSettle(); |
| expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.copyButtonLabel), findsOneWidget); |
| expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); |
| expect(find.text('◀'), findsOneWidget); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '◀'), true); |
| expect(appearsEnabled(tester, '▶'), true); |
| |
| // Tap back to go to the first page again. |
| await tester.tap(find.text('◀')); |
| await tester.pumpAndSettle(); |
| expect(find.text(_longLocalizations.cutButtonLabel), findsOneWidget); |
| expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); |
| expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsOneWidget); |
| expect(appearsEnabled(tester, '▶'), true); |
| }, |
| skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets( |
| 'When selecting multiple lines over max lines', |
| (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(text: 'abc\ndef\nghi\njkl\nmno\npqr'); |
| await tester.pumpWidget(CupertinoApp( |
| home: Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(size: Size(800.0, 600.0)), |
| child: Center( |
| child: CupertinoTextField( |
| padding: const EdgeInsets.all(8.0), |
| controller: controller, |
| maxLines: 2, |
| ), |
| ), |
| ), |
| ), |
| )); |
| |
| // 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); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsNothing); |
| |
| // 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); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsNothing); |
| |
| // 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); |
| expect(find.text('◀'), findsNothing); |
| expect(find.text('▶'), findsNothing); |
| |
| // 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)); |
| |
| // 7.0 + 43.0 + 8.0 - 8.0 = _kToolbarArrowSize + _kToolbarHeight + _kToolbarContentDistance - padding |
| expect(selectionOffset.dy + 7.0 + 43.0 + 8.0 - 8.0, equals(textFieldOffset.dy)); |
| }, |
| skip: isBrowser, // [intended] the selection menu isn't required by web |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| }); |
| |
| testWidgets('iOS selection handles scale with rich text (selection style 1)', (WidgetTester tester) async { |
| 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); |
| }, |
| skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets('iOS selection handles scale with rich text (selection style 2)', (WidgetTester tester) async { |
| 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); |
| }, |
| skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets('iOS selection handles scale with rich text (grapheme clusters)', (WidgetTester tester) async { |
| 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); |
| }, |
| skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| |
| testWidgets('iOS selection handles scaling falls back to preferredLineHeight when the current frame does not match the previous', (WidgetTester tester) async { |
| 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. |
| }, |
| skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), |
| ); |
| } |