| // 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/material.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import 'clipboard_utils.dart'; |
| import 'editable_text_utils.dart'; |
| |
| void main() { |
| final MockClipboard mockClipboard = MockClipboard(); |
| TestWidgetsFlutterBinding.ensureInitialized() |
| .defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall); |
| |
| setUp(() async { |
| // Fill the clipboard so that the Paste option is available in the text |
| // selection menu. |
| await Clipboard.setData(const ClipboardData(text: 'Clipboard data')); |
| }); |
| |
| testWidgets('Hides and shows only a single menu', (WidgetTester tester) async { |
| final GlobalKey key1 = GlobalKey(); |
| final GlobalKey key2 = GlobalKey(); |
| late final BuildContext context; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext localContext) { |
| context = localContext; |
| return const SizedBox.shrink(); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byKey(key1), findsNothing); |
| expect(find.byKey(key2), findsNothing); |
| |
| final ContextMenuController controller1 = ContextMenuController(); |
| await tester.pump(); |
| expect(find.byKey(key1), findsNothing); |
| expect(find.byKey(key2), findsNothing); |
| |
| controller1.show( |
| context: context, |
| contextMenuBuilder: (BuildContext context) { |
| return Placeholder(key: key1); |
| }, |
| ); |
| await tester.pump(); |
| |
| expect(find.byKey(key1), findsOneWidget); |
| expect(find.byKey(key2), findsNothing); |
| |
| // Showing the same thing again does nothing and is not an error. |
| controller1.show( |
| context: context, |
| contextMenuBuilder: (BuildContext context) { |
| return Placeholder(key: key1); |
| }, |
| ); |
| await tester.pump(); |
| |
| expect(tester.takeException(), null); |
| expect(find.byKey(key1), findsOneWidget); |
| expect(find.byKey(key2), findsNothing); |
| |
| // Showing a new menu hides the first. |
| final ContextMenuController controller2 = ContextMenuController(); |
| controller2.show( |
| context: context, |
| contextMenuBuilder: (BuildContext context) { |
| return Placeholder(key: key2); |
| }, |
| ); |
| await tester.pump(); |
| |
| expect(find.byKey(key1), findsNothing); |
| expect(find.byKey(key2), findsOneWidget); |
| |
| controller2.remove(); |
| await tester.pump(); |
| |
| expect(find.byKey(key1), findsNothing); |
| expect(find.byKey(key2), findsNothing); |
| }); |
| |
| testWidgets('A menu can be hidden and then reshown', (WidgetTester tester) async { |
| final GlobalKey key1 = GlobalKey(); |
| late final BuildContext context; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext localContext) { |
| context = localContext; |
| return const SizedBox.shrink(); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byKey(key1), findsNothing); |
| |
| final ContextMenuController controller = ContextMenuController(); |
| addTearDown(controller.remove); |
| |
| // Instantiating the controller does not shown it. |
| await tester.pump(); |
| expect(find.byKey(key1), findsNothing); |
| |
| controller.show( |
| context: context, |
| contextMenuBuilder: (BuildContext context) { |
| return Placeholder(key: key1); |
| }, |
| ); |
| await tester.pump(); |
| |
| expect(find.byKey(key1), findsOneWidget); |
| |
| controller.remove(); |
| await tester.pump(); |
| |
| expect(find.byKey(key1), findsNothing); |
| |
| controller.show( |
| context: context, |
| contextMenuBuilder: (BuildContext context) { |
| return Placeholder(key: key1); |
| }, |
| ); |
| await tester.pump(); |
| |
| expect(find.byKey(key1), findsOneWidget); |
| }); |
| |
| testWidgets('markNeedsBuild causes the builder to update', (WidgetTester tester) async { |
| int buildCount = 0; |
| late final BuildContext context; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext localContext) { |
| context = localContext; |
| return const SizedBox.shrink(); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| final ContextMenuController controller = ContextMenuController(); |
| controller.show( |
| context: context, |
| contextMenuBuilder: (BuildContext context) { |
| buildCount++; |
| return const Placeholder(); |
| }, |
| ); |
| expect(buildCount, 0); |
| await tester.pump(); |
| expect(buildCount, 1); |
| |
| controller.markNeedsBuild(); |
| expect(buildCount, 1); |
| await tester.pump(); |
| expect(buildCount, 2); |
| |
| controller.remove(); |
| }); |
| |
| testWidgets('Calling show when a built-in widget is already showing its context menu hides the built-in menu', (WidgetTester tester) async { |
| final GlobalKey builtInKey = GlobalKey(); |
| final GlobalKey directKey = GlobalKey(); |
| late final BuildContext context; |
| |
| final TextEditingController textEditingController = TextEditingController(); |
| addTearDown(textEditingController.dispose); |
| |
| final FocusNode focusNode = FocusNode(); |
| addTearDown(focusNode.dispose); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext localContext) { |
| context = localContext; |
| return EditableText( |
| controller: textEditingController, |
| backgroundCursorColor: Colors.grey, |
| focusNode: focusNode, |
| style: const TextStyle(), |
| cursorColor: Colors.red, |
| selectionControls: materialTextSelectionHandleControls, |
| contextMenuBuilder: ( |
| BuildContext context, |
| EditableTextState editableTextState, |
| ) { |
| return Placeholder(key: builtInKey); |
| }, |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byKey(builtInKey), findsNothing); |
| expect(find.byKey(directKey), findsNothing); |
| |
| final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); |
| |
| await tester.tapAt(textOffsetToPosition(tester, 0)); |
| await tester.pump(); |
| expect(state.showToolbar(), true); |
| await tester.pump(); |
| |
| expect(find.byKey(builtInKey), findsOneWidget); |
| expect(find.byKey(directKey), findsNothing); |
| |
| final ContextMenuController controller = ContextMenuController(); |
| controller.show( |
| context: context, |
| contextMenuBuilder: (BuildContext context) { |
| return Placeholder(key: directKey); |
| }, |
| ); |
| await tester.pump(); |
| |
| expect(find.byKey(builtInKey), findsNothing); |
| expect(find.byKey(directKey), findsOneWidget); |
| expect(controller.isShown, isTrue); |
| |
| // And showing the built-in menu hides the directly shown menu. |
| expect(state.showToolbar(), isTrue); |
| await tester.pump(); |
| |
| expect(find.byKey(builtInKey), findsOneWidget); |
| expect(find.byKey(directKey), findsNothing); |
| expect(controller.isShown, isFalse); |
| |
| // Calling remove on the hidden ContextMenuController does not hide the |
| // built-in menu. |
| controller.remove(); |
| await tester.pump(); |
| |
| expect(find.byKey(builtInKey), findsOneWidget); |
| expect(find.byKey(directKey), findsNothing); |
| expect(controller.isShown, isFalse); |
| |
| state.hideToolbar(); |
| await tester.pump(); |
| expect(find.byKey(builtInKey), findsNothing); |
| expect(find.byKey(directKey), findsNothing); |
| expect(controller.isShown, isFalse); |
| }, |
| skip: isContextMenuProvidedByPlatform, // [intended] no Flutter-drawn text selection toolbar on web. |
| ); |
| } |