// 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/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/editable_text_utils.dart' show textOffsetToPosition; const double _kToolbarContentDistance = 8.0; // A custom text selection menu that just displays a single custom button. class _CustomMaterialTextSelectionControls extends MaterialTextSelectionControls { @override Widget buildToolbar( BuildContext context, Rect globalEditableRegion, double textLineHeight, Offset selectionMidpoint, List<TextSelectionPoint> endpoints, TextSelectionDelegate delegate, ValueListenable<ClipboardStatus>? clipboardStatus, Offset? lastSecondaryTapDownPosition, ) { final TextSelectionPoint startTextSelectionPoint = endpoints[0]; final TextSelectionPoint endTextSelectionPoint = endpoints.length > 1 ? endpoints[1] : endpoints[0]; final Offset anchorAbove = Offset( globalEditableRegion.left + selectionMidpoint.dx, globalEditableRegion.top + startTextSelectionPoint.point.dy - textLineHeight - _kToolbarContentDistance, ); final Offset anchorBelow = Offset( globalEditableRegion.left + selectionMidpoint.dx, globalEditableRegion.top + endTextSelectionPoint.point.dy + TextSelectionToolbar.kToolbarContentDistanceBelow, ); return TextSelectionToolbar( anchorAbove: anchorAbove, anchorBelow: anchorBelow, children: <Widget>[ TextSelectionToolbarTextButton( padding: TextSelectionToolbarTextButton.getPadding(0, 1), 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(MaterialApp), matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == type), ); } // Finding TextSelectionToolbar 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('_TextSelectionToolbarOverflowable'); Finder findOverflowButton() => findPrivate('_TextSelectionToolbarOverflowButton'); testWidgetsWithLeakTracking('puts children in an overflow menu if they overflow', (WidgetTester tester) async { late StateSetter setState; final List<Widget> children = List<Widget>.generate(7, (int i) => const TestBox()); await tester.pumpWidget( MaterialApp( home: Scaffold( body: StatefulBuilder( builder: (BuildContext context, StateSetter setter) { setState = setter; return TextSelectionToolbar( 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(findOverflowButton(), 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(findOverflowButton(), findsOneWidget); // Tap the overflow button to show the overflow menu. await tester.tap(findOverflowButton()); await tester.pumpAndSettle(); expect(find.byType(TestBox), findsNWidgets(1)); expect(findOverflowButton(), findsOneWidget); // Tap the overflow button again to hide the overflow menu. await tester.tap(findOverflowButton()); await tester.pumpAndSettle(); expect(find.byType(TestBox), findsNWidgets(children.length - 1)); expect(findOverflowButton(), findsOneWidget); }); testWidgets('positions itself at anchorAbove if it fits', (WidgetTester tester) async { late StateSetter setState; const double height = 44.0; const double anchorBelowY = 500.0; double anchorAboveY = 0.0; await tester.pumpWidget( MaterialApp( home: Scaffold( body: StatefulBuilder( builder: (BuildContext context, StateSetter setter) { setState = setter; return TextSelectionToolbar( anchorAbove: Offset(50.0, anchorAboveY), anchorBelow: const Offset(50.0, anchorBelowY), children: <Widget>[ Container(color: Colors.red, width: 50.0, height: height), Container(color: Colors.green, width: 50.0, height: height), Container(color: Colors.blue, 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 + TextSelectionToolbar.kToolbarContentDistanceBelow)); // Even when it barely doesn't fit. setState(() { anchorAboveY = 60.0; }); await tester.pump(); toolbarY = tester.getTopLeft(findToolbar()).dy; expect(toolbarY, equals(anchorBelowY + TextSelectionToolbar.kToolbarContentDistanceBelow)); // When it does fit above aboveAnchor, it positions itself there. setState(() { anchorAboveY = 70.0; }); await tester.pump(); toolbarY = tester.getTopLeft(findToolbar()).dy; expect(toolbarY, equals(anchorAboveY - height - _kToolbarContentDistance)); }); testWidgets('can create and use a custom toolbar', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: SelectableText( 'Select me custom menu', selectionControls: _CustomMaterialTextSelectionControls(), ), ), ), ), ); // 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 don't show the toolbar on the web. for (final ColorScheme colorScheme in <ColorScheme>[ThemeData.light().colorScheme, ThemeData.dark().colorScheme]) { testWidgetsWithLeakTracking('default background color', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( colorScheme: colorScheme, ), home: Scaffold( body: Center( child: TextSelectionToolbar( anchorAbove: Offset.zero, anchorBelow: Offset.zero, children: <Widget>[ TextSelectionToolbarTextButton( padding: TextSelectionToolbarTextButton.getPadding(0, 1), onPressed: () {}, child: const Text('Custom button'), ), ], ), ), ), ), ); Finder findToolbarContainer() { return find.descendant( of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionToolbarContainer'), matching: find.byType(Material), ); } expect(findToolbarContainer(), findsAtLeastNWidgets(1)); final Material toolbarContainer = tester.widget(findToolbarContainer().first); expect( toolbarContainer.color, // The default colors are hardcoded and don't take the default value of // the theme's surface color. switch (colorScheme.brightness) { Brightness.light => const Color(0xffffffff), Brightness.dark => const Color(0xff424242), }, ); }); testWidgetsWithLeakTracking('custom background color', (WidgetTester tester) async { const Color customBackgroundColor = Colors.red; await tester.pumpWidget( MaterialApp( theme: ThemeData( colorScheme: colorScheme.copyWith( surface: customBackgroundColor, ), ), home: Scaffold( body: Center( child: TextSelectionToolbar( anchorAbove: Offset.zero, anchorBelow: Offset.zero, children: <Widget>[ TextSelectionToolbarTextButton( padding: TextSelectionToolbarTextButton.getPadding(0, 1), onPressed: () {}, child: const Text('Custom button'), ), ], ), ), ), ), ); Finder findToolbarContainer() { return find.descendant( of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionToolbarContainer'), matching: find.byType(Material), ); } expect(findToolbarContainer(), findsAtLeastNWidgets(1)); final Material toolbarContainer = tester.widget(findToolbarContainer().first); expect( toolbarContainer.color, customBackgroundColor, ); }); } }