blob: b75c976f10ff8652b2cf02f63d2e5f3d43647c5f [file] [log] [blame]
// 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 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 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 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 itemsReceived = <List<IOSSystemContextMenuItemData>>[];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(MethodCall methodCall) async {
switch (methodCall.method) {
case 'ContextMenu.showSystemContextMenu':
final arguments = methodCall.arguments as Map<String, dynamic>;
final untypedItems = arguments['items'] as List<dynamic>;
final List<IOSSystemContextMenuItemData> lastItems = untypedItems.map((
dynamic value,
) {
final itemJson = value as Map<String, dynamic>;
return systemContextMenuItemDataFromJson(itemJson);
}).toList();
itemsReceived.add(lastItems);
}
return;
},
);
addTearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
null,
);
});
const items1 = <IOSSystemContextMenuItem>[
IOSSystemContextMenuItemCopy(),
IOSSystemContextMenuItemShare(title: 'My Share Title'),
IOSSystemContextMenuItemLiveText(),
];
final 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 itemsReceived = <List<IOSSystemContextMenuItemData>>[];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(MethodCall methodCall) async {
switch (methodCall.method) {
case 'ContextMenu.showSystemContextMenu':
final arguments = methodCall.arguments as Map<String, dynamic>;
final untypedItems = arguments['items'] as List<dynamic>;
final List<IOSSystemContextMenuItemData> lastItems = untypedItems.map((
dynamic value,
) {
final itemJson = value as Map<String, dynamic>;
return systemContextMenuItemDataFromJson(itemJson);
}).toList();
itemsReceived.add(lastItems);
}
return;
},
);
addTearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
null,
);
});
const items1 = <IOSSystemContextMenuItem>[];
final 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 itemsReceived = <List<IOSSystemContextMenuItemData>>[];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(MethodCall methodCall) async {
switch (methodCall.method) {
case 'ContextMenu.showSystemContextMenu':
final arguments = methodCall.arguments as Map<String, dynamic>;
final untypedItems = arguments['items'] as List<dynamic>;
final List<IOSSystemContextMenuItemData> lastItems = untypedItems.map((
dynamic value,
) {
final itemJson = value as Map<String, dynamic>;
return systemContextMenuItemDataFromJson(itemJson);
}).toList();
itemsReceived.add(lastItems);
}
return;
},
);
addTearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
null,
);
});
const 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 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 targetRects = <Map<String, double>>[];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(MethodCall methodCall) async {
if (methodCall.method == 'ContextMenu.showSystemContextMenu') {
final arguments = methodCall.arguments as Map<String, dynamic>;
final 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 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 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 controller1 = TextEditingController(text: 'one two three');
addTearDown(controller1.dispose);
final 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 item = IOSSystemContextMenuItemCopy();
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
expect(item.getData(localizations), const IOSSystemContextMenuItemDataCopy());
},
);
test(
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemCut',
() {
const item = IOSSystemContextMenuItemCut();
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
expect(item.getData(localizations), const IOSSystemContextMenuItemDataCut());
},
);
test(
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemPaste',
() {
const item = IOSSystemContextMenuItemPaste();
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
expect(item.getData(localizations), const IOSSystemContextMenuItemDataPaste());
},
);
test(
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemSelectAll',
() {
const item = IOSSystemContextMenuItemSelectAll();
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
expect(item.getData(localizations), const IOSSystemContextMenuItemDataSelectAll());
},
);
test(
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemLookUp',
() {
const item = IOSSystemContextMenuItemLookUp();
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
expect(
item.getData(localizations),
IOSSystemContextMenuItemDataLookUp(title: localizations.lookUpButtonLabel),
);
},
);
test(
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemSearchWeb',
() {
const item = IOSSystemContextMenuItemSearchWeb();
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
expect(
item.getData(localizations),
IOSSystemContextMenuItemDataSearchWeb(title: localizations.searchWebButtonLabel),
);
},
);
test(
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemShare',
() {
const item = IOSSystemContextMenuItemShare();
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
expect(
item.getData(localizations),
IOSSystemContextMenuItemDataShare(title: localizations.shareButtonLabel),
);
},
);
test(
'can get the IOSSystemContextMenuItemData representation of an IOSSystemContextMenuItemLiveText',
() {
const item = IOSSystemContextMenuItemLiveText();
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
final IOSSystemContextMenuItemData data = item.getData(localizations);
expect(data, isA<IOSSystemContextMenuItemDataLiveText>());
},
);
test('systemContextMenuItemDataFromJson handles Live Text', () {
final 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 title = 'my title';
const 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 title = 'my title';
const 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 title = 'my title';
const 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 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 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 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 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 {
var customAction1Called = false;
var customAction2Called = false;
final itemsReceived = <List<IOSSystemContextMenuItemData>>[];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(MethodCall methodCall) async {
switch (methodCall.method) {
case 'ContextMenu.showSystemContextMenu':
final arguments = methodCall.arguments as Map<String, dynamic>;
final untypedItems = arguments['items'] as List<dynamic>;
final List<IOSSystemContextMenuItemData> lastItems = untypedItems.map((
dynamic value,
) {
final itemJson = value as Map<String, dynamic>;
return systemContextMenuItemDataFromJson(itemJson);
}).toList();
itemsReceived.add(lastItems);
}
return;
},
);
addTearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
null,
);
});
final items = <IOSSystemContextMenuItem>[
const IOSSystemContextMenuItemCopy(),
IOSSystemContextMenuItemCustom(
title: 'Custom Action 1',
onPressed: () {
customAction1Called = true;
},
),
IOSSystemContextMenuItemCustom(
title: 'Custom Action 2',
onPressed: () {
customAction2Called = true;
},
),
];
final 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 customItem1 = items[1] as IOSSystemContextMenuItemCustom;
final 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 {
var customActionCalled = false;
final 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 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 controller = TextEditingController(text: 'one two three');
addTearDown(controller.dispose);
var customActionCalled = false;
final 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 selection = TextSelection(baseOffset: 0, extentOffset: 3);
controller.selection = selection;
await tester.longPress(find.byType(TextField));
await tester.pumpAndSettle();
expect(find.byType(SystemContextMenu), findsOneWidget);
final customItem = items[1] as IOSSystemContextMenuItemCustom;
final 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 {
var field1ActionCalled = false;
var field2ActionCalled = false;
final controller1 = TextEditingController(text: 'Field 1 text');
final 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 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 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),
);
testWidgets(
'Default iOS SystemContextMenu includes Share for non-empty selection',
(WidgetTester tester) async {
final itemsReceived = <List<IOSSystemContextMenuItemData>>[];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(MethodCall methodCall) async {
if (methodCall.method == 'ContextMenu.showSystemContextMenu') {
final arguments = methodCall.arguments as Map<String, dynamic>;
final untypedItems = arguments['items'] as List<dynamic>;
final lastItems = <IOSSystemContextMenuItemData>[
for (final dynamic value in untypedItems)
systemContextMenuItemDataFromJson(value as Map<String, dynamic>),
];
itemsReceived.add(lastItems);
}
return;
},
);
addTearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
null,
);
});
final controller = TextEditingController(text: 'Hello world');
addTearDown(controller.dispose);
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(supportsShowingSystemContextMenu: true),
child: MaterialApp(
home: Scaffold(
body: Center(
child: TextField(
controller: controller,
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
return SystemContextMenu.editableText(editableTextState: editableTextState);
},
),
),
),
),
),
);
// Focus the field first (this establishes the TextInputConnection).
await tester.tap(find.byType(TextField));
await tester.pump();
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
// Set a non-empty selection to enable sharing.
controller.selection = const TextSelection(baseOffset: 0, extentOffset: 5); // "Hello"
await tester.pump();
// Nit: ensure no platform message sent before showing toolbar.
expect(itemsReceived, isEmpty);
// Show the context menu.
expect(state.showToolbar(), true);
await tester.pump();
// Assert that the platform message included a Share item.
expect(itemsReceived, isNotEmpty);
expect(itemsReceived.last, contains(isA<IOSSystemContextMenuItemDataShare>()));
},
skip: kIsWeb, // [intended]
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
}