mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
<!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> This PR implements support for custom action items in the native edit menu on iOS, with changes to both framework and engine. This PR will be updated incrementally until the full feature is complete. Phase 1: Add hardcoded custom menu item for iOS edit menu. Phase 2: Add Framework API for custom iOS context menu items. Phase 3: Add Optimization, Testing, and Documentation. Part of https://github.com/flutter/flutter/issues/103163 Part of https://github.com/flutter/flutter/issues/140184 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1341 lines
51 KiB
Dart
1341 lines
51 KiB
Dart
// 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/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../system_context_menu_utils.dart';
|
|
|
|
void main() {
|
|
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
testWidgets(
|
|
'asserts when built on an unsupported device',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(text: 'one two three');
|
|
addTearDown(controller.dispose);
|
|
await tester.pumpWidget(
|
|
// By default, MediaQueryData.supportsShowingSystemContextMenu is false.
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: TextField(
|
|
controller: controller,
|
|
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(editableTextState: editableTextState);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
|
expect(state.showToolbar(), true);
|
|
await tester.pump();
|
|
|
|
expect(tester.takeException(), isAssertionError);
|
|
},
|
|
skip: kIsWeb, // [intended]
|
|
variant: TargetPlatformVariant.all(),
|
|
);
|
|
|
|
testWidgets(
|
|
'asserts when built on web',
|
|
(WidgetTester tester) async {
|
|
// Disable the browser context menu so that contextMenuBuilder will be used.
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.contextMenu,
|
|
(MethodCall call) {
|
|
// Just complete successfully, so that BrowserContextMenu thinks that
|
|
// the engine successfully received its call.
|
|
return Future<void>.value();
|
|
},
|
|
);
|
|
await BrowserContextMenu.disableContextMenu();
|
|
addTearDown(() async {
|
|
await BrowserContextMenu.enableContextMenu();
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.contextMenu,
|
|
null,
|
|
);
|
|
});
|
|
|
|
final TextEditingController controller = TextEditingController(text: 'one two three');
|
|
addTearDown(controller.dispose);
|
|
await tester.pumpWidget(
|
|
// By default, MediaQueryData.supportsShowingSystemContextMenu is false.
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: TextField(
|
|
controller: controller,
|
|
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(editableTextState: editableTextState);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
|
expect(state.showToolbar(), true);
|
|
await tester.pump();
|
|
|
|
expect(tester.takeException(), isAssertionError);
|
|
},
|
|
skip: !kIsWeb, // [intended]
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'can be shown and hidden like a normal context menu',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(text: 'one two three');
|
|
addTearDown(controller.dispose);
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: TextField(
|
|
controller: controller,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
|
expect(state.showToolbar(), true);
|
|
await tester.pump();
|
|
expect(find.byType(SystemContextMenu), findsOneWidget);
|
|
|
|
state.hideToolbar();
|
|
await tester.pump();
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
},
|
|
skip: kIsWeb, // [intended]
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'can customize the menu items',
|
|
(WidgetTester tester) async {
|
|
final List<List<IOSSystemContextMenuItemData>> itemsReceived =
|
|
<List<IOSSystemContextMenuItemData>>[];
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
(MethodCall methodCall) async {
|
|
switch (methodCall.method) {
|
|
case 'ContextMenu.showSystemContextMenu':
|
|
final Map<String, dynamic> arguments = methodCall.arguments as Map<String, dynamic>;
|
|
final List<dynamic> untypedItems = arguments['items'] as List<dynamic>;
|
|
final List<IOSSystemContextMenuItemData> lastItems = untypedItems.map((
|
|
dynamic value,
|
|
) {
|
|
final Map<String, dynamic> itemJson = value as Map<String, dynamic>;
|
|
return systemContextMenuItemDataFromJson(itemJson);
|
|
}).toList();
|
|
itemsReceived.add(lastItems);
|
|
}
|
|
return;
|
|
},
|
|
);
|
|
addTearDown(() {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
null,
|
|
);
|
|
});
|
|
|
|
const List<IOSSystemContextMenuItem> items1 = <IOSSystemContextMenuItem>[
|
|
IOSSystemContextMenuItemCopy(),
|
|
IOSSystemContextMenuItemShare(title: 'My Share Title'),
|
|
IOSSystemContextMenuItemLiveText(),
|
|
];
|
|
final TextEditingController controller = TextEditingController(text: 'one two three');
|
|
addTearDown(controller.dispose);
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: TextField(
|
|
controller: controller,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
items: items1,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
expect(itemsReceived, hasLength(0));
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
|
expect(state.showToolbar(), true);
|
|
await tester.pump();
|
|
expect(find.byType(SystemContextMenu), findsOneWidget);
|
|
|
|
expect(itemsReceived, hasLength(1));
|
|
expect(itemsReceived.last, hasLength(items1.length));
|
|
expect(itemsReceived.last[0], equals(const IOSSystemContextMenuItemDataCopy()));
|
|
expect(
|
|
itemsReceived.last[1],
|
|
equals(const IOSSystemContextMenuItemDataShare(title: 'My Share Title')),
|
|
);
|
|
expect(itemsReceived.last[2], equals(const IOSSystemContextMenuItemDataLiveText()));
|
|
|
|
state.hideToolbar();
|
|
await tester.pump();
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
},
|
|
skip: kIsWeb, // [intended]
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets(
|
|
"passing empty items builds the widget but doesn't show the system context menu",
|
|
(WidgetTester tester) async {
|
|
final List<List<IOSSystemContextMenuItemData>> itemsReceived =
|
|
<List<IOSSystemContextMenuItemData>>[];
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
(MethodCall methodCall) async {
|
|
switch (methodCall.method) {
|
|
case 'ContextMenu.showSystemContextMenu':
|
|
final Map<String, dynamic> arguments = methodCall.arguments as Map<String, dynamic>;
|
|
final List<dynamic> untypedItems = arguments['items'] as List<dynamic>;
|
|
final List<IOSSystemContextMenuItemData> lastItems = untypedItems.map((
|
|
dynamic value,
|
|
) {
|
|
final Map<String, dynamic> itemJson = value as Map<String, dynamic>;
|
|
return systemContextMenuItemDataFromJson(itemJson);
|
|
}).toList();
|
|
itemsReceived.add(lastItems);
|
|
}
|
|
return;
|
|
},
|
|
);
|
|
addTearDown(() {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
null,
|
|
);
|
|
});
|
|
|
|
const List<IOSSystemContextMenuItem> items1 = <IOSSystemContextMenuItem>[];
|
|
final TextEditingController controller = TextEditingController(text: 'one two three');
|
|
addTearDown(controller.dispose);
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: TextField(
|
|
controller: controller,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
items: items1,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
expect(itemsReceived, hasLength(0));
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
|
expect(state.showToolbar(), true);
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await tester.pump();
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.byType(SystemContextMenu), findsOneWidget);
|
|
expect(itemsReceived, hasLength(0));
|
|
},
|
|
skip: kIsWeb, // [intended]
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'items receive a default title',
|
|
(WidgetTester tester) async {
|
|
final List<List<IOSSystemContextMenuItemData>> itemsReceived =
|
|
<List<IOSSystemContextMenuItemData>>[];
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
(MethodCall methodCall) async {
|
|
switch (methodCall.method) {
|
|
case 'ContextMenu.showSystemContextMenu':
|
|
final Map<String, dynamic> arguments = methodCall.arguments as Map<String, dynamic>;
|
|
final List<dynamic> untypedItems = arguments['items'] as List<dynamic>;
|
|
final List<IOSSystemContextMenuItemData> lastItems = untypedItems.map((
|
|
dynamic value,
|
|
) {
|
|
final Map<String, dynamic> itemJson = value as Map<String, dynamic>;
|
|
return systemContextMenuItemDataFromJson(itemJson);
|
|
}).toList();
|
|
itemsReceived.add(lastItems);
|
|
}
|
|
return;
|
|
},
|
|
);
|
|
addTearDown(() {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
null,
|
|
);
|
|
});
|
|
|
|
const List<IOSSystemContextMenuItem> items1 = <IOSSystemContextMenuItem>[
|
|
// Copy gets no title, it's set by the platform.
|
|
IOSSystemContextMenuItemCopy(),
|
|
// Share could take a title, but if not, it gets a localized default.
|
|
IOSSystemContextMenuItemShare(),
|
|
];
|
|
final TextEditingController controller = TextEditingController(text: 'one two three');
|
|
addTearDown(controller.dispose);
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: TextField(
|
|
controller: controller,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
items: items1,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
expect(itemsReceived, hasLength(0));
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
|
expect(state.showToolbar(), true);
|
|
await tester.pump();
|
|
expect(find.byType(SystemContextMenu), findsOneWidget);
|
|
|
|
expect(itemsReceived, hasLength(1));
|
|
expect(itemsReceived.last, hasLength(items1.length));
|
|
expect(itemsReceived.last[0], equals(const IOSSystemContextMenuItemDataCopy()));
|
|
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
|
|
expect(
|
|
itemsReceived.last[1],
|
|
equals(IOSSystemContextMenuItemDataShare(title: localizations.shareButtonLabel)),
|
|
);
|
|
|
|
state.hideToolbar();
|
|
await tester.pump();
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
},
|
|
skip: kIsWeb, // [intended]
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'can be updated.',
|
|
(WidgetTester tester) async {
|
|
final List<Map<String, double>> targetRects = <Map<String, double>>[];
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
(MethodCall methodCall) async {
|
|
if (methodCall.method == 'ContextMenu.showSystemContextMenu') {
|
|
final Map<String, dynamic> arguments = methodCall.arguments as Map<String, dynamic>;
|
|
final Map<String, dynamic> untypedTargetRect =
|
|
arguments['targetRect'] as Map<String, dynamic>;
|
|
final Map<String, double> lastTargetRect = untypedTargetRect.map((
|
|
String key,
|
|
dynamic value,
|
|
) {
|
|
return MapEntry<String, double>(key, value as double);
|
|
});
|
|
targetRects.add(lastTargetRect);
|
|
}
|
|
return;
|
|
},
|
|
);
|
|
addTearDown(() {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
null,
|
|
);
|
|
});
|
|
|
|
final TextEditingController controller = TextEditingController(text: 'one two three');
|
|
addTearDown(controller.dispose);
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: TextField(
|
|
controller: controller,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(targetRects, isEmpty);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
|
expect(state.showToolbar(), true);
|
|
await tester.pump();
|
|
|
|
expect(targetRects, hasLength(1));
|
|
expect(targetRects.last, containsPair('width', 0.0));
|
|
|
|
controller.selection = const TextSelection(baseOffset: 4, extentOffset: 7);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(targetRects, hasLength(2));
|
|
expect(targetRects.last['width'], greaterThan(0.0));
|
|
},
|
|
skip: kIsWeb, // [intended]
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'can be rebuilt',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(text: 'one two three');
|
|
addTearDown(controller.dispose);
|
|
late StateSetter setState;
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter localSetState) {
|
|
setState = localSetState;
|
|
return TextField(
|
|
controller: controller,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
|
expect(state.showToolbar(), true);
|
|
await tester.pump();
|
|
|
|
setState(() {});
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
skip: kIsWeb, // [intended]
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'can handle multiple instances',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller1 = TextEditingController(text: 'one two three');
|
|
addTearDown(controller1.dispose);
|
|
final TextEditingController controller2 = TextEditingController(text: 'four five six');
|
|
addTearDown(controller2.dispose);
|
|
final GlobalKey field1Key = GlobalKey();
|
|
final GlobalKey field2Key = GlobalKey();
|
|
final GlobalKey menu1Key = GlobalKey();
|
|
final GlobalKey menu2Key = GlobalKey();
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: Column(
|
|
children: <Widget>[
|
|
TextField(
|
|
key: field1Key,
|
|
controller: controller1,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
key: menu1Key,
|
|
editableTextState: editableTextState,
|
|
);
|
|
},
|
|
),
|
|
TextField(
|
|
key: field2Key,
|
|
controller: controller2,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
key: menu2Key,
|
|
editableTextState: editableTextState,
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
|
|
await tester.tap(find.byKey(field1Key));
|
|
final EditableTextState state1 = tester.state<EditableTextState>(
|
|
find.descendant(of: find.byKey(field1Key), matching: find.byType(EditableText)),
|
|
);
|
|
expect(state1.showToolbar(), true);
|
|
await tester.pump();
|
|
expect(find.byKey(menu1Key), findsOneWidget);
|
|
expect(find.byKey(menu2Key), findsNothing);
|
|
|
|
// In a real app, this message is sent by iOS when the user taps anywhere
|
|
// outside of the system context menu.
|
|
final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
|
|
'method': 'ContextMenu.onDismissSystemContextMenu',
|
|
});
|
|
await binding.defaultBinaryMessenger.handlePlatformMessage(
|
|
'flutter/platform',
|
|
messageBytes,
|
|
(ByteData? data) {},
|
|
);
|
|
await tester.pump();
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
|
|
await tester.tap(find.byKey(field2Key));
|
|
final EditableTextState state2 = tester.state<EditableTextState>(
|
|
find.descendant(of: find.byKey(field2Key), matching: find.byType(EditableText)),
|
|
);
|
|
expect(state2.showToolbar(), true);
|
|
await tester.pump();
|
|
expect(find.byKey(menu1Key), findsNothing);
|
|
expect(find.byKey(menu2Key), findsOneWidget);
|
|
},
|
|
skip: kIsWeb, // [intended]
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
test(
|
|
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemCopy',
|
|
() {
|
|
const IOSSystemContextMenuItemCopy item = IOSSystemContextMenuItemCopy();
|
|
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
|
|
expect(item.getData(localizations), const IOSSystemContextMenuItemDataCopy());
|
|
},
|
|
);
|
|
|
|
test(
|
|
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemCut',
|
|
() {
|
|
const IOSSystemContextMenuItemCut item = IOSSystemContextMenuItemCut();
|
|
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
|
|
expect(item.getData(localizations), const IOSSystemContextMenuItemDataCut());
|
|
},
|
|
);
|
|
|
|
test(
|
|
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemPaste',
|
|
() {
|
|
const IOSSystemContextMenuItemPaste item = IOSSystemContextMenuItemPaste();
|
|
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
|
|
expect(item.getData(localizations), const IOSSystemContextMenuItemDataPaste());
|
|
},
|
|
);
|
|
|
|
test(
|
|
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemSelectAll',
|
|
() {
|
|
const IOSSystemContextMenuItemSelectAll item = IOSSystemContextMenuItemSelectAll();
|
|
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
|
|
expect(item.getData(localizations), const IOSSystemContextMenuItemDataSelectAll());
|
|
},
|
|
);
|
|
|
|
test(
|
|
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemLookUp',
|
|
() {
|
|
const IOSSystemContextMenuItemLookUp item = IOSSystemContextMenuItemLookUp();
|
|
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
|
|
expect(
|
|
item.getData(localizations),
|
|
IOSSystemContextMenuItemDataLookUp(title: localizations.lookUpButtonLabel),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemSearchWeb',
|
|
() {
|
|
const IOSSystemContextMenuItemSearchWeb item = IOSSystemContextMenuItemSearchWeb();
|
|
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
|
|
expect(
|
|
item.getData(localizations),
|
|
IOSSystemContextMenuItemDataSearchWeb(title: localizations.searchWebButtonLabel),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemShare',
|
|
() {
|
|
const IOSSystemContextMenuItemShare item = IOSSystemContextMenuItemShare();
|
|
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
|
|
expect(
|
|
item.getData(localizations),
|
|
IOSSystemContextMenuItemDataShare(title: localizations.shareButtonLabel),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemLiveText',
|
|
() {
|
|
const IOSSystemContextMenuItemLiveText item = IOSSystemContextMenuItemLiveText();
|
|
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
|
|
final IOSSystemContextMenuItemData data = item.getData(localizations);
|
|
expect(data, isA<IOSSystemContextMenuItemDataLiveText>());
|
|
},
|
|
);
|
|
|
|
test('systemContextMenuItemDataFromJson handles Live Text', () {
|
|
final Map<String, dynamic> json = <String, dynamic>{'type': 'captureTextFromCamera'};
|
|
final IOSSystemContextMenuItemData item = systemContextMenuItemDataFromJson(json);
|
|
expect(item, isA<IOSSystemContextMenuItemDataLiveText>());
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/169696.
|
|
test('IOSSystemContextMenuItemLookUp debugFillProperties', () {
|
|
const String title = 'my title';
|
|
const IOSSystemContextMenuItemLookUp item = IOSSystemContextMenuItemLookUp(title: title);
|
|
final List<DiagnosticsNode> diagnosticsNodes = item.toDiagnosticsNode().getProperties();
|
|
expect(diagnosticsNodes, hasLength(1));
|
|
expect(diagnosticsNodes.first.name, 'title');
|
|
expect(diagnosticsNodes.first.value, title);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/169696.
|
|
test('IOSSystemContextMenuItemSearchWeb debugFillProperties', () {
|
|
const String title = 'my title';
|
|
const IOSSystemContextMenuItemSearchWeb item = IOSSystemContextMenuItemSearchWeb(title: title);
|
|
final List<DiagnosticsNode> diagnosticsNodes = item.toDiagnosticsNode().getProperties();
|
|
expect(diagnosticsNodes, hasLength(1));
|
|
expect(diagnosticsNodes.first.name, 'title');
|
|
expect(diagnosticsNodes.first.value, title);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/169696.
|
|
test('IOSSystemContextMenuItemShare debugFillProperties', () {
|
|
const String title = 'my title';
|
|
const IOSSystemContextMenuItemShare item = IOSSystemContextMenuItemShare(title: title);
|
|
final List<DiagnosticsNode> diagnosticsNodes = item.toDiagnosticsNode().getProperties();
|
|
expect(diagnosticsNodes, hasLength(1));
|
|
expect(diagnosticsNodes.first.name, 'title');
|
|
expect(diagnosticsNodes.first.value, title);
|
|
});
|
|
|
|
testWidgets(
|
|
'when supportsShowingSystemContextMenu is false, isSupported is false',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(text: 'one two three');
|
|
addTearDown(controller.dispose);
|
|
late BuildContext buildContext;
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: false),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Builder(
|
|
builder: (BuildContext context) {
|
|
buildContext = context;
|
|
return TextField(
|
|
controller: controller,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(SystemContextMenu.isSupported(buildContext), isFalse);
|
|
},
|
|
skip: kIsWeb, // [intended] SystemContextMenu is not supported on web.
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'when supportsShowingSystemContextMenu is true and the platform is iOS, isSupported is true',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(text: 'one two three');
|
|
addTearDown(controller.dispose);
|
|
late BuildContext buildContext;
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Builder(
|
|
builder: (BuildContext context) {
|
|
buildContext = context;
|
|
return TextField(
|
|
controller: controller,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(SystemContextMenu.isSupported(buildContext), switch (defaultTargetPlatform) {
|
|
TargetPlatform.iOS => isTrue,
|
|
_ => isFalse,
|
|
});
|
|
},
|
|
skip: kIsWeb, // [intended] SystemContextMenu is not supported on web.
|
|
variant: TargetPlatformVariant.all(),
|
|
);
|
|
|
|
for (final bool readOnly in <bool>[true, false]) {
|
|
testWidgets(
|
|
'read only fields do not support the system context menu',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true),
|
|
child: MaterialApp(
|
|
home: Scaffold(body: TextField(readOnly: readOnly)),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
final EditableTextState editableTextState = tester.state(find.byType(EditableText));
|
|
expect(SystemContextMenu.isSupportedByField(editableTextState), switch (readOnly) {
|
|
true => isFalse,
|
|
false => isTrue,
|
|
});
|
|
},
|
|
skip: kIsWeb, // [intended] SystemContextMenu is not supported on web.
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
}
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/170521.
|
|
testWidgets(
|
|
'when supportsShowingSystemContextMenu is false, SystemContextMenu throws',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(text: 'one two three');
|
|
addTearDown(controller.dispose);
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: false),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: TextField(
|
|
controller: controller,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
|
expect(state.showToolbar(), true);
|
|
await tester.pump();
|
|
|
|
expect(tester.takeException(), isAssertionError);
|
|
},
|
|
skip: kIsWeb, // [intended] SystemContextMenu is not supported on web.
|
|
);
|
|
|
|
testWidgets(
|
|
'can use custom menu items',
|
|
(WidgetTester tester) async {
|
|
bool customAction1Called = false;
|
|
bool customAction2Called = false;
|
|
final List<List<IOSSystemContextMenuItemData>> itemsReceived =
|
|
<List<IOSSystemContextMenuItemData>>[];
|
|
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
(MethodCall methodCall) async {
|
|
switch (methodCall.method) {
|
|
case 'ContextMenu.showSystemContextMenu':
|
|
final Map<String, dynamic> arguments = methodCall.arguments as Map<String, dynamic>;
|
|
final List<dynamic> untypedItems = arguments['items'] as List<dynamic>;
|
|
final List<IOSSystemContextMenuItemData> lastItems = untypedItems.map((
|
|
dynamic value,
|
|
) {
|
|
final Map<String, dynamic> itemJson = value as Map<String, dynamic>;
|
|
return systemContextMenuItemDataFromJson(itemJson);
|
|
}).toList();
|
|
itemsReceived.add(lastItems);
|
|
}
|
|
return;
|
|
},
|
|
);
|
|
addTearDown(() {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
null,
|
|
);
|
|
});
|
|
|
|
final List<IOSSystemContextMenuItem> items = <IOSSystemContextMenuItem>[
|
|
const IOSSystemContextMenuItemCopy(),
|
|
IOSSystemContextMenuItemCustom(
|
|
title: 'Custom Action 1',
|
|
onPressed: () {
|
|
customAction1Called = true;
|
|
},
|
|
),
|
|
IOSSystemContextMenuItemCustom(
|
|
title: 'Custom Action 2',
|
|
onPressed: () {
|
|
customAction2Called = true;
|
|
},
|
|
),
|
|
];
|
|
|
|
final TextEditingController controller = TextEditingController(text: 'test text');
|
|
addTearDown(controller.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: TextField(
|
|
controller: controller,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
items: items,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
|
expect(state.showToolbar(), true);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(SystemContextMenu), findsOneWidget);
|
|
expect(itemsReceived, hasLength(1));
|
|
expect(itemsReceived.last, hasLength(3));
|
|
|
|
expect(itemsReceived.last[0], equals(const IOSSystemContextMenuItemDataCopy()));
|
|
expect(itemsReceived.last[1], isA<IOSSystemContextMenuItemDataCustom>());
|
|
expect(
|
|
(itemsReceived.last[1] as IOSSystemContextMenuItemDataCustom).title,
|
|
'Custom Action 1',
|
|
);
|
|
expect(itemsReceived.last[2], isA<IOSSystemContextMenuItemDataCustom>());
|
|
expect(
|
|
(itemsReceived.last[2] as IOSSystemContextMenuItemDataCustom).title,
|
|
'Custom Action 2',
|
|
);
|
|
|
|
final IOSSystemContextMenuItemCustom customItem1 = items[1] as IOSSystemContextMenuItemCustom;
|
|
final IOSSystemContextMenuItemCustom customItem2 = items[2] as IOSSystemContextMenuItemCustom;
|
|
|
|
ByteData? message = const JSONMethodCodec().encodeMethodCall(
|
|
MethodCall('ContextMenu.onPerformCustomAction', <dynamic>[
|
|
0,
|
|
customItem1.hashCode.toString(),
|
|
]),
|
|
);
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
'flutter/platform',
|
|
message,
|
|
(_) {},
|
|
);
|
|
expect(customAction1Called, isTrue);
|
|
expect(customAction2Called, isFalse);
|
|
|
|
message = const JSONMethodCodec().encodeMethodCall(
|
|
MethodCall('ContextMenu.onPerformCustomAction', <dynamic>[
|
|
0,
|
|
customItem2.hashCode.toString(),
|
|
]),
|
|
);
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
'flutter/platform',
|
|
message,
|
|
(_) {},
|
|
);
|
|
expect(customAction2Called, isTrue);
|
|
|
|
state.hideToolbar();
|
|
await tester.pump();
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
},
|
|
skip: kIsWeb, // [intended]
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'system context menu auto-closes after custom action',
|
|
(WidgetTester tester) async {
|
|
bool customActionCalled = false;
|
|
final TextEditingController controller = TextEditingController(text: 'test text');
|
|
addTearDown(controller.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: TextField(
|
|
controller: controller,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
items: <IOSSystemContextMenuItem>[
|
|
IOSSystemContextMenuItemCustom(
|
|
title: 'Test Action',
|
|
onPressed: () {
|
|
customActionCalled = true;
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
|
expect(state.showToolbar(), true);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(SystemContextMenu), findsOneWidget);
|
|
|
|
final SystemContextMenu menu = tester.widget<SystemContextMenu>(
|
|
find.byType(SystemContextMenu),
|
|
);
|
|
final IOSSystemContextMenuItemCustom item = menu.items[0] as IOSSystemContextMenuItemCustom;
|
|
|
|
final ByteData message = const JSONMethodCodec().encodeMethodCall(
|
|
MethodCall('ContextMenu.onPerformCustomAction', <dynamic>[0, item.hashCode.toString()]),
|
|
);
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
'flutter/platform',
|
|
message,
|
|
(_) {},
|
|
);
|
|
|
|
expect(customActionCalled, isTrue);
|
|
|
|
// iOS system menus auto-close after custom actions on real devices.
|
|
// Simulate this by sending the platform dismiss message.
|
|
final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
|
|
'method': 'ContextMenu.onDismissSystemContextMenu',
|
|
});
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
'flutter/platform',
|
|
messageBytes,
|
|
(_) {},
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
},
|
|
skip: kIsWeb, // [intended]
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'can trigger custom menu action through platform channel message',
|
|
(WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController(text: 'one two three');
|
|
addTearDown(controller.dispose);
|
|
|
|
bool customActionCalled = false;
|
|
final List<IOSSystemContextMenuItem> items = <IOSSystemContextMenuItem>[
|
|
const IOSSystemContextMenuItemCut(),
|
|
IOSSystemContextMenuItemCustom(
|
|
title: 'Test Action',
|
|
onPressed: () {
|
|
customActionCalled = true;
|
|
},
|
|
),
|
|
];
|
|
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: TextField(
|
|
controller: controller,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
items: items,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
|
|
const TextSelection selection = TextSelection(baseOffset: 0, extentOffset: 3);
|
|
controller.selection = selection;
|
|
|
|
await tester.longPress(find.byType(TextField));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(SystemContextMenu), findsOneWidget);
|
|
|
|
final IOSSystemContextMenuItemCustom customItem = items[1] as IOSSystemContextMenuItemCustom;
|
|
final String callbackId = customItem.hashCode.toString();
|
|
|
|
final ByteData message = const JSONMethodCodec().encodeMethodCall(
|
|
MethodCall('ContextMenu.onPerformCustomAction', <dynamic>[0, callbackId]),
|
|
);
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
'flutter/platform',
|
|
message,
|
|
(_) {},
|
|
);
|
|
|
|
expect(customActionCalled, isTrue);
|
|
|
|
// Verify menu closes after custom action.
|
|
// Simulate platform dismiss message for consistency.
|
|
final ByteData? dismissMessage = const JSONMessageCodec().encodeMessage(<String, dynamic>{
|
|
'method': 'ContextMenu.onDismissSystemContextMenu',
|
|
});
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
'flutter/platform',
|
|
dismissMessage,
|
|
(_) {},
|
|
);
|
|
await tester.pump();
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
},
|
|
skip: kIsWeb, // [intended]
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'two TextFields can have different custom SystemContextMenu items',
|
|
(WidgetTester tester) async {
|
|
bool field1ActionCalled = false;
|
|
bool field2ActionCalled = false;
|
|
|
|
final TextEditingController controller1 = TextEditingController(text: 'Field 1 text');
|
|
final TextEditingController controller2 = TextEditingController(text: 'Field 2 text');
|
|
addTearDown(() {
|
|
controller1.dispose();
|
|
controller2.dispose();
|
|
});
|
|
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
return MediaQuery(
|
|
data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true),
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Column(
|
|
children: <Widget>[
|
|
TextField(
|
|
controller: controller1,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
items: <IOSSystemContextMenuItem>[
|
|
IOSSystemContextMenuItemCustom(
|
|
title: 'Field 1 Action',
|
|
onPressed: () {
|
|
field1ActionCalled = true;
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
TextField(
|
|
controller: controller2,
|
|
contextMenuBuilder:
|
|
(BuildContext context, EditableTextState editableTextState) {
|
|
return SystemContextMenu.editableText(
|
|
editableTextState: editableTextState,
|
|
items: <IOSSystemContextMenuItem>[
|
|
IOSSystemContextMenuItemCustom(
|
|
title: 'Field 2 Action',
|
|
onPressed: () {
|
|
field2ActionCalled = true;
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
await tester.longPress(find.byType(TextField).first);
|
|
await tester.pump();
|
|
expect(find.byType(SystemContextMenu), findsOneWidget);
|
|
|
|
final SystemContextMenu menu1 = tester.widget<SystemContextMenu>(
|
|
find.byType(SystemContextMenu),
|
|
);
|
|
final IOSSystemContextMenuItemCustom item1 = menu1.items[0] as IOSSystemContextMenuItemCustom;
|
|
|
|
ByteData message = const JSONMethodCodec().encodeMethodCall(
|
|
MethodCall('ContextMenu.onPerformCustomAction', <dynamic>[0, item1.hashCode.toString()]),
|
|
);
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
'flutter/platform',
|
|
message,
|
|
(_) {},
|
|
);
|
|
|
|
expect(field1ActionCalled, isTrue);
|
|
expect(field2ActionCalled, isFalse);
|
|
|
|
final ByteData? messageBytes1 = const JSONMessageCodec().encodeMessage(<String, dynamic>{
|
|
'method': 'ContextMenu.onDismissSystemContextMenu',
|
|
});
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
'flutter/platform',
|
|
messageBytes1,
|
|
(_) {},
|
|
);
|
|
await tester.pump();
|
|
|
|
field1ActionCalled = false;
|
|
|
|
await tester.longPress(find.byType(TextField).last);
|
|
await tester.pump();
|
|
expect(find.byType(SystemContextMenu), findsOneWidget);
|
|
|
|
final SystemContextMenu menu2 = tester.widget<SystemContextMenu>(
|
|
find.byType(SystemContextMenu),
|
|
);
|
|
final IOSSystemContextMenuItemCustom item2 = menu2.items[0] as IOSSystemContextMenuItemCustom;
|
|
|
|
message = const JSONMethodCodec().encodeMethodCall(
|
|
MethodCall('ContextMenu.onPerformCustomAction', <dynamic>[0, item2.hashCode.toString()]),
|
|
);
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
'flutter/platform',
|
|
message,
|
|
(_) {},
|
|
);
|
|
|
|
expect(field1ActionCalled, isFalse);
|
|
expect(field2ActionCalled, isTrue);
|
|
|
|
final ByteData? messageBytes2 = const JSONMessageCodec().encodeMessage(<String, dynamic>{
|
|
'method': 'ContextMenu.onDismissSystemContextMenu',
|
|
});
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
'flutter/platform',
|
|
messageBytes2,
|
|
(_) {},
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(SystemContextMenu), findsNothing);
|
|
},
|
|
skip: kIsWeb, // [intended]
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
}
|