// 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/rendering.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({Key? key}) : super(key: key, 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); 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); 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); }