| // 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/cupertino.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../widgets/editable_text_utils.dart' show textOffsetToPosition; |
| |
| // These constants are copied from cupertino/text_selection_toolbar.dart. |
| const double _kArrowScreenPadding = 26.0; |
| const double _kToolbarContentDistance = 8.0; |
| const double _kToolbarHeight = 43.0; |
| |
| // A custom text selection menu that just displays a single custom button. |
| class _CustomCupertinoTextSelectionControls extends CupertinoTextSelectionControls { |
| @override |
| Widget buildToolbar( |
| BuildContext context, |
| Rect globalEditableRegion, |
| double textLineHeight, |
| Offset selectionMidpoint, |
| List<TextSelectionPoint> endpoints, |
| TextSelectionDelegate delegate, |
| ClipboardStatusNotifier? clipboardStatus, |
| Offset? lastSecondaryTapDownPosition, |
| ) { |
| final MediaQueryData mediaQuery = MediaQuery.of(context); |
| final double anchorX = (selectionMidpoint.dx + globalEditableRegion.left).clamp( |
| _kArrowScreenPadding + mediaQuery.padding.left, |
| mediaQuery.size.width - mediaQuery.padding.right - _kArrowScreenPadding, |
| ); |
| final Offset anchorAbove = Offset( |
| anchorX, |
| endpoints.first.point.dy - textLineHeight + globalEditableRegion.top, |
| ); |
| final Offset anchorBelow = Offset( |
| anchorX, |
| endpoints.last.point.dy + globalEditableRegion.top, |
| ); |
| |
| return CupertinoTextSelectionToolbar( |
| anchorAbove: anchorAbove, |
| anchorBelow: anchorBelow, |
| children: <Widget>[ |
| CupertinoTextSelectionToolbarButton( |
| onPressed: () {}, |
| child: const Text('Custom button'), |
| ), |
| ], |
| ); |
| } |
| } |
| |
| class TestBox extends SizedBox { |
| const TestBox({super.key}) : super(width: itemWidth, height: itemHeight); |
| |
| static const double itemHeight = 44.0; |
| static const double itemWidth = 100.0; |
| } |
| |
| void main() { |
| TestWidgetsFlutterBinding.ensureInitialized(); |
| |
| // Find by a runtimeType String, including private types. |
| Finder findPrivate(String type) { |
| return find.descendant( |
| of: find.byType(CupertinoApp), |
| matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == type), |
| ); |
| } |
| |
| // Finding CupertinoTextSelectionToolbar won't give you the position as the user sees |
| // it because it's a full-sized Stack at the top level. This method finds the |
| // visible part of the toolbar for use in measurements. |
| Finder findToolbar() => findPrivate('_CupertinoTextSelectionToolbarContent'); |
| |
| Finder findOverflowNextButton() => find.text('▶'); |
| Finder findOverflowBackButton() => find.text('◀'); |
| |
| testWidgets('paginates children if they overflow', (WidgetTester tester) async { |
| late StateSetter setState; |
| final List<Widget> children = List<Widget>.generate(7, (int i) => const TestBox()); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setter) { |
| setState = setter; |
| return CupertinoTextSelectionToolbar( |
| anchorAbove: const Offset(50.0, 100.0), |
| anchorBelow: const Offset(50.0, 200.0), |
| children: children, |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // All children fit on the screen, so they are all rendered. |
| expect(find.byType(TestBox), findsNWidgets(children.length)); |
| expect(findOverflowNextButton(), findsNothing); |
| expect(findOverflowBackButton(), findsNothing); |
| |
| // Adding one more child makes the children overflow. |
| setState(() { |
| children.add( |
| const TestBox(), |
| ); |
| }); |
| await tester.pumpAndSettle(); |
| expect(find.byType(TestBox), findsNWidgets(children.length - 1)); |
| expect(findOverflowNextButton(), findsOneWidget); |
| expect(findOverflowBackButton(), findsNothing); |
| |
| // Tap the overflow next button to show the next page of children. |
| await tester.tap(findOverflowNextButton()); |
| await tester.pumpAndSettle(); |
| expect(find.byType(TestBox), findsNWidgets(1)); |
| expect(findOverflowNextButton(), findsOneWidget); |
| expect(findOverflowBackButton(), findsOneWidget); |
| |
| // Tapping the overflow next button again does nothing because it is |
| // disabled and there are no more children to display. |
| await tester.tap(findOverflowNextButton()); |
| await tester.pumpAndSettle(); |
| expect(find.byType(TestBox), findsNWidgets(1)); |
| expect(findOverflowNextButton(), findsOneWidget); |
| expect(findOverflowBackButton(), findsOneWidget); |
| |
| // Tap the overflow back button to go back to the first page. |
| await tester.tap(findOverflowBackButton()); |
| await tester.pumpAndSettle(); |
| expect(find.byType(TestBox), findsNWidgets(7)); |
| expect(findOverflowNextButton(), findsOneWidget); |
| expect(findOverflowBackButton(), findsNothing); |
| |
| // Adding 7 more children overflows onto a third page. |
| setState(() { |
| children.add(const TestBox()); |
| children.add(const TestBox()); |
| children.add(const TestBox()); |
| children.add(const TestBox()); |
| children.add(const TestBox()); |
| children.add(const TestBox()); |
| }); |
| await tester.pumpAndSettle(); |
| expect(find.byType(TestBox), findsNWidgets(7)); |
| expect(findOverflowNextButton(), findsOneWidget); |
| expect(findOverflowBackButton(), findsNothing); |
| |
| // Tap the overflow next button to show the second page of children. |
| await tester.tap(findOverflowNextButton()); |
| await tester.pumpAndSettle(); |
| // With the back button, only six children fit on this page. |
| expect(find.byType(TestBox), findsNWidgets(6)); |
| expect(findOverflowNextButton(), findsOneWidget); |
| expect(findOverflowBackButton(), findsOneWidget); |
| |
| // Tap the overflow next button again to show the third page of children. |
| await tester.tap(findOverflowNextButton()); |
| await tester.pumpAndSettle(); |
| expect(find.byType(TestBox), findsNWidgets(1)); |
| expect(findOverflowNextButton(), findsOneWidget); |
| expect(findOverflowBackButton(), findsOneWidget); |
| |
| // Tap the overflow back button to go back to the second page. |
| await tester.tap(findOverflowBackButton()); |
| await tester.pumpAndSettle(); |
| expect(find.byType(TestBox), findsNWidgets(6)); |
| expect(findOverflowNextButton(), findsOneWidget); |
| expect(findOverflowBackButton(), findsOneWidget); |
| |
| // Tap the overflow back button to go back to the first page. |
| await tester.tap(findOverflowBackButton()); |
| await tester.pumpAndSettle(); |
| expect(find.byType(TestBox), findsNWidgets(7)); |
| expect(findOverflowNextButton(), findsOneWidget); |
| expect(findOverflowBackButton(), findsNothing); |
| }, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web. |
| |
| testWidgets('positions itself at anchorAbove if it fits', (WidgetTester tester) async { |
| late StateSetter setState; |
| const double height = _kToolbarHeight; |
| const double anchorBelowY = 500.0; |
| double anchorAboveY = 0.0; |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setter) { |
| setState = setter; |
| return CupertinoTextSelectionToolbar( |
| anchorAbove: Offset(50.0, anchorAboveY), |
| anchorBelow: const Offset(50.0, anchorBelowY), |
| children: <Widget>[ |
| Container(color: const Color(0xffff0000), width: 50.0, height: height), |
| Container(color: const Color(0xff00ff00), width: 50.0, height: height), |
| Container(color: const Color(0xff0000ff), width: 50.0, height: height), |
| ], |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // When the toolbar doesn't fit above aboveAnchor, it positions itself below |
| // belowAnchor. |
| double toolbarY = tester.getTopLeft(findToolbar()).dy; |
| expect(toolbarY, equals(anchorBelowY + _kToolbarContentDistance)); |
| |
| // Even when it barely doesn't fit. |
| setState(() { |
| anchorAboveY = 50.0; |
| }); |
| await tester.pump(); |
| toolbarY = tester.getTopLeft(findToolbar()).dy; |
| expect(toolbarY, equals(anchorBelowY + _kToolbarContentDistance)); |
| |
| // When it does fit above aboveAnchor, it positions itself there. |
| setState(() { |
| anchorAboveY = 60.0; |
| }); |
| await tester.pump(); |
| toolbarY = tester.getTopLeft(findToolbar()).dy; |
| expect(toolbarY, equals(anchorAboveY - height - _kToolbarContentDistance)); |
| }, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web. |
| |
| testWidgets('can create and use a custom toolbar', (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController( |
| text: 'Select me custom menu', |
| ); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoTextField( |
| controller: controller, |
| selectionControls: _CustomCupertinoTextSelectionControls(), |
| ), |
| ), |
| ), |
| ); |
| |
| // The selection menu is not initially shown. |
| expect(find.text('Custom button'), findsNothing); |
| |
| // Long press on "custom" to select it. |
| final Offset customPos = textOffsetToPosition(tester, 11); |
| final TestGesture gesture = await tester.startGesture(customPos, pointer: 7); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pump(); |
| |
| // The custom selection menu is shown. |
| expect(find.text('Custom button'), findsOneWidget); |
| expect(find.text('Cut'), findsNothing); |
| expect(find.text('Copy'), findsNothing); |
| expect(find.text('Paste'), findsNothing); |
| expect(find.text('Select all'), findsNothing); |
| }, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web. |
| } |