// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; 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/widgets.dart'; import '../widgets/text.dart' show textOffsetToPosition; class MockClipboard { Object _clipboardData = <String, dynamic>{ 'text': null, }; Future<dynamic> handleMethodCall(MethodCall methodCall) async { switch (methodCall.method) { case 'Clipboard.getData': return _clipboardData; case 'Clipboard.setData': _clipboardData = methodCall.arguments! as Object; break; } } } 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(); SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall); // 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; } 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'), ); }); }); // TODO(justinmc): https://github.com/flutter/flutter/issues/60145 testWidgets('Paste always appears regardless of clipboard content on iOS', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( CupertinoApp( home: Column( children: <Widget>[ CupertinoTextField( controller: controller, ), ], ), ), ); // Make sure the clipboard is empty to start. await Clipboard.setData(const ClipboardData(text: '')); // Double tap to select the first 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(); // Paste is showing even though clipboard is empty. expect(find.text('Paste'), findsOneWidget); expect(find.text('Copy'), findsOneWidget); expect(find.text('Cut'), findsOneWidget); // Tap copy to add something to the clipboard and close the menu. await tester.tapAt(tester.getCenter(find.text('Copy'))); await tester.pumpAndSettle(); expect(find.text('Copy'), findsNothing); expect(find.text('Cut'), findsNothing); // Double tap to show the menu again. await tester.tapAt(textOffsetToPosition(tester, index)); await tester.pump(const Duration(milliseconds: 50)); await tester.tapAt(textOffsetToPosition(tester, index)); await tester.pumpAndSettle(); // Paste still shows. expect(find.text('Paste'), findsOneWidget); expect(find.text('Copy'), findsOneWidget); expect(find.text('Cut'), findsOneWidget); }, skip: isBrowser, // We do not use Flutter-rendered context menu on the Web variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); 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, // 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, // 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, // 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, // We do not use Flutter-rendered context menu on the Web variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); }); }