[ios][secure_paste]show menu item based on info sent from framework (#161103)
This is moved from https://github.com/flutter/engine/pull/56362 *List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.* *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]. - [x] 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
This commit is contained in:
parent
6a412a5f71
commit
b37e7aaaab
@ -1050,6 +1050,21 @@ static void SetEntryPoint(flutter::Settings* settings, NSString* entrypoint, NSS
|
||||
arguments:@[ @(client) ]];
|
||||
}
|
||||
|
||||
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView
|
||||
shareSelectedText:(NSString*)selectedText {
|
||||
[self.platformPlugin showShareViewController:selectedText];
|
||||
}
|
||||
|
||||
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView
|
||||
searchWebWithSelectedText:(NSString*)selectedText {
|
||||
[self.platformPlugin searchWeb:selectedText];
|
||||
}
|
||||
|
||||
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView
|
||||
lookUpSelectedText:(NSString*)selectedText {
|
||||
[self.platformPlugin showLookUpViewController:selectedText];
|
||||
}
|
||||
|
||||
#pragma mark - FlutterViewEngineDelegate
|
||||
|
||||
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView showToolbar:(int)client {
|
||||
|
@ -14,6 +14,9 @@
|
||||
- (instancetype)initWithEngine:(FlutterEngine*)engine NS_DESIGNATED_INITIALIZER;
|
||||
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
|
||||
|
||||
- (void)showShareViewController:(NSString*)content;
|
||||
- (void)searchWeb:(NSString*)searchTerm;
|
||||
- (void)showLookUpViewController:(NSString*)term;
|
||||
@end
|
||||
|
||||
namespace flutter {
|
||||
|
@ -67,6 +67,12 @@ typedef NS_ENUM(NSInteger, FlutterFloatingCursorDragState) {
|
||||
didResignFirstResponderWithTextInputClient:(int)client;
|
||||
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView
|
||||
willDismissEditMenuWithTextInputClient:(int)client;
|
||||
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView
|
||||
shareSelectedText:(NSString*)selectedText;
|
||||
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView
|
||||
searchWebWithSelectedText:(NSString*)selectedText;
|
||||
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView
|
||||
lookUpSelectedText:(NSString*)selectedText;
|
||||
@end
|
||||
|
||||
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTDELEGATE_H_
|
||||
|
@ -802,6 +802,7 @@ static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point,
|
||||
// etc)
|
||||
@property(nonatomic, copy) NSString* temporarilyDeletedComposedCharacter;
|
||||
@property(nonatomic, assign) CGRect editMenuTargetRect;
|
||||
@property(nonatomic, strong) NSArray<NSDictionary*>* editMenuItems;
|
||||
|
||||
- (void)setEditableTransform:(NSArray*)matrix;
|
||||
@end
|
||||
@ -875,10 +876,123 @@ static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point,
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)handleSearchWebAction {
|
||||
[self.textInputDelegate flutterTextInputView:self
|
||||
searchWebWithSelectedText:[self textInRange:_selectedTextRange]];
|
||||
}
|
||||
|
||||
- (void)handleLookUpAction {
|
||||
[self.textInputDelegate flutterTextInputView:self
|
||||
lookUpSelectedText:[self textInRange:_selectedTextRange]];
|
||||
}
|
||||
|
||||
- (void)handleShareAction {
|
||||
[self.textInputDelegate flutterTextInputView:self
|
||||
shareSelectedText:[self textInRange:_selectedTextRange]];
|
||||
}
|
||||
|
||||
// DFS algorithm to search a UICommand from the menu tree.
|
||||
- (UICommand*)searchCommandWithSelector:(SEL)selector
|
||||
element:(UIMenuElement*)element API_AVAILABLE(ios(16.0)) {
|
||||
if ([element isKindOfClass:UICommand.class]) {
|
||||
UICommand* command = (UICommand*)element;
|
||||
return command.action == selector ? command : nil;
|
||||
} else if ([element isKindOfClass:UIMenu.class]) {
|
||||
NSArray<UIMenuElement*>* children = ((UIMenu*)element).children;
|
||||
for (UIMenuElement* child in children) {
|
||||
UICommand* result = [self searchCommandWithSelector:selector element:child];
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addBasicEditingCommandToItems:(NSMutableArray*)items
|
||||
type:(NSString*)type
|
||||
selector:(SEL)selector
|
||||
suggestedMenu:(UIMenu*)suggestedMenu {
|
||||
UICommand* command = [self searchCommandWithSelector:selector element:suggestedMenu];
|
||||
if (command) {
|
||||
[items addObject:command];
|
||||
} else {
|
||||
FML_LOG(ERROR) << "Cannot find context menu item of type \"" << type.UTF8String << "\".";
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addAdditionalBasicCommandToItems:(NSMutableArray*)items
|
||||
type:(NSString*)type
|
||||
selector:(SEL)selector
|
||||
encodedItem:(NSDictionary<NSString*, id>*)encodedItem {
|
||||
NSString* title = encodedItem[@"title"];
|
||||
if (title) {
|
||||
UICommand* command = [UICommand commandWithTitle:title
|
||||
image:nil
|
||||
action:selector
|
||||
propertyList:nil];
|
||||
[items addObject:command];
|
||||
} else {
|
||||
FML_LOG(ERROR) << "Missing title for context menu item of type \"" << type.UTF8String << "\".";
|
||||
}
|
||||
}
|
||||
|
||||
- (UIMenu*)editMenuInteraction:(UIEditMenuInteraction*)interaction
|
||||
menuForConfiguration:(UIEditMenuConfiguration*)configuration
|
||||
suggestedActions:(NSArray<UIMenuElement*>*)suggestedActions API_AVAILABLE(ios(16.0)) {
|
||||
return [UIMenu menuWithChildren:suggestedActions];
|
||||
UIMenu* suggestedMenu = [UIMenu menuWithChildren:suggestedActions];
|
||||
if (!_editMenuItems) {
|
||||
return suggestedMenu;
|
||||
}
|
||||
|
||||
NSMutableArray* items = [NSMutableArray array];
|
||||
for (NSDictionary<NSString*, id>* encodedItem in _editMenuItems) {
|
||||
NSString* type = encodedItem[@"type"];
|
||||
if ([type isEqualToString:@"copy"]) {
|
||||
[self addBasicEditingCommandToItems:items
|
||||
type:type
|
||||
selector:@selector(copy:)
|
||||
suggestedMenu:suggestedMenu];
|
||||
} else if ([type isEqualToString:@"paste"]) {
|
||||
[self addBasicEditingCommandToItems:items
|
||||
type:type
|
||||
selector:@selector(paste:)
|
||||
suggestedMenu:suggestedMenu];
|
||||
} else if ([type isEqualToString:@"cut"]) {
|
||||
[self addBasicEditingCommandToItems:items
|
||||
type:type
|
||||
selector:@selector(cut:)
|
||||
suggestedMenu:suggestedMenu];
|
||||
} else if ([type isEqualToString:@"delete"]) {
|
||||
[self addBasicEditingCommandToItems:items
|
||||
type:type
|
||||
selector:@selector(delete:)
|
||||
suggestedMenu:suggestedMenu];
|
||||
} else if ([type isEqualToString:@"selectAll"]) {
|
||||
[self addBasicEditingCommandToItems:items
|
||||
type:type
|
||||
selector:@selector(selectAll:)
|
||||
suggestedMenu:suggestedMenu];
|
||||
} else if ([type isEqualToString:@"searchWeb"]) {
|
||||
[self addAdditionalBasicCommandToItems:items
|
||||
type:type
|
||||
selector:@selector(handleSearchWebAction)
|
||||
encodedItem:encodedItem];
|
||||
} else if ([type isEqualToString:@"share"]) {
|
||||
[self addAdditionalBasicCommandToItems:items
|
||||
type:type
|
||||
selector:@selector(handleShareAction)
|
||||
encodedItem:encodedItem];
|
||||
} else if ([type isEqualToString:@"lookUp"]) {
|
||||
[self addAdditionalBasicCommandToItems:items
|
||||
type:type
|
||||
selector:@selector(handleLookUpAction)
|
||||
encodedItem:encodedItem];
|
||||
}
|
||||
}
|
||||
return [UIMenu menuWithChildren:items];
|
||||
}
|
||||
|
||||
- (void)editMenuInteraction:(UIEditMenuInteraction*)interaction
|
||||
@ -894,8 +1008,10 @@ static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point,
|
||||
return _editMenuTargetRect;
|
||||
}
|
||||
|
||||
- (void)showEditMenuWithTargetRect:(CGRect)targetRect API_AVAILABLE(ios(16.0)) {
|
||||
- (void)showEditMenuWithTargetRect:(CGRect)targetRect
|
||||
items:(NSArray<NSDictionary*>*)items API_AVAILABLE(ios(16.0)) {
|
||||
_editMenuTargetRect = targetRect;
|
||||
_editMenuItems = items;
|
||||
UIEditMenuConfiguration* config =
|
||||
[UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:CGPointZero];
|
||||
[self.editMenuInteraction presentEditMenuWithConfiguration:config];
|
||||
@ -2574,7 +2690,7 @@ static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point,
|
||||
[encodedTargetRect[@"x"] doubleValue], [encodedTargetRect[@"y"] doubleValue],
|
||||
[encodedTargetRect[@"width"] doubleValue], [encodedTargetRect[@"height"] doubleValue]);
|
||||
CGRect localTargetRect = [self.hostView convertRect:globalTargetRect toView:self.activeView];
|
||||
[self.activeView showEditMenuWithTargetRect:localTargetRect];
|
||||
[self.activeView showEditMenuWithTargetRect:localTargetRect items:args[@"items"]];
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,9 @@ FLUTTER_ASSERT_ARC
|
||||
- (BOOL)isVisibleToAutofill;
|
||||
- (id<FlutterTextInputDelegate>)textInputDelegate;
|
||||
- (void)configureWithDictionary:(NSDictionary*)configuration;
|
||||
- (void)handleSearchWebAction;
|
||||
- (void)handleLookUpAction;
|
||||
- (void)handleShareAction;
|
||||
@end
|
||||
|
||||
@interface FlutterTextInputViewSpy : FlutterTextInputView
|
||||
@ -3035,6 +3038,264 @@ FLUTTER_ASSERT_ARC
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testEditMenu_shouldPresentEditMenuWithSuggestedItemsByDefaultIfNoFrameworkData {
|
||||
if (@available(iOS 16.0, *)) {
|
||||
FlutterTextInputPlugin* myInputPlugin =
|
||||
[[FlutterTextInputPlugin alloc] initWithDelegate:OCMClassMock([FlutterEngine class])];
|
||||
FlutterViewController* myViewController = [[FlutterViewController alloc] init];
|
||||
myInputPlugin.viewController = myViewController;
|
||||
[myViewController loadView];
|
||||
|
||||
FlutterMethodCall* setClientCall =
|
||||
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
|
||||
arguments:@[ @(123), self.mutableTemplateCopy ]];
|
||||
[myInputPlugin handleMethodCall:setClientCall
|
||||
result:^(id _Nullable result){
|
||||
}];
|
||||
|
||||
FlutterTextInputView* myInputView = myInputPlugin.activeView;
|
||||
|
||||
FlutterTextInputView* mockInputView = OCMPartialMock(myInputView);
|
||||
OCMStub([mockInputView isFirstResponder]).andReturn(YES);
|
||||
|
||||
XCTestExpectation* expectation = [[XCTestExpectation alloc]
|
||||
initWithDescription:@"presentEditMenuWithConfiguration must be called."];
|
||||
|
||||
id mockInteraction = OCMClassMock([UIEditMenuInteraction class]);
|
||||
OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
|
||||
OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
|
||||
.andDo(^(NSInvocation* invocation) {
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
myInputView.frame = CGRectMake(10, 20, 30, 40);
|
||||
NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
|
||||
@{@"x" : @(100), @"y" : @(200), @"width" : @(300), @"height" : @(400)};
|
||||
// No items provided from framework. Show the suggested items by default.
|
||||
BOOL shownEditMenu = [myInputPlugin showEditMenu:@{@"targetRect" : encodedTargetRect}];
|
||||
XCTAssertTrue(shownEditMenu, @"Should show edit menu with correct configuration.");
|
||||
[self waitForExpectations:@[ expectation ] timeout:1.0];
|
||||
|
||||
UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
|
||||
image:nil
|
||||
action:@selector(copy:)
|
||||
propertyList:nil];
|
||||
UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
|
||||
image:nil
|
||||
action:@selector(paste:)
|
||||
propertyList:nil];
|
||||
NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
|
||||
|
||||
UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
|
||||
menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
|
||||
suggestedActions:suggestedActions];
|
||||
XCTAssertEqualObjects(menu.children, suggestedActions,
|
||||
@"Must show suggested items by default.");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testEditMenu_shouldPresentEditMenuWithCorectItemsAndCorrectOrderingForBasicEditingActions {
|
||||
if (@available(iOS 16.0, *)) {
|
||||
FlutterTextInputPlugin* myInputPlugin =
|
||||
[[FlutterTextInputPlugin alloc] initWithDelegate:OCMClassMock([FlutterEngine class])];
|
||||
FlutterViewController* myViewController = [[FlutterViewController alloc] init];
|
||||
myInputPlugin.viewController = myViewController;
|
||||
[myViewController loadView];
|
||||
|
||||
FlutterMethodCall* setClientCall =
|
||||
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
|
||||
arguments:@[ @(123), self.mutableTemplateCopy ]];
|
||||
[myInputPlugin handleMethodCall:setClientCall
|
||||
result:^(id _Nullable result){
|
||||
}];
|
||||
|
||||
FlutterTextInputView* myInputView = myInputPlugin.activeView;
|
||||
|
||||
FlutterTextInputView* mockInputView = OCMPartialMock(myInputView);
|
||||
OCMStub([mockInputView isFirstResponder]).andReturn(YES);
|
||||
|
||||
XCTestExpectation* expectation = [[XCTestExpectation alloc]
|
||||
initWithDescription:@"presentEditMenuWithConfiguration must be called."];
|
||||
|
||||
id mockInteraction = OCMClassMock([UIEditMenuInteraction class]);
|
||||
OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
|
||||
OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
|
||||
.andDo(^(NSInvocation* invocation) {
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
myInputView.frame = CGRectMake(10, 20, 30, 40);
|
||||
NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
|
||||
@{@"x" : @(100), @"y" : @(200), @"width" : @(300), @"height" : @(400)};
|
||||
|
||||
NSArray<NSDictionary<NSString*, id>*>* encodedItems =
|
||||
@[ @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
|
||||
|
||||
BOOL shownEditMenu =
|
||||
[myInputPlugin showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
|
||||
XCTAssertTrue(shownEditMenu, @"Should show edit menu with correct configuration.");
|
||||
[self waitForExpectations:@[ expectation ] timeout:1.0];
|
||||
|
||||
UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
|
||||
image:nil
|
||||
action:@selector(copy:)
|
||||
propertyList:nil];
|
||||
UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
|
||||
image:nil
|
||||
action:@selector(paste:)
|
||||
propertyList:nil];
|
||||
NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
|
||||
|
||||
UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
|
||||
menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
|
||||
suggestedActions:suggestedActions];
|
||||
// The item ordering should follow the encoded data sent from the framework.
|
||||
NSArray<UICommand*>* expectedChildren = @[ pasteItem, copyItem ];
|
||||
XCTAssertEqualObjects(menu.children, expectedChildren);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testEditMenu_shouldPresentEditMenuWithCorectItemsUnderNestedSubtreeForBasicEditingActions {
|
||||
if (@available(iOS 16.0, *)) {
|
||||
FlutterTextInputPlugin* myInputPlugin =
|
||||
[[FlutterTextInputPlugin alloc] initWithDelegate:OCMClassMock([FlutterEngine class])];
|
||||
FlutterViewController* myViewController = [[FlutterViewController alloc] init];
|
||||
myInputPlugin.viewController = myViewController;
|
||||
[myViewController loadView];
|
||||
|
||||
FlutterMethodCall* setClientCall =
|
||||
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
|
||||
arguments:@[ @(123), self.mutableTemplateCopy ]];
|
||||
[myInputPlugin handleMethodCall:setClientCall
|
||||
result:^(id _Nullable result){
|
||||
}];
|
||||
|
||||
FlutterTextInputView* myInputView = myInputPlugin.activeView;
|
||||
|
||||
FlutterTextInputView* mockInputView = OCMPartialMock(myInputView);
|
||||
OCMStub([mockInputView isFirstResponder]).andReturn(YES);
|
||||
|
||||
XCTestExpectation* expectation = [[XCTestExpectation alloc]
|
||||
initWithDescription:@"presentEditMenuWithConfiguration must be called."];
|
||||
|
||||
id mockInteraction = OCMClassMock([UIEditMenuInteraction class]);
|
||||
OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
|
||||
OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
|
||||
.andDo(^(NSInvocation* invocation) {
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
myInputView.frame = CGRectMake(10, 20, 30, 40);
|
||||
NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
|
||||
@{@"x" : @(100), @"y" : @(200), @"width" : @(300), @"height" : @(400)};
|
||||
|
||||
NSArray<NSDictionary<NSString*, id>*>* encodedItems =
|
||||
@[ @{@"type" : @"cut"}, @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
|
||||
|
||||
BOOL shownEditMenu =
|
||||
[myInputPlugin showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
|
||||
XCTAssertTrue(shownEditMenu, @"Should show edit menu with correct configuration.");
|
||||
[self waitForExpectations:@[ expectation ] timeout:1.0];
|
||||
|
||||
UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
|
||||
image:nil
|
||||
action:@selector(copy:)
|
||||
propertyList:nil];
|
||||
UICommand* cutItem = [UICommand commandWithTitle:@"Cut"
|
||||
image:nil
|
||||
action:@selector(cut:)
|
||||
propertyList:nil];
|
||||
UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
|
||||
image:nil
|
||||
action:@selector(paste:)
|
||||
propertyList:nil];
|
||||
/*
|
||||
A more complex menu hierarchy for DFS:
|
||||
|
||||
menu
|
||||
/ | \
|
||||
copy menu menu
|
||||
| \
|
||||
paste menu
|
||||
|
|
||||
cut
|
||||
*/
|
||||
NSArray<UIMenuElement*>* suggestedActions = @[
|
||||
copyItem, [UIMenu menuWithChildren:@[ pasteItem ]],
|
||||
[UIMenu menuWithChildren:@[ [UIMenu menuWithChildren:@[ cutItem ]] ]]
|
||||
];
|
||||
|
||||
UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
|
||||
menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
|
||||
suggestedActions:suggestedActions];
|
||||
// The item ordering should follow the encoded data sent from the framework.
|
||||
NSArray<UICommand*>* expectedActions = @[ cutItem, pasteItem, copyItem ];
|
||||
XCTAssertEqualObjects(menu.children, expectedActions);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testEditMenu_shouldPresentEditMenuWithCorectItemsForMoreAdditionalItems {
|
||||
if (@available(iOS 16.0, *)) {
|
||||
FlutterTextInputPlugin* myInputPlugin =
|
||||
[[FlutterTextInputPlugin alloc] initWithDelegate:OCMClassMock([FlutterEngine class])];
|
||||
FlutterViewController* myViewController = [[FlutterViewController alloc] init];
|
||||
myInputPlugin.viewController = myViewController;
|
||||
[myViewController loadView];
|
||||
|
||||
FlutterMethodCall* setClientCall =
|
||||
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
|
||||
arguments:@[ @(123), self.mutableTemplateCopy ]];
|
||||
[myInputPlugin handleMethodCall:setClientCall
|
||||
result:^(id _Nullable result){
|
||||
}];
|
||||
|
||||
FlutterTextInputView* myInputView = myInputPlugin.activeView;
|
||||
|
||||
FlutterTextInputView* mockInputView = OCMPartialMock(myInputView);
|
||||
OCMStub([mockInputView isFirstResponder]).andReturn(YES);
|
||||
|
||||
XCTestExpectation* expectation = [[XCTestExpectation alloc]
|
||||
initWithDescription:@"presentEditMenuWithConfiguration must be called."];
|
||||
|
||||
id mockInteraction = OCMClassMock([UIEditMenuInteraction class]);
|
||||
OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
|
||||
OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
|
||||
.andDo(^(NSInvocation* invocation) {
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
myInputView.frame = CGRectMake(10, 20, 30, 40);
|
||||
NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
|
||||
@{@"x" : @(100), @"y" : @(200), @"width" : @(300), @"height" : @(400)};
|
||||
|
||||
NSArray<NSDictionary<NSString*, id>*>* encodedItems = @[
|
||||
@{@"type" : @"searchWeb", @"title" : @"Search Web"},
|
||||
@{@"type" : @"lookUp", @"title" : @"Look Up"}, @{@"type" : @"share", @"title" : @"Share"}
|
||||
];
|
||||
|
||||
BOOL shownEditMenu =
|
||||
[myInputPlugin showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
|
||||
XCTAssertTrue(shownEditMenu, @"Should show edit menu with correct configuration.");
|
||||
[self waitForExpectations:@[ expectation ] timeout:1.0];
|
||||
|
||||
NSArray<UICommand*>* suggestedActions = @[
|
||||
[UICommand commandWithTitle:@"copy" image:nil action:@selector(copy:) propertyList:nil],
|
||||
];
|
||||
|
||||
UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
|
||||
menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
|
||||
suggestedActions:suggestedActions];
|
||||
XCTAssert(menu.children.count == 3, @"There must be 3 menu items");
|
||||
|
||||
XCTAssert(((UICommand*)menu.children[0]).action == @selector(handleSearchWebAction),
|
||||
@"Must create search web item in the tree.");
|
||||
XCTAssert(((UICommand*)menu.children[1]).action == @selector(handleLookUpAction),
|
||||
@"Must create look up item in the tree.");
|
||||
XCTAssert(((UICommand*)menu.children[2]).action == @selector(handleShareAction),
|
||||
@"Must create share item in the tree.");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testInteractiveKeyboardAfterUserScrollWillResignFirstResponder {
|
||||
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
|
||||
[UIApplication.sharedApplication.keyWindow addSubview:inputView];
|
||||
|
Loading…
x
Reference in New Issue
Block a user