mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
hasStrings support for eliminating clipboard notifications (#87678)
Use the hasStrings clipboard method when possible instead of reading the contents of the clipboard.
This commit is contained in:
parent
b2adffa9fe
commit
91ef92d2b2
@ -59,4 +59,20 @@ class Clipboard {
|
||||
return null;
|
||||
return ClipboardData(text: result['text'] as String?);
|
||||
}
|
||||
|
||||
/// Returns a future that resolves to true iff the clipboard contains string
|
||||
/// data.
|
||||
///
|
||||
/// See also:
|
||||
/// * [The iOS hasStrings method](https://developer.apple.com/documentation/uikit/uipasteboard/1829416-hasstrings?language=objc).
|
||||
static Future<bool> hasStrings() async {
|
||||
final Map<String, dynamic>? result = await SystemChannels.platform.invokeMethod(
|
||||
'Clipboard.hasStrings',
|
||||
Clipboard.kTextPlain,
|
||||
);
|
||||
if (result == null) {
|
||||
return false;
|
||||
}
|
||||
return result['value'] as bool;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1579,28 +1579,13 @@ class ClipboardStatusNotifier extends ValueNotifier<ClipboardStatus> with Widget
|
||||
|
||||
/// Check the [Clipboard] and update [value] if needed.
|
||||
Future<void> update() async {
|
||||
// iOS 14 added a notification that appears when an app accesses the
|
||||
// clipboard. To avoid the notification, don't access the clipboard on iOS,
|
||||
// and instead always show the paste button, even when the clipboard is
|
||||
// empty.
|
||||
// TODO(justinmc): Use the new iOS 14 clipboard API method hasStrings that
|
||||
// won't trigger the notification.
|
||||
// https://github.com/flutter/flutter/issues/60145
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.iOS:
|
||||
value = ClipboardStatus.pasteable;
|
||||
return;
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.windows:
|
||||
break;
|
||||
if (_disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClipboardData? data;
|
||||
final bool hasStrings;
|
||||
try {
|
||||
data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
hasStrings = await Clipboard.hasStrings();
|
||||
} catch (stacktrace) {
|
||||
// In the case of an error from the Clipboard API, set the value to
|
||||
// unknown so that it will try to update again later.
|
||||
@ -1611,13 +1596,14 @@ class ClipboardStatusNotifier extends ValueNotifier<ClipboardStatus> with Widget
|
||||
return;
|
||||
}
|
||||
|
||||
final ClipboardStatus clipboardStatus = data != null && data.text != null && data.text!.isNotEmpty
|
||||
final ClipboardStatus nextStatus = hasStrings
|
||||
? ClipboardStatus.pasteable
|
||||
: ClipboardStatus.notPasteable;
|
||||
if (_disposed || clipboardStatus == value) {
|
||||
|
||||
if (_disposed || nextStatus == value) {
|
||||
return;
|
||||
}
|
||||
value = clipboardStatus;
|
||||
value = nextStatus;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -18,27 +18,12 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
import '../widgets/clipboard_utils.dart';
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
// On web, the context menu (aka toolbar) is provided by the browser.
|
||||
final bool isContextMenuProvidedByPlatform = isBrowser;
|
||||
|
||||
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 MockTextSelectionControls extends TextSelectionControls {
|
||||
@override
|
||||
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {
|
||||
|
||||
@ -11,24 +11,9 @@ import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../widgets/clipboard_utils.dart';
|
||||
import '../widgets/editable_text_utils.dart' show textOffsetToPosition, findRenderEditable;
|
||||
|
||||
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();
|
||||
|
||||
@ -69,7 +54,6 @@ const _LongCupertinoLocalizations _longLocalizations = _LongCupertinoLocalizatio
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final MockClipboard mockClipboard = MockClipboard();
|
||||
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);
|
||||
|
||||
// Returns true iff the button is visually enabled.
|
||||
bool appearsEnabled(WidgetTester tester, String text) {
|
||||
@ -92,6 +76,23 @@ void main() {
|
||||
}).toList();
|
||||
}
|
||||
|
||||
setUp(() async {
|
||||
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
mockClipboard.handleMethodCall,
|
||||
);
|
||||
// Fill the clipboard so that the Paste option is available in the text
|
||||
// selection menu.
|
||||
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
group('canSelectAll', () {
|
||||
Widget createEditableText({
|
||||
Key? key,
|
||||
@ -185,76 +186,6 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
// 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();
|
||||
expect(controller.selection.isCollapsed, isFalse);
|
||||
expect(controller.selection.baseOffset, 0);
|
||||
expect(controller.selection.extentOffset, 7);
|
||||
|
||||
// Paste is showing even though clipboard is empty.
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
expect(find.text('Copy'), findsOneWidget);
|
||||
expect(find.text('Cut'), findsOneWidget);
|
||||
expect(find.descendant(
|
||||
of: find.byType(Overlay),
|
||||
matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionHandleOverlay'),
|
||||
), findsNWidgets(2));
|
||||
|
||||
// Tap copy to add something to the clipboard and close the menu.
|
||||
await tester.tapAt(tester.getCenter(find.text('Copy')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The menu is gone, but the handles are visible on the existing selection.
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
expect(controller.selection.isCollapsed, isFalse);
|
||||
expect(controller.selection.baseOffset, 0);
|
||||
expect(controller.selection.extentOffset, 7);
|
||||
expect(find.descendant(
|
||||
of: find.byType(Overlay),
|
||||
matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionHandleOverlay'),
|
||||
), findsNWidgets(2));
|
||||
|
||||
// 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, // [intended] 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');
|
||||
|
||||
@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../widgets/clipboard_utils.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final MockClipboard mockClipboard = MockClipboard();
|
||||
@ -301,19 +303,3 @@ void main() {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
class MockClipboard {
|
||||
dynamic _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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,24 +7,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../widgets/clipboard_utils.dart';
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
class MockClipboard {
|
||||
dynamic _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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final MockClipboard mockClipboard = MockClipboard();
|
||||
|
||||
@ -19,6 +19,7 @@ import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../widgets/clipboard_utils.dart';
|
||||
import '../widgets/editable_text_utils.dart' show findRenderEditable, globalize, textOffsetToPosition;
|
||||
import '../widgets/semantics_tester.dart';
|
||||
import 'feedback_tester.dart';
|
||||
@ -31,22 +32,6 @@ final bool isContextMenuProvidedByPlatform = isBrowser;
|
||||
// On web, key events in text fields are handled by the browser.
|
||||
final bool areKeyEventsHandledByPlatform = isBrowser;
|
||||
|
||||
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 MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
|
||||
@override
|
||||
bool isSupported(Locale locale) => true;
|
||||
@ -6220,8 +6205,8 @@ void main() {
|
||||
|
||||
semantics.dispose();
|
||||
|
||||
// On web (just like iOS), we don't check for pasteability because that
|
||||
// triggers a permission dialog in the browser.
|
||||
// On web, we don't check for pasteability because that triggers a
|
||||
// permission dialog in the browser.
|
||||
// https://github.com/flutter/flutter/pull/57139#issuecomment-629048058
|
||||
}, skip: isBrowser); // [intended] see above.
|
||||
|
||||
@ -9550,11 +9535,19 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
bool triedToReadClipboard = false;
|
||||
bool calledGetData = false;
|
||||
bool calledHasStrings = false;
|
||||
tester.binding.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||
if (methodCall.method == 'Clipboard.getData') {
|
||||
triedToReadClipboard = true;
|
||||
switch (methodCall.method) {
|
||||
case 'Clipboard.getData':
|
||||
calledGetData = true;
|
||||
break;
|
||||
case 'Clipboard.hasStrings':
|
||||
calledHasStrings = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@ -9567,14 +9560,16 @@ void main() {
|
||||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||||
await tester.pump();
|
||||
|
||||
// getData is not called unless something is pasted. hasStrings is used to
|
||||
// check the status of the clipboard.
|
||||
expect(calledGetData, false);
|
||||
if (kIsWeb) {
|
||||
// The clipboard is not checked because it requires user permissions and
|
||||
// web doesn't show a custom text selection menu.
|
||||
expect(triedToReadClipboard, false);
|
||||
// hasStrings is not checked because web doesn't show a custom text
|
||||
// selection menu.
|
||||
expect(calledHasStrings, false);
|
||||
} else {
|
||||
// The clipboard is checked in order to decide if the content can be
|
||||
// pasted.
|
||||
expect(triedToReadClipboard, true);
|
||||
// hasStrings is checked in order to decide if the content can be pasted.
|
||||
expect(calledHasStrings, true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -11,24 +11,9 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
import '../widgets/clipboard_utils.dart';
|
||||
import '../widgets/editable_text_utils.dart';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final MockClipboard mockClipboard = MockClipboard();
|
||||
|
||||
@ -7,33 +7,31 @@ import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../widgets/clipboard_utils.dart';
|
||||
import '../widgets/editable_text_utils.dart' show findRenderEditable, globalize, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final MockClipboard mockClipboard = MockClipboard();
|
||||
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);
|
||||
|
||||
setUp(() async {
|
||||
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
mockClipboard.handleMethodCall,
|
||||
);
|
||||
// Fill the clipboard so that the Paste option is available in the text
|
||||
// selection menu.
|
||||
await Clipboard.setData(const ClipboardData(text: 'clipboard data'));
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
group('canSelectAll', () {
|
||||
Widget createEditableText({
|
||||
required Key key,
|
||||
@ -671,59 +669,4 @@ void main() {
|
||||
skip: isBrowser, // [intended] we don't supply the cut/copy/paste buttons on the web.
|
||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android })
|
||||
);
|
||||
|
||||
// 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(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Make sure the clipboard is empty.
|
||||
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('Copy'), findsOneWidget);
|
||||
expect(find.text('Cut'), findsOneWidget);
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
},
|
||||
skip: isBrowser, // [intended] we don't supply the cut/copy/paste buttons on the web.
|
||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })
|
||||
);
|
||||
}
|
||||
|
||||
33
packages/flutter/test/widgets/clipboard_utils.dart
Normal file
33
packages/flutter/test/widgets/clipboard_utils.dart
Normal file
@ -0,0 +1,33 @@
|
||||
// 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/services.dart';
|
||||
|
||||
class MockClipboard {
|
||||
MockClipboard({
|
||||
this.hasStringsThrows = false,
|
||||
});
|
||||
|
||||
final bool hasStringsThrows;
|
||||
|
||||
dynamic _clipboardData = <String, dynamic>{
|
||||
'text': null,
|
||||
};
|
||||
|
||||
Future<Object?> handleMethodCall(MethodCall methodCall) async {
|
||||
switch (methodCall.method) {
|
||||
case 'Clipboard.getData':
|
||||
return _clipboardData;
|
||||
case 'Clipboard.hasStrings':
|
||||
if (hasStringsThrows)
|
||||
throw Exception();
|
||||
final Map<String, dynamic>? clipboardDataMap = _clipboardData as Map<String, dynamic>?;
|
||||
final String? text = clipboardDataMap?['text'] as String?;
|
||||
return <String, bool>{'value': text != null && text.isNotEmpty};
|
||||
case 'Clipboard.setData':
|
||||
_clipboardData = methodCall.arguments;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,6 +82,8 @@ void main() {
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||
if (methodCall.method == 'Clipboard.getData')
|
||||
return const <String, dynamic>{'text': clipboardContent};
|
||||
if (methodCall.method == 'Clipboard.hasStrings')
|
||||
return <String, dynamic>{'value': clipboardContent.isNotEmpty};
|
||||
return null;
|
||||
});
|
||||
|
||||
@ -134,6 +136,8 @@ void main() {
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||
if (methodCall.method == 'Clipboard.getData')
|
||||
return const <String, dynamic>{'text': clipboardContent};
|
||||
if (methodCall.method == 'Clipboard.hasStrings')
|
||||
return <String, dynamic>{'value': clipboardContent.isNotEmpty};
|
||||
return null;
|
||||
});
|
||||
|
||||
@ -860,6 +864,8 @@ void main() {
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||
if (methodCall.method == 'Clipboard.getData')
|
||||
return const <String, dynamic>{'text': clipboardContent};
|
||||
if (methodCall.method == 'Clipboard.hasStrings')
|
||||
return <String, dynamic>{'value': clipboardContent.isNotEmpty};
|
||||
return null;
|
||||
});
|
||||
|
||||
@ -918,6 +924,8 @@ void main() {
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||
if (methodCall.method == 'Clipboard.getData')
|
||||
return const <String, dynamic>{'text': clipboardContent};
|
||||
if (methodCall.method == 'Clipboard.hasStrings')
|
||||
return <String, dynamic>{'value': clipboardContent.isNotEmpty};
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
import '../widgets/clipboard_utils.dart';
|
||||
import 'editable_text_utils.dart';
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
@ -48,22 +49,6 @@ enum HandlePositionInViewport {
|
||||
leftEdge, rightEdge, within,
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
final MockClipboard mockClipboard = MockClipboard();
|
||||
(TestWidgetsFlutterBinding.ensureInitialized() as TestWidgetsFlutterBinding)
|
||||
@ -1336,6 +1321,56 @@ void main() {
|
||||
expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Paste is shown only when there is something to paste', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: EditableText(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: cursorColor,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final EditableTextState state =
|
||||
tester.state<EditableTextState>(find.byType(EditableText));
|
||||
|
||||
// Make sure the clipboard has a valid string on it.
|
||||
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||||
|
||||
// Show the toolbar.
|
||||
state.renderEditable.selectWordsInRange(
|
||||
from: Offset.zero,
|
||||
cause: SelectionChangedCause.tap,
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
// The Paste button is shown (except on web, which doesn't show the Flutter
|
||||
// toolbar).
|
||||
expect(state.showToolbar(), kIsWeb ? isFalse : isTrue);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget);
|
||||
|
||||
// Hide the menu again.
|
||||
state.hideToolbar();
|
||||
await tester.pump();
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
|
||||
// Clear the clipboard
|
||||
await Clipboard.setData(const ClipboardData(text: ''));
|
||||
|
||||
// Show the toolbar again.
|
||||
expect(state.showToolbar(), kIsWeb ? isFalse : isTrue);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Paste is not shown.
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('can show the toolbar after clearing all text', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/35998.
|
||||
await tester.pumpWidget(
|
||||
|
||||
@ -13,25 +13,10 @@ import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../widgets/clipboard_utils.dart';
|
||||
import '../widgets/editable_text_utils.dart' show textOffsetToPosition;
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
class MockClipboard {
|
||||
dynamic _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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
|
||||
@override
|
||||
bool isSupported(Locale locale) => true;
|
||||
@ -126,7 +111,6 @@ double getOpacity(WidgetTester tester, Finder finder) {
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final MockClipboard mockClipboard = MockClipboard();
|
||||
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);
|
||||
|
||||
const String kThreeLines =
|
||||
'First line of text is\n'
|
||||
@ -165,11 +149,22 @@ void main() {
|
||||
|
||||
setUp(() async {
|
||||
debugResetSemanticsIdCounter();
|
||||
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
mockClipboard.handleMethodCall,
|
||||
);
|
||||
// Fill the clipboard so that the Paste option is available in the text
|
||||
// selection menu.
|
||||
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
Widget selectableTextBuilder({
|
||||
String text = '',
|
||||
int? maxLines = 1,
|
||||
|
||||
@ -8,29 +8,7 @@ import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class MockClipboard {
|
||||
MockClipboard({
|
||||
this.getDataThrows = false,
|
||||
});
|
||||
|
||||
final bool getDataThrows;
|
||||
|
||||
dynamic _clipboardData = <String, dynamic>{
|
||||
'text': null,
|
||||
};
|
||||
|
||||
Future<Object?> handleMethodCall(MethodCall methodCall) async {
|
||||
switch (methodCall.method) {
|
||||
case 'Clipboard.getData':
|
||||
if (getDataThrows)
|
||||
throw Exception();
|
||||
return _clipboardData;
|
||||
case 'Clipboard.setData':
|
||||
_clipboardData = methodCall.arguments;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
import 'clipboard_utils.dart';
|
||||
|
||||
void main() {
|
||||
late int tapCount;
|
||||
@ -757,7 +735,7 @@ void main() {
|
||||
group('ClipboardStatusNotifier', () {
|
||||
group('when Clipboard fails', () {
|
||||
setUp(() {
|
||||
final MockClipboard mockClipboard = MockClipboard(getDataThrows: true);
|
||||
final MockClipboard mockClipboard = MockClipboard(hasStringsThrows: true);
|
||||
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user